UNPKG

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