diff --git a/backlog/tasks/task-231 - Restore-controller-input-while-subtitle-sidebar-is-open.md b/backlog/tasks/task-231 - Restore-controller-input-while-subtitle-sidebar-is-open.md new file mode 100644 index 0000000..c7357c7 --- /dev/null +++ b/backlog/tasks/task-231 - Restore-controller-input-while-subtitle-sidebar-is-open.md @@ -0,0 +1,57 @@ +--- +id: TASK-231 +title: Restore controller input while subtitle sidebar is open +status: Done +assignee: + - '@codex' +created_date: '2026-03-24 00:15' +updated_date: '2026-03-24 00:15' +labels: + - bug + - controller + - subtitle-sidebar + - overlay +dependencies: [] +references: + - /home/sudacode/projects/japanese/SubMiner/src/renderer/renderer.ts + - /home/sudacode/projects/japanese/SubMiner/src/renderer/controller-interaction-blocking.ts + - /home/sudacode/projects/japanese/SubMiner/src/renderer/controller-interaction-blocking.test.ts +priority: high +ordinal: 54900 +--- + +## Description + + + +When keyboard-only mode is active, opening the subtitle sidebar should not disable controller navigation and lookup/mining controls. Restore controller input while the sidebar is open, while keeping true modal dialogs blocking controller actions. + + + +## Acceptance Criteria + + + +- [x] #1 Opening the subtitle sidebar does not block controller input for keyboard-only mode actions. +- [x] #2 Controller-select/debug and other true modal dialogs still block controller actions while open. +- [x] #3 Focused regression coverage exists for the sidebar-open controller gating rule. + + + +## Implementation Notes + + + +Root cause: renderer gamepad polling used the broad `isAnyModalOpen()` check as its interaction gate, and that list includes `subtitleSidebarModalOpen`. The subtitle sidebar is non-modal for controller usage, so gamepad input was being suppressed whenever the sidebar was visible. + +Fixed by extracting a dedicated controller-interaction blocking helper that excludes the subtitle sidebar but keeps the existing blocking behavior for true modal dialogs. + + + +## Final Summary + + + +Restored controller input while the subtitle sidebar is open by switching gamepad polling to a dedicated modal-blocking rule that leaves the sidebar controller-passive. Added a regression test covering the sidebar-open exception and preserving hard blocks for actual modal dialogs. + + diff --git a/src/renderer/controller-interaction-blocking.test.ts b/src/renderer/controller-interaction-blocking.test.ts new file mode 100644 index 0000000..a040bc0 --- /dev/null +++ b/src/renderer/controller-interaction-blocking.test.ts @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { isControllerInteractionBlocked } from './controller-interaction-blocking.js'; + +test('subtitle sidebar stays controller-passive while other modals block controller input', () => { + assert.equal( + isControllerInteractionBlocked({ + controllerSelectModalOpen: false, + controllerDebugModalOpen: false, + jimakuModalOpen: false, + kikuModalOpen: false, + runtimeOptionsModalOpen: false, + subsyncModalOpen: false, + youtubePickerModalOpen: false, + sessionHelpModalOpen: false, + subtitleSidebarModalOpen: true, + }), + false, + ); + + assert.equal( + isControllerInteractionBlocked({ + controllerSelectModalOpen: false, + controllerDebugModalOpen: false, + jimakuModalOpen: false, + kikuModalOpen: false, + runtimeOptionsModalOpen: true, + subsyncModalOpen: false, + youtubePickerModalOpen: false, + sessionHelpModalOpen: false, + subtitleSidebarModalOpen: false, + }), + true, + ); +}); diff --git a/src/renderer/controller-interaction-blocking.ts b/src/renderer/controller-interaction-blocking.ts new file mode 100644 index 0000000..55e2fd7 --- /dev/null +++ b/src/renderer/controller-interaction-blocking.ts @@ -0,0 +1,24 @@ +type ControllerInteractionModalState = { + controllerSelectModalOpen: boolean; + controllerDebugModalOpen: boolean; + jimakuModalOpen: boolean; + kikuModalOpen: boolean; + runtimeOptionsModalOpen: boolean; + subsyncModalOpen: boolean; + youtubePickerModalOpen: boolean; + sessionHelpModalOpen: boolean; + subtitleSidebarModalOpen: boolean; +}; + +export function isControllerInteractionBlocked(state: ControllerInteractionModalState): boolean { + return ( + state.controllerSelectModalOpen || + state.controllerDebugModalOpen || + state.jimakuModalOpen || + state.kikuModalOpen || + state.runtimeOptionsModalOpen || + state.subsyncModalOpen || + state.youtubePickerModalOpen || + state.sessionHelpModalOpen + ); +} diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index 4bfc019..20633f4 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -35,6 +35,7 @@ import { createJimakuModal } from './modals/jimaku.js'; import { createKikuModal } from './modals/kiku.js'; import { createSessionHelpModal } from './modals/session-help.js'; import { createSubtitleSidebarModal } from './modals/subtitle-sidebar.js'; +import { isControllerInteractionBlocked } from './controller-interaction-blocking.js'; import { createRuntimeOptionsModal } from './modals/runtime-options.js'; import { createSubsyncModal } from './modals/subsync.js'; import { createYoutubeTrackPickerModal } from './modals/youtube-track-picker.js'; @@ -88,6 +89,10 @@ function isAnyModalOpen(): boolean { ); } +function isControllerInputBlocked(): boolean { + return isControllerInteractionBlocked(ctx.state); +} + function syncSettingsModalSubtitleSuppression(): void { const suppressSubtitles = isAnySettingsModalOpen(); document.body.classList.toggle('settings-modal-open', suppressSubtitles); @@ -323,7 +328,7 @@ function startControllerPolling(): void { }, getKeyboardModeEnabled: () => ctx.state.keyboardDrivenModeEnabled, getLookupWindowOpen: () => ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document), - getInteractionBlocked: () => isAnyModalOpen(), + getInteractionBlocked: () => isControllerInputBlocked(), toggleKeyboardMode: () => keyboardHandlers.handleKeyboardModeToggleRequested(), toggleLookup: () => keyboardHandlers.handleLookupWindowToggleRequested(), closeLookup: () => {