fix(hachidori): treat only an open pane as an open popup for lookup controls

This commit is contained in:
sudacode committed 2026-10-11 00:44:07 -07:00
1 parent 06fc578830
commit 0523beef2b
2 files changed
+38 -12

No files matched your search

+25
View File
@@ -1288,6 +1288,31 @@ test('Hachidori attention without an open popup pane still forwards imported mpv
} }
}); });
test('keyboard mode: lookup toggle opens a lookup during Hachidori attention without a pane', async () => {
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
try {
await handlers.setupMpvInputForwarding();
handlers.handleKeyboardModeToggleRequested();
ctx.state.keyboardSelectedWordIndex = 1;
handlers.syncKeyboardTokenSelection();
ctx.state.hachidoriReaderSeen = true;
ctx.state.yomitanPopupVisible = true;
handlers.handleLookupWindowToggleRequested();
await wait(0);
assert.equal(ctx.dom.subtitleRoot.classList.contains('has-selection'), true);
assert.deepEqual(
testGlobals.commandEvents.filter((event) => event.type === 'setVisible'),
[],
);
} finally {
ctx.state.keyboardDrivenModeEnabled = false;
testGlobals.restore();
}
});
test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => { test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness(); const { handlers, testGlobals } = createKeyboardHandlerHarness();
+13 -12
View File
@@ -520,8 +520,7 @@ export function createKeyboardHandlers(
pendingSelectionAnchorAfterSubtitleSeek = null; pendingSelectionAnchorAfterSubtitleSeek = null;
resetSelectionToStartOnNextSubtitleSync = false; resetSelectionToStartOnNextSubtitleSync = false;
const shouldRefreshLookup = const shouldRefreshLookup =
pendingLookupRefreshAfterSubtitleSeek && pendingLookupRefreshAfterSubtitleSeek && isLookupPopupOpen(ctx.state, document);
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document));
pendingLookupRefreshAfterSubtitleSeek = false; pendingLookupRefreshAfterSubtitleSeek = false;
if (shouldRefreshLookup) { if (shouldRefreshLookup) {
queueMicrotask(() => { queueMicrotask(() => {
@@ -713,7 +712,7 @@ export function createKeyboardHandlers(
if (lookupScanFallbackTimer !== null) clearTimeout(lookupScanFallbackTimer); if (lookupScanFallbackTimer !== null) clearTimeout(lookupScanFallbackTimer);
lookupScanFallbackTimer = setTimeout(() => { lookupScanFallbackTimer = setTimeout(() => {
lookupScanFallbackTimer = null; lookupScanFallbackTimer = null;
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) { if (isLookupPopupOpen(ctx.state, document)) {
return; return;
} }
// Dispatch directly on the selected token span; when overlay pointer-events are disabled, // Dispatch directly on the selected token span; when overlay pointer-events are disabled,
@@ -743,13 +742,15 @@ export function createKeyboardHandlers(
} }
function handleLookupWindowToggleRequested(): void { function handleLookupWindowToggleRequested(): void {
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) { if (isLookupPopupOpen(ctx.state, document)) {
closeLookupWindow(); closeLookupWindow();
return; return;
} }
triggerLookupForSelectedWord(); triggerLookupForSelectedWord();
} }
// Also clears Hachidori attention without an open pane, so callers like the
// Kiku modal opener leave no popup state behind.
function closeLookupWindow(): boolean { function closeLookupWindow(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
return false; return false;
@@ -775,7 +776,7 @@ export function createKeyboardHandlers(
return false; return false;
} }
const popupVisible = ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document); const popupVisible = isLookupPopupOpen(ctx.state, document);
const result = moveKeyboardSelection(delta); const result = moveKeyboardSelection(delta);
if (result === 'no-words') { if (result === 'no-words') {
seekAdjacentSubtitleAndQueueSelection(delta, popupVisible); seekAdjacentSubtitleAndQueueSelection(delta, popupVisible);
@@ -796,7 +797,7 @@ export function createKeyboardHandlers(
code: string, code: string,
repeat: boolean = true, repeat: boolean = true,
): boolean { ): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!isLookupPopupOpen(ctx.state, document)) {
return false; return false;
} }
dispatchYomitanPopupKeydown(key, code, [], repeat); dispatchYomitanPopupKeydown(key, code, [], repeat);
@@ -804,7 +805,7 @@ export function createKeyboardHandlers(
} }
function mineSelectedFromController(): boolean { function mineSelectedFromController(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!isLookupPopupOpen(ctx.state, document)) {
return false; return false;
} }
dispatchYomitanPopupMineSelected(); dispatchYomitanPopupMineSelected();
@@ -812,7 +813,7 @@ export function createKeyboardHandlers(
} }
function cyclePopupAudioSourceForController(direction: -1 | 1): boolean { function cyclePopupAudioSourceForController(direction: -1 | 1): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!isLookupPopupOpen(ctx.state, document)) {
return false; return false;
} }
dispatchYomitanPopupCycleAudioSource(direction); dispatchYomitanPopupCycleAudioSource(direction);
@@ -820,7 +821,7 @@ export function createKeyboardHandlers(
} }
function playCurrentAudioForController(): boolean { function playCurrentAudioForController(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!isLookupPopupOpen(ctx.state, document)) {
return false; return false;
} }
dispatchYomitanPopupPlayCurrentAudio(); dispatchYomitanPopupPlayCurrentAudio();
@@ -828,7 +829,7 @@ export function createKeyboardHandlers(
} }
function scrollPopupByController(deltaX: number, deltaY: number): boolean { function scrollPopupByController(deltaX: number, deltaY: number): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!isLookupPopupOpen(ctx.state, document)) {
return false; return false;
} }
dispatchYomitanPopupScrollBy(deltaX, deltaY); dispatchYomitanPopupScrollBy(deltaX, deltaY);
@@ -855,7 +856,7 @@ export function createKeyboardHandlers(
if (!ctx.state.keyboardDrivenModeEnabled) { if (!ctx.state.keyboardDrivenModeEnabled) {
return; return;
} }
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { if (!isLookupPopupOpen(ctx.state, document)) {
return; return;
} }
restoreOverlayKeyboardFocus(); restoreOverlayKeyboardFocus();
@@ -900,7 +901,7 @@ export function createKeyboardHandlers(
} }
const key = e.code; const key = e.code;
const popupVisible = ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document); const popupVisible = isLookupPopupOpen(ctx.state, document);
if (key === 'ArrowLeft' || key === 'KeyH') { if (key === 'ArrowLeft' || key === 'KeyH') {
const result = moveKeyboardSelection(-1); const result = moveKeyboardSelection(-1);
if (result === 'start-boundary' || result === 'no-words') { if (result === 'start-boundary' || result === 'no-words') {