1 | 'use client';
|
2 |
|
3 | import * as React from 'react';
|
4 | import { isFragment } from 'react-is';
|
5 | import PropTypes from 'prop-types';
|
6 | import ownerDocument from "../utils/ownerDocument.js";
|
7 | import List from "../List/index.js";
|
8 | import getScrollbarSize from "../utils/getScrollbarSize.js";
|
9 | import useForkRef from "../utils/useForkRef.js";
|
10 | import useEnhancedEffect from "../utils/useEnhancedEffect.js";
|
11 | import { ownerWindow } from "../utils/index.js";
|
12 | import { jsx as _jsx } from "react/jsx-runtime";
|
13 | function nextItem(list, item, disableListWrap) {
|
14 | if (list === item) {
|
15 | return list.firstChild;
|
16 | }
|
17 | if (item && item.nextElementSibling) {
|
18 | return item.nextElementSibling;
|
19 | }
|
20 | return disableListWrap ? null : list.firstChild;
|
21 | }
|
22 | function previousItem(list, item, disableListWrap) {
|
23 | if (list === item) {
|
24 | return disableListWrap ? list.firstChild : list.lastChild;
|
25 | }
|
26 | if (item && item.previousElementSibling) {
|
27 | return item.previousElementSibling;
|
28 | }
|
29 | return disableListWrap ? null : list.lastChild;
|
30 | }
|
31 | function textCriteriaMatches(nextFocus, textCriteria) {
|
32 | if (textCriteria === undefined) {
|
33 | return true;
|
34 | }
|
35 | let text = nextFocus.innerText;
|
36 | if (text === undefined) {
|
37 |
|
38 | text = nextFocus.textContent;
|
39 | }
|
40 | text = text.trim().toLowerCase();
|
41 | if (text.length === 0) {
|
42 | return false;
|
43 | }
|
44 | if (textCriteria.repeating) {
|
45 | return text[0] === textCriteria.keys[0];
|
46 | }
|
47 | return text.startsWith(textCriteria.keys.join(''));
|
48 | }
|
49 | function moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, traversalFunction, textCriteria) {
|
50 | let wrappedOnce = false;
|
51 | let nextFocus = traversalFunction(list, currentFocus, currentFocus ? disableListWrap : false);
|
52 | while (nextFocus) {
|
53 |
|
54 | if (nextFocus === list.firstChild) {
|
55 | if (wrappedOnce) {
|
56 | return false;
|
57 | }
|
58 | wrappedOnce = true;
|
59 | }
|
60 |
|
61 |
|
62 | const nextFocusDisabled = disabledItemsFocusable ? false : nextFocus.disabled || nextFocus.getAttribute('aria-disabled') === 'true';
|
63 | if (!nextFocus.hasAttribute('tabindex') || !textCriteriaMatches(nextFocus, textCriteria) || nextFocusDisabled) {
|
64 |
|
65 | nextFocus = traversalFunction(list, nextFocus, disableListWrap);
|
66 | } else {
|
67 | nextFocus.focus();
|
68 | return true;
|
69 | }
|
70 | }
|
71 | return false;
|
72 | }
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 |
|
80 | const MenuList = React.forwardRef(function MenuList(props, ref) {
|
81 | const {
|
82 |
|
83 |
|
84 | actions,
|
85 | autoFocus = false,
|
86 | autoFocusItem = false,
|
87 | children,
|
88 | className,
|
89 | disabledItemsFocusable = false,
|
90 | disableListWrap = false,
|
91 | onKeyDown,
|
92 | variant = 'selectedMenu',
|
93 | ...other
|
94 | } = props;
|
95 | const listRef = React.useRef(null);
|
96 | const textCriteriaRef = React.useRef({
|
97 | keys: [],
|
98 | repeating: true,
|
99 | previousKeyMatched: true,
|
100 | lastTime: null
|
101 | });
|
102 | useEnhancedEffect(() => {
|
103 | if (autoFocus) {
|
104 | listRef.current.focus();
|
105 | }
|
106 | }, [autoFocus]);
|
107 | React.useImperativeHandle(actions, () => ({
|
108 | adjustStyleForScrollbar: (containerElement, {
|
109 | direction
|
110 | }) => {
|
111 |
|
112 |
|
113 | const noExplicitWidth = !listRef.current.style.width;
|
114 | if (containerElement.clientHeight < listRef.current.clientHeight && noExplicitWidth) {
|
115 | const scrollbarSize = `${getScrollbarSize(ownerWindow(containerElement))}px`;
|
116 | listRef.current.style[direction === 'rtl' ? 'paddingLeft' : 'paddingRight'] = scrollbarSize;
|
117 | listRef.current.style.width = `calc(100% + ${scrollbarSize})`;
|
118 | }
|
119 | return listRef.current;
|
120 | }
|
121 | }), []);
|
122 | const handleKeyDown = event => {
|
123 | const list = listRef.current;
|
124 | const key = event.key;
|
125 | const isModifierKeyPressed = event.ctrlKey || event.metaKey || event.altKey;
|
126 | if (isModifierKeyPressed) {
|
127 | if (onKeyDown) {
|
128 | onKeyDown(event);
|
129 | }
|
130 | return;
|
131 | }
|
132 |
|
133 | |
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | const currentFocus = ownerDocument(list).activeElement;
|
140 | if (key === 'ArrowDown') {
|
141 |
|
142 | event.preventDefault();
|
143 | moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, nextItem);
|
144 | } else if (key === 'ArrowUp') {
|
145 | event.preventDefault();
|
146 | moveFocus(list, currentFocus, disableListWrap, disabledItemsFocusable, previousItem);
|
147 | } else if (key === 'Home') {
|
148 | event.preventDefault();
|
149 | moveFocus(list, null, disableListWrap, disabledItemsFocusable, nextItem);
|
150 | } else if (key === 'End') {
|
151 | event.preventDefault();
|
152 | moveFocus(list, null, disableListWrap, disabledItemsFocusable, previousItem);
|
153 | } else if (key.length === 1) {
|
154 | const criteria = textCriteriaRef.current;
|
155 | const lowerKey = key.toLowerCase();
|
156 | const currTime = performance.now();
|
157 | if (criteria.keys.length > 0) {
|
158 |
|
159 | if (currTime - criteria.lastTime > 500) {
|
160 | criteria.keys = [];
|
161 | criteria.repeating = true;
|
162 | criteria.previousKeyMatched = true;
|
163 | } else if (criteria.repeating && lowerKey !== criteria.keys[0]) {
|
164 | criteria.repeating = false;
|
165 | }
|
166 | }
|
167 | criteria.lastTime = currTime;
|
168 | criteria.keys.push(lowerKey);
|
169 | const keepFocusOnCurrent = currentFocus && !criteria.repeating && textCriteriaMatches(currentFocus, criteria);
|
170 | if (criteria.previousKeyMatched && (keepFocusOnCurrent || moveFocus(list, currentFocus, false, disabledItemsFocusable, nextItem, criteria))) {
|
171 | event.preventDefault();
|
172 | } else {
|
173 | criteria.previousKeyMatched = false;
|
174 | }
|
175 | }
|
176 | if (onKeyDown) {
|
177 | onKeyDown(event);
|
178 | }
|
179 | };
|
180 | const handleRef = useForkRef(listRef, ref);
|
181 |
|
182 | |
183 |
|
184 |
|
185 |
|
186 |
|
187 | let activeItemIndex = -1;
|
188 |
|
189 |
|
190 |
|
191 | React.Children.forEach(children, (child, index) => {
|
192 | if (! React.isValidElement(child)) {
|
193 | if (activeItemIndex === index) {
|
194 | activeItemIndex += 1;
|
195 | if (activeItemIndex >= children.length) {
|
196 |
|
197 | activeItemIndex = -1;
|
198 | }
|
199 | }
|
200 | return;
|
201 | }
|
202 | if (process.env.NODE_ENV !== 'production') {
|
203 | if (isFragment(child)) {
|
204 | console.error(["MUI: The Menu component doesn't accept a Fragment as a child.", 'Consider providing an array instead.'].join('\n'));
|
205 | }
|
206 | }
|
207 | if (!child.props.disabled) {
|
208 | if (variant === 'selectedMenu' && child.props.selected) {
|
209 | activeItemIndex = index;
|
210 | } else if (activeItemIndex === -1) {
|
211 | activeItemIndex = index;
|
212 | }
|
213 | }
|
214 | if (activeItemIndex === index && (child.props.disabled || child.props.muiSkipListHighlight || child.type.muiSkipListHighlight)) {
|
215 | activeItemIndex += 1;
|
216 | if (activeItemIndex >= children.length) {
|
217 |
|
218 | activeItemIndex = -1;
|
219 | }
|
220 | }
|
221 | });
|
222 | const items = React.Children.map(children, (child, index) => {
|
223 | if (index === activeItemIndex) {
|
224 | const newChildProps = {};
|
225 | if (autoFocusItem) {
|
226 | newChildProps.autoFocus = true;
|
227 | }
|
228 | if (child.props.tabIndex === undefined && variant === 'selectedMenu') {
|
229 | newChildProps.tabIndex = 0;
|
230 | }
|
231 | return React.cloneElement(child, newChildProps);
|
232 | }
|
233 | return child;
|
234 | });
|
235 | return _jsx(List, {
|
236 | role: "menu",
|
237 | ref: handleRef,
|
238 | className: className,
|
239 | onKeyDown: handleKeyDown,
|
240 | tabIndex: autoFocus ? 0 : -1,
|
241 | ...other,
|
242 | children: items
|
243 | });
|
244 | });
|
245 | process.env.NODE_ENV !== "production" ? MenuList.propTypes = {
|
246 |
|
247 |
|
248 |
|
249 |
|
250 | |
251 |
|
252 |
|
253 |
|
254 | autoFocus: PropTypes.bool,
|
255 | |
256 |
|
257 |
|
258 |
|
259 |
|
260 | autoFocusItem: PropTypes.bool,
|
261 | |
262 |
|
263 |
|
264 | children: PropTypes.node,
|
265 | |
266 |
|
267 |
|
268 | className: PropTypes.string,
|
269 | |
270 |
|
271 |
|
272 |
|
273 | disabledItemsFocusable: PropTypes.bool,
|
274 | |
275 |
|
276 |
|
277 |
|
278 | disableListWrap: PropTypes.bool,
|
279 | |
280 |
|
281 |
|
282 | onKeyDown: PropTypes.func,
|
283 | |
284 |
|
285 |
|
286 |
|
287 |
|
288 | variant: PropTypes.oneOf(['menu', 'selectedMenu'])
|
289 | } : void 0;
|
290 | export default MenuList; |
\ | No newline at end of file |