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 subtitleRootClassList = createClassList();
const subtitleContainerClassList = createClassList();
let mediaTimingReviewKeydownCount = 0;
let controllerSelectKeydownCount = 0;
let openControllerSelectCount = 0;
let openControllerDebugCount = 0;
@@ -494,7 +495,10 @@ function createKeyboardHandlerHarness() {
handleKikuKeydown: () => false,
handleJimakuKeydown: () => false,
handleTsukihimeKeydown: () => false,
handleMediaTimingReviewKeydown: () => false,
handleMediaTimingReviewKeydown: () => {
mediaTimingReviewKeydownCount += 1;
return false;
},
handleControllerSelectKeydown: () => {
controllerSelectKeydownCount += 1;
return true;
@@ -524,6 +528,7 @@ function createKeyboardHandlerHarness() {
ctx,
handlers,
testGlobals,
mediaTimingReviewKeydownCount: () => mediaTimingReviewKeydownCount,
controllerSelectKeydownCount: () => controllerSelectKeydownCount,
openControllerSelectCount: () => openControllerSelectCount,
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 () => {
const { ctx, testGlobals, handlers, playlistBrowserKeydownCount } =
createKeyboardHandlerHarness();
+17 -1
View File
@@ -1,5 +1,6 @@
import type { MediaTimingReviewDecision, MediaTimingReviewOpenPayload } from '../../types/anki';
import type { ModalStateReader, RendererContext } from '../context';
import { createModalFocusGuard } from './modal-focus-guard';
const MINIMUM_CLIP_SECONDS = 0.1;
const FINE_ADJUST_SECONDS = 0.1;
@@ -139,6 +140,17 @@ export function createMediaTimingReviewModal(
trackWidth: number;
grabOffset: number;
} | 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();
function setStatus(message: string, isError = false): void {
@@ -453,6 +465,7 @@ export function createMediaTimingReviewModal(
ctx.state.mediaTimingReviewModalOpen = false;
ctx.dom.mediaTimingReviewModal.classList.add('hidden');
ctx.dom.mediaTimingReviewModal.setAttribute('aria-hidden', 'true');
focus.detach();
window.electronAPI.notifyOverlayModalClosed('media-timing-review');
options.syncSettingsModalSubtitleSuppression();
payload = null;
@@ -566,7 +579,10 @@ export function createMediaTimingReviewModal(
ctx.dom.mediaTimingReviewModal.classList.remove('hidden');
ctx.dom.mediaTimingReviewModal.setAttribute('aria-hidden', 'false');
window.electronAPI.notifyOverlayModalOpened('media-timing-review');
ctx.dom.mediaTimingReviewStartHandle.focus();
focus.attach();
focus.requestOverlayFocus();
window.focus();
focus.enforceModalFocus();
queueWaveformLoad();
}
+1 -1
View File
@@ -1711,7 +1711,7 @@ body:focus-visible,
}
.media-timing-review-handle:focus-visible {
outline: none;
outline: 2px solid transparent;
box-shadow:
inset 0 0 0 2px var(--ctp-yellow),
0 0 16px color-mix(in srgb, var(--ctp-yellow) 55%, transparent);