fix(overlay): keep popup pause working with Hachidori 0.4.0

Hachidori 0.4.0 closed its popup shadow root, so isHachidoriPopupOpen always
returned false. The popup never took over the subtitle hover pause, and moving
off the subtitle resumed playback with the popup open. Popup key and controller
routing read the same check. The sidebar kept working because its hold follows
the attention event instead of the pane.

The fork now publishes the pane state on the host element, and the renderer
reads that attribute instead of upstream's shadow root and CSS class. The unit
stubs modelled an open root and hid this, so test:dictionary:electron now drives
the real reader and asserts the attribute follows the pane.
This commit is contained in:
sudacode committed 2026-11-04 22:46:25 -08:00
1 parent bc7e82fee6
commit c2dac537da
5 files changed
+95 -30

No files matched your search

+3 -6
View File
@@ -3,7 +3,7 @@ import test from 'node:test';
import { createKeyboardHandlers } from './keyboard.js';
import { createRendererState } from '../state.js';
import { HACHIDORI_HOST_SELECTOR } from '../yomitan-popup.js';
import { HACHIDORI_HOST_SELECTOR, HACHIDORI_POPUP_OPEN_SELECTOR } from '../yomitan-popup.js';
import type { CompiledSessionBinding } from '../../types';
import type { MpvInputBindingsSnapshot } from '../../types/session-bindings';
import { DEFAULT_KEYBINDINGS, SPECIAL_COMMANDS } from '../../config/definitions';
@@ -156,9 +156,6 @@ function installKeyboardTestGlobals() {
let popupVisible = false;
let hachidoriPaneOpen = false;
const hachidoriHost = {
shadowRoot: { querySelectorAll: () => [{ hidden: false }] },
};
const popupIframe = {
tagName: 'IFRAME',
@@ -293,8 +290,8 @@ function installKeyboardTestGlobals() {
documentListeners.set(type, listeners);
},
querySelectorAll: (selector: string) => {
if (selector === HACHIDORI_HOST_SELECTOR) {
return hachidoriPaneOpen ? [hachidoriHost] : [];
if (selector === HACHIDORI_POPUP_OPEN_SELECTOR) {
return hachidoriPaneOpen ? [{}] : [];
}
if (popupVisible) {
return [popupIframe];
+30 -15
View File
@@ -5,9 +5,8 @@ import { SUBTITLE_DEFAULT_CONFIG } from '../../config/definitions/defaults-subti
import { createRendererState } from '../state';
import { createMouseHandlers } from './mouse.js';
import {
HACHIDORI_HOST_SELECTOR,
HACHIDORI_POPUP_HIDDEN_EVENT,
HACHIDORI_POPUP_SELECTOR,
HACHIDORI_POPUP_OPEN_SELECTOR,
HACHIDORI_POPUP_SHOWN_EVENT,
YOMITAN_LOOKUP_EVENT,
YOMITAN_POPUP_HIDDEN_EVENT,
@@ -110,23 +109,11 @@ function createMouseHarness(
};
const ignoreCalls: IgnoreCall[] = [];
const mpvCommands: Array<(string | number)[]> = [];
const popupPane = {
get hidden() {
return !harness.popupPaneOpen;
},
};
const popupHost = {
tagName: options.hachidori ? 'HACHIDORI-HOST' : 'DIV',
getAttribute: (name: string) =>
name === 'data-subminer-yomitan-popup-visible' ? String(harness.popupHostVisible) : null,
shadowRoot: {
querySelectorAll: (selector: string) =>
selector === HACHIDORI_POPUP_SELECTOR ? [popupPane] : [],
},
};
const hostSelectors = options.hachidori
? [HACHIDORI_HOST_SELECTOR, YOMITAN_POPUP_HOST_SELECTOR]
: [YOMITAN_POPUP_HOST_SELECTOR];
const harness = {
ctx,
@@ -183,7 +170,10 @@ function createMouseHarness(
if (selector === YOMITAN_POPUP_VISIBLE_HOST_SELECTOR) {
return harness.popupHostVisible ? [popupHost] : [];
}
return hostSelectors.includes(selector) &&
if (selector === HACHIDORI_POPUP_OPEN_SELECTOR) {
return harness.popupPaneOpen ? [popupHost] : [];
}
return selector === YOMITAN_POPUP_HOST_SELECTOR &&
(harness.popupHostVisible || harness.popupHostAttached)
? [popupHost]
: [];
@@ -529,6 +519,31 @@ test('Hachidori hover popup pauses when shown', async (t) => {
assert.deepEqual(h.mpvCommands, [PAUSE]);
});
test('leaving the subtitle keeps playback paused while a Hachidori popup is open', async (t) => {
let paused = false;
const h = createMouseHarness(t, {
hoverPause: true,
popupPause: true,
hachidori: true,
getPlaybackPaused: async () => paused,
});
h.handlers.setupYomitanObserver();
await h.handlers.handleMouseEnter();
paused = true;
h.popupPaneOpen = true;
h.popupHostVisible = true;
h.fire('window', HACHIDORI_POPUP_SHOWN_EVENT);
await waitForNextTick();
await h.handlers.handleMouseLeave();
assert.deepEqual(h.mpvCommands, [PAUSE]);
h.popupPaneOpen = false;
h.popupHostVisible = false;
h.fire('window', HACHIDORI_POPUP_HIDDEN_EVENT);
assert.deepEqual(h.mpvCommands, [PAUSE, RESUME]);
});
test('popup shown reclaims overlay focus on macOS and captures click-away', (t) => {
const h = createMouseHarness(t, { platform: 'macos' });
h.handlers.setupYomitanObserver();
+5 -8
View File
@@ -12,11 +12,13 @@ export const YOMITAN_LOOKUP_EVENT = 'subminer-yomitan-lookup';
export const PRIMARY_SUB_VISIBLE_ON_YOMITAN_POPUP_CLASS = 'primary-sub-visible-on-yomitan-popup';
// Hachidori's shown/hidden pair means "the reader needs mouse events", which
// also covers a left press on subtitle text that may start a selection. Its
// popup panes live in the host's open shadow root.
// popup panes live in a closed shadow root, so the fork's subminer-host.js
// publishes whether one is on screen as an attribute on the host.
export const HACHIDORI_POPUP_SHOWN_EVENT = 'hachidori-popup-shown';
export const HACHIDORI_POPUP_HIDDEN_EVENT = 'hachidori-popup-hidden';
export const HACHIDORI_HOST_SELECTOR = 'hachidori-host';
export const HACHIDORI_POPUP_SELECTOR = '.gsm-hoshidicts-popup';
export const HACHIDORI_POPUP_OPEN_SELECTOR =
'hachidori-host[data-subminer-hachidori-popup-open="true"]';
export type DictionaryReader = 'yomitan' | 'hachidori';
@@ -114,12 +116,7 @@ function queryPopupElements<T extends Element>(
* attaches its host on the first lookup, so no host means no popup.
*/
export function isHachidoriPopupOpen(root: ParentNode | null | undefined = document): boolean {
const hosts = queryPopupElements<HTMLElement>(root, HACHIDORI_HOST_SELECTOR);
return hosts.some((host) =>
queryPopupElements<HTMLElement>(host.shadowRoot, HACHIDORI_POPUP_SELECTOR).some(
(pane) => !pane.hidden,
),
);
return queryPopupElements(root, HACHIDORI_POPUP_OPEN_SELECTOR).length > 0;
}
/**