Skip to content

fix: preserve active FocusScope when sibling unmounts - #10596

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

dvd233 wants to merge 3 commits 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 an exiting portaled overlay opens a sibling overlay, removing the old scope can discard the containment owner of the surviving scope. The independent reverse-Tab regression reproduces focus moving to Outside after the closing scope stops containing focus and unmounts.

The cleanup resets the active scope only when the scope being removed is itself active. A still-mounted active descendant is preserved while tree removal reparents it; whole-subtree unmount behavior stays covered.

The unit regressions now use separate fresh fixtures for forward and reverse navigation, remove the non-behavioral tree-size assertion, and release containment on the exiting scope. Against the source without the production change, forward navigation passes but Shift+Tab escapes in Jest and Chromium, Firefox, and WebKit. With the fix, both directions pass. This minimal repro establishes reverse containment loss; it does not independently reproduce the reported forward skipping.

✅ 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. Automated review was performed; this does not attest to a separate human review.
  • 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.

Current local validation after merging main 4cea3b10 without rewriting history:

  • Full Jest: 379 suites / 8,074 tests passed; four suites / 16 tests failed in the existing NumberField, NumberParser, codemod CLI and LocalesResolver cases; 16 skipped. The FocusScope suite passes, including the independent forward/reverse fixtures.
  • Full browser run with one worker: 417 passed, 6 failed, 69 skipped. All six FocusScope cases pass across Chromium, Firefox and WebKit. The failures are two TokenField word-delete cases (Chromium/Firefox) and four current-main Chat scroll-button cases (Chromium/WebKit); all six also reproduce with pure main production files in the ListBox verification checkout.
  • Full SSR: 60 suites / 74 tests passed, with BROWSERSLIST_IGNORE_OLD_DATA unset. Main includes upstream fix: browser tests failing due to browsers list data old #10688's warning-matcher correction; this PR adds no suppression or duplicate fix.
  • Full lint: type-check, oxlint, package lint and constraints passed; format-check reports 6,515 files in the Windows checkout. All three contribution files pass the repository formatter, oxlint and the diff check.

Commands: yarn test --maxWorkers=2, yarn test:ssr, yarn lint, and yarn exec vitest run --config=vitest.browser.config.ts --maxWorkers=1 using existing browser binaries. No unrelated assertions or rules were disabled. Chromatic is reserved for maintainers and was not run. Screen readers, mobile devices, themes and zoom were not tested locally; this does not establish complete accessibility conformance.

🧢 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.

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.

Thanks for checking that, I had a feeling that was the case but hadn't pulled it down yet. I appreciate the help, hopefully they'll look back over the PR and get it fixed up.

I'm not actually sure there is a bug yet, I'm unclear why they are using FocusScope directly, and not using Modal. #10593 (comment)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

You're right that the previous unit test did not establish the regression independently: it checked Shift+Tab only after forward Tab and also asserted the tree size. I removed that assertion and gave each direction a fresh fixture. The exiting sibling now has contain={false} while it remains mounted, matching the reporter's clarification and the exiting-overlay behavior you pointed out.

I checked both variants against the source without the production change. Forward Tab passes, but the independent reverse case moves focus to Outside in Jest and Chromium, Firefox, and WebKit. The existing fix makes both directions pass (63 unit tests and 6 browser cases). I narrowed the PR summary accordingly: this repro demonstrates reverse containment loss, not an independent reproduction of the reported forward skipping.

I also reran the full suites and recorded their local failures in the PR body. The TokenField shortcut and stale-Browserslist SSR failures persist without the production change; I have not claimed a full-suite all-green result.

@dvd233 dvd233 closed this Sep 13, 2026
@dvd233 dvd233 reopened this Sep 13, 2026
@snowystinger snowystinger added the waiting Waiting on Issue Author label Sep 15, 2026
@dvd233

dvd233 commented Oct 1, 2026

Copy link
Copy Markdown
Author

The updated unit, browser, lint, and type jobs passed. All four SSR jobs fail for the same reason: the pinned Browserslist/caniuse-lite data is reported as seven months old, and the test setup converts that console warning into a thrown error. For example, https://circleci.com/gh/adobe/react-spectrum/700986 reports 60 failed suites and 60 failed / 14 passed tests; the same warning appears in jobs 700984, 700991, and 700978.

I reproduced the same warning failure in a local Flex SSR case with the production change removed, and restored the source afterwards. I have left the warning checks and test thresholds intact. This appears to need a shared browser-data dependency refresh rather than another FocusScope change; could a maintainer confirm the preferred way to handle that dependency update?

@dvd233

dvd233 commented Oct 2, 2026

Copy link
Copy Markdown
Author

Merged main (4cea3b10) into this branch without rewriting history. The existing FocusScope correction and independent forward/reverse fixtures are unchanged. Main includes #10688, so full SSR now passes all 60 suites / 74 tests with the Browserslist warning left enabled.

The full Jest run passes the FocusScope suite, and all six FocusScope browser cases pass in Chromium, Firefox and WebKit. Full Jest reports 8,074 passed / 16 existing Windows failures / 16 skipped; the complete browser suite reports 417 passed / 6 current-main TokenField/Chat failures / 69 skipped. Type-check, oxlint, package checks and constraints pass; full checkout formatting remains a local failure. The PR test plan records these current results without expanding the earlier reproduction claim: the independent baseline failure is reverse containment loss, not reported forward skipping.

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

Labels

waiting Waiting on Issue Author

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