fix(overlay): forward imported mpv input while Hachidori only claims attention

Hachidori's popup-shown event also fires for any press that may start a
selection, so treating it as an open popup blocked imported mpv mouse
buttons and routed every key to the popup keybind handler. Once Hachidori
is the reader, forwarding, popup key routing and the controller lookup
state now require an unhidden popup pane. Wheel events over an in-document
dictionary popup stay with the popup.

Bump vendor/hachidori so presses outside a text box no longer start a
glyph drag that prevented the press.
This commit is contained in:
sudacode committed 2026-10-01 23:56:14 -07:00
1 parent 4423dd56af
commit 3091d7b2a7
8 files changed
+73 -12

No files matched your search

+41 -1
View File
@@ -479,7 +479,12 @@ function installKeyboardTestGlobals() {
createDictionaryPopupHostTarget: () => {
const target = new TestElement();
target.closest = (selector: string) =>
selector === YOMITAN_POPUP_HOST_SELECTOR ? target : null;
selector
.split(',')
.map((part) => part.trim())
.includes(YOMITAN_POPUP_HOST_SELECTOR)
? target
: null;
return target;
},
setGetSessionBindings: (value: () => Promise<CompiledSessionBinding[]>) => {
@@ -1203,7 +1208,13 @@ test('imported mpv wheel bindings forward from the overlay but not over modals',
testGlobals.dispatchWheel({ deltaY: 120 });
testGlobals.dispatchWheel({ deltaY: -120, target: testGlobals.createInteractiveTarget() });
// A Hachidori popup scrolls itself; its events arrive retargeted to its host.
const popupScrolled = !testGlobals.dispatchWheel({
deltaY: -120,
target: testGlobals.createDictionaryPopupHostTarget(),
});
assert.equal(popupScrolled, true);
assert.deepEqual(testGlobals.mpvCommands, [['keypress', 'WHEEL_DOWN', 1]]);
} finally {
testGlobals.restore();
@@ -1248,6 +1259,35 @@ test('imported mpv mouse buttons forward only after SubMiner mouse handling', as
}
});
test('Hachidori attention without an open popup pane still forwards imported mpv input', async () => {
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
try {
testGlobals.setGetMpvInputBindings(async () => ({
keys: ['0', 'MBTN_LEFT_DBL'],
blockedKeys: [],
}));
await handlers.setupMpvInputForwarding();
await wait(0);
// Hachidori claims attention for any press that may start a selection.
ctx.state.hachidoriReaderSeen = true;
ctx.state.yomitanPopupVisible = true;
testGlobals.dispatchKeydown({ key: '0', code: 'Digit0' });
testGlobals.dispatchMousedown({ button: 0 });
testGlobals.dispatchMousedown({ button: 0, type: 'mouseup' });
assert.deepEqual(testGlobals.commandEvents, []);
assert.deepEqual(testGlobals.mpvCommands, [
['keydown', '0'],
['keydown', 'MBTN_LEFT'],
['keyup', 'MBTN_LEFT'],
]);
} finally {
testGlobals.restore();
}
});
test('configured subtitle-jump keybinding preserves pause when pause state is unknown', async () => {
const { handlers, testGlobals } = createKeyboardHandlerHarness();
+7 -4
View File
@@ -5,8 +5,10 @@ import { MPV_MOUSE_BUTTON_BY_BUTTON, wheelEventToMpvWheel } from '../../shared/m
import { dispatchConfiguredMpvCommand } from '../utils/mpv-command-dispatch';
import {
registerDictionaryPopupVisibilityListener,
HACHIDORI_HOST_SELECTOR,
YOMITAN_POPUP_COMMAND_EVENT,
YOMITAN_POPUP_HOST_SELECTOR,
isLookupPopupOpen,
isYomitanPopupVisible,
isYomitanPopupIframe,
} from '../yomitan-popup.js';
@@ -108,11 +110,13 @@ export function createKeyboardHandlers(
}
// Overlay UI that handles its own mouse input (menus, sidebar, notifications, controls).
// Hachidori's popup lives in the overlay document, unlike Yomitan's iframe, so its
// wheel events reach these handlers retargeted to its host.
function isOverlayControlTarget(target: EventTarget | null): boolean {
if (!(target instanceof Element)) return false;
return Boolean(
target.closest(
'.modal, .notification-history, .overlay-notification-stack, button, a, input, select, textarea',
`.modal, .notification-history, .overlay-notification-stack, button, a, input, select, textarea, ${YOMITAN_POPUP_HOST_SELECTOR}, ${HACHIDORI_HOST_SELECTOR}`,
),
);
}
@@ -124,8 +128,7 @@ export function createKeyboardHandlers(
!ctx.state.playlistBrowserModalOpen &&
!ctx.state.youtubePickerModalOpen &&
!ctx.state.subtitleSidebarModalOpen &&
!ctx.state.yomitanPopupVisible &&
!isYomitanPopupVisible(document) &&
!isLookupPopupOpen(ctx.state, document) &&
!isInteractiveTarget(target)
);
}
@@ -1262,7 +1265,7 @@ export function createKeyboardHandlers(
return;
}
if (ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document)) {
if (isLookupPopupOpen(ctx.state, document)) {
if (handleYomitanPopupKeybind(e)) {
e.preventDefault();
return;
+3 -3
View File
@@ -36,7 +36,6 @@ export function createMouseHandlers(
let yomitanPopupVisible = false;
let hoverPauseRequestId = 0;
let popupPauseRequestId = 0;
let hachidoriReaderSeen = false;
let pausedBySubtitleHover = false;
let pausedByYomitanPopup = false;
let lastPointerPosition: { clientX: number; clientY: number } | null = null;
@@ -266,7 +265,8 @@ export function createMouseHandlers(
return (
yomitanPopupVisible &&
options.getYomitanPopupAutoPauseEnabled() &&
(!hachidoriReaderSeen || (typeof document !== 'undefined' && isHachidoriPopupOpen(document)))
(!ctx.state.hachidoriReaderSeen ||
(typeof document !== 'undefined' && isHachidoriPopupOpen(document)))
);
}
@@ -480,7 +480,7 @@ export function createMouseHandlers(
reconcilePopupInteraction({ allowPause: true });
registerDictionaryPopupVisibilityListener('shown', (reader) => {
if (reader === 'hachidori') hachidoriReaderSeen = true;
if (reader === 'hachidori') ctx.state.hachidoriReaderSeen = true;
reconcilePopupInteraction({
assumeVisible: true,
allowPause: true,