diff --git a/src/renderer/handlers/keyboard.test.ts b/src/renderer/handlers/keyboard.test.ts index a0fce076..35392ed1 100644 --- a/src/renderer/handlers/keyboard.test.ts +++ b/src/renderer/handlers/keyboard.test.ts @@ -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(); diff --git a/src/renderer/modals/media-timing-review.ts b/src/renderer/modals/media-timing-review.ts index b39bfb19..dacc2d81 100644 --- a/src/renderer/modals/media-timing-review.ts +++ b/src/renderer/modals/media-timing-review.ts @@ -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(); } diff --git a/src/renderer/style.css b/src/renderer/style.css index b7084a17..b4702e82 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -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);