UNPKG

@material-ui/core

Version:

React components that implement Google's Material Design.

258 lines (204 loc) 7.63 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); var _react = _interopRequireDefault(require("react")); var _propTypes = _interopRequireDefault(require("prop-types")); var _reactDom = _interopRequireDefault(require("react-dom")); var _ownerDocument = _interopRequireDefault(require("../utils/ownerDocument")); var _List = _interopRequireDefault(require("../List")); var _getScrollbarSize = _interopRequireDefault(require("../utils/getScrollbarSize")); var _reactHelpers = require("../utils/reactHelpers"); function nextItem(list, item, disableListWrap) { if (list === item) { return list.firstChild; } if (item && item.nextElementSibling) { return item.nextElementSibling; } return disableListWrap ? null : list.firstChild; } function previousItem(list, item, disableListWrap) { if (list === item) { return disableListWrap ? list.firstChild : list.lastChild; } if (item && item.previousElementSibling) { return item.previousElementSibling; } return disableListWrap ? null : list.lastChild; } function textCriteriaMatches(nextFocus, textCriteria) { if (textCriteria === undefined) { return true; } var text = nextFocus.innerText; if (text === undefined) { // jsdom doesn't support innerText text = nextFocus.textContent; } if (text === undefined) { return false; } text = text.trim().toLowerCase(); if (text.length === 0) { return false; } if (textCriteria.repeating) { return text[0] === textCriteria.keys[0]; } return text.indexOf(textCriteria.keys.join('')) === 0; } function moveFocus(list, currentFocus, disableListWrap, traversalFunction, textCriteria) { var wrappedOnce = false; var nextFocus = traversalFunction(list, currentFocus, currentFocus ? disableListWrap : false); while (nextFocus) { // Prevent infinite loop. if (nextFocus === list.firstChild) { if (wrappedOnce) { return false; } wrappedOnce = true; } // Move to the next element. if (!nextFocus.hasAttribute('tabindex') || nextFocus.disabled || nextFocus.getAttribute('aria-disabled') === 'true' || !textCriteriaMatches(nextFocus, textCriteria)) { nextFocus = traversalFunction(list, nextFocus, disableListWrap); } else { nextFocus.focus(); return true; } } return false; } var useEnhancedEffect = typeof window === 'undefined' ? _react.default.useEffect : _react.default.useLayoutEffect; var MenuList = _react.default.forwardRef(function MenuList(props, ref) { var actions = props.actions, _props$autoFocus = props.autoFocus, autoFocus = _props$autoFocus === void 0 ? false : _props$autoFocus, className = props.className, onKeyDown = props.onKeyDown, _props$disableListWra = props.disableListWrap, disableListWrap = _props$disableListWra === void 0 ? false : _props$disableListWra, other = (0, _objectWithoutProperties2.default)(props, ["actions", "autoFocus", "className", "onKeyDown", "disableListWrap"]); var listRef = _react.default.useRef(null); var textCriteriaRef = _react.default.useRef({ keys: [], repeating: true, previousKeyMatched: true, lastTime: null }); useEnhancedEffect(function () { if (autoFocus) { listRef.current.focus(); } }, [autoFocus]); _react.default.useImperativeHandle(actions, function () { return { adjustStyleForScrollbar: function adjustStyleForScrollbar(containerElement, theme) { // Let's ignore that piece of logic if users are already overriding the width // of the menu. var noExplicitWidth = !listRef.current.style.width; if (containerElement.clientHeight < listRef.current.clientHeight && noExplicitWidth) { var scrollbarSize = "".concat((0, _getScrollbarSize.default)(true), "px"); listRef.current.style[theme.direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = scrollbarSize; listRef.current.style.width = "calc(100% + ".concat(scrollbarSize, ")"); } return listRef.current; } }; }, []); var handleKeyDown = function handleKeyDown(event) { var list = listRef.current; var key = event.key; /** * @type {Element} - will always be defined since we are in a keydown handler * attached to an element. A keydown event is either dispatched to the activeElement * or document.body or document.documentElement. Only the first case will * trigger this specific handler. */ var currentFocus = (0, _ownerDocument.default)(list).activeElement; if (key === 'ArrowDown') { event.preventDefault(); moveFocus(list, currentFocus, disableListWrap, nextItem); } else if (key === 'ArrowUp') { event.preventDefault(); moveFocus(list, currentFocus, disableListWrap, previousItem); } else if (key === 'Home') { event.preventDefault(); moveFocus(list, null, disableListWrap, nextItem); } else if (key === 'End') { event.preventDefault(); moveFocus(list, null, disableListWrap, previousItem); } else if (key.length === 1) { var criteria = textCriteriaRef.current; var lowerKey = key.toLowerCase(); var currTime = performance.now(); if (criteria.keys.length > 0) { // Reset if (currTime - criteria.lastTime > 500) { criteria.keys = []; criteria.repeating = true; criteria.previousKeyMatched = true; } else if (criteria.repeating && lowerKey !== criteria.keys[0]) { criteria.repeating = false; } } criteria.lastTime = currTime; criteria.keys.push(lowerKey); var keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria); if (criteria.previousKeyMatched && (keepFocusOnCurrent || moveFocus(list, currentFocus, false, nextItem, criteria))) { event.preventDefault(); } else { criteria.previousKeyMatched = false; } } if (onKeyDown) { onKeyDown(event); } }; var handleOwnRef = _react.default.useCallback(function (instance) { // #StrictMode ready listRef.current = _reactDom.default.findDOMNode(instance); }, []); var handleRef = (0, _reactHelpers.useForkRef)(handleOwnRef, ref); return _react.default.createElement(_List.default, (0, _extends2.default)({ role: "menu", ref: handleRef, className: className, onKeyDown: handleKeyDown, tabIndex: autoFocus ? 0 : -1 }, other)); }); process.env.NODE_ENV !== "production" ? MenuList.propTypes = { /** * @ignore */ actions: _propTypes.default.shape({ current: _propTypes.default.object }), /** * If `true`, the list will be focused during the first mount. * Focus will also be triggered if the value changes from false to true. */ autoFocus: _propTypes.default.bool, /** * MenuList contents, normally `MenuItem`s. */ children: _propTypes.default.node, /** * @ignore */ className: _propTypes.default.string, /** * If `true`, the menu items will not wrap focus. */ disableListWrap: _propTypes.default.bool, /** * @ignore */ onKeyDown: _propTypes.default.func } : void 0; var _default = MenuList; exports.default = _default;