fix(anki): keep media timing review keyboard-focused

- Route modal keydown events before later modal handlers
- Restore focus and preserve visible focus styling
This commit is contained in:
2026-08-16 18:49:09 -07:00
parent 66bf0db0fc
commit 504e15ae0e
3 changed files with 47 additions and 3 deletions
+29 -1
View File
@@ -452,6 +452,7 @@ function createKeyboardHandlerHarness() {
const testGlobals = installKeyboardTestGlobals(); const testGlobals = installKeyboardTestGlobals();
const subtitleRootClassList = createClassList(); const subtitleRootClassList = createClassList();
const subtitleContainerClassList = createClassList(); const subtitleContainerClassList = createClassList();
let mediaTimingReviewKeydownCount = 0;
let controllerSelectKeydownCount = 0; let controllerSelectKeydownCount = 0;
let openControllerSelectCount = 0; let openControllerSelectCount = 0;
let openControllerDebugCount = 0; let openControllerDebugCount = 0;
@@ -494,7 +495,10 @@ function createKeyboardHandlerHarness() {
handleKikuKeydown: () => false, handleKikuKeydown: () => false,
handleJimakuKeydown: () => false, handleJimakuKeydown: () => false,
handleTsukihimeKeydown: () => false, handleTsukihimeKeydown: () => false,
handleMediaTimingReviewKeydown: () => false, handleMediaTimingReviewKeydown: () => {
mediaTimingReviewKeydownCount += 1;
return false;
},
handleControllerSelectKeydown: () => { handleControllerSelectKeydown: () => {
controllerSelectKeydownCount += 1; controllerSelectKeydownCount += 1;
return true; return true;
@@ -524,6 +528,7 @@ function createKeyboardHandlerHarness() {
ctx, ctx,
handlers, handlers,
testGlobals, testGlobals,
mediaTimingReviewKeydownCount: () => mediaTimingReviewKeydownCount,
controllerSelectKeydownCount: () => controllerSelectKeydownCount, controllerSelectKeydownCount: () => controllerSelectKeydownCount,
openControllerSelectCount: () => openControllerSelectCount, openControllerSelectCount: () => openControllerSelectCount,
openControllerDebugCount: () => openControllerDebugCount, openControllerDebugCount: () => openControllerDebugCount,
@@ -1368,6 +1373,29 @@ test('keyboard mode: controller select modal handles arrow keys before yomitan p
} }
}); });
test('media timing review modal handles keys before later modal handlers', async () => {
const {
ctx,
testGlobals,
handlers,
mediaTimingReviewKeydownCount,
controllerSelectKeydownCount,
} = createKeyboardHandlerHarness();
try {
await handlers.setupMpvInputForwarding();
ctx.state.mediaTimingReviewModalOpen = true;
ctx.state.controllerSelectModalOpen = true;
testGlobals.dispatchKeydown({ key: 'ArrowDown', code: 'ArrowDown' });
assert.equal(mediaTimingReviewKeydownCount(), 1);
assert.equal(controllerSelectKeydownCount(), 0);
} finally {
testGlobals.restore();
}
});
test('keyboard mode: playlist browser modal handles arrow keys before yomitan popup', async () => { test('keyboard mode: playlist browser modal handles arrow keys before yomitan popup', async () => {
const { ctx, testGlobals, handlers, playlistBrowserKeydownCount } = const { ctx, testGlobals, handlers, playlistBrowserKeydownCount } =
createKeyboardHandlerHarness(); createKeyboardHandlerHarness();
+17 -1
View File
@@ -1,5 +1,6 @@
import type { MediaTimingReviewDecision, MediaTimingReviewOpenPayload } from '../../types/anki'; import type { MediaTimingReviewDecision, MediaTimingReviewOpenPayload } from '../../types/anki';
import type { ModalStateReader, RendererContext } from '../context'; import type { ModalStateReader, RendererContext } from '../context';
import { createModalFocusGuard } from './modal-focus-guard';
const MINIMUM_CLIP_SECONDS = 0.1; const MINIMUM_CLIP_SECONDS = 0.1;
const FINE_ADJUST_SECONDS = 0.1; const FINE_ADJUST_SECONDS = 0.1;
@@ -139,6 +140,17 @@ export function createMediaTimingReviewModal(
trackWidth: number; trackWidth: number;
grabOffset: number; grabOffset: number;
} | null = null; } | null = null;
const focus = createModalFocusGuard({
isOpen: () => ctx.state.mediaTimingReviewModalOpen,
getModalRoot: () => ctx.dom.mediaTimingReviewModal,
getPreferredFocusTargets: () => [
ctx.dom.mediaTimingReviewStartHandle,
ctx.dom.mediaTimingReviewCancelBack,
],
getFallbackFocusTarget: () => ctx.dom.mediaTimingReviewCancel,
isModalLayer: ctx.platform.isModalLayer,
});
const previewRequest = createMediaTimingPreviewRequestGuard(); const previewRequest = createMediaTimingPreviewRequestGuard();
function setStatus(message: string, isError = false): void { function setStatus(message: string, isError = false): void {
@@ -453,6 +465,7 @@ export function createMediaTimingReviewModal(
ctx.state.mediaTimingReviewModalOpen = false; ctx.state.mediaTimingReviewModalOpen = false;
ctx.dom.mediaTimingReviewModal.classList.add('hidden'); ctx.dom.mediaTimingReviewModal.classList.add('hidden');
ctx.dom.mediaTimingReviewModal.setAttribute('aria-hidden', 'true'); ctx.dom.mediaTimingReviewModal.setAttribute('aria-hidden', 'true');
focus.detach();
window.electronAPI.notifyOverlayModalClosed('media-timing-review'); window.electronAPI.notifyOverlayModalClosed('media-timing-review');
options.syncSettingsModalSubtitleSuppression(); options.syncSettingsModalSubtitleSuppression();
payload = null; payload = null;
@@ -566,7 +579,10 @@ export function createMediaTimingReviewModal(
ctx.dom.mediaTimingReviewModal.classList.remove('hidden'); ctx.dom.mediaTimingReviewModal.classList.remove('hidden');
ctx.dom.mediaTimingReviewModal.setAttribute('aria-hidden', 'false'); ctx.dom.mediaTimingReviewModal.setAttribute('aria-hidden', 'false');
window.electronAPI.notifyOverlayModalOpened('media-timing-review'); window.electronAPI.notifyOverlayModalOpened('media-timing-review');
ctx.dom.mediaTimingReviewStartHandle.focus(); focus.attach();
focus.requestOverlayFocus();
window.focus();
focus.enforceModalFocus();
queueWaveformLoad(); queueWaveformLoad();
} }
+1 -1
View File
@@ -1711,7 +1711,7 @@ body:focus-visible,
} }
.media-timing-review-handle:focus-visible { .media-timing-review-handle:focus-visible {
outline: none; outline: 2px solid transparent;
box-shadow: box-shadow:
inset 0 0 0 2px var(--ctp-yellow), inset 0 0 0 2px var(--ctp-yellow),
0 0 16px color-mix(in srgb, var(--ctp-yellow) 55%, transparent); 0 0 16px color-mix(in srgb, var(--ctp-yellow) 55%, transparent);