UNPKG

11 kBJavaScriptView Raw
1'use client';
2
3import _extends from "@babel/runtime/helpers/esm/extends";
4import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
5import * as React from 'react';
6import { isFragment } from 'react-is';
7import PropTypes from 'prop-types';
8import ownerDocument from '../utils/ownerDocument';
9import List from '../List';
10import getScrollbarSize from '../utils/getScrollbarSize';
11import useForkRef from '../utils/useForkRef';
12import useEnhancedEffect from '../utils/useEnhancedEffect';
13import { jsx as _jsx } from "react/jsx-runtime";
14function nextItem(list, item, disableListWrap) {
15 if (list === item) {
16 return list.firstChild;
17 }
18 if (item && item.nextElementSibling) {
19 return item.nextElementSibling;
20 }
21 return disableListWrap ? null : list.firstChild;
22}
23function previousItem(list, item, disableListWrap) {
24 if (list === item) {
25 return disableListWrap ? list.firstChild : list.lastChild;
26 }
27 if (item && item.previousElementSibling) {
28 return item.previousElementSibling;
29 }
30 return disableListWrap ? null : list.lastChild;
31}
32function textCriteriaMatches(nextFocus, textCriteria) {
33 if (textCriteria === undefined) {
34 return true;
35 }
36 var text = nextFocus.innerText;
37 if (text === undefined) {
38 // jsdom doesn't support innerText
39 text = nextFocus.textContent;
40 }
41 text = text.trim().toLowerCase();
42 if (text.length === 0) {
43 return false;
44 }
45 if (textCriteria.repeating) {
46 return text[0] === textCriteria.keys[0];
47 }
48 return text.indexOf(textCriteria.keys.join('')) === 0;
49}
50function moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, traversalFunction, textCriteria) {
51 var wrappedOnce = false;
52 var nextFocus = traversalFunction(list, currentFocus, currentFocus ? disableListWrap : false);
53 while (nextFocus) {
54 // Prevent infinite loop.
55 if (nextFocus === list.firstChild) {
56 if (wrappedOnce) {
57 return false;
58 }
59 wrappedOnce = true;
60 }
61
62 // Same logic as useAutocomplete.js
63 var nextFocusDisabled = disabledItemsFocusable ? false : nextFocus.disabled || nextFocus.getAttribute('aria-disabled') === 'true';
64 if (!nextFocus.hasAttribute('tabindex') || !textCriteriaMatches(nextFocus, textCriteria) || nextFocusDisabled) {
65 // Move to the next element.
66 nextFocus = traversalFunction(list, nextFocus, disableListWrap);
67 } else {
68 nextFocus.focus();
69 return true;
70 }
71 }
72 return false;
73}
74
75/**
76 * A permanently displayed menu following https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/.
77 * It's exposed to help customization of the [`Menu`](/material-ui/api/menu/) component if you
78 * use it separately you need to move focus into the component manually. Once
79 * the focus is placed inside the component it is fully keyboard accessible.
80 */
81var MenuList = /*#__PURE__*/React.forwardRef(function MenuList(props, ref) {
82 var actions = props.actions,
83 _props$autoFocus = props.autoFocus,
84 autoFocus = _props$autoFocus === void 0 ? false : _props$autoFocus,
85 _props$autoFocusItem = props.autoFocusItem,
86 autoFocusItem = _props$autoFocusItem === void 0 ? false : _props$autoFocusItem,
87 children = props.children,
88 className = props.className,
89 _props$disabledItemsF = props.disabledItemsFocusable,
90 disabledItemsFocusable = _props$disabledItemsF === void 0 ? false : _props$disabledItemsF,
91 _props$disableListWra = props.disableListWrap,
92 disableListWrap = _props$disableListWra === void 0 ? false : _props$disableListWra,
93 onKeyDown = props.onKeyDown,
94 _props$variant = props.variant,
95 variant = _props$variant === void 0 ? 'selectedMenu' : _props$variant,
96 other = _objectWithoutProperties(props, ["actions", "autoFocus", "autoFocusItem", "children", "className", "disabledItemsFocusable", "disableListWrap", "onKeyDown", "variant"]);
97 var listRef = React.useRef(null);
98 var textCriteriaRef = React.useRef({
99 keys: [],
100 repeating: true,
101 previousKeyMatched: true,
102 lastTime: null
103 });
104 useEnhancedEffect(function () {
105 if (autoFocus) {
106 listRef.current.focus();
107 }
108 }, [autoFocus]);
109 React.useImperativeHandle(actions, function () {
110 return {
111 adjustStyleForScrollbar: function adjustStyleForScrollbar(containerElement, _ref) {
112 var direction = _ref.direction;
113 // Let's ignore that piece of logic if users are already overriding the width
114 // of the menu.
115 var noExplicitWidth = !listRef.current.style.width;
116 if (containerElement.clientHeight < listRef.current.clientHeight && noExplicitWidth) {
117 var scrollbarSize = "".concat(getScrollbarSize(ownerDocument(containerElement)), "px");
118 listRef.current.style[direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = scrollbarSize;
119 listRef.current.style.width = "calc(100% + ".concat(scrollbarSize, ")");
120 }
121 return listRef.current;
122 }
123 };
124 }, []);
125 var handleKeyDown = function handleKeyDown(event) {
126 var list = listRef.current;
127 var key = event.key;
128 /**
129 * @type {Element} - will always be defined since we are in a keydown handler
130 * attached to an element. A keydown event is either dispatched to the activeElement
131 * or document.body or document.documentElement. Only the first case will
132 * trigger this specific handler.
133 */
134 var currentFocus = ownerDocument(list).activeElement;
135 if (key === 'ArrowDown') {
136 // Prevent scroll of the page
137 event.preventDefault();
138 moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, nextItem);
139 } else if (key === 'ArrowUp') {
140 event.preventDefault();
141 moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, previousItem);
142 } else if (key === 'Home') {
143 event.preventDefault();
144 moveFocus(list, null, disableListWrap, disabledItemsFocusable, nextItem);
145 } else if (key === 'End') {
146 event.preventDefault();
147 moveFocus(list, null, disableListWrap, disabledItemsFocusable, previousItem);
148 } else if (key.length === 1) {
149 var criteria = textCriteriaRef.current;
150 var lowerKey = key.toLowerCase();
151 var currTime = performance.now();
152 if (criteria.keys.length > 0) {
153 // Reset
154 if (currTime - criteria.lastTime > 500) {
155 criteria.keys = [];
156 criteria.repeating = true;
157 criteria.previousKeyMatched = true;
158 } else if (criteria.repeating && lowerKey !== criteria.keys[0]) {
159 criteria.repeating = false;
160 }
161 }
162 criteria.lastTime = currTime;
163 criteria.keys.push(lowerKey);
164 var keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria);
165 if (criteria.previousKeyMatched && (keepFocusOnCurrent || moveFocus(list, currentFocus, false, disabledItemsFocusable, nextItem, criteria))) {
166 event.preventDefault();
167 } else {
168 criteria.previousKeyMatched = false;
169 }
170 }
171 if (onKeyDown) {
172 onKeyDown(event);
173 }
174 };
175 var handleRef = useForkRef(listRef, ref);
176
177 /**
178 * the index of the item should receive focus
179 * in a `variant="selectedMenu"` it's the first `selected` item
180 * otherwise it's the very first item.
181 */
182 var activeItemIndex = -1;
183 // since we inject focus related props into children we have to do a lookahead
184 // to check if there is a `selected` item. We're looking for the last `selected`
185 // item and use the first valid item as a fallback
186 React.Children.forEach(children, function (child, index) {
187 if (! /*#__PURE__*/React.isValidElement(child)) {
188 if (activeItemIndex === index) {
189 activeItemIndex += 1;
190 if (activeItemIndex >= children.length) {
191 // there are no focusable items within the list.
192 activeItemIndex = -1;
193 }
194 }
195 return;
196 }
197 if (process.env.NODE_ENV !== 'production') {
198 if (isFragment(child)) {
199 console.error(["MUI: The Menu component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
200 }
201 }
202 if (!child.props.disabled) {
203 if (variant === 'selectedMenu' && child.props.selected) {
204 activeItemIndex = index;
205 } else if (activeItemIndex === -1) {
206 activeItemIndex = index;
207 }
208 }
209 if (activeItemIndex === index && (child.props.disabled || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight)) {
210 activeItemIndex += 1;
211 if (activeItemIndex >= children.length) {
212 // there are no focusable items within the list.
213 activeItemIndex = -1;
214 }
215 }
216 });
217 var items = React.Children.map(children, function (child, index) {
218 if (index === activeItemIndex) {
219 var newChildProps = {};
220 if (autoFocusItem) {
221 newChildProps.autoFocus = true;
222 }
223 if (child.props.tabIndex === undefined && variant === 'selectedMenu') {
224 newChildProps.tabIndex = 0;
225 }
226 return /*#__PURE__*/React.cloneElement(child, newChildProps);
227 }
228 return child;
229 });
230 return /*#__PURE__*/_jsx(List, _extends({
231 role: "menu",
232 ref: handleRef,
233 className: className,
234 onKeyDown: handleKeyDown,
235 tabIndex: autoFocus ? 0 : -1
236 }, other, {
237 children: items
238 }));
239});
240process.env.NODE_ENV !== "production" ? MenuList.propTypes /* remove-proptypes */ = {
241 // ┌────────────────────────────── Warning ──────────────────────────────┐
242 // │ These PropTypes are generated from the TypeScript type definitions. │
243 // │ To update them, edit the d.ts file and run `pnpm proptypes`. │
244 // └─────────────────────────────────────────────────────────────────────┘
245 /**
246 * If `true`, will focus the `[role="menu"]` container and move into tab order.
247 * @default false
248 */
249 autoFocus: PropTypes.bool,
250 /**
251 * If `true`, will focus the first menuitem if `variant="menu"` or selected item
252 * if `variant="selectedMenu"`.
253 * @default false
254 */
255 autoFocusItem: PropTypes.bool,
256 /**
257 * MenuList contents, normally `MenuItem`s.
258 */
259 children: PropTypes.node,
260 /**
261 * @ignore
262 */
263 className: PropTypes.string,
264 /**
265 * If `true`, will allow focus on disabled items.
266 * @default false
267 */
268 disabledItemsFocusable: PropTypes.bool,
269 /**
270 * If `true`, the menu items will not wrap focus.
271 * @default false
272 */
273 disableListWrap: PropTypes.bool,
274 /**
275 * @ignore
276 */
277 onKeyDown: PropTypes.func,
278 /**
279 * The variant to use. Use `menu` to prevent selected items from impacting the initial focus
280 * and the vertical alignment relative to the anchor element.
281 * @default 'selectedMenu'
282 */
283 variant: PropTypes.oneOf(['menu', 'selectedMenu'])
284} : void 0;
285export default MenuList;
\No newline at end of file