fix: escape key works in braille mode for subplot exit#593
Conversation
|
Test comment to verify posting works. |
Code ReviewThanks for the fix! The overall approach is well-reasoned and the inline comments clearly explain the screen-reader-specific constraints. Here are my observations: Correctness / Potential Bugs 1. Focus stack inconsistency after ExitBrailleAndSubplotCommand After the command runs, focusStack = [TRACE] while hotkeys scope = SUBPLOT, leaving them out of sync. The correction logic in toggleFocus does handle this indirectly when returning from other modals, but this relies on an implicit invariant. A more explicit fix would push SUBPLOT onto the focusStack inside dismissModalScope (or a new atomic method) so the stack always reflects reality. 2. Direct notifyStateUpdate() call in MoveToTraceContextCommand Calling notifyStateUpdate() from a Command is unusual -- the Model normally decides when to fire notifications. More critically, enterSubplot() was just called immediately before. Does resetToInitialEntry() or the internal toggleScope already notify observers? If so, this is a double-notification for all registered services (audio, text, braille, highlight). If not, a comment explaining why enterSubplot does not trigger this itself would prevent the call from being removed in a future cleanup. Code Quality 3. role=application on the Braille wrapper div This is a broader change than just fixing Escape passthrough. role=application tells screen readers to suppress all browse-mode shortcuts for the entire subtree, not just the textarea. If the braille div ever grows to include non-interactive content (labels, status text), users lose the ability to read it with standard SR navigation commands. Consider placing this attribute directly on the textarea for tighter scoping, or using aria-modal=true on the div as an alternative approach. 4. notifyFocusChange has no cancellation mechanism Rapid Escape presses could queue multiple deferred firings. A 0 ms delay makes this very unlikely to cause visible issues, but storing the timeout ID and cancelling on re-entry would make the method safer to reuse. Missing Tests The checklist notes unit tests were not added. Given this fix targets a multi-step, ordering-sensitive sequence (focus stack manipulation, scope transitions, deferred events), tests for ExitBrailleAndSubplotCommand and the modified MoveToTraceContextCommand would guard against regressions -- the ordering is non-obvious and easy to break silently. What is Good
Summary: The fix is on the right track. The three main items to address before merging: (1) the focus stack inconsistency after ExitBrailleAndSubplotCommand, (2) confirming whether the direct notifyStateUpdate() call in MoveToTraceContextCommand causes double-notifications, and (3) reconsidering the scope of role=application on the Braille wrapper div. |
- Sync focus stack with hotkeys scope in dismissModalScope by accepting a targetScope parameter, preventing stale [TRACE] vs SUBPLOT mismatch - Add cancellation to notifyFocusChange to prevent stale deferred events from rapid repeated calls, and clean up timer in dispose() - Move role="application" from wrapper div to textarea element to avoid suppressing browse-mode navigation for potential sibling content - Expand comment on notifyStateUpdate() call explaining why enterSubplot does not notify observers itself Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code ReviewGood fix for a real accessibility pain point. The overall approach of deferring the display change event so screen readers can process focus before the braille textarea unmounts is sound. A few items worth addressing before merge. Bug: Redundant removeLast in dismissModalScope (src/service/display.ts) The removeLast(focus) call is a no-op because clear() immediately discards the entire stack. The first parameter is effectively unused. Either drop removeLast (since clear already handles cleanup), or drop clear and only pop the modal scope. If the intent is a full reset, the signature simplifies to: Architectural concern: Command calling notifyStateUpdate() directly (src/command/move.ts, MoveToTraceContextCommand.execute) Calling this.context.active.notifyStateUpdate() from a command bypasses the normal flow where the model notifies its own observers. The inline comment explains why it is needed (enterSubplot does not trigger a notification), but it introduces cross-layer coupling between Command and Model. A cleaner long-term approach would be to have Context.enterSubplot() call notifyStateUpdate() itself, or add a Context.notifyCurrentState() method. For this PR it works, but worth a follow-up issue to move notification responsibility back to the model layer. Missing test coverage The PR checklist acknowledges no unit tests were added. The new commands (ExitBrailleAndSubplotCommand, updated MoveToTraceContextCommand) and the new DisplayService methods (notifyFocusChange, dismissModalScope) are testable without a screen reader. Consider adding:
Minor: role=application on textarea (src/ui/component/Braille.tsx) The inline comment is good. One thing to verify: role=application on a textarea is technically redundant since textarea already has an implicit widget role and NVDA/JAWS switch to forms mode on focus regardless. The actual fix may come from the focus sequencing changes rather than this attribute. Worth confirming whether removing it changes behavior. If it is genuinely needed, keep it; if not, removing it avoids potential accessibility regressions for users relying on virtual cursor navigation within the textarea. What is good
Overall the fix addresses a real screen reader compatibility issue. The redundant removeLast call and missing tests are the main things to address before merge. |
- Remove redundant removeLast call in dismissModalScope; simplify signature to only accept targetScope since the full stack is cleared - Move notifyStateUpdate() from MoveToTraceContextCommand into Context.enterSubplot() so the model layer owns observer notification Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code ReviewGood fix for the Escape-key-in-braille-mode issue. The three-step sequence in Correctness
The new line in Potential double-update when entering a subplot with braille enabled In Design
Reasonable trade-off for the fix. If this pattern grows, it might be worth exploring whether
The method clears the entire focus stack. That is correct for the escape-from-braille flow, but a future caller passing the wrong Missing TestsNo tests are added for:
The Minor
|
Revert notifyStateUpdate() from Context.enterSubplot() as it caused unwanted audio on subplot entry. Update braille service directly from MoveToTraceContextCommand instead — only braille needs refreshing. Add unit tests for ExitBrailleAndSubplotCommand, MoveToTraceContextCommand, and DisplayService (dismissModalScope, notifyFocusChange debounce/dispose). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code Review (Updated)The PR has been significantly improved since earlier reviews — tests are now included and the redundant What's well done
One remaining architectural concern
The inline comment explains the intent: avoid triggering AudioService (unwanted tone on entry) and other observers. That is a valid concern, but the mechanism crosses a layer boundary — a Command is directly invoking a Service's Two lower-risk alternatives worth considering:
Either way, a follow-up issue to track this debt would be appropriate. The current approach works; it is just worth keeping the coupling documented. Minor:
|
… updates Add a dedicated refreshDisplay() method on BrailleService to make the intent explicit when updating braille outside the observer chain. Add console.warn for the defensive state type guard after enterSubplot(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… updates Add a dedicated refreshDisplay() method on BrailleService to make the intent explicit when updating braille outside the observer chain. Add console.warn for the defensive state type guard after enterSubplot(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code ReviewThanks for this fix — the bug (Escape not working in braille mode) is a real accessibility gap and the overall approach is well-reasoned. Here are my findings: What's working well
Issues / Concerns1.
|
…ity/maidr into fix-multipanel_plot_escape
- Correct refreshDisplay JSDoc: other services are not triggered because the model's notifyStateUpdate() is not called, not because this method bypasses observers - Add architectural exception comment on MoveToTraceContextCommand documenting why it holds service references directly - Add test for state.type !== 'trace' branch (console.warn path) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…scape # Conflicts: # src/ui/component/Braille.tsx
Code ReviewPR: This is a solid accessibility fix with well-documented ordering logic for screen-reader compatibility. The core approach is sound. Below are observations and suggestions before merging. Positive Observations
IssuesCorrectnessC1. The doc says the method avoids triggering "other observers," but Suggested correction: describe it as bypassing C2. Verify no double-update path in After ArchitectureA1. Commands holding Service references — create a tracking issue The warning comment in source code is good, but without a GitHub issue on record the exception can silently spread to other commands. A2.
if (process.env.NODE_ENV !== 'production' && hotkeys.getScope() !== targetScope) {
console.warn('[DisplayService.dismissModalScope] targetScope does not match hotkeys scope');
}A3. Raw Per TypeScriptT1. Several mocks use TestsT2. No unit test for A single test confirming that AccessibilityAC1. Confirm A Summary
Recommendation: Approve with minor revisions. The fix is architecturally sound and the screen-reader safety rationale is well-justified. Please address C1 and T1 before merging, and verify AC1 empirically. The remaining items are acceptable as follow-up issues. |
Replace jest.fn(() => ...) as any with jest.fn<() => void>().mockImplementation(...) and use Record<string, unknown> for mock factory overrides to comply with the project's no-any policy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR Review: Escape key exits subplot when braille mode is activeGood fix overall - the root cause is clear, the solution is thoughtfully documented, and the test coverage is solid. Here are my observations: ArchitectureDeliberate MVVC exception in commands MoveToTraceContextCommand and ExitBrailleAndSubplotCommand now hold direct references to BrailleService and DisplayService. The architectural note is helpful. One alternative worth considering: add a method on Context (e.g. enterSubplotWithBraille()) that encapsulates the conditional refresh, keeping commands thin and services out of the command layer. If the team agrees the current approach is acceptable as a one-off, the inline justification comment is sufficient. refreshDisplay is a pass-through alias The method refreshDisplay(state) just calls this.update(state) with no additional behavior. It is fine as an intentional public API boundary, but a doc comment clarifying "called manually, not from the observer chain" would help future readers understand why it exists separately from the observer-based update method. Potential BugExitBrailleAndSubplotCommand executes unconditionally The command is only mapped in BRAILLE_KEYMAP so it should only fire in braille scope. However, the command body does not guard against braille mode being disabled. If scopes are ever entered or exited in an unexpected sequence, the deferred focus change event could fire unnecessarily. Low risk given current wiring, but worth a defensive guard or a comment explaining the assumed invariant. Code Style Issueconsole.warn in production code Per DEBUGGING.md, console logs should be removed before commit. In src/command/move.ts inside MoveToTraceContextCommand.execute(), there is a console.warn for the unexpected-state branch. Either remove it or replace with a project logger utility to avoid leaking internal state info in production. Accessibilityrole="application" on the braille textarea This is the correct technique to prevent NVDA/JAWS from intercepting Escape. The inline comment is clear and well-scoped. One suggestion: add a note referencing the specific screen reader versions validated against (e.g. NVDA 2024.x, JAWS 2024) so future maintainers know the compatibility baseline. Test CoverageThe unit tests in braille-escape.test.ts and display.test.ts are well-structured: call ordering, deferred timer behavior, and dispose-cancels-pending-timer are all covered. Minor gap: There are no direct tests for BrailleService.isEnabled or BrailleService.refreshDisplay. Since refreshDisplay just delegates to update, a smoke test verifying that delegation would help catch regressions if update is ever refactored. SummaryThe console.warn is the only item I would flag for cleanup before merge. The rest are suggestions:
|
Replace console.warn with a defensive comment explaining the invariant. Update test to verify refreshDisplay is skipped without asserting on console output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Code ReviewGood fix for a real accessibility gap. The PR is well-documented and includes meaningful tests. Here are my observations. What Works Well
Issues and Suggestions1. Missing test:
test('calls refreshDisplay when braille is enabled and state is a trace', () => {
const context = createMockContext();
const brailleService = createMockBrailleService(true);
const displayService = createMockDisplayService();
const command = new MoveToTraceContextCommand(context, brailleService, displayService);
command.execute();
expect(brailleService.refreshDisplay).toHaveBeenCalledWith(context.state);
});2.
3. Architectural deviation deserves a TODO or issue link The 4. Adding
This trade-off is worth documenting in the PR description or a follow-up issue. 5. All Summary
The missing |
|
I have addressed the comments by claude bot and tested the functionality of the fix locally with both JAWS and NVDA. @jooyoungseo @nk1408 I believe it should be ready to merge. |
|
Looking good to me and works as expected! |
|
🎉 This PR is included in version 3.59.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Pull Request
Description
Escape key now exits the currently opened subplot and focuses on the list of subplots in multipanel and facet plots even if braille mode is on.
Related Issues
Fixes #574
Changes Made
Earlier behavior: In multipanel and facet plots, the escape key was used to exit the currently opened subplot. This did not work with braille mode on. This PR fixes the issue: Escape exits the subplot and focuses on the subplot list even if braille mode is on. Further, it preserves the state of braille mode along with sonification and text I.E. If the user exits the subplot while braille mode is on and opens a new subplot, braille representation of the new subplot is displayed.
Screenshots (if applicable)
Checklist
ManualTestingProcess.md, and all tests related to this pull request pass.Additional Notes