fix: let Escape close a dialog right after its form closed

This commit is contained in:
Linus Rath
2026-10-29 15:35:36 +01:00
parent 2dbe5d0244
commit d42fc053dd
2 changed files with 25 additions and 2 deletions
+20
View File
@@ -157,3 +157,23 @@ describe('useFocusTrap and the page behind', () => {
}
});
});
// Presses Escape in its layout effect: after the DOM of a render is in
// place, before that render's passive effects have run.
function EscapeOnCommit({ press }: { press: number }) {
React.useLayoutEffect(() => {
if (press) fireEvent.keyDown(document.body, { key: 'Escape' });
}, [press]);
return null;
}
describe('useFocusTrap handler timing', () => {
it('answers an Escape right after a render with that render\'s handler', () => {
const before = vi.fn();
const after = vi.fn();
const { rerender } = render(<><Dialog onEscape={before} /><EscapeOnCommit press={0} /></>);
rerender(<><Dialog onEscape={after} /><EscapeOnCommit press={1} /></>);
expect(after).toHaveBeenCalledTimes(1);
expect(before).not.toHaveBeenCalled();
});
});
+5 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useLayoutEffect, useRef } from 'react';
// The traps that are active, the one opened last at the end. Only that one
// answers Escape, so a dialog over a dialog closes alone.
@@ -24,9 +24,12 @@ export function useFocusTrap({
// one from here instead of starting over for it: each new start moved the
// focus back to the opener and then to the first element, out of the field
// the user was typing in whenever the page behind rendered again.
// Updated during the commit, not after it: an Escape pressed once the
// DOM has changed must reach the handler of that render, not the one
// before (a form that just closed would ask about its input again).
const onEscapeRef = useRef(onEscape);
const initialFocusRef = useRef(initialFocus);
useEffect(() => {
useLayoutEffect(() => {
onEscapeRef.current = onEscape;
initialFocusRef.current = initialFocus;
});