fix(ui): a modal owns the keyboard, not just the pointer (#631)#665
Merged
Conversation
push_modal made a dialog modal for the pointer only. Keyboard focus never consulted _ui.modal_stack, so a user could Tab to a widget BEHIND an open dialog and activate it with Enter/Space — an "Are you sure? [Cancel]" dialog could fire the destructive action it exists to guard against. Two halves, one root cause (focus ignored the modal stack): 1. dispatch scoped the mouse to the top modal but returned keys with the real root ABOVE the rebind. 2. the focus list was only ever built from the real root, so a dialog's own children were never focusable and the background's always were. Fix — focus respects the modal stack: - dispatch: move the modal rebind ABOVE the key early-return, so handle_key (and thus _focus_next/_build_focus_list) scopes to the top modal exactly as hit-testing does. Escape still pops the modal (handle_key checks the stack directly, so it survives the rebind). - push_modal: rebuild the focus list from the dialog and clear the focus pointers, so Tab cycles inside the modal, the dialog's own children (the file_dialog text_input/item_list from #575) are immediately focusable, and a stray Enter before any Tab cannot reach a background widget. - pop_modal: rebuild focus from the newly-exposed context — the next modal down, or edit_root when the stack empties (null-guarded for headless use). Pre-existing, not introduced by #575, but #575 made it reachable a second way (dialogs can now host focusable children a user would expect to Tab between). Tests: 7 asserts in test_ui.eigs (gfx-stubbed, keyboard-only so no gfx dep) — Tab stays in the dialog, Enter fires the dialog child not the background button, Tab cycles between the dialog's children, and pop_modal returns focus to the background. 5 of the 7 fail on the pre-fix toolkit (the other two are weaker guards the stronger ones subsume). Suite 3085/3085 release, 3089/3089 ASan+UBSan (detect_leaks=1, tally 0).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #631
push_modalmade a dialog modal for the pointer only. Keyboard focus never consulted_ui.modal_stack, so a user could Tab to a widget behind an open dialog and activate it with Enter/Space.The concrete failure: an "Are you sure? [Cancel]" dialog is up, the user presses Tab+Enter, and the destructive action behind the dialog fires — the opposite of what a confirmation dialog is for.
Two halves, one root cause
Focus ignored the modal stack:
dispatchscoped the mouse to the top modal but returned keys with the real root above the rebind.Fix — focus respects the modal stack
dispatch: move the modal rebind above the key early-return, sohandle_key(→_focus_next→_build_focus_list) scopes to the top modal exactly as hit-testing does. Escape still pops the modal —handle_keychecks the stack directly, so it survives the rebind.push_modal: rebuild the focus list from the dialog and clear the focus pointers, so Tab cycles inside the modal, the dialog's own children (thefile_dialogtext_input/item_listfrom lib/ui: no file/path picker dialog, and the dialog widget cannot host child widgets #575) are immediately focusable, and a stray Enter before any Tab can't reach a background widget.pop_modal: rebuild focus from the newly-exposed context — the next modal down, oredit_rootwhen the stack empties (null-guarded for headless use).Pre-existing (v0.32.0's
dispatchhad the same shape), but #575 made it reachable a second way — dialogs can now host focusable children a user would reasonably expect to Tab between.Verification
The issue's reproduction, after the fix:
7 regression asserts in
test_ui.eigs(gfx-stubbed, keyboard-only so no gfx dependency): Tab stays in the dialog, Enter fires the dialog child rather than the background button, Tab cycles between the dialog's own children, andpop_modalreturns focus to the background. 5 of the 7 fail on the pre-fix toolkit (Tab... got M631_BEHIND,Enter... fired the button behind, got 1); the other two are weaker guards the stronger ones subsume.Gates
detect_leaks=13089/3089, leak tally 0🤖 Generated with Claude Code