UNPKG

@mui/core

Version:

Unstyled React components with which to implement custom design systems.

382 lines (313 loc) 13.8 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var React = _interopRequireWildcard(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _utils = require("@mui/utils"); var _jsxRuntime = require("react/jsx-runtime"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /* eslint-disable @typescript-eslint/naming-convention, consistent-return, jsx-a11y/no-noninteractive-tabindex */ // Inspired by https://github.com/focus-trap/tabbable const candidatesSelector = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])'].join(','); function getTabIndex(node) { const tabindexAttr = parseInt(node.getAttribute('tabindex'), 10); if (!Number.isNaN(tabindexAttr)) { return tabindexAttr; } // Browsers do not return `tabIndex` correctly for contentEditable nodes; // https://bugs.chromium.org/p/chromium/issues/detail?id=661108&q=contenteditable%20tabindex&can=2 // so if they don't have a tabindex attribute specifically set, assume it's 0. // in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default // `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM, // yet they are still part of the regular tab order; in FF, they get a default // `tabIndex` of 0; since Chrome still puts those elements in the regular tab // order, consider their tab index to be 0. if (node.contentEditable === 'true' || (node.nodeName === 'AUDIO' || node.nodeName === 'VIDEO' || node.nodeName === 'DETAILS') && node.getAttribute('tabindex') === null) { return 0; } return node.tabIndex; } function isNonTabbableRadio(node) { if (node.tagName !== 'INPUT' || node.type !== 'radio') { return false; } if (!node.name) { return false; } const getRadio = selector => node.ownerDocument.querySelector(`input[type="radio"]${selector}`); let roving = getRadio(`[name="${node.name}"]:checked`); if (!roving) { roving = getRadio(`[name="${node.name}"]`); } return roving !== node; } function isNodeMatchingSelectorFocusable(node) { if (node.disabled || node.tagName === 'INPUT' && node.type === 'hidden' || isNonTabbableRadio(node)) { return false; } return true; } function defaultGetTabbable(root) { const regularTabNodes = []; const orderedTabNodes = []; Array.from(root.querySelectorAll(candidatesSelector)).forEach((node, i) => { const nodeTabIndex = getTabIndex(node); if (nodeTabIndex === -1 || !isNodeMatchingSelectorFocusable(node)) { return; } if (nodeTabIndex === 0) { regularTabNodes.push(node); } else { orderedTabNodes.push({ documentOrder: i, tabIndex: nodeTabIndex, node }); } }); return orderedTabNodes.sort((a, b) => a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex).map(a => a.node).concat(regularTabNodes); } function defaultIsEnabled() { return true; } /** * Utility component that locks focus inside the component. */ function Unstable_TrapFocus(props) { const { children, disableAutoFocus = false, disableEnforceFocus = false, disableRestoreFocus = false, getTabbable = defaultGetTabbable, isEnabled = defaultIsEnabled, open } = props; const ignoreNextEnforceFocus = React.useRef(); const sentinelStart = React.useRef(null); const sentinelEnd = React.useRef(null); const nodeToRestore = React.useRef(null); const reactFocusEventTarget = React.useRef(null); // This variable is useful when disableAutoFocus is true. // It waits for the active element to move into the component to activate. const activated = React.useRef(false); const rootRef = React.useRef(null); const handleRef = (0, _utils.unstable_useForkRef)(children.ref, rootRef); const lastKeydown = React.useRef(null); React.useEffect(() => { // We might render an empty child. if (!open || !rootRef.current) { return; } activated.current = !disableAutoFocus; }, [disableAutoFocus, open]); React.useEffect(() => { // We might render an empty child. if (!open || !rootRef.current) { return; } const doc = (0, _utils.unstable_ownerDocument)(rootRef.current); if (!rootRef.current.contains(doc.activeElement)) { if (!rootRef.current.hasAttribute('tabIndex')) { if (process.env.NODE_ENV !== 'production') { console.error(['MUI: 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')); } rootRef.current.setAttribute('tabIndex', -1); } if (activated.current) { rootRef.current.focus(); } } return () => { // restoreLastFocus() if (!disableRestoreFocus) { // In IE11 it is possible for document.activeElement to be null resulting // in nodeToRestore.current being null. // Not all elements in IE11 have a focus method. // Once IE11 support is dropped the focus() call can be unconditional. if (nodeToRestore.current && nodeToRestore.current.focus) { ignoreNextEnforceFocus.current = true; nodeToRestore.current.focus(); } nodeToRestore.current = null; } }; // Missing `disableRestoreFocus` which is fine. // We don't support changing that prop on an open TrapFocus // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); React.useEffect(() => { // We might render an empty child. if (!open || !rootRef.current) { return; } const doc = (0, _utils.unstable_ownerDocument)(rootRef.current); const contain = nativeEvent => { const { current: rootElement } = rootRef; // Cleanup functions are executed lazily in React 17. // Contain can be called between the component being unmounted and its cleanup function being run. if (rootElement === null) { return; } if (!doc.hasFocus() || disableEnforceFocus || !isEnabled() || ignoreNextEnforceFocus.current) { ignoreNextEnforceFocus.current = false; return; } if (!rootElement.contains(doc.activeElement)) { // if the focus event is not coming from inside the children's react tree, reset the refs if (nativeEvent && reactFocusEventTarget.current !== nativeEvent.target || doc.activeElement !== reactFocusEventTarget.current) { reactFocusEventTarget.current = null; } else if (reactFocusEventTarget.current !== null) { return; } if (!activated.current) { return; } let tabbable = []; if (doc.activeElement === sentinelStart.current || doc.activeElement === sentinelEnd.current) { tabbable = getTabbable(rootRef.current); } if (tabbable.length > 0) { var _lastKeydown$current, _lastKeydown$current2; const isShiftTab = Boolean(((_lastKeydown$current = lastKeydown.current) == null ? void 0 : _lastKeydown$current.shiftKey) && ((_lastKeydown$current2 = lastKeydown.current) == null ? void 0 : _lastKeydown$current2.key) === 'Tab'); const focusNext = tabbable[0]; const focusPrevious = tabbable[tabbable.length - 1]; if (isShiftTab) { focusPrevious.focus(); } else { focusNext.focus(); } } else { rootElement.focus(); } } }; const loopFocus = nativeEvent => { lastKeydown.current = nativeEvent; if (disableEnforceFocus || !isEnabled() || nativeEvent.key !== 'Tab') { return; } // Make sure the next tab starts from the right place. // doc.activeElement referes to the origin. if (doc.activeElement === rootRef.current && nativeEvent.shiftKey) { // We need to ignore the next contain as // it will try to move the focus back to the rootRef element. ignoreNextEnforceFocus.current = true; sentinelEnd.current.focus(); } }; doc.addEventListener('focusin', contain); 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. // e.g. https://bugzilla.mozilla.org/show_bug.cgi?id=559561. // Instead, we can look if the active element was restored on the BODY element. // // The whatwg spec defines how the browser should behave but does not explicitly mention any events: // https://html.spec.whatwg.org/multipage/interaction.html#focus-fixup-rule. const interval = setInterval(() => { if (doc.activeElement.tagName === 'BODY') { contain(); } }, 50); return () => { clearInterval(interval); doc.removeEventListener('focusin', contain); doc.removeEventListener('keydown', loopFocus, true); }; }, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open, getTabbable]); const onFocus = event => { if (nodeToRestore.current === null) { nodeToRestore.current = event.relatedTarget; } activated.current = true; reactFocusEventTarget.current = event.target; const childrenPropsHandler = children.props.onFocus; if (childrenPropsHandler) { childrenPropsHandler(event); } }; const handleFocusSentinel = event => { if (nodeToRestore.current === null) { nodeToRestore.current = event.relatedTarget; } activated.current = true; }; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(React.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", { tabIndex: 0, onFocus: handleFocusSentinel, ref: sentinelStart, "data-test": "sentinelStart" }), /*#__PURE__*/React.cloneElement(children, { ref: handleRef, onFocus }), /*#__PURE__*/(0, _jsxRuntime.jsx)("div", { tabIndex: 0, onFocus: handleFocusSentinel, ref: sentinelEnd, "data-test": "sentinelEnd" })] }); } process.env.NODE_ENV !== "production" ? Unstable_TrapFocus.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the d.ts file and run "yarn proptypes" | // ---------------------------------------------------------------------- /** * A single child content element. */ children: _utils.elementAcceptingRef, /** * If `true`, 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 `disableAutoFocus` prop. * * Generally this should never be set to `true` as it makes the trap focus less * accessible to assistive technologies, like screen readers. * @default false */ disableAutoFocus: _propTypes.default.bool, /** * If `true`, the trap focus will not prevent focus from leaving the trap focus while open. * * Generally this should never be set to `true` as it makes the trap focus less * accessible to assistive technologies, like screen readers. * @default false */ disableEnforceFocus: _propTypes.default.bool, /** * If `true`, the trap focus will not restore focus to previously focused element once * trap focus is hidden. * @default false */ disableRestoreFocus: _propTypes.default.bool, /** * Returns an array of ordered tabbable nodes (i.e. in tab order) within the root. * For instance, you can provide the "tabbable" npm dependency. * @param {HTMLElement} root */ getTabbable: _propTypes.default.func, /** * This prop extends the `open` prop. * It allows to toggle the open state without having to wait for a rerender when changing the `open` prop. * This prop should be memoized. * It can be used to support multiple trap focus mounted at the same time. * @default function defaultIsEnabled() { * return true; * } */ isEnabled: _propTypes.default.func, /** * If `true`, focus is locked. */ open: _propTypes.default.bool.isRequired } : void 0; if (process.env.NODE_ENV !== 'production') { // eslint-disable-next-line Unstable_TrapFocus['propTypes' + ''] = (0, _utils.exactProp)(Unstable_TrapFocus.propTypes); } var _default = Unstable_TrapFocus; exports.default = _default;