From 6c295586cb4edefcad0b78af6161722458651451 Mon Sep 17 00:00:00 2001 From: sudacode Date: Sun, 11 Oct 2026 00:54:01 -0700 Subject: [PATCH] fix(hachidori): close an open pane after its attention is released --- src/renderer/handlers/keyboard.test.ts | 38 +++++++++++++++++++++++++- src/renderer/handlers/keyboard.ts | 12 ++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index e9db97d8..281e3a42 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -153,6 +153,10 @@ function installKeyboardTestGlobals() { let selectionAddCount = 0; let popupVisible = false; + let hachidoriPaneOpen = false; + const hachidoriHost = { + shadowRoot: { querySelectorAll: () => [{ hidden: false }] }, + }; const popupIframe = { tagName: 'IFRAME', @@ -287,7 +291,10 @@ function installKeyboardTestGlobals() { listeners.push(listener); documentListeners.set(type, listeners); }, - querySelectorAll: () => { + querySelectorAll: (selector: string) => { + if (selector === HACHIDORI_HOST_SELECTOR) { + return hachidoriPaneOpen ? [hachidoriHost] : []; + } if (popupVisible) { return [popupIframe]; } @@ -457,6 +464,9 @@ function installKeyboardTestGlobals() { setPopupVisible: (value: boolean) => { popupVisible = value; }, + setHachidoriPaneOpen: (value: boolean) => { + hachidoriPaneOpen = value; + }, setStatsToggleKey: (value: string) => { statsToggleKey = value; }, @@ -1313,6 +1323,32 @@ test('keyboard mode: lookup toggle opens a lookup during Hachidori attention wit } }); +test('keyboard mode: lookup toggle closes an open Hachidori pane after attention is released', async () => { + const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness(); + + try { + await handlers.setupMpvInputForwarding(); + handlers.handleKeyboardModeToggleRequested(); + // A selection drag leaves the pane open while the Yomitan-shaped visible marker clears. + ctx.state.hachidoriReaderSeen = true; + ctx.state.yomitanPopupVisible = false; + testGlobals.setHachidoriPaneOpen(true); + + handlers.handleLookupWindowToggleRequested(); + await wait(0); + + assert.deepEqual( + testGlobals.commandEvents.filter( + (event) => event.type === 'setVisible' || event.type === 'clearActiveTextSource', + ), + [{ type: 'setVisible', visible: false }, { type: 'clearActiveTextSource' }], + ); + } finally { + ctx.state.keyboardDrivenModeEnabled = false; + testGlobals.restore(); + } +}); + test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => { const { handlers, testGlobals } = createKeyboardHandlerHarness(); diff --git a/src/renderer/handlers/keyboard.ts b/src/renderer/handlers/keyboard.ts index 5eb12201..262399b4 100644 --- a/src/renderer/handlers/keyboard.ts +++ b/src/renderer/handlers/keyboard.ts @@ -8,6 +8,7 @@ import { HACHIDORI_HOST_SELECTOR, YOMITAN_POPUP_COMMAND_EVENT, YOMITAN_POPUP_HOST_SELECTOR, + isHachidoriPopupOpen, isLookupPopupOpen, isYomitanPopupVisible, isYomitanPopupIframe, @@ -749,10 +750,15 @@ export function createKeyboardHandlers( triggerLookupForSelectedWord(); } - // Also clears Hachidori attention without an open pane, so callers like the - // Kiku modal opener leave no popup state behind. + // Closes any popup state: an open Hachidori pane (its attention may already be + // released) and attention without a pane, so callers like the Kiku modal + // opener leave nothing behind. function closeLookupWindow(): boolean { - if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) { + if ( + !ctx.state.yomitanPopupVisible && + !isYomitanPopupVisible(document) && + !isHachidoriPopupOpen(document) + ) { return false; }