adobe / adobe/react-spectrum

FocusScope skips a Tab stop after an exiting sibling scope unmounts

Open Beginner friendly
#10,593 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
15.9k
Forks
1.6k
Avg merge
3d 9m
Merged PRs (30d)
59

Description

### 🐛 Bug Report

When one overlay opens another before it finishes closing, unmounting the first overlay can break keyboard navigation in the second. One Tab skips a focusable element. On a fresh opening, Shift+Tab can move focus outside the second overlay even though its `FocusScope` has `contain` enabled.

The two scopes are siblings under an outer `FocusScope` in the React component tree. The second scope stays mounted, and focus is already inside it when the first scope unmounts.

### 🤔 Expected Behavior?

Removing the closing overlay should not change keyboard navigation in the overlay that remains open. Tab should move to the next focusable element, and Shift+Tab from the first element should wrap to the last. Focus should stay within the containing scope.

### 😯 Current Behavior

After the closing overlay unmounts, the first Tab skips the next button. Testing Shift+Tab instead on a fresh opening moves focus to a button outside the remaining overlay.

### 💁 Possible Solution

Change `FocusScope` cleanup so that it resets `activeScope` only when the scope being removed is itself active. If the active scope is a descendant that remains mounted, preserve it while removing its ancestor and reparenting it.

In the cleanup condition, this means replacing `scopeRef === activeScope || isAncestorScope(scopeRef, activeScope)` with `scopeRef === activeScope`. The `isAncestorScope` helper remains in its other call sites.

The added forward and reverse Tab regression tests fail without this change and pass with it. The existing whole-subtree focus restoration test also passes unchanged.

### 🔦 Context

Selecting a menu item opens another popover. The menu briefly remains mounted while the new popover opens. Once the menu unmounts, the first Tab in the new popover skips a button.

### 🖥️ Steps to Reproduce

https://codesandbox.io/p/sandbox/xfgnvy

1. Open the standalone preview.
2. Focus **Open menu** and press Space. Wait for focus to reach **Choose date and time**.
3. Press Space again, then wait for the first overlay to fade out. Focus is now on **September**, the first enabled button in the second overlay.
4. Press Tab once. Expected: **2026**. Actual: **Next month**, skipping **2026**.
5. Reload and repeat steps 2–3. Press Shift+Tab instead. Expected: **Next month** (wrap to the last button). Actual: **Open menu**, outside the second overlay.

After the closing overlay unmounts, a single Tab skips a button in the overlay that remains open, even though its FocusScope still has contain enabled.

### Version

react-aria@3.52.1

### What browsers are you seeing the problem on?

Chrome

### If other, please specify.

_No response_

### What operating system are you using?

macOS 26.4.1, arm64

### 🧢 Your Company/Team

_No response_

### 🕷 Tracking Issue

N/A

Contributor guide

Open the contributing guide

Research direction

Start with the FocusScope cleanup condition and the other call sites of isAncestorScope; then run the forward- and reverse-Tab regression tests mentioned in the issue. Done means preserving an active descendant when an ancestor unmounts, with both regression tests passing and the existing whole-subtree focus restoration test unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
accessibility, frontend
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.