[P2] autoFocus does nothing in always-mounted modal dialogs, so the confirmations open with the destructive Accept button focused and Save as opens with Cancel focused instead of the name field #113
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Priority: P2
Type: bug
Source: full codebase review requested 2026-10-31 (independently reproduced against
2cac7cf)Location:
src/App.tsx:213-232,src/App.tsx:3012-3018,src/App.tsx:3036-3045,src/App.tsx:3063-3073,src/planner/SavePlanDialog.tsx:50-56,src/planner/SavePlanDialog.tsx:109-112Problem and evidence
The Reset to Defaults, model-change and gas-replacement
<dialog>s are always mounted and start closed. Each is opened withshowModal()from an effect. Each one putsautoFocuson its Reject button. On the client, React does not write anautofocusattribute. It only callsfocus()once, when the element mounts, and at that point the dialog is closed (display:none), so the call does nothing. WhenshowModal()later runs the dialog focusing steps, it finds no[autofocus]element and focuses the first focusable descendant. In all three dialogs that is the Accept button, because it comes before Reject in the DOM. One Space or Enter press then confirms the destructive action. Space is the app's documented play/pause key, which makes an accidental press likely. SavePlanDialog has the same problem: PlanLibrary always mounts it, so itsautoFocusfile-name input is never focused, and focus lands on the Cancel button in the heading.I ran Playwright Chromium against the HEAD production build in /tmp/divesim-review-dist-SqbiNx on a temporary port. The server has since been stopped.
document.querySelectorAll('[autofocus]').lengthreturned 0.document.activeElementwasBUTTON "Accept · reset to defaults". I pressed Space. The dialog closed, dive time was 0:00 and units were back to imperial, so the reset ran.BUTTON "Accept · reset dive and switch model"focused.BUTTON "Cancel". Typing 'abc' left the Export file name as 'My dive.json'.showModal(), and the Accept button comes before the autoFocus Reject button in the DOM (App.tsx:3063-3073), so the same result follows.Suggested correction
In each open effect, call
showModal()and then focus the intended element through a ref. For example,if (open && !dialog.open) { dialog.showModal(); rejectRef.current?.focus(); }in the reset, model and gas effects. In SavePlanDialog, callinputRef.current?.focus()(optionally.select()) aftershowModal().Other options:
ref={el => el?.setAttribute('autofocus','')}, so the native dialog focusing steps pick it up.Optionally also apply the ref fix to ModelSelector's Close button for consistency.
Acceptance criteria
After
showModal(),document.activeElementshould be:Pressing Space or Enter right after opening a confirmation must leave the dive and settings unchanged: dive time and units are preserved and the dialog closes as rejected.
Regression test: a component or Playwright test that opens each dialog and asserts the focused element. For example, for Reset to Defaults: start a dive, click Reset to Defaults, press Space, then assert that dive time is not 0:00 and units are unchanged. pnpm verify passes.
Scope: maintenance of the educational simulator. Preserve historical R1 acceptance and operational-validation limits.