@mui/material
Version:
Material UI is an open-source React component library that implements Google's Material Design. It's comprehensive and can be used in production out of the box.
301 lines (297 loc) • 12.2 kB
JavaScript
"use strict";
'use client';
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _useRovingTabIndex = require("@mui/utils/useRovingTabIndex");
var _ownerDocument = _interopRequireDefault(require("../utils/ownerDocument"));
var _getActiveElement = _interopRequireDefault(require("../utils/getActiveElement"));
var _getScrollbarSize = _interopRequireDefault(require("../utils/getScrollbarSize"));
var _focusWithVisible = _interopRequireDefault(require("../utils/focusWithVisible"));
var _useEventCallback = _interopRequireDefault(require("../utils/useEventCallback"));
var _useForkRef = _interopRequireDefault(require("../utils/useForkRef"));
var _useEnhancedEffect = _interopRequireDefault(require("../utils/useEnhancedEffect"));
var _useRovingTabIndex2 = require("../utils/useRovingTabIndex");
var _ownerWindow = _interopRequireDefault(require("../utils/ownerWindow"));
var _List = _interopRequireDefault(require("../List"));
var _utils = require("../Select/utils");
var _MenuListContext = require("./MenuListContext");
var _jsxRuntime = require("react/jsx-runtime");
function getItemText(itemOrElement) {
const element = itemOrElement?.element ?? itemOrElement;
if (!element) {
return '';
}
if (itemOrElement?.textValue !== undefined) {
return itemOrElement.textValue;
}
let text = element.innerText;
if (text === undefined) {
// jsdom doesn't support innerText
text = element.textContent;
}
return text ?? '';
}
function textCriteriaMatches(itemOrElement, textCriteria) {
if (textCriteria === undefined) {
return true;
}
let text = getItemText(itemOrElement);
text = text.trim().toLowerCase();
if (text.length === 0) {
return false;
}
if (textCriteria.repeating) {
return text[0] === textCriteria.keys[0];
}
return text.startsWith(textCriteria.keys.join(''));
}
function isItemFocusableWithTextCriteria(item, criteria) {
if (!textCriteriaMatches(item, criteria)) {
return false;
}
return (0, _useRovingTabIndex.isItemFocusable)(item);
}
// Menu auto-focus is not always keyboard-driven. On open we often move focus to the
// active item programmatically so arrow-key navigation starts from the right place.
function focusInitialItem(element, focusSource) {
(0, _focusWithVisible.default)(element, focusSource);
}
/**
* 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.
*/
const MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
const {
// private
// eslint-disable-next-line react/prop-types
actions,
autoFocus: autoFocusList = false,
autoFocusItem: autoFocusActiveItem = false,
children,
className,
disabledItemsFocusable = false,
disableListWrap = false,
onKeyDown,
variant = 'selectedMenu',
...other
} = props;
const listRef = React.useRef(null);
const hasFocusedInitialTargetRef = React.useRef(false);
// Escape hatch for <Menu variant="menu"> (items have no selection state). When opened with
// mouse/pointer, the initial focused item should still receive DOM focus, but ButtonBase
// should suppress its focus-visible state for that one initial handoff.
const [suppressInitialFocusVisible, setSuppressInitialFocusVisible] = React.useState(false);
// Current anchored <Menu>s cannot receive a `openInteractionType` signal from a trigger
// the API only receives `open` and `anchorEl`. When <MenuList> is used in <Select>, the
// internal <SelectInput> is able to achieve this via `useSelectFocusSource`.
const focusSource = (0, _utils.useSelectFocusSource)();
const textCriteriaRef = React.useRef({
keys: [],
repeating: true,
previousKeyMatched: true,
lastTime: null
});
const getDefaultActiveItemId = React.useCallback(items => {
if (variant === 'selectedMenu') {
return items.find(item => item.selected && (0, _useRovingTabIndex.isItemFocusable)(item))?.id ?? items.find(item => (0, _useRovingTabIndex.isItemFocusable)(item))?.id ?? null;
}
return items.find(item => (0, _useRovingTabIndex.isItemFocusable)(item))?.id ?? null;
}, [variant]);
const rovingContainer = (0, _useRovingTabIndex2.useRovingTabIndexRoot)({
activeItemId: undefined,
getDefaultActiveItemId,
orientation: 'vertical',
wrap: !disableListWrap
});
const {
activeItemId,
focusNext,
getActiveItem,
getContainerProps,
getItemMap
} = rovingContainer;
const focusInitialTarget = (0, _useEventCallback.default)((force = false) => {
// `force` is used by the imperative action when `Menu` asks `MenuList` to restore its
// initial focus target after the popover finishes entering, even if this list already
// completed its normal one-time initial-focus path on an earlier render.
if (!listRef.current || !force && hasFocusedInitialTargetRef.current) {
return null;
}
if (autoFocusActiveItem) {
const activeItem = getActiveItem();
if (activeItem?.element) {
const hasSelectedItem = Array.from(getItemMap().values()).some(item => item.selected);
const shouldSuppressInitialFocusVisible = variant === 'menu' && hasSelectedItem && !activeItem.selected && focusSource == null;
setSuppressInitialFocusVisible(shouldSuppressInitialFocusVisible);
focusInitialItem(activeItem.element, focusSource);
hasFocusedInitialTargetRef.current = true;
return activeItem.element;
}
if (!autoFocusList) {
return null;
}
// Keep the list container focusable while waiting for items to register,
// or when there is no focusable item to move to.
setSuppressInitialFocusVisible(false);
listRef.current.focus();
return listRef.current;
}
if (!autoFocusList) {
setSuppressInitialFocusVisible(false);
return null;
}
setSuppressInitialFocusVisible(false);
listRef.current.focus();
hasFocusedInitialTargetRef.current = true;
return listRef.current;
});
(0, _useEnhancedEffect.default)(() => {
if (!autoFocusList && !autoFocusActiveItem) {
hasFocusedInitialTargetRef.current = false;
setSuppressInitialFocusVisible(false);
return undefined;
}
focusInitialTarget();
return undefined;
}, [activeItemId, autoFocusActiveItem, autoFocusList, focusInitialTarget]);
React.useImperativeHandle(actions, () => ({
adjustStyleForScrollbar: (containerElement, {
direction
}) => {
// Let's ignore that piece of logic if users are already overriding the width
// of the menu.
const noExplicitWidth = !listRef.current.style.width;
if (containerElement.clientHeight < listRef.current.clientHeight && noExplicitWidth) {
const scrollbarSize = `${(0, _getScrollbarSize.default)((0, _ownerWindow.default)(containerElement))}px`;
listRef.current.style[direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = scrollbarSize;
listRef.current.style.width = `calc(100% + ${scrollbarSize})`;
}
return listRef.current;
},
focusInitialTarget: () => {
if (!listRef.current) {
return null;
}
const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(listRef.current));
if (currentFocus && listRef.current.contains(currentFocus)) {
return currentFocus;
}
return focusInitialTarget(true);
}
}), [focusInitialTarget]);
const rovingContainerProps = getContainerProps();
const handleRef = (0, _useForkRef.default)(listRef, rovingContainerProps.ref, ref);
const menuListContextValue = React.useMemo(() => ({
itemsFocusableWhenDisabled: disabledItemsFocusable,
suppressInitialFocusVisible,
variant
}), [disabledItemsFocusable, suppressInitialFocusVisible, variant]);
const handleKeyDown = (0, _useEventCallback.default)(event => {
if (suppressInitialFocusVisible) {
setSuppressInitialFocusVisible(false);
}
const isModifierKeyPressed = event.ctrlKey || event.metaKey || event.altKey;
if (isModifierKeyPressed && onKeyDown) {
onKeyDown(event);
return;
}
rovingContainerProps.onKeyDown(event);
if (event.key.length === 1) {
const criteria = textCriteriaRef.current;
const lowerKey = event.key.toLowerCase();
const 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);
const currentFocus = (0, _getActiveElement.default)((0, _ownerDocument.default)(listRef.current));
const keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria);
if (criteria.previousKeyMatched && (keepFocusOnCurrent || focusNext(item => isItemFocusableWithTextCriteria(item, criteria)) != null)) {
event.preventDefault();
} else {
criteria.previousKeyMatched = false;
}
}
if (onKeyDown) {
onKeyDown(event);
}
});
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_List.default, {
role: "menu",
ref: handleRef,
className: className,
onKeyDown: handleKeyDown,
onFocus: rovingContainerProps.onFocus,
tabIndex: -1,
...other,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_MenuListContext.MenuListContext.Provider, {
value: menuListContextValue,
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_useRovingTabIndex2.RovingTabIndexContext.Provider, {
value: rovingContainer,
children: children
})
})
});
});
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.default.bool,
/**
* If `true`, will focus the first menuitem if `variant="menu"` or selected item
* if `variant="selectedMenu"`.
* @default false
*/
autoFocusItem: _propTypes.default.bool,
/**
* MenuList contents, normally `MenuItem`s.
*/
children: _propTypes.default.node,
/**
* @ignore
*/
className: _propTypes.default.string,
/**
* If `true`, will allow focus on disabled items.
* @default false
*/
disabledItemsFocusable: _propTypes.default.bool,
/**
* If `true`, the menu items will not wrap focus.
* @default false
*/
disableListWrap: _propTypes.default.bool,
/**
* @ignore
*/
onKeyDown: _propTypes.default.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.default.oneOf(['menu', 'selectedMenu'])
} : void 0;
var _default = exports.default = MenuList;