Skip to content

Commit aafb54a

Browse files
authored
[TrapFocus] Capture nodeToRestore via relatedTarget (#26696)
1 parent 9acf8be commit aafb54a

File tree

9 files changed

+42
-49
lines changed

9 files changed

+42
-49
lines changed

docs/pages/api-docs/unstable-trap-focus.json

-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@
55
"disableAutoFocus": { "type": { "name": "bool" } },
66
"disableEnforceFocus": { "type": { "name": "bool" } },
77
"disableRestoreFocus": { "type": { "name": "bool" } },
8-
"getDoc": {
9-
"type": { "name": "func" },
10-
"default": "function defaultGetDoc() {\n return document;\n}"
11-
},
128
"getTabbable": { "type": { "name": "func" } },
139
"isEnabled": {
1410
"type": { "name": "func" },

docs/translations/api-docs/unstable-trap-focus/unstable-trap-focus.json

-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"disableAutoFocus": "If <code>true</code>, the trap focus will not automatically shift focus to itself when it opens, and replace it to the last focused element when it closes. This also works correctly with any trap focus children that have the <code>disableAutoFocus</code> prop.<br>Generally this should never be set to <code>true</code> as it makes the trap focus less accessible to assistive technologies, like screen readers.",
66
"disableEnforceFocus": "If <code>true</code>, the trap focus will not prevent focus from leaving the trap focus while open.<br>Generally this should never be set to <code>true</code> as it makes the trap focus less accessible to assistive technologies, like screen readers.",
77
"disableRestoreFocus": "If <code>true</code>, the trap focus will not restore focus to previously focused element once trap focus is hidden.",
8-
"getDoc": "Return the document the trap focus is mounted into. Provide the prop if you need the restore focus to work between different documents.",
98
"getTabbable": "Returns an array of ordered tabbable nodes (i.e. in tab order) within the root. For instance, you can provide the &quot;tabbable&quot; npm dependency.<br><br><strong>Signature:</strong><br><code>function(root: HTMLElement) =&gt; void</code><br>",
109
"isEnabled": "This prop extends the <code>open</code> prop. It allows to toggle the open state without having to wait for a rerender when changing the <code>open</code> prop. This prop should be memoized. It can be used to support multiple trap focus mounted at the same time.",
1110
"open": "If <code>true</code>, focus is locked."

packages/material-ui-lab/src/internal/pickers/PickersPopper.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ const PickersPopper = (props: PickerPopperProps) => {
253253
disableAutoFocus
254254
disableEnforceFocus={role === 'tooltip'}
255255
isEnabled={() => true}
256-
getDoc={() => paperRef.current?.ownerDocument ?? document}
257256
{...TrapFocusProps}
258257
>
259258
<TransitionComponent {...TransitionProps}>

packages/material-ui-unstyled/src/ModalUnstyled/ModalUnstyled.js

-1
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@ const ModalUnstyled = React.forwardRef(function ModalUnstyled(props, ref) {
273273
disableEnforceFocus={disableEnforceFocus}
274274
disableAutoFocus={disableAutoFocus}
275275
disableRestoreFocus={disableRestoreFocus}
276-
getDoc={getDoc}
277276
isEnabled={isTopModal}
278277
open={open}
279278
>

packages/material-ui-unstyled/src/Unstable_TrapFocus/Unstable_TrapFocus.d.ts

-8
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@ export interface TrapFocusProps {
66
* If `true`, focus is locked.
77
*/
88
open: boolean;
9-
/**
10-
* Return the document the trap focus is mounted into.
11-
* Provide the prop if you need the restore focus to work between different documents.
12-
* @default function defaultGetDoc() {
13-
* return document;
14-
* }
15-
*/
16-
getDoc?: () => Document;
179
/**
1810
* Returns an array of ordered tabbable nodes (i.e. in tab order) within the root.
1911
* For instance, you can provide the "tabbable" npm dependency.

packages/material-ui-unstyled/src/Unstable_TrapFocus/Unstable_TrapFocus.js

+3-33
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,6 @@ function defaultGetTabbable(root) {
108108
.concat(regularTabNodes);
109109
}
110110

111-
function defaultGetDoc() {
112-
return document;
113-
}
114-
115111
function defaultIsEnabled() {
116112
return true;
117113
}
@@ -125,15 +121,14 @@ function Unstable_TrapFocus(props) {
125121
disableAutoFocus = false,
126122
disableEnforceFocus = false,
127123
disableRestoreFocus = false,
128-
getDoc = defaultGetDoc,
129124
getTabbable = defaultGetTabbable,
130125
isEnabled = defaultIsEnabled,
131126
open,
132127
} = props;
133128
const ignoreNextEnforceFocus = React.useRef();
134129
const sentinelStart = React.useRef(null);
135130
const sentinelEnd = React.useRef(null);
136-
const nodeToRestore = React.useRef();
131+
const nodeToRestore = React.useRef(null);
137132
const reactFocusEventTarget = React.useRef(null);
138133
// This variable is useful when disableAutoFocus is true.
139134
// It waits for the active element to move into the component to activate.
@@ -143,23 +138,6 @@ function Unstable_TrapFocus(props) {
143138
const handleRef = useForkRef(children.ref, rootRef);
144139
const lastKeydown = React.useRef(null);
145140

146-
const prevOpenRef = React.useRef();
147-
React.useEffect(() => {
148-
prevOpenRef.current = open;
149-
}, [open]);
150-
151-
if (!prevOpenRef.current && open && typeof window !== 'undefined' && !disableAutoFocus) {
152-
// WARNING: Potentially unsafe in concurrent mode.
153-
// The way the read on `nodeToRestore` is setup could make this actually safe.
154-
// Say we render `open={false}` -> `open={true}` but never commit.
155-
// We have now written a state that wasn't committed. But no committed effect
156-
// will read this wrong value. We only read from `nodeToRestore` in effects
157-
// that were committed on `open={true}`
158-
// WARNING: Prevents the instance from being garbage collected. Should only
159-
// hold a weak ref.
160-
nodeToRestore.current = getDoc().activeElement;
161-
}
162-
163141
React.useEffect(() => {
164142
// We might render an empty child.
165143
if (!open || !rootRef.current) {
@@ -325,7 +303,7 @@ function Unstable_TrapFocus(props) {
325303
}, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open, getTabbable]);
326304

327305
const onFocus = (event) => {
328-
if (!activated.current) {
306+
if (nodeToRestore.current === null) {
329307
nodeToRestore.current = event.relatedTarget;
330308
}
331309
activated.current = true;
@@ -338,7 +316,7 @@ function Unstable_TrapFocus(props) {
338316
};
339317

340318
const handleFocusSentinel = (event) => {
341-
if (!activated.current) {
319+
if (nodeToRestore.current === null) {
342320
nodeToRestore.current = event.relatedTarget;
343321
}
344322
activated.current = true;
@@ -391,14 +369,6 @@ Unstable_TrapFocus.propTypes /* remove-proptypes */ = {
391369
* @default false
392370
*/
393371
disableRestoreFocus: PropTypes.bool,
394-
/**
395-
* Return the document the trap focus is mounted into.
396-
* Provide the prop if you need the restore focus to work between different documents.
397-
* @default function defaultGetDoc() {
398-
* return document;
399-
* }
400-
*/
401-
getDoc: PropTypes.func,
402372
/**
403373
* Returns an array of ordered tabbable nodes (i.e. in tab order) within the root.
404374
* For instance, you can provide the "tabbable" npm dependency.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as React from 'react';
2+
import TrapFocus from '@material-ui/core/Unstable_TrapFocus';
3+
4+
export default function BaseTrapFocus() {
5+
const [open, close] = React.useReducer(() => false, true);
6+
7+
return (
8+
<React.Fragment>
9+
<button type="button" autoFocus data-testid="initial-focus">
10+
initial focus
11+
</button>
12+
<TrapFocus isEnabled={() => true} open={open} disableAutoFocus>
13+
<div data-testid="root">
14+
<div>Title</div>
15+
<button type="button" onClick={close}>
16+
close
17+
</button>
18+
<button type="button">noop</button>
19+
</div>
20+
</TrapFocus>
21+
</React.Fragment>
22+
);
23+
}

test/e2e/fixtures/Unstable_TrapFocus/OpenTrapFocus.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export default function BaseTrapFocus() {
77
<button type="button" autoFocus data-testid="initial-focus">
88
initial focus
99
</button>
10-
<TrapFocus getDoc={() => document} isEnabled={() => true} open>
10+
<TrapFocus isEnabled={() => true} open>
1111
<div tabIndex={-1} data-testid="root">
1212
<div>Title</div>
1313
<button type="button">x</button>

test/e2e/index.test.ts

+15
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,21 @@ describe('e2e', () => {
137137
await expect(screen.getByText('ok')).toHaveFocus();
138138
});
139139

140+
it('should loop the tab key after activation', async () => {
141+
await renderFixture('Unstable_TrapFocus/DefaultOpenLazyTrapFocus');
142+
143+
await expect(screen.getByTestId('initial-focus')).toHaveFocus();
144+
145+
await page.keyboard.press('Tab');
146+
await expect(screen.getByText('close')).toHaveFocus();
147+
await page.keyboard.press('Tab');
148+
await expect(screen.getByText('noop')).toHaveFocus();
149+
await page.keyboard.press('Tab');
150+
await expect(screen.getByText('close')).toHaveFocus();
151+
await page.keyboard.press('Enter');
152+
await expect(screen.getByTestId('initial-focus')).toHaveFocus();
153+
});
154+
140155
it('should focus on first focus element after last has received a tab click', async () => {
141156
await renderFixture('Unstable_TrapFocus/OpenTrapFocus');
142157

0 commit comments

Comments
 (0)