You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
284 lines
11 KiB
JavaScript
284 lines
11 KiB
JavaScript
'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, 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(getScrollbarSize(ownerDocument(containerElement)), "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 = 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; |