diff --git a/packages/react-aria/src/focus/FocusScope.tsx b/packages/react-aria/src/focus/FocusScope.tsx
index 41114eeadcf..18379b34e3e 100644
--- a/packages/react-aria/src/focus/FocusScope.tsx
+++ b/packages/react-aria/src/focus/FocusScope.tsx
@@ -180,10 +180,8 @@ export function FocusScope(props: FocusScopeProps): JSX.Element {
// Scope may have been re-parented.
let parentScope = focusScopeTree.getTreeNode(scopeRef)?.parent?.scopeRef ?? null;
- if (
- (scopeRef === activeScope || isAncestorScope(scopeRef, activeScope)) &&
- (!parentScope || focusScopeTree.getTreeNode(parentScope))
- ) {
+ // A descendant may remain active while an ancestor scope is unmounted.
+ if (scopeRef === activeScope && (!parentScope || focusScopeTree.getTreeNode(parentScope))) {
activeScope = parentScope;
}
focusScopeTree.removeTreeNode(scopeRef);
diff --git a/packages/react-aria/test/focus/FocusScope.browser.test.tsx b/packages/react-aria/test/focus/FocusScope.browser.test.tsx
new file mode 100644
index 00000000000..d57e7480805
--- /dev/null
+++ b/packages/react-aria/test/focus/FocusScope.browser.test.tsx
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2026 Adobe. All rights reserved.
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License. You may obtain a copy
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
+ * OF ANY KIND, either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+
+import {expect, it} from 'vitest';
+import {FocusScope} from '../../src/focus/FocusScope';
+import React, {useState} from 'react';
+import ReactDOM from 'react-dom';
+import {render} from 'vitest-browser-react';
+import {userEvent} from 'vitest/browser';
+
+function Portal({children}: {children: React.ReactNode}) {
+ return ReactDOM.createPortal(children, document.body);
+}
+
+function SiblingScopes() {
+ let [showFirst, setShowFirst] = useState(true);
+ let [showSecond, setShowSecond] = useState(false);
+
+ let openSecond = () => {
+ setShowSecond(true);
+ setTimeout(() => setShowFirst(false), 50);
+ };
+
+ return (
+
+
+ {showFirst && (
+
+
+
+
+
+ )}
+ {showSecond && (
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+it.each([
+ {direction: 'forward', shift: false, expected: '2026'},
+ {direction: 'reverse', shift: true, expected: 'Next month'}
+])(
+ 'keeps $direction Tab navigation in a sibling scope after unmount',
+ async ({shift, expected}) => {
+ let {getByRole} = await render();
+
+ await userEvent.click(getByRole('button', {name: 'Choose date and time'}));
+ await expect
+ .element(getByRole('button', {name: 'Choose date and time'}))
+ .not.toBeInTheDocument();
+ await expect.element(getByRole('button', {name: 'September'})).toHaveFocus();
+
+ await userEvent.tab({shift});
+ await expect.element(getByRole('button', {name: expected})).toHaveFocus();
+ }
+);
diff --git a/packages/react-aria/test/focus/FocusScope.test.js b/packages/react-aria/test/focus/FocusScope.test.js
index e5ed0e1d3c4..098adbee3d6 100644
--- a/packages/react-aria/test/focus/FocusScope.test.js
+++ b/packages/react-aria/test/focus/FocusScope.test.js
@@ -1927,6 +1927,52 @@ describe('FocusScope', function () {
expect(document.activeElement).toBe(child3);
});
+ it('should keep focus contained in a sibling scope when the previous scope unmounts', async function () {
+ function Portal({children}) {
+ return ReactDOM.createPortal(children, document.body);
+ }
+
+ function Test({showFirst, showSecond}) {
+ return (
+
+
+ {showFirst && (
+
+
+
+
+
+ )}
+ {showSecond && (
+
+
+
+
+
+
+
+ )}
+
+ );
+ }
+
+ let {getByTestId, rerender} = render();
+ expect(document.activeElement).toBe(getByTestId('first'));
+
+ rerender();
+ expect(document.activeElement).toBe(getByTestId('second1'));
+
+ rerender();
+ expect(document.activeElement).toBe(getByTestId('second1'));
+
+ expect(focusScopeTree.size).toBe(3);
+ await user.tab();
+ expect(document.activeElement).toBe(getByTestId('second2'));
+ act(() => getByTestId('second1').focus());
+ await user.tab({shift: true});
+ expect(document.activeElement).toBe(getByTestId('second3'));
+ });
+
it('should restore to the correct scope on unmount', async function () {
function Test({show1, show2, show3}) {
return (