@material-ui/core
Version:
React components that implement Google's Material Design.
234 lines (200 loc) • 8.64 kB
JavaScript
/* eslint-disable @typescript-eslint/naming-convention, consistent-return, jsx-a11y/no-noninteractive-tabindex */
import * as React from 'react';
import PropTypes from 'prop-types';
import { exactProp, elementAcceptingRef } from '@material-ui/utils';
import ownerDocument from '../utils/ownerDocument';
import useForkRef from '../utils/useForkRef';
/**
* Utility component that locks focus inside the component.
*/
function Unstable_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 nodeToRestore = React.useRef();
var 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.
var activated = React.useRef(false);
var rootRef = React.useRef(null);
var handleRef = useForkRef(children.ref, rootRef);
var prevOpenRef = React.useRef();
React.useEffect(function () {
prevOpenRef.current = open;
}, [open]);
if (!prevOpenRef.current && open && typeof window !== 'undefined' && !disableAutoFocus) {
// WARNING: Potentially unsafe in concurrent mode.
// The way the read on `nodeToRestore` is setup could make this actually safe.
// Say we render `open={false}` -> `open={true}` but never commit.
// We have now written a state that wasn't committed. But no committed effect
// will read this wrong value. We only read from `nodeToRestore` in effects
// that were committed on `open={true}`
// WARNING: Prevents the instance from being garbage collected. Should only
// hold a weak ref.
nodeToRestore.current = getDoc().activeElement;
}
React.useEffect(function () {
// We might render an empty child.
if (!open || !rootRef.current) {
return;
}
activated.current = !disableAutoFocus;
var doc = ownerDocument(rootRef.current);
if (!rootRef.current.contains(doc.activeElement)) {
if (!rootRef.current.hasAttribute('tabIndex')) {
if (process.env.NODE_ENV !== 'production') {
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'));
}
rootRef.current.setAttribute('tabIndex', -1);
}
if (activated.current) {
rootRef.current.focus();
}
}
var contain = function contain(nativeEvent) {
if (!doc.hasFocus() || disableEnforceFocus || !isEnabled() || ignoreNextEnforceFocus.current) {
ignoreNextEnforceFocus.current = false;
return;
}
if (!activated.current) {
nodeToRestore.current = doc.activeElement;
}
if (!rootRef.current.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;
}
rootRef.current.focus();
} else {
activated.current = true;
}
};
var loopFocus = function loopFocus(nativeEvent) {
// 9 = Tab
if (disableEnforceFocus || !isEnabled() || nativeEvent.keyCode !== 9) {
return;
} // Make sure the next tab starts from the right place.
if (doc.activeElement === rootRef.current) {
// We need to ignore the next contain as
// it will try to move the focus back to the rootRef element.
ignoreNextEnforceFocus.current = true;
if (nativeEvent.shiftKey) {
sentinelEnd.current.focus();
} else {
sentinelStart.current.focus();
}
}
};
doc.addEventListener('focus', contain, true);
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.
var interval = setInterval(function () {
if (doc.activeElement.tagName === 'BODY') {
contain();
}
}, 50);
return function () {
clearInterval(interval);
doc.removeEventListener('focus', contain, true);
doc.removeEventListener('keydown', loopFocus, true); // restoreLastFocus()
if (!disableRestoreFocus) {
// In IE 11 it is possible for document.activeElement to be null resulting
// in nodeToRestore.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 (nodeToRestore.current && nodeToRestore.current.focus) {
nodeToRestore.current.focus();
}
nodeToRestore.current = null;
}
};
}, [disableAutoFocus, disableEnforceFocus, disableRestoreFocus, isEnabled, open]);
var onFocus = function onFocus(event) {
activated.current = true;
reactFocusEventTarget.current = event.target;
var childrenPropsHandler = children.props.onFocus;
if (childrenPropsHandler) {
childrenPropsHandler(event);
}
};
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
tabIndex: 0,
ref: sentinelStart,
"data-test": "sentinelStart"
}), /*#__PURE__*/React.cloneElement(children, {
ref: handleRef,
onFocus: onFocus
}), /*#__PURE__*/React.createElement("div", {
tabIndex: 0,
ref: sentinelEnd,
"data-test": "sentinelEnd"
}));
}
process.env.NODE_ENV !== "production" ? Unstable_TrapFocus.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: 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.
*/
disableAutoFocus: PropTypes.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.
*/
disableEnforceFocus: PropTypes.bool,
/**
* If `true`, the trap focus will not restore focus to previously focused element once
* trap focus 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 prop helps nesting TrapFocus elements.
*/
isEnabled: PropTypes.func.isRequired,
/**
* If `true`, focus will be locked.
*/
open: PropTypes.bool.isRequired
} : void 0;
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line
Unstable_TrapFocus['propTypes' + ''] = exactProp(Unstable_TrapFocus.propTypes);
}
export default Unstable_TrapFocus;