Let a dialog choose where initial focus lands - #116
Conversation
Initial focus only exists in a DOM: which element ends up under the user's keyboard cannot be observed from server-rendered markup. The specs that cover it therefore render into jsdom with the real PrimeReact components. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRf73wBo2QQQ2FRoVLqgJu
Dialog focused its confirm button on open with no way to say otherwise. That also arms it: a browser fires click from the keydown of Enter, so a key still held from the control that opened the dialog, or the ordinary habit of pressing Enter twice, confirms it immediately. A dialog that collects input is protected by isValid; a dialog that needs no input is not, which is backwards when the action is irreversible. The only escape was replacing the footer with a custom ReactNode, which also removes the close (X), stops Escape closing the dialog and leaves onConfirm/onCancel/onClose uncalled — so it was never a workaround. initialFocus changes focus and nothing else. Confirm stays the default so no existing dialog changes; Cancel focuses the dismissing button; Content focuses the dialog's own title so nothing is armed. Cancel degrades to Content where there is no dismissing button, because a modal that leaves focus on document.body strands keyboard and screen-reader users outside the content that just interrupted them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRf73wBo2QQQ2FRoVLqgJu
A command whose form has required fields is protected from a held Enter for free, because isCommandFormValid keeps confirm disabled until something is filled in. A command that takes no input has no such gate, so its confirm button is armed the instant the dialog appears — and those are exactly the delete-this-permanently commands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRf73wBo2QQQ2FRoVLqgJu
A busy indicator has no buttons, so there was nothing for the browser to focus and focus stayed on document.body — outside the modal that blocks it, which leaves a keyboard or screen-reader user with nothing to work with and nothing announced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRf73wBo2QQQ2FRoVLqgJu
Rendered into jsdom with the real PrimeReact Dialog and Button, because the thing under test is which element ends up under the user's keyboard. Covers the default being unchanged for every button set, the new prop for each target and each degradation, the keyboard path from the incident that prompted this — an Enter repeating onto a dialog that just mounted must not confirm it — CommandDialog forwarding the choice while still running its command, and the three behaviors a custom footer takes away that initialFocus keeps: the close (X), Escape, and the confirm callback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRf73wBo2QQQ2FRoVLqgJu
Also spells out what a custom buttons node costs, since the docs previously presented it as a plain styling choice. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XRf73wBo2QQQ2FRoVLqgJu
|
Reviewer context, kept out of the body so it stays out of the release notes. The trigger was a one-keystroke destruction path found in a consuming application: a native Replacing the footer was not a usable workaround:
The default is unchanged and byte-identical, argued rather than assumed: the framework cannot know which dialogs are destructive. Verification: 30 new tests (suite 188 → 218), 14 mutations, all red. Specs render into jsdom with the real PrimeReact Also flagged, not fixed: |
Summary
A destructive dialog could not stop its OK button taking focus on open without discarding the footer's behavior.
Added
initialFocusonDialog, forwarded byCommandDialog, choosing where focus lands when the dialog opens:DialogInitialFocus.Confirm(the default, unchanged),Cancel, orContent.Canceldegrades toContentwhere no dismissing button exists.Fixed
buttons={null}left focus ondocument.body, outside the modal.BusyIndicatorDialogshipped in that state; focus now moves to the dialog's title.