Skip to content

fix: preserve active FocusScope when sibling unmounts - #10596

Open
dvd233 wants to merge 1 commit into
adobe:mainfrom
dvd233:fix/focusscope-sibling-unmount-10593
Open

fix: preserve active FocusScope when sibling unmounts#10596
dvd233 wants to merge 1 commit into
adobe:mainfrom
dvd233:fix/focusscope-sibling-unmount-10593

Conversation

@dvd233

@dvd233 dvd233 commented Sep 10, 2026

Copy link
Copy Markdown

Closes #10593

Summary

When one portaled overlay opens another before the first finishes closing, the active FocusScope tree can temporarily reparent the new scope beneath the old one. The old cleanup treated an active descendant as a reason to reset activeScope, even when that descendant remained mounted. This left the surviving overlay without the correct containment owner, so keyboard navigation could skip a control or escape the overlay.

This change resets activeScope only when the scope being removed is itself active. Removing an ancestor now preserves a still-mounted active descendant while removeTreeNode reparents it. The existing nested whole-subtree unmount behavior remains covered and unchanged.

Regression coverage includes:

  • a focused unit test for sibling portaled scopes and forward/reverse Tab navigation;
  • a real-browser test matching the delayed sibling-overlay unmount sequence in the report, run in Chromium, Firefox, and WebKit.

✅ Pull Request Checklist:

  • Included link to corresponding React Spectrum GitHub Issue.
  • Added/updated unit and browser tests. No Storybook change is needed because this is non-visual focus-management behavior.
  • Filled out test instructions.
  • Reviewed existing documentation; no update is needed because this fixes containment without changing the API or documented contract.
  • Looked at the ARIA Authoring Practices dialog pattern, which requires Tab and Shift+Tab to remain within a modal dialog's tab sequence.
  • I understand every change in this PR and can explain why it's there.
  • This was AI-assisted. I followed the repository's AI contribution guidance and pointed the assistant at AGENTS.md, CLAUDE.md, and the relevant files under docs/contributing/.

📝 Test Instructions:

  1. Run yarn jest packages/react-aria/test/focus/FocusScope.test.js --runInBand.
  2. Run yarn vitest run --config=vitest.browser.config.ts packages/react-aria/test/focus/FocusScope.browser.test.tsx.
  3. In the browser regression, activate Choose date and time, wait for that first scope to unmount, and verify:
    • Tab moves from September to 2026.
    • Shift+Tab moves from September to Next month, not Outside.

Additional local validation:

  • yarn test:ssr: 60 suites / 74 tests passed.
  • yarn test: 373 suites and 7,989 tests passed; four unrelated Windows/locale/codemod suites failed (locale fixture path separator mismatch, empty generated locale sets, and a codemod package-manager e2e expectation).
  • yarn test:browser: the target test passed in Chromium, Firefox, and WebKit; the full run later ended on an unrelated Firefox browser-session connection timeout while running ListBox.browser.test.tsx.
  • yarn lint: type-check, oxlint, package lint, and Yarn constraints passed. The repository-wide format check reports checkout-wide CRLF differences on Windows; yarn format:check passes for all three changed files.

🧢 Your Project:

Open-source contribution by dvd233; no company project.

@snowystinger snowystinger left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This left the surviving overlay without the correct containment owner, so keyboard navigation could skip a control or escape the overlay

Can you explain this in a little more detail? why would this result in the skip or escape? what was the flow of logic that the incorrect containment owner resulted in?

rerender(<Test showSecond />);
expect(document.activeElement).toBe(getByTestId('second1'));

expect(focusScopeTree.size).toBe(3);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we asserting this? what does it tell us?

@nwidynski nwidynski Sep 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It tells us we are looking at slop. This test already passes on main without changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FocusScope skips a Tab stop after an exiting sibling scope unmounts

3 participants