Skip to content

Commit 26f9102

Browse files
authored
Revert "chore: Use inert in ariaHideOutside (#8317)" (#8371)
1 parent d312e0a commit 26f9102

File tree

15 files changed

+224
-88
lines changed

15 files changed

+224
-88
lines changed

packages/@react-aria/dnd/src/DragManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ class DragSession {
408408
this.dragTarget.element,
409409
...validDropItems.flatMap(item => item.activateButtonRef?.current ? [item.element, item.activateButtonRef?.current] : [item.element]),
410410
...visibleDropTargets.flatMap(target => target.activateButtonRef?.current ? [target.element, target.activateButtonRef?.current] : [target.element])
411-
], {shouldUseInert: true});
411+
]);
412412

413413
this.mutationObserver.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['aria-hidden']});
414414
}

packages/@react-aria/focus/src/FocusScope.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '@react-aria/utils';
2525
import {FocusableElement, RefObject} from '@react-types/shared';
2626
import {focusSafely, getInteractionModality} from '@react-aria/interactions';
27+
import {isElementVisible} from './isElementVisible';
2728
import React, {JSX, ReactNode, useContext, useEffect, useMemo, useRef} from 'react';
2829

2930
export interface FocusScopeProps {
@@ -757,6 +758,7 @@ export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions
757758
}
758759

759760
if (filter(node as Element)
761+
&& isElementVisible(node as Element)
760762
&& (!scope || isElementInScope(node as Element, scope))
761763
&& (!opts?.accept || opts.accept(node as Element))
762764
) {

packages/@react-aria/utils/src/isElementVisible.ts renamed to packages/@react-aria/focus/src/isElementVisible.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {getOwnerWindow} from './domHelpers';
14-
15-
const supportsCheckVisibility = typeof Element !== 'undefined' && 'checkVisibility' in Element.prototype;
13+
import {getOwnerWindow} from '@react-aria/utils';
1614

1715
function isStyleVisible(element: Element) {
1816
const windowObject = getOwnerWindow(element);
@@ -62,10 +60,6 @@ function isAttributeVisible(element: Element, childElement?: Element) {
6260
* @param element - Element to evaluate for display or visibility.
6361
*/
6462
export function isElementVisible(element: Element, childElement?: Element): boolean {
65-
if (supportsCheckVisibility) {
66-
return element.checkVisibility();
67-
}
68-
6963
return (
7064
element.nodeName !== '#comment' &&
7165
isStyleVisible(element) &&

packages/@react-aria/overlays/src/ariaHideOutside.ts

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,6 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import {getOwnerWindow} from '@react-aria/utils';
14-
15-
const supportsInert = typeof HTMLElement !== 'undefined' && 'inert' in HTMLElement.prototype;
16-
17-
interface AriaHideOutsideOptions {
18-
root?: Element,
19-
shouldUseInert?: boolean
20-
}
21-
2213
// Keeps a ref count of all hidden elements. Added to when hiding an element, and
2314
// subtracted from when showing it again. When it reaches zero, aria-hidden is removed.
2415
let refCountMap = new WeakMap<Element, number>();
@@ -38,28 +29,10 @@ let observerStack: Array<ObserverWrapper> = [];
3829
* @param root - Nothing will be hidden above this element.
3930
* @returns - A function to restore all hidden elements.
4031
*/
41-
export function ariaHideOutside(targets: Element[], options?: AriaHideOutsideOptions | Element) {
42-
let windowObj = getOwnerWindow(targets?.[0]);
43-
let opts = options instanceof windowObj.Element ? {root: options} : options;
44-
let root = opts?.root ?? document.body;
45-
let shouldUseInert = opts?.shouldUseInert && supportsInert;
32+
export function ariaHideOutside(targets: Element[], root = document.body) {
4633
let visibleNodes = new Set<Element>(targets);
4734
let hiddenNodes = new Set<Element>();
4835

49-
let getHidden = (element: Element) => {
50-
return shouldUseInert && element instanceof windowObj.HTMLElement ? element.inert : element.getAttribute('aria-hidden') === 'true';
51-
};
52-
53-
let setHidden = (element: Element, hidden: boolean) => {
54-
if (shouldUseInert && element instanceof windowObj.HTMLElement) {
55-
element.inert = hidden;
56-
} else if (hidden) {
57-
element.setAttribute('aria-hidden', 'true');
58-
} else {
59-
element.removeAttribute('aria-hidden');
60-
}
61-
};
62-
6336
let walk = (root: Element) => {
6437
// Keep live announcer and top layer elements (e.g. toasts) visible.
6538
for (let element of root.querySelectorAll('[data-live-announcer], [data-react-aria-top-layer]')) {
@@ -114,12 +87,12 @@ export function ariaHideOutside(targets: Element[], options?: AriaHideOutsideOpt
11487

11588
// If already aria-hidden, and the ref count is zero, then this element
11689
// was already hidden and there's nothing for us to do.
117-
if (getHidden(node) && refCount === 0) {
90+
if (node.getAttribute('aria-hidden') === 'true' && refCount === 0) {
11891
return;
11992
}
12093

12194
if (refCount === 0) {
122-
setHidden(node, true);
95+
node.setAttribute('aria-hidden', 'true');
12396
}
12497

12598
hiddenNodes.add(node);
@@ -188,7 +161,7 @@ export function ariaHideOutside(targets: Element[], options?: AriaHideOutsideOpt
188161
continue;
189162
}
190163
if (count === 1) {
191-
setHidden(node, false);
164+
node.removeAttribute('aria-hidden');
192165
refCountMap.delete(node);
193166
} else {
194167
refCountMap.set(node, count - 1);

packages/@react-aria/overlays/src/useModalOverlay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export function useModalOverlay(props: AriaModalOverlayProps, state: OverlayTrig
5858

5959
useEffect(() => {
6060
if (state.isOpen && ref.current) {
61-
return ariaHideOutside([ref.current], {shouldUseInert: true});
61+
return ariaHideOutside([ref.current]);
6262
}
6363
}, [state.isOpen, ref]);
6464

packages/@react-aria/overlays/src/usePopover.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@
1313
import {ariaHideOutside, keepVisible} from './ariaHideOutside';
1414
import {AriaPositionProps, useOverlayPosition} from './useOverlayPosition';
1515
import {DOMAttributes, RefObject} from '@react-types/shared';
16-
import {mergeProps} from '@react-aria/utils';
16+
import {mergeProps, useLayoutEffect} from '@react-aria/utils';
1717
import {OverlayTriggerState} from '@react-stately/overlays';
1818
import {PlacementAxis} from '@react-types/overlays';
19-
import {useEffect} from 'react';
2019
import {useOverlay} from './useOverlay';
2120
import {usePreventScroll} from './usePreventScroll';
2221

@@ -114,12 +113,12 @@ export function usePopover(props: AriaPopoverProps, state: OverlayTriggerState):
114113
isDisabled: isNonModal || !state.isOpen
115114
});
116115

117-
useEffect(() => {
116+
useLayoutEffect(() => {
118117
if (state.isOpen && popoverRef.current) {
119118
if (isNonModal) {
120119
return keepVisible(groupRef?.current ?? popoverRef.current);
121120
} else {
122-
return ariaHideOutside([groupRef?.current ?? popoverRef.current], {shouldUseInert: true});
121+
return ariaHideOutside([groupRef?.current ?? popoverRef.current]);
123122
}
124123
}
125124
}, [isNonModal, state.isOpen, popoverRef, groupRef]);
Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,3 @@
1-
/*
2-
* Copyright 2025 Adobe. All rights reserved.
3-
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4-
* you may not use this file except in compliance with the License. You may obtain a copy
5-
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6-
*
7-
* Unless required by applicable law or agreed to in writing, software distributed under
8-
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9-
* OF ANY KIND, either express or implied. See the License for the specific language
10-
* governing permissions and limitations under the License.
11-
*/
12-
13-
import {isElementVisible} from './isElementVisible';
14-
151
const focusableElements = [
162
'input:not([disabled]):not([type=hidden])',
173
'select:not([disabled])',
@@ -34,22 +20,9 @@ focusableElements.push('[tabindex]:not([tabindex="-1"]):not([disabled])');
3420
const TABBABLE_ELEMENT_SELECTOR = focusableElements.join(':not([hidden]):not([tabindex="-1"]),');
3521

3622
export function isFocusable(element: Element): boolean {
37-
return element.matches(FOCUSABLE_ELEMENT_SELECTOR) && isElementVisible(element) && !isInert(element);
23+
return element.matches(FOCUSABLE_ELEMENT_SELECTOR);
3824
}
3925

4026
export function isTabbable(element: Element): boolean {
41-
return element.matches(TABBABLE_ELEMENT_SELECTOR) && isElementVisible(element) && !isInert(element);
42-
}
43-
44-
function isInert(element: Element): boolean {
45-
let node: Element | null = element;
46-
while (node != null) {
47-
if (node instanceof node.ownerDocument.defaultView!.HTMLElement && node.inert) {
48-
return true;
49-
}
50-
51-
node = node.parentElement;
52-
}
53-
54-
return false;
27+
return element.matches(TABBABLE_ELEMENT_SELECTOR);
5528
}

packages/@react-spectrum/dialog/test/DialogTrigger.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {ActionButton, Button} from '@react-spectrum/button';
1515
import {ButtonGroup} from '@react-spectrum/buttongroup';
1616
import {Content} from '@react-spectrum/view';
1717
import {Dialog, DialogTrigger} from '../';
18+
import {Heading} from '@react-spectrum/text';
1819
import {Item, Menu, MenuTrigger} from '@react-spectrum/menu';
1920
import {Provider} from '@react-spectrum/provider';
2021
import React from 'react';
@@ -976,6 +977,55 @@ describe('DialogTrigger', function () {
976977
expect(document.activeElement).toBe(innerInput);
977978
});
978979

980+
it('will not lose focus to body', async () => {
981+
let {getByRole, getByTestId} = render(
982+
<Provider theme={theme}>
983+
<DialogTrigger type="popover">
984+
<ActionButton>Trigger</ActionButton>
985+
<Dialog>
986+
<Heading>The Heading</Heading>
987+
<Content>
988+
<MenuTrigger>
989+
<ActionButton data-testid="innerButton">Test</ActionButton>
990+
<Menu autoFocus="first">
991+
<Item>Item 1</Item>
992+
<Item>Item 2</Item>
993+
<Item>Item 3</Item>
994+
</Menu>
995+
</MenuTrigger>
996+
</Content>
997+
</Dialog>
998+
</DialogTrigger>
999+
</Provider>
1000+
);
1001+
let button = getByRole('button');
1002+
await user.click(button);
1003+
1004+
act(() => {
1005+
jest.runAllTimers();
1006+
});
1007+
1008+
let outerDialog = getByRole('dialog');
1009+
1010+
await waitFor(() => {
1011+
expect(outerDialog).toBeVisible();
1012+
}); // wait for animation
1013+
let innerButton = getByTestId('innerButton');
1014+
await user.tab();
1015+
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
1016+
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
1017+
1018+
act(() => {
1019+
jest.runAllTimers();
1020+
});
1021+
await user.tab();
1022+
act(() => {
1023+
jest.runAllTimers();
1024+
});
1025+
1026+
expect(document.activeElement).toBe(innerButton);
1027+
});
1028+
9791029
describe('portalContainer', () => {
9801030
function InfoDialog(props) {
9811031
let {container} = props;

packages/@react-spectrum/menu/src/MenuTrigger.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,7 @@ export const MenuTrigger = forwardRef(function MenuTrigger(props: SpectrumMenuTr
103103
scrollRef={menuRef}
104104
placement={initialPlacement}
105105
hideArrow
106-
shouldFlip={shouldFlip}
107-
shouldContainFocus>
106+
shouldFlip={shouldFlip}>
108107
{menu}
109108
</Popover>
110109
);

packages/@react-spectrum/menu/test/MenuTrigger.test.js

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ describe('MenuTrigger', function () {
809809
});
810810
});
811811

812-
it('does not close if menu is tabbed away from', async function () {
812+
it('closes if menu is tabbed away from', async function () {
813813
let tree = render(
814814
<Provider theme={theme}>
815815
<MenuTrigger>
@@ -835,8 +835,8 @@ describe('MenuTrigger', function () {
835835
await user.tab();
836836
act(() => {jest.runAllTimers();});
837837
act(() => {jest.runAllTimers();});
838-
expect(menu).toBeInTheDocument();
839-
expect(document.activeElement).toBe(menuTester.options()[0]);
838+
expect(menu).not.toBeInTheDocument();
839+
expect(document.activeElement).toBe(menuTester.trigger);
840840
});
841841
});
842842

@@ -930,6 +930,22 @@ AriaMenuTests({
930930
multipleSelection: () => render(
931931
<SelectionStatic selectionMode="multiple" />
932932
),
933+
siblingFocusableElement: () => render(
934+
<Provider theme={theme}>
935+
<input aria-label="before" />
936+
<MenuTrigger>
937+
<Button variant="primary">
938+
{triggerText}
939+
</Button>
940+
<Menu>
941+
<Item id="1">One</Item>
942+
<Item id="2">Two</Item>
943+
<Item id="3">Three</Item>
944+
</Menu>
945+
</MenuTrigger>
946+
<input aria-label="after" />
947+
</Provider>
948+
),
933949
multipleMenus: () => render(
934950
<Provider theme={theme}>
935951
<MenuTrigger>
@@ -1066,6 +1082,24 @@ AriaMenuTests({
10661082
multipleSelection: () => render(
10671083
<SelectionStatic selectionMode="multiple" />
10681084
),
1085+
siblingFocusableElement: () => render(
1086+
<Provider theme={theme}>
1087+
<input aria-label="before" />
1088+
<MenuTrigger>
1089+
<Button variant="primary">
1090+
{triggerText}
1091+
</Button>
1092+
<Menu items={ariaWithSection}>
1093+
{item => (
1094+
<Section key={item.name} items={item.children} title={item.name}>
1095+
{item => <Item key={item.name} childItems={item.children}>{item.name}</Item>}
1096+
</Section>
1097+
)}
1098+
</Menu>
1099+
</MenuTrigger>
1100+
<input aria-label="after" />
1101+
</Provider>
1102+
),
10691103
multipleMenus: () => render(
10701104
<Provider theme={theme}>
10711105
<MenuTrigger>

0 commit comments

Comments
 (0)