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 () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness();
+13 -12
View File
@@ -520,8 +520,7 @@ export function createKeyboardHandlers(
pendingSelectionAnchorAfterSubtitleSeek = null;
resetSelectionToStartOnNextSubtitleSync = false;
const shouldRefreshLookup =
pendingLookupRefreshAfterSubtitleSeek &&
(ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document));
pendingLookupRefreshAfterSubtitleSeek && isLookupPopupOpen(ctx.state, document);
pendingLookupRefreshAfterSubtitleSeek = false;
if (shouldRefreshLookup) {
queueMicrotask(() => {
@@ -713,7 +712,7 @@ export function createKeyboardHandlers(
if (lookupScanFallbackTimer !== null) clearTimeout(lookupScanFallbackTimer);
lookupScanFallbackTimer = setTimeout(() => {
lookupScanFallbackTimer = null;
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
if (isLookupPopupOpen(ctx.state, document)) {
return;
}
// Dispatch directly on the selected token span; when overlay pointer-events are disabled,
@@ -743,13 +742,15 @@ export function createKeyboardHandlers(
}
function handleLookupWindowToggleRequested(): void {
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
if (isLookupPopupOpen(ctx.state, document)) {
closeLookupWindow();
return;
}
triggerLookupForSelectedWord();
}
// Also clears Hachidori attention without an open pane, so callers like the
// Kiku modal opener leave no popup state behind.
function closeLookupWindow(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
return false;
@@ -775,7 +776,7 @@ export function createKeyboardHandlers(
return false;
}
const popupVisible = ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document);
const popupVisible = isLookupPopupOpen(ctx.state, document);
const result = moveKeyboardSelection(delta);
if (result === 'no-words') {
seekAdjacentSubtitleAndQueueSelection(delta, popupVisible);
@@ -796,7 +797,7 @@ export function createKeyboardHandlers(
code: string,
repeat: boolean = true,
): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupKeydown(key, code, [], repeat);
@@ -804,7 +805,7 @@ export function createKeyboardHandlers(
}
function mineSelectedFromController(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupMineSelected();
@@ -812,7 +813,7 @@ export function createKeyboardHandlers(
}
function cyclePopupAudioSourceForController(direction: -1 | 1): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupCycleAudioSource(direction);
@@ -820,7 +821,7 @@ export function createKeyboardHandlers(
}
function playCurrentAudioForController(): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupPlayCurrentAudio();
@@ -828,7 +829,7 @@ export function createKeyboardHandlers(
}
function scrollPopupByController(deltaX: number, deltaY: number): boolean {
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return false;
}
dispatchYomitanPopupScrollBy(deltaX, deltaY);
@@ -855,7 +856,7 @@ export function createKeyboardHandlers(
if (!ctx.state.keyboardDrivenModeEnabled) {
return;
}
if (!ctx.state.yomitanPopupVisible && !isYomitanPopupVisible(document)) {
if (!isLookupPopupOpen(ctx.state, document)) {
return;
}
restoreOverlayKeyboardFocus();
@@ -900,7 +901,7 @@ export function createKeyboardHandlers(
}
const key = e.code;
const popupVisible = ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document);
const popupVisible = isLookupPopupOpen(ctx.state, document);
if (key === 'ArrowLeft' || key === 'KeyH') {
const result = moveKeyboardSelection(-1);
if (result === 'start-boundary' || result === 'no-words') {