UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

171 lines (146 loc) 5.97 kB
/* eslint-disable consistent-return, jsx-a11y/no-noninteractive-tabindex */ import React from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import warning from 'warning'; import ownerDocument from '../utils/ownerDocument'; import { useForkRef } from '../utils/reactHelpers'; function TrapFocus(props) { var children = props.children, _props$disableAutoFoc = props.disableAutoFocus, disableAutoFocus = _props$disableAutoFoc === void 0 ? false : _props$disableAutoFoc, _props$disableEnforce = props.disableEnforceFocus, disableEnforceFocus = _props$disableEnforce === void 0 ? false : _props$disableEnforce, _props$disableRestore = props.disableRestoreFocus, disableRestoreFocus = _props$disableRestore === void 0 ? false : _props$disableRestore, getDoc = props.getDoc, isEnabled = props.isEnabled, open = props.open; var ignoreNextEnforceFocus = React.useRef(); var sentinelStart = React.useRef(null); var sentinelEnd = React.useRef(null); var lastFocus = React.useRef(); var rootRef = React.useRef(null); // can be removed once we drop support for non ref forwarding class components var handleOwnRef = React.useCallback(function (instance) { // #StrictMode ready rootRef.current = ReactDOM.findDOMNode(instance); }, []); var handleRef = useForkRef(children.ref, handleOwnRef); // ⚠️ You may rely on React.useMemo as a performance optimization, not as a semantic guarantee. // https://reactjs.org/docs/hooks-reference.html#usememo React.useMemo(function () { if (!open) { return; } lastFocus.current = getDoc().activeElement; }, [open]); // eslint-disable-line react-hooks/exhaustive-deps React.useEffect(function () { if (!open) { return; } var doc = ownerDocument(rootRef.current); // We might render an empty child. if (!disableAutoFocus && rootRef.current && !rootRef.current.contains(doc.activeElement)) { if (!rootRef.current.hasAttribute('tabIndex')) { process.env.NODE_ENV !== "production" ? warning(false, ['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')) : void 0; rootRef.current.setAttribute('tabIndex', -1); } rootRef.current.focus(); } var enforceFocus = function enforceFocus() { if (disableEnforceFocus || !isEnabled() || ignoreNextEnforceFocus.current) { ignoreNextEnforceFocus.current = false; return; } if (rootRef.current && !rootRef.current.contains(doc.activeElement)) { rootRef.current.focus(); } }; var loopFocus = function loopFocus(event) { // 9 = Tab if (disableEnforceFocus || !isEnabled() || event.keyCode !== 9) { return; } // Make sure the next tab starts from the right place. if (doc.activeElement === rootRef.current) { // We need to ignore the next enforceFocus as // it will try to move the focus back to the rootRef element. ignoreNextEnforceFocus.current = true; if (event.shiftKey) { sentinelEnd.current.focus(); } else { sentinelStart.current.focus(); } } }; doc.addEventListener('focus', enforceFocus, true); doc.addEventListener('keydown', loopFocus, true); return function () { doc.removeEventListener('focus', enforceFocus, true); doc.removeEventListener('keydown', loopFocus, true); // restoreLastFocus() if (!disableRestoreFocus) { // In IE 11 it is possible for document.activeElement to be null resulting // in lastFocus.current being null. // Not all elements in IE 11 have a focus method. // Once IE 11 support is dropped the focus() call can be unconditional. if (lastFocus.current && lastFocus.current.focus) { lastFocus.current.focus(); } lastFocus.current = null; } }; }, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open]); return React.createElement(React.Fragment, null, React.createElement("div", { tabIndex: 0, ref: sentinelStart, "data-test": "sentinelStart" }), React.cloneElement(children, { ref: handleRef }), React.createElement("div", { tabIndex: 0, ref: sentinelEnd, "data-test": "sentinelEnd" })); } /** * @ignore - internal component. */ process.env.NODE_ENV !== "production" ? TrapFocus.propTypes = { /** * A single child content element. */ children: PropTypes.element.isRequired, /** * If `true`, the modal 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 modal children that have the `disableAutoFocus` prop. * * Generally this should never be set to `true` as it makes the modal less * accessible to assistive technologies, like screen readers. */ disableAutoFocus: PropTypes.bool, /** * If `true`, the modal will not prevent focus from leaving the modal while open. * * Generally this should never be set to `true` as it makes the modal less * accessible to assistive technologies, like screen readers. */ disableEnforceFocus: PropTypes.bool, /** * If `true`, the modal will not restore focus to previously focused element once * modal is hidden. */ disableRestoreFocus: PropTypes.bool, /** * Return the document to consider. * We use it to implement the restore focus between different browser documents. */ getDoc: PropTypes.func.isRequired, /** * Do we still want to enforce the focus? * This property helps nesting TrapFocus elements. */ isEnabled: PropTypes.func.isRequired, /** * If `true`, the modal is open. */ open: PropTypes.bool.isRequired } : void 0; export default TrapFocus;