fix(renderer): keep controller input active with sidebar open

This commit is contained in:
2026-03-24 00:23:00 -07:00
parent 5feed360ca
commit 6f56a0bcf6
4 changed files with 123 additions and 1 deletions

View File

@@ -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
<!-- SECTION:DESCRIPTION:BEGIN -->
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.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [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.
<!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
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.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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.
<!-- SECTION:FINAL_SUMMARY:END -->

View File

@@ -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,
);
});

View File

@@ -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
);
}

View File

@@ -35,6 +35,7 @@ import { createJimakuModal } from './modals/jimaku.js';
import { createKikuModal } from './modals/kiku.js'; import { createKikuModal } from './modals/kiku.js';
import { createSessionHelpModal } from './modals/session-help.js'; import { createSessionHelpModal } from './modals/session-help.js';
import { createSubtitleSidebarModal } from './modals/subtitle-sidebar.js'; import { createSubtitleSidebarModal } from './modals/subtitle-sidebar.js';
import { isControllerInteractionBlocked } from './controller-interaction-blocking.js';
import { createRuntimeOptionsModal } from './modals/runtime-options.js'; import { createRuntimeOptionsModal } from './modals/runtime-options.js';
import { createSubsyncModal } from './modals/subsync.js'; import { createSubsyncModal } from './modals/subsync.js';
import { createYoutubeTrackPickerModal } from './modals/youtube-track-picker.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 { function syncSettingsModalSubtitleSuppression(): void {
const suppressSubtitles = isAnySettingsModalOpen(); const suppressSubtitles = isAnySettingsModalOpen();
document.body.classList.toggle('settings-modal-open', suppressSubtitles); document.body.classList.toggle('settings-modal-open', suppressSubtitles);
@@ -323,7 +328,7 @@ function startControllerPolling(): void {
}, },
getKeyboardModeEnabled: () => ctx.state.keyboardDrivenModeEnabled, getKeyboardModeEnabled: () => ctx.state.keyboardDrivenModeEnabled,
getLookupWindowOpen: () => ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document), getLookupWindowOpen: () => ctx.state.yomitanPopupVisible || isYomitanPopupVisible(document),
getInteractionBlocked: () => isAnyModalOpen(), getInteractionBlocked: () => isControllerInputBlocked(),
toggleKeyboardMode: () => keyboardHandlers.handleKeyboardModeToggleRequested(), toggleKeyboardMode: () => keyboardHandlers.handleKeyboardModeToggleRequested(),
toggleLookup: () => keyboardHandlers.handleLookupWindowToggleRequested(), toggleLookup: () => keyboardHandlers.handleLookupWindowToggleRequested(),
closeLookup: () => { closeLookup: () => {