UNPKG

7.13 kBJavaScriptView Raw
1/* eslint-disable consistent-return, jsx-a11y/no-noninteractive-tabindex */
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';
7/**
8 * @ignore - internal component.
9 */
10
11function TrapFocus(props) {
12 var children = props.children,
13 _props$disableAutoFoc = props.disableAutoFocus,
14 disableAutoFocus = _props$disableAutoFoc === void 0 ? false : _props$disableAutoFoc,
15 _props$disableEnforce = props.disableEnforceFocus,
16 disableEnforceFocus = _props$disableEnforce === void 0 ? false : _props$disableEnforce,
17 _props$disableRestore = props.disableRestoreFocus,
18 disableRestoreFocus = _props$disableRestore === void 0 ? false : _props$disableRestore,
19 getDoc = props.getDoc,
20 isEnabled = props.isEnabled,
21 open = props.open;
22 var ignoreNextEnforceFocus = React.useRef();
23 var sentinelStart = React.useRef(null);
24 var sentinelEnd = React.useRef(null);
25 var nodeToRestore = React.useRef();
26 var rootRef = React.useRef(null); // can be removed once we drop support for non ref forwarding class components
27
28 var handleOwnRef = React.useCallback(function (instance) {
29 // #StrictMode ready
30 rootRef.current = ReactDOM.findDOMNode(instance);
31 }, []);
32 var handleRef = useForkRef(children.ref, handleOwnRef); // ⚠️ You may rely on React.useMemo as a performance optimization, not as a semantic guarantee.
33 // https://reactjs.org/docs/hooks-reference.html#usememo
34
35 React.useMemo(function () {
36 if (!open || typeof window === 'undefined') {
37 return;
38 }
39
40 nodeToRestore.current = getDoc().activeElement;
41 }, [open]); // eslint-disable-line react-hooks/exhaustive-deps
42
43 React.useEffect(function () {
44 if (!open) {
45 return;
46 }
47
48 var doc = ownerDocument(rootRef.current); // We might render an empty child.
49
50 if (!disableAutoFocus && rootRef.current && !rootRef.current.contains(doc.activeElement)) {
51 if (!rootRef.current.hasAttribute('tabIndex')) {
52 if (process.env.NODE_ENV !== 'production') {
53 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'));
54 }
55
56 rootRef.current.setAttribute('tabIndex', -1);
57 }
58
59 rootRef.current.focus();
60 }
61
62 var contain = function contain() {
63 if (disableEnforceFocus || !isEnabled() || ignoreNextEnforceFocus.current) {
64 ignoreNextEnforceFocus.current = false;
65 return;
66 }
67
68 if (rootRef.current && !rootRef.current.contains(doc.activeElement)) {
69 rootRef.current.focus();
70 }
71 };
72
73 var loopFocus = function loopFocus(event) {
74 // 9 = Tab
75 if (disableEnforceFocus || !isEnabled() || event.keyCode !== 9) {
76 return;
77 } // Make sure the next tab starts from the right place.
78
79
80 if (doc.activeElement === rootRef.current) {
81 // We need to ignore the next contain as
82 // it will try to move the focus back to the rootRef element.
83 ignoreNextEnforceFocus.current = true;
84
85 if (event.shiftKey) {
86 sentinelEnd.current.focus();
87 } else {
88 sentinelStart.current.focus();
89 }
90 }
91 };
92
93 doc.addEventListener('focus', contain, true);
94 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
95 // e.g. https://bugzilla.mozilla.org/show_bug.cgi?id=559561.
96 //
97 // The whatwg spec defines how the browser should behave but does not explicitly mention any events:
98 // https://html.spec.whatwg.org/multipage/interaction.html#focus-fixup-rule.
99
100 var interval = setInterval(function () {
101 contain();
102 }, 50);
103 return function () {
104 clearInterval(interval);
105 doc.removeEventListener('focus', contain, true);
106 doc.removeEventListener('keydown', loopFocus, true); // restoreLastFocus()
107
108 if (!disableRestoreFocus) {
109 // In IE 11 it is possible for document.activeElement to be null resulting
110 // in nodeToRestore.current being null.
111 // Not all elements in IE 11 have a focus method.
112 // Once IE 11 support is dropped the focus() call can be unconditional.
113 if (nodeToRestore.current && nodeToRestore.current.focus) {
114 nodeToRestore.current.focus();
115 }
116
117 nodeToRestore.current = null;
118 }
119 };
120 }, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open]);
121 return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
122 tabIndex: 0,
123 ref: sentinelStart,
124 "data-test": "sentinelStart"
125 }), React.cloneElement(children, {
126 ref: handleRef
127 }), /*#__PURE__*/React.createElement("div", {
128 tabIndex: 0,
129 ref: sentinelEnd,
130 "data-test": "sentinelEnd"
131 }));
132}
133
134process.env.NODE_ENV !== "production" ? TrapFocus.propTypes = {
135 /**
136 * A single child content element.
137 */
138 children: PropTypes.element.isRequired,
139
140 /**
141 * If `true`, the modal will not automatically shift focus to itself when it opens, and
142 * replace it to the last focused element when it closes.
143 * This also works correctly with any modal children that have the `disableAutoFocus` prop.
144 *
145 * Generally this should never be set to `true` as it makes the modal less
146 * accessible to assistive technologies, like screen readers.
147 */
148 disableAutoFocus: PropTypes.bool,
149
150 /**
151 * If `true`, the modal will not prevent focus from leaving the modal while open.
152 *
153 * Generally this should never be set to `true` as it makes the modal less
154 * accessible to assistive technologies, like screen readers.
155 */
156 disableEnforceFocus: PropTypes.bool,
157
158 /**
159 * If `true`, the modal will not restore focus to previously focused element once
160 * modal is hidden.
161 */
162 disableRestoreFocus: PropTypes.bool,
163
164 /**
165 * Return the document to consider.
166 * We use it to implement the restore focus between different browser documents.
167 */
168 getDoc: PropTypes.func.isRequired,
169
170 /**
171 * Do we still want to enforce the focus?
172 * This prop helps nesting TrapFocus elements.
173 */
174 isEnabled: PropTypes.func.isRequired,
175
176 /**
177 * If `true`, the modal is open.
178 */
179 open: PropTypes.bool.isRequired
180} : void 0;
181/*
182
183In the future, we should be able to replace TrapFocus with:
184https://github.com/facebook/react/blob/master/packages/react-events/docs/FocusScope.md
185
186```jsx
187import FocusScope from 'react-dom/FocusScope';
188
189function TrapFocus(props) {
190 const {
191 children
192 disableAutoFocus = false,
193 disableEnforceFocus = false,
194 disableRestoreFocus = false,
195 open,
196 } = props;
197
198 if (!open) {
199 return children;
200 }
201
202 return (
203 <FocusScope
204 autoFocus={!disableAutoFocus}
205 contain={!disableEnforceFocus}
206 restoreFocus={!disableRestoreFocus}
207 >
208 {children}
209 </FocusScope>
210 );
211}
212```
213
214*/
215
216export default TrapFocus;
\No newline at end of file