UNPKG

7.37 kBJavaScriptView Raw
1/* eslint-disable consistent-return, jsx-a11y/no-noninteractive-tabindex, camelcase */
2import * as React from 'react';
3import * as ReactDOM from 'react-dom';
4import PropTypes from 'prop-types';
5import ownerDocument from '../utils/ownerDocument';
6import useForkRef from '../utils/useForkRef';
7import { exactProp } from '@material-ui/utils';
8/**
9 * Utility component that locks focus inside the component.
10 */
11
12function Unstable_TrapFocus(props) {
13 const {
14 children,
15 disableAutoFocus = false,
16 disableEnforceFocus = false,
17 disableRestoreFocus = false,
18 getDoc,
19 isEnabled,
20 open
21 } = props;
22 const ignoreNextEnforceFocus = React.useRef();
23 const sentinelStart = React.useRef(null);
24 const sentinelEnd = React.useRef(null);
25 const nodeToRestore = React.useRef();
26 const rootRef = React.useRef(null); // can be removed once we drop support for non ref forwarding class components
27
28 const handleOwnRef = React.useCallback(instance => {
29 // #StrictMode ready
30 rootRef.current = ReactDOM.findDOMNode(instance);
31 }, []);
32 const handleRef = useForkRef(children.ref, handleOwnRef);
33 const prevOpenRef = React.useRef();
34 React.useEffect(() => {
35 prevOpenRef.current = open;
36 }, [open]);
37
38 if (!prevOpenRef.current && open && typeof window !== 'undefined') {
39 // WARNING: Potentially unsafe in concurrent mode.
40 // The way the read on `nodeToRestore` is setup could make this actually safe.
41 // Say we render `open={false}` -> `open={true}` but never commit.
42 // We have now written a state that wasn't committed. But no committed effect
43 // will read this wrong value. We only read from `nodeToRestore` in effects
44 // that were committed on `open={true}`
45 // WARNING: Prevents the instance from being garbage collected. Should only
46 // hold a weak ref.
47 nodeToRestore.current = getDoc().activeElement;
48 }
49
50 React.useEffect(() => {
51 if (!open) {
52 return;
53 }
54
55 const doc = ownerDocument(rootRef.current); // We might render an empty child.
56
57 if (!disableAutoFocus && rootRef.current && !rootRef.current.contains(doc.activeElement)) {
58 if (!rootRef.current.hasAttribute('tabIndex')) {
59 if (process.env.NODE_ENV !== 'production') {
60 console.error(['Material-UI: The modal content node does not accept focus.', 'For the benefit of assistive technologies, ' + 'the tabIndex of the node is being set to "-1".'].join('\n'));
61 }
62
63 rootRef.current.setAttribute('tabIndex', -1);
64 }
65
66 rootRef.current.focus();
67 }
68
69 const contain = () => {
70 const {
71 current: rootElement
72 } = rootRef; // Cleanup functions are executed lazily in React 17.
73 // Contain can be called between the component being unmounted and its cleanup function being run.
74
75 if (rootElement === null) {
76 return;
77 }
78
79 if (!doc.hasFocus() || disableEnforceFocus || !isEnabled() || ignoreNextEnforceFocus.current) {
80 ignoreNextEnforceFocus.current = false;
81 return;
82 }
83
84 if (rootRef.current && !rootRef.current.contains(doc.activeElement)) {
85 rootRef.current.focus();
86 }
87 };
88
89 const loopFocus = event => {
90 // 9 = Tab
91 if (disableEnforceFocus || !isEnabled() || event.keyCode !== 9) {
92 return;
93 } // Make sure the next tab starts from the right place.
94
95
96 if (doc.activeElement === rootRef.current) {
97 // We need to ignore the next contain as
98 // it will try to move the focus back to the rootRef element.
99 ignoreNextEnforceFocus.current = true;
100
101 if (event.shiftKey) {
102 sentinelEnd.current.focus();
103 } else {
104 sentinelStart.current.focus();
105 }
106 }
107 };
108
109 doc.addEventListener('focus', contain, true);
110 doc.addEventListener('keydown', loopFocus, true); // With Edge, Safari and Firefox, no focus related events are fired when the focused area stops being a focused area
111 // e.g. https://bugzilla.mozilla.org/show_bug.cgi?id=559561.
112 //
113 // The whatwg spec defines how the browser should behave but does not explicitly mention any events:
114 // https://html.spec.whatwg.org/multipage/interaction.html#focus-fixup-rule.
115
116 const interval = setInterval(() => {
117 contain();
118 }, 50);
119 return () => {
120 clearInterval(interval);
121 doc.removeEventListener('focus', contain, true);
122 doc.removeEventListener('keydown', loopFocus, true); // restoreLastFocus()
123
124 if (!disableRestoreFocus) {
125 // In IE 11 it is possible for document.activeElement to be null resulting
126 // in nodeToRestore.current being null.
127 // Not all elements in IE 11 have a focus method.
128 // Once IE 11 support is dropped the focus() call can be unconditional.
129 if (nodeToRestore.current && nodeToRestore.current.focus) {
130 nodeToRestore.current.focus();
131 }
132
133 nodeToRestore.current = null;
134 }
135 };
136 }, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open]);
137 return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
138 tabIndex: 0,
139 ref: sentinelStart,
140 "data-test": "sentinelStart"
141 }), /*#__PURE__*/React.cloneElement(children, {
142 ref: handleRef
143 }), /*#__PURE__*/React.createElement("div", {
144 tabIndex: 0,
145 ref: sentinelEnd,
146 "data-test": "sentinelEnd"
147 }));
148}
149
150process.env.NODE_ENV !== "production" ? Unstable_TrapFocus.propTypes = {
151 // ----------------------------- Warning --------------------------------
152 // | These PropTypes are generated from the TypeScript type definitions |
153 // | To update them edit the d.ts file and run "yarn proptypes" |
154 // ----------------------------------------------------------------------
155
156 /**
157 * A single child content element.
158 */
159 children: PropTypes.node,
160
161 /**
162 * If `true`, the trap focus will not automatically shift focus to itself when it opens, and
163 * replace it to the last focused element when it closes.
164 * This also works correctly with any trap focus children that have the `disableAutoFocus` prop.
165 *
166 * Generally this should never be set to `true` as it makes the trap focus less
167 * accessible to assistive technologies, like screen readers.
168 */
169 disableAutoFocus: PropTypes.bool,
170
171 /**
172 * If `true`, the trap focus will not prevent focus from leaving the trap focus while open.
173 *
174 * Generally this should never be set to `true` as it makes the trap focus less
175 * accessible to assistive technologies, like screen readers.
176 */
177 disableEnforceFocus: PropTypes.bool,
178
179 /**
180 * If `true`, the trap focus will not restore focus to previously focused element once
181 * trap focus is hidden.
182 */
183 disableRestoreFocus: PropTypes.bool,
184
185 /**
186 * Return the document to consider.
187 * We use it to implement the restore focus between different browser documents.
188 */
189 getDoc: PropTypes.func.isRequired,
190
191 /**
192 * Do we still want to enforce the focus?
193 * This prop helps nesting TrapFocus elements.
194 */
195 isEnabled: PropTypes.func.isRequired,
196
197 /**
198 * If `true`, focus will be locked.
199 */
200 open: PropTypes.bool.isRequired
201} : void 0;
202
203if (process.env.NODE_ENV !== 'production') {
204 // eslint-disable-next-line
205 Unstable_TrapFocus['propTypes' + ''] = exactProp(Unstable_TrapFocus.propTypes);
206}
207
208export default Unstable_TrapFocus;
\No newline at end of file