Modal Popover: 当恢复框与惰性清理同时出现时,将放弃恢复焦点。

作者: alexandru-bereghici-to创建于 2026年9月16日更新于 2026年9月17日

Feature Request / Bug Report A modal Popover can close without restoring focus: FocusScope restores from a requestAnimationFrame scheduled in a layout-effect cleanup, while usePopover takes the inert off the rest of the page in a passive-effect cleanup. React runs layout-effect cleanups during the commit and flushes passive-effect cleanups afterwards, so when the animation frame arrives before that flush, restoreFocusToElement calls focus() on a trigger that is still inert. Per the HTML focusing steps — "If new focus target is a focusable area and its DOM anchor is inert, then return" (https://html.spec.whatwg.org/multipage/interaction.html#focusing-steps) — the call does nothing and reports nothing. Nothing retries, so focus stays on <body>. It fails when the frame is fast, not when it is slow: on a loaded machine the frame lands 0.5–1.5 ms after the unmount instead of the 5–7 ms that lets the passive flush go first. ### The code path (main @ 4693fcc844a341107e7dd26fe456edb1e269e44c) - packages/react-aria/src/overlays/usePopover.ts#L132-L141 — a passive useEffect calls ariaHideOutside([...], {shouldUseInert: true}) for a modal popover. - packages/react-aria/src/overlays/ariaHideOutside.ts#L64-L66setHidden sets element.inert = true on every node outside the popover, the trigger included; only that effect's cleanup (#L274-L286) clears it. - packages/react-aria/src/focus/FocusScope.tsx#L760-L800useRestoreFocus's useLayoutEffect cleanup schedules the requestAnimationFrame that calls restoreFocusToElement. Nothing orders the two, and focusElement does not check that the focus landed. ## Expected Behavior Closing a modal popover returns focus to the trigger. ## Current Behavior document.activeElement is <body>, and stays there. Instrumented output from a failing run, recorded by patching HTMLElement.prototype.focus to log its stack and, when the call did not move document.activeElement, the element's connectivity, computed style and every ancestor carrying inert, aria-hidden, hidden, …

内容来源: adobe/react-spectrum