mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-20 17:01:24 -07:00
fix(overlay): keep keyboard mode from swallowing modal and text-field keys (#291)
This commit is contained in:
1 parent
78a9b6346f
commit
7108e086ba
3 files changed
+89
-25
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
type: fixed
|
||||||
|
area: overlay
|
||||||
|
|
||||||
|
- Keyboard-only mode no longer swallows `h`, `l`, and the left/right arrows while a modal is open or a text field has focus, so typing into fields like the Jimaku or TsukiHime title works and no longer seeks mpv. Pressing `Ctrl/Cmd+Shift+Y` (or the controller toggle) inside a modal no longer turns on a hidden keyboard-only mode in the modal window.
|
||||||
@@ -315,8 +315,9 @@ function installKeyboardTestGlobals() {
|
|||||||
shiftKey?: boolean;
|
shiftKey?: boolean;
|
||||||
repeat?: boolean;
|
repeat?: boolean;
|
||||||
target?: unknown;
|
target?: unknown;
|
||||||
}): void {
|
}): boolean {
|
||||||
const listeners = documentListeners.get('keydown') ?? [];
|
const listeners = documentListeners.get('keydown') ?? [];
|
||||||
|
let prevented = false;
|
||||||
const keyboardEvent = {
|
const keyboardEvent = {
|
||||||
key: event.key,
|
key: event.key,
|
||||||
code: event.code,
|
code: event.code,
|
||||||
@@ -325,12 +326,15 @@ function installKeyboardTestGlobals() {
|
|||||||
altKey: event.altKey ?? false,
|
altKey: event.altKey ?? false,
|
||||||
shiftKey: event.shiftKey ?? false,
|
shiftKey: event.shiftKey ?? false,
|
||||||
repeat: event.repeat ?? false,
|
repeat: event.repeat ?? false,
|
||||||
preventDefault: () => {},
|
preventDefault: () => {
|
||||||
|
prevented = true;
|
||||||
|
},
|
||||||
target: event.target ?? null,
|
target: event.target ?? null,
|
||||||
};
|
};
|
||||||
for (const listener of listeners) {
|
for (const listener of listeners) {
|
||||||
listener(keyboardEvent);
|
listener(keyboardEvent);
|
||||||
}
|
}
|
||||||
|
return prevented;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchMousedown(event: {
|
function dispatchMousedown(event: {
|
||||||
@@ -472,6 +476,12 @@ function installKeyboardTestGlobals() {
|
|||||||
target.closest = (selector: string) => (selector.includes('.modal') ? target : null);
|
target.closest = (selector: string) => (selector.includes('.modal') ? target : null);
|
||||||
return target;
|
return target;
|
||||||
},
|
},
|
||||||
|
createTextInputTarget: () => {
|
||||||
|
const target = new TestElement();
|
||||||
|
target.tagName = 'INPUT';
|
||||||
|
target.closest = (selector: string) => (selector.includes('input') ? target : null);
|
||||||
|
return target;
|
||||||
|
},
|
||||||
setGetSessionBindings: (value: () => Promise<CompiledSessionBinding[]>) => {
|
setGetSessionBindings: (value: () => Promise<CompiledSessionBinding[]>) => {
|
||||||
getSessionBindingsImpl = value;
|
getSessionBindingsImpl = value;
|
||||||
},
|
},
|
||||||
@@ -501,6 +511,7 @@ function createKeyboardHandlerHarness() {
|
|||||||
let openControllerDebugCount = 0;
|
let openControllerDebugCount = 0;
|
||||||
let playlistBrowserKeydownCount = 0;
|
let playlistBrowserKeydownCount = 0;
|
||||||
let changelogKeydownCount = 0;
|
let changelogKeydownCount = 0;
|
||||||
|
let jimakuKeydownCount = 0;
|
||||||
|
|
||||||
const createWordNode = (left: number) => ({
|
const createWordNode = (left: number) => ({
|
||||||
classList: createClassList(),
|
classList: createClassList(),
|
||||||
@@ -536,7 +547,10 @@ function createKeyboardHandlerHarness() {
|
|||||||
handleCharacterDictionaryKeydown: () => false,
|
handleCharacterDictionaryKeydown: () => false,
|
||||||
handleSubsyncKeydown: () => false,
|
handleSubsyncKeydown: () => false,
|
||||||
handleKikuKeydown: () => false,
|
handleKikuKeydown: () => false,
|
||||||
handleJimakuKeydown: () => false,
|
handleJimakuKeydown: () => {
|
||||||
|
jimakuKeydownCount += 1;
|
||||||
|
return false;
|
||||||
|
},
|
||||||
handleTsukihimeKeydown: () => false,
|
handleTsukihimeKeydown: () => false,
|
||||||
handleMediaTimingReviewKeydown: () => {
|
handleMediaTimingReviewKeydown: () => {
|
||||||
mediaTimingReviewKeydownCount += 1;
|
mediaTimingReviewKeydownCount += 1;
|
||||||
@@ -577,6 +591,7 @@ function createKeyboardHandlerHarness() {
|
|||||||
openControllerDebugCount: () => openControllerDebugCount,
|
openControllerDebugCount: () => openControllerDebugCount,
|
||||||
playlistBrowserKeydownCount: () => playlistBrowserKeydownCount,
|
playlistBrowserKeydownCount: () => playlistBrowserKeydownCount,
|
||||||
changelogKeydownCount: () => changelogKeydownCount,
|
changelogKeydownCount: () => changelogKeydownCount,
|
||||||
|
jimakuKeydownCount: () => jimakuKeydownCount,
|
||||||
setWordCount: (count: number) => {
|
setWordCount: (count: number) => {
|
||||||
wordNodes = Array.from({ length: count }, (_, index) => createWordNode(10 + index * 70));
|
wordNodes = Array.from({ length: count }, (_, index) => createWordNode(10 + index * 70));
|
||||||
},
|
},
|
||||||
@@ -1417,6 +1432,59 @@ test('keyboard mode: changelog modal handles h/l fold keys before lookup control
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const KEYBOARD_MODE_LOOKUP_KEYS = [
|
||||||
|
{ key: 'h', code: 'KeyH' },
|
||||||
|
{ key: 'l', code: 'KeyL' },
|
||||||
|
{ key: 'ArrowLeft', code: 'ArrowLeft' },
|
||||||
|
{ key: 'ArrowRight', code: 'ArrowRight' },
|
||||||
|
];
|
||||||
|
|
||||||
|
test('keyboard mode: an open modal receives h/l/arrows instead of lookup controls', async () => {
|
||||||
|
const { ctx, testGlobals, handlers, jimakuKeydownCount } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
handlers.handleKeyboardModeToggleRequested();
|
||||||
|
ctx.state.jimakuModalOpen = true;
|
||||||
|
ctx.state.keyboardSelectedWordIndex = 1;
|
||||||
|
const titleInput = testGlobals.createTextInputTarget();
|
||||||
|
|
||||||
|
for (const key of KEYBOARD_MODE_LOOKUP_KEYS) {
|
||||||
|
const prevented = testGlobals.dispatchKeydown({ ...key, target: titleInput });
|
||||||
|
assert.equal(prevented, false, `${key.code} must reach the focused modal input`);
|
||||||
|
}
|
||||||
|
await wait(0);
|
||||||
|
|
||||||
|
assert.equal(jimakuKeydownCount(), KEYBOARD_MODE_LOOKUP_KEYS.length);
|
||||||
|
assert.equal(ctx.state.keyboardSelectedWordIndex, 1);
|
||||||
|
assert.deepEqual(testGlobals.mpvCommands, []);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keyboard mode: h/l/arrows typed into a text field are not consumed by lookup controls', async () => {
|
||||||
|
const { ctx, testGlobals, handlers, setWordCount } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await handlers.setupMpvInputForwarding();
|
||||||
|
handlers.handleKeyboardModeToggleRequested();
|
||||||
|
// No words means lookup controls would seek mpv to the adjacent subtitle.
|
||||||
|
setWordCount(0);
|
||||||
|
const input = testGlobals.createTextInputTarget();
|
||||||
|
|
||||||
|
for (const key of KEYBOARD_MODE_LOOKUP_KEYS) {
|
||||||
|
const prevented = testGlobals.dispatchKeydown({ ...key, target: input });
|
||||||
|
assert.equal(prevented, false, `${key.code} must reach the text field`);
|
||||||
|
}
|
||||||
|
await wait(0);
|
||||||
|
|
||||||
|
assert.deepEqual(testGlobals.mpvCommands, []);
|
||||||
|
} finally {
|
||||||
|
testGlobals.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('keyboard mode: configured stats toggle works even while popup is open', async () => {
|
test('keyboard mode: configured stats toggle works even while popup is open', async () => {
|
||||||
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
@@ -1683,7 +1751,7 @@ test('keyboard mode: visible-layer Ctrl+Shift+Y should not be toggled by rendere
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keyboard mode: modal-layer Ctrl+Shift+Y still toggles via renderer keydown', async () => {
|
test('keyboard mode: modal layer never enables keyboard mode', async () => {
|
||||||
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
const { ctx, handlers, testGlobals } = createKeyboardHandlerHarness();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1691,9 +1759,10 @@ test('keyboard mode: modal-layer Ctrl+Shift+Y still toggles via renderer keydown
|
|||||||
ctx.platform.isModalLayer = true;
|
ctx.platform.isModalLayer = true;
|
||||||
|
|
||||||
testGlobals.dispatchKeydown({ key: 'Y', code: 'KeyY', ctrlKey: true, shiftKey: true });
|
testGlobals.dispatchKeydown({ key: 'Y', code: 'KeyY', ctrlKey: true, shiftKey: true });
|
||||||
assert.equal(ctx.state.keyboardDrivenModeEnabled, true);
|
assert.equal(ctx.state.keyboardDrivenModeEnabled, false);
|
||||||
|
|
||||||
testGlobals.dispatchKeydown({ key: 'Y', code: 'KeyY', ctrlKey: true, shiftKey: true });
|
// Controller toggle path.
|
||||||
|
handlers.handleKeyboardModeToggleRequested();
|
||||||
assert.equal(ctx.state.keyboardDrivenModeEnabled, false);
|
assert.equal(ctx.state.keyboardDrivenModeEnabled, false);
|
||||||
} finally {
|
} finally {
|
||||||
ctx.state.keyboardDrivenModeEnabled = false;
|
ctx.state.keyboardDrivenModeEnabled = false;
|
||||||
|
|||||||
@@ -403,11 +403,6 @@ export function createKeyboardHandlers(
|
|||||||
return e.ctrlKey || e.metaKey;
|
return e.ctrlKey || e.metaKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKeyboardDrivenModeToggle(e: KeyboardEvent): boolean {
|
|
||||||
const isYKey = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
|
|
||||||
return isPrimaryModifierPressed(e) && !e.altKey && e.shiftKey && isYKey && !e.repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLookupWindowToggle(e: KeyboardEvent): boolean {
|
function isLookupWindowToggle(e: KeyboardEvent): boolean {
|
||||||
const isYKey = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
|
const isYKey = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
|
||||||
return isPrimaryModifierPressed(e) && !e.altKey && !e.shiftKey && isYKey && !e.repeat;
|
return isPrimaryModifierPressed(e) && !e.altKey && !e.shiftKey && isYKey && !e.repeat;
|
||||||
@@ -723,7 +718,10 @@ export function createKeyboardHandlers(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keyboard mode drives the visible overlay's subtitle words. The modal layer hides
|
||||||
|
// subtitles, so enabling it there only lets arrows/H/L seek mpv from behind a modal.
|
||||||
function handleKeyboardModeToggleRequested(): void {
|
function handleKeyboardModeToggleRequested(): void {
|
||||||
|
if (ctx.platform.isModalLayer) return;
|
||||||
toggleKeyboardDrivenMode();
|
toggleKeyboardDrivenMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,12 +1142,6 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isKeyboardDrivenModeToggle(e) && ctx.platform.isModalLayer) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleKeyboardModeToggleRequested();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLookupWindowToggle(e)) {
|
if (isLookupWindowToggle(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleLookupWindowToggleRequested();
|
handleLookupWindowToggleRequested();
|
||||||
@@ -1162,19 +1154,11 @@ export function createKeyboardHandlers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ahead of the keyboard-driven lookup controls: the changelog modal binds
|
|
||||||
// arrows/H/L for folding, and those would otherwise move the subtitle word
|
|
||||||
// selection (and seek mpv) behind the open modal.
|
|
||||||
if (ctx.state.changelogModalOpen) {
|
if (ctx.state.changelogModalOpen) {
|
||||||
options.handleChangelogKeydown(e);
|
options.handleChangelogKeydown(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (handleKeyboardDrivenModeLookupControls(e)) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.state.runtimeOptionsModalOpen) {
|
if (ctx.state.runtimeOptionsModalOpen) {
|
||||||
options.handleRuntimeOptionsKeydown(e);
|
options.handleRuntimeOptionsKeydown(e);
|
||||||
return;
|
return;
|
||||||
@@ -1222,6 +1206,13 @@ export function createKeyboardHandlers(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After open modals and text fields, which own arrows/H/L; before Yomitan popup
|
||||||
|
// keybinds so word navigation keeps working while the popup is open.
|
||||||
|
if (handleKeyboardDrivenModeLookupControls(e)) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (handlePendingNumericSelection(e)) {
|
if (handlePendingNumericSelection(e)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user