fix(focus-scope): defer MutationObserver focus recovery to prevent hijacking during Tab transitions#3870
Open
ralphholzmann wants to merge 1 commit into
Conversation
…jacking during Tab transitions When a React re-render removes and re-adds DOM nodes in response to a blur event (e.g. react-hook-form's onBlur validation), the MutationObserver fires while activeElement is temporarily document.body — the browser is mid-transition between the old focused element and the next tabbable target. The synchronous focus(container) call intercepts this transition and incorrectly moves focus to the FocusScope container. Deferring the check to requestAnimationFrame allows the browser to complete the focus transition. If activeElement is still document.body after the frame, the focused element was genuinely removed and we correctly recover by focusing the container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
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.
Summary
Fixes a bug where focus is incorrectly hijacked to the FocusScope container during normal Tab key navigation when a React re-render coincides with the focus transition.
Root cause: When tabbing away from an input that triggers a state update on blur (e.g.,
react-hook-formvalidation), React's commit phase can remove and re-add DOM nodes viacommitMutationEffects→commitDeletions. This fires theMutationObservercallback inhandleMutationswhiledocument.activeElementis temporarilydocument.body— the browser is mid-transition between the old focused element and the next tabbable target. The synchronousfocus(container)call intercepts this transition and moves focus to the FocusScope container (tabIndex: -1) instead of letting the browser complete the Tab to the next element.Fix: Defer the
document.activeElement === document.bodycheck torequestAnimationFrame. This allows the browser to complete the focus transition first:activeElementhas moved to the next tabbable element → no-op (correct behavior)activeElementis stilldocument.body→ the focused element was genuinely removed, so we correctly recover by focusing the containerReproduction
Dialogwithtrappedfocus scopereact-hook-formwithonBlurvalidation oronChangein a blur handler)Test plan
onBlurhandler triggers a React re-render — focus should still move to the next elementloop={true})🤖 Generated with Claude Code