'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties"; import * as React from 'react'; import { isFragment } from 'react-is'; import PropTypes from 'prop-types'; import ownerDocument from '../utils/ownerDocument'; import List from '../List'; import getScrollbarSize from '../utils/getScrollbarSize'; import useForkRef from '../utils/useForkRef'; import useEnhancedEffect from '../utils/useEnhancedEffect'; import { jsx as _jsx } from "react/jsx-runtime"; 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; } 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, disabledItemsFocusable, 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; } // Same logic as useAutocomplete.js var nextFocusDisabled = disabledItemsFocusable ? false : nextFocus.disabled || nextFocus.getAttribute('aria-disabled') === 'true'; if (!nextFocus.hasAttribute('tabindex') || !textCriteriaMatches(nextFocus, textCriteria) || nextFocusDisabled) { // Move to the next element. nextFocus = traversalFunction(list, nextFocus, disableListWrap); } else { nextFocus.focus(); return true; } } return false; } /** * A permanently displayed menu following https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/. * It's exposed to help customization of the [`Menu`](/material-ui/api/menu/) component if you * use it separately you need to move focus into the component manually. Once * the focus is placed inside the component it is fully keyboard accessible. */ var MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) { var actions = props.actions, _props$autoFocus = props.autoFocus, autoFocus = _props$autoFocus === void 0 ? false : _props$autoFocus, _props$autoFocusItem = props.autoFocusItem, autoFocusItem = _props$autoFocusItem === void 0 ? false : _props$autoFocusItem, children = props.children, className = props.className, _props$disabledItemsF = props.disabledItemsFocusable, disabledItemsFocusable = _props$disabledItemsF === void 0 ? false : _props$disabledItemsF, _props$disableListWra = props.disableListWrap, disableListWrap = _props$disableListWra === void 0 ? false : _props$disableListWra, onKeyDown = props.onKeyDown, _props$variant = props.variant, variant = _props$variant === void 0 ? 'selectedMenu' : _props$variant, other = _objectWithoutProperties(props, ["actions", "autoFocus", "autoFocusItem", "children", "className", "disabledItemsFocusable", "disableListWrap", "onKeyDown", "variant"]); var listRef = React.useRef(null); var textCriteriaRef = React.useRef({ keys: [], repeating: true, previousKeyMatched: true, lastTime: null }); useEnhancedEffect(function () { if (autoFocus) { listRef.current.focus(); } }, [autoFocus]); React.useImperativeHandle(actions, function () { return { adjustStyleForScrollbar: function adjustStyleForScrollbar(containerElement, _ref) { var direction = _ref.direction; // 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(getScrollbarSize(ownerDocument(containerElement)), "px"); listRef.current.style[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 = ownerDocument(list).activeElement; if (key === 'ArrowDown') { // Prevent scroll of the page event.preventDefault(); moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, nextItem); } else if (key === 'ArrowUp') { event.preventDefault(); moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, previousItem); } else if (key === 'Home') { event.preventDefault(); moveFocus(list, null, disableListWrap, disabledItemsFocusable, nextItem); } else if (key === 'End') { event.preventDefault(); moveFocus(list, null, disableListWrap, disabledItemsFocusable, 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, disabledItemsFocusable, nextItem, criteria))) { event.preventDefault(); } else { criteria.previousKeyMatched = false; } } if (onKeyDown) { onKeyDown(event); } }; var handleRef = useForkRef(listRef, ref); /** * the index of the item should receive focus * in a `variant="selectedMenu"` it's the first `selected` item * otherwise it's the very first item. */ var activeItemIndex = -1; // since we inject focus related props into children we have to do a lookahead // to check if there is a `selected` item. We're looking for the last `selected` // item and use the first valid item as a fallback React.Children.forEach(children, function (child, index) { if (! /*#__PURE__*/React.isValidElement(child)) { if (activeItemIndex === index) { activeItemIndex += 1; if (activeItemIndex >= children.length) { // there are no focusable items within the list. activeItemIndex = -1; } } return; } if (process.env.NODE_ENV !== 'production') { if (isFragment(child)) { console.error(["MUI: The Menu component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n')); } } if (!child.props.disabled) { if (variant === 'selectedMenu' && child.props.selected) { activeItemIndex = index; } else if (activeItemIndex === -1) { activeItemIndex = index; } } if (activeItemIndex === index && (child.props.disabled || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight)) { activeItemIndex += 1; if (activeItemIndex >= children.length) { // there are no focusable items within the list. activeItemIndex = -1; } } }); var items = React.Children.map(children, function (child, index) { if (index === activeItemIndex) { var newChildProps = {}; if (autoFocusItem) { newChildProps.autoFocus = true; } if (child.props.tabIndex === undefined && variant === 'selectedMenu') { newChildProps.tabIndex = 0; } return /*#__PURE__*/React.cloneElement(child, newChildProps); } return child; }); return /*#__PURE__*/_jsx(List, _extends({ role: "menu", ref: handleRef, className: className, onKeyDown: handleKeyDown, tabIndex: autoFocus ? 0 : -1 }, other, { children: items })); }); process.env.NODE_ENV !== "production" ? MenuList.propTypes /* remove-proptypes */ = { // ┌────────────────────────────── Warning ──────────────────────────────┐ // │ These PropTypes are generated from the TypeScript type definitions. │ // │ To update them, edit the d.ts file and run `pnpm proptypes`. │ // └─────────────────────────────────────────────────────────────────────┘ /** * If `true`, will focus the `[role="menu"]` container and move into tab order. * @default false */ autoFocus: PropTypes.bool, /** * If `true`, will focus the first menuitem if `variant="menu"` or selected item * if `variant="selectedMenu"`. * @default false */ autoFocusItem: PropTypes.bool, /** * MenuList contents, normally `MenuItem`s. */ children: PropTypes.node, /** * @ignore */ className: PropTypes.string, /** * If `true`, will allow focus on disabled items. * @default false */ disabledItemsFocusable: PropTypes.bool, /** * If `true`, the menu items will not wrap focus. * @default false */ disableListWrap: PropTypes.bool, /** * @ignore */ onKeyDown: PropTypes.func, /** * The variant to use. Use `menu` to prevent selected items from impacting the initial focus * and the vertical alignment relative to the anchor element. * @default 'selectedMenu' */ variant: PropTypes.oneOf(['menu', 'selectedMenu']) } : void 0; export default MenuList;