UNPKG

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