UNPKG

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