UNPKG

29.8 kBJavaScriptView Raw
1import React, { useRef, useReducer, useEffect, useContext, forwardRef, useMemo, useState, useCallback } from 'react';
2import PropTypes from 'prop-types';
3import { useId } from '@reach/auto-id';
4import Popover from '@reach/popover';
5import { useDescendantsInit, DescendantProvider, useDescendants, useDescendantKeyDown, createDescendantContext, useDescendant } from '@reach/descendants';
6import { makeId, checkStyles, isFunction, forwardRefWithAs, useForkedRef, wrapEvent, usePrevious, isString, noop, createNamedContext, getOwnerDocument } from '@reach/utils';
7
8function _extends() {
9 _extends = Object.assign || function (target) {
10 for (var i = 1; i < arguments.length; i++) {
11 var source = arguments[i];
12
13 for (var key in source) {
14 if (Object.prototype.hasOwnProperty.call(source, key)) {
15 target[key] = source[key];
16 }
17 }
18 }
19
20 return target;
21 };
22
23 return _extends.apply(this, arguments);
24}
25
26function _objectWithoutPropertiesLoose(source, excluded) {
27 if (source == null) return {};
28 var target = {};
29 var sourceKeys = Object.keys(source);
30 var key, i;
31
32 for (i = 0; i < sourceKeys.length; i++) {
33 key = sourceKeys[i];
34 if (excluded.indexOf(key) >= 0) continue;
35 target[key] = source[key];
36 }
37
38 return target;
39}
40
41// Actions
42
43var CLEAR_SELECTION_INDEX = "CLEAR_SELECTION_INDEX";
44var CLICK_MENU_ITEM = "CLICK_MENU_ITEM";
45var CLOSE_MENU = "CLOSE_MENU";
46var OPEN_MENU_AT_FIRST_ITEM = "OPEN_MENU_AT_FIRST_ITEM";
47var OPEN_MENU_CLEARED = "OPEN_MENU_CLEARED";
48var SEARCH_FOR_ITEM = "SEARCH_FOR_ITEM";
49var SELECT_ITEM_AT_INDEX = "SELECT_ITEM_AT_INDEX";
50var SET_BUTTON_ID = "SET_BUTTON_ID";
51var MenuDescendantContext = /*#__PURE__*/createDescendantContext("MenuDescendantContext");
52var MenuContext = /*#__PURE__*/createNamedContext("MenuContext", {});
53var initialState = {
54 // The button ID is needed for aria controls and can be set directly and
55 // updated for top-level use via context. Otherwise a default is set by useId.
56 // TODO: Consider deprecating direct ID in 1.0 in favor of id at the top level
57 // for passing deterministic IDs to descendent components.
58 buttonId: null,
59 // Whether or not the menu is expanded
60 isExpanded: false,
61 // When a user begins typing a character string, the selection will change if
62 // a matching item is found
63 typeaheadQuery: "",
64 // The index of the current selected item. When the selection is cleared a
65 // value of -1 is used.
66 selectionIndex: -1
67}; ////////////////////////////////////////////////////////////////////////////////
68
69/**
70 * Menu
71 *
72 * The wrapper component for the other components. No DOM element is rendered.
73 *
74 * @see Docs https://reacttraining.com/reach-ui/menu-button#menu
75 */
76
77var Menu = function Menu(_ref) {
78 var id = _ref.id,
79 children = _ref.children;
80 var buttonRef = useRef(null);
81 var menuRef = useRef(null);
82 var popoverRef = useRef(null);
83
84 var _useDescendantsInit = useDescendantsInit(),
85 descendants = _useDescendantsInit[0],
86 setDescendants = _useDescendantsInit[1];
87
88 var _useReducer = useReducer(reducer, initialState),
89 state = _useReducer[0],
90 dispatch = _useReducer[1];
91
92 var _id = useId(id);
93
94 var menuId = id || makeId("menu", _id); // We use an event listener attached to the window to capture outside clicks
95 // that close the menu. We don't want the initial button click to trigger this
96 // when a menu is closed, so we can track this behavior in a ref for now.
97 // We shouldn't need this when we rewrite with state machine logic.
98
99 var buttonClickedRef = useRef(false); // We will put children callbacks in a ref to avoid triggering endless render
100 // loops when using render props if the app code doesn't useCallback
101 // https://github.com/reach/reach-ui/issues/523
102
103 var selectCallbacks = useRef([]); // If the popover's position overlaps with an option when the popover
104 // initially opens, the mouseup event will trigger a select. To prevent that,
105 // we decide the menu button is only ready to make a selection if the pointer
106 // moves first, otherwise the user is just registering the initial button
107 // click rather than selecting an item. This is similar to a native select
108 // on most platforms, and our menu button popover works similarly.
109
110 var readyToSelect = useRef(false);
111 var context = {
112 buttonRef: buttonRef,
113 dispatch: dispatch,
114 menuId: menuId,
115 menuRef: menuRef,
116 popoverRef: popoverRef,
117 buttonClickedRef: buttonClickedRef,
118 readyToSelect: readyToSelect,
119 selectCallbacks: selectCallbacks,
120 state: state
121 }; // When the menu is open, focus is placed on the menu itself so that
122 // keyboard navigation is still possible.
123
124 useEffect(function () {
125 if (state.isExpanded) {
126 // @ts-ignore
127 window.__REACH_DISABLE_TOOLTIPS = true;
128 window.requestAnimationFrame(function () {
129 focus(menuRef.current);
130 });
131 } else {
132 // We want to ignore the immediate focus of a tooltip so it doesn't pop
133 // up again when the menu closes, only pops up when focus returns again
134 // to the tooltip (like native OS tooltips).
135 // @ts-ignore
136 window.__REACH_DISABLE_TOOLTIPS = false;
137 }
138 }, [state.isExpanded]);
139 useEffect(function () {
140 return checkStyles("menu-button");
141 }, []);
142 return React.createElement(DescendantProvider, {
143 context: MenuDescendantContext,
144 items: descendants,
145 set: setDescendants
146 }, React.createElement(MenuContext.Provider, {
147 value: context
148 }, isFunction(children) ? children({
149 isExpanded: state.isExpanded,
150 // TODO: Remove in 1.0
151 isOpen: state.isExpanded
152 }) : children));
153};
154
155if (process.env.NODE_ENV !== "production") {
156 Menu.displayName = "Menu";
157 Menu.propTypes = {
158 children: /*#__PURE__*/PropTypes.oneOfType([PropTypes.func, PropTypes.node])
159 };
160} ////////////////////////////////////////////////////////////////////////////////
161
162/**
163 * MenuButton
164 *
165 * Wraps a DOM `button` that toggles the opening and closing of the dropdown
166 * menu. Must be rendered inside of a `<Menu>`.
167 *
168 * @see Docs https://reacttraining.com/reach-ui/menu-button#menubutton
169 */
170
171
172var MenuButton = /*#__PURE__*/forwardRefWithAs(function MenuButton(_ref2, forwardedRef) {
173 var _ref2$as = _ref2.as,
174 Comp = _ref2$as === void 0 ? "button" : _ref2$as,
175 onKeyDown = _ref2.onKeyDown,
176 onMouseDown = _ref2.onMouseDown,
177 id = _ref2.id,
178 props = _objectWithoutPropertiesLoose(_ref2, ["as", "onKeyDown", "onMouseDown", "id"]);
179
180 var _useContext = useContext(MenuContext),
181 buttonRef = _useContext.buttonRef,
182 buttonClickedRef = _useContext.buttonClickedRef,
183 menuId = _useContext.menuId,
184 _useContext$state = _useContext.state,
185 buttonId = _useContext$state.buttonId,
186 isExpanded = _useContext$state.isExpanded,
187 dispatch = _useContext.dispatch;
188
189 var ref = useForkedRef(buttonRef, forwardedRef);
190 useEffect(function () {
191 var newButtonId = id != null ? id : menuId ? makeId("menu-button", menuId) : "menu-button";
192
193 if (buttonId !== newButtonId) {
194 dispatch({
195 type: SET_BUTTON_ID,
196 payload: newButtonId
197 });
198 }
199 }, [buttonId, dispatch, id, menuId]);
200
201 function handleKeyDown(event) {
202 switch (event.key) {
203 case "ArrowDown":
204 case "ArrowUp":
205 event.preventDefault(); // prevent scroll
206
207 dispatch({
208 type: OPEN_MENU_AT_FIRST_ITEM
209 });
210 break;
211
212 case "Enter":
213 case " ":
214 dispatch({
215 type: OPEN_MENU_AT_FIRST_ITEM
216 });
217 break;
218 }
219 }
220
221 function handleMouseDown(event) {
222 if (!isExpanded) {
223 buttonClickedRef.current = true;
224 }
225
226 if (isRightClick(event.nativeEvent)) {
227 return;
228 } else if (isExpanded) {
229 dispatch({
230 type: CLOSE_MENU,
231 payload: {
232 buttonRef: buttonRef
233 }
234 });
235 } else {
236 dispatch({
237 type: OPEN_MENU_CLEARED
238 });
239 }
240 }
241
242 return React.createElement(Comp // When the menu is displayed, the element with role `button` has
243 // `aria-expanded` set to `true`. When the menu is hidden, it is
244 // recommended that `aria-expanded` is not present.
245 // https://www.w3.org/TR/wai-aria-practices-1.2/#menubutton
246 , Object.assign({
247 "aria-expanded": isExpanded ? true : undefined,
248 "aria-haspopup": true,
249 "aria-controls": menuId
250 }, props, {
251 ref: ref,
252 "data-reach-menu-button": "",
253 id: buttonId || undefined,
254 onKeyDown: wrapEvent(onKeyDown, handleKeyDown),
255 onMouseDown: wrapEvent(onMouseDown, handleMouseDown),
256 type: "button"
257 }));
258});
259
260if (process.env.NODE_ENV !== "production") {
261 MenuButton.displayName = "MenuButton";
262 MenuButton.propTypes = {
263 children: PropTypes.node
264 };
265} ////////////////////////////////////////////////////////////////////////////////
266
267/**
268 * MenuItemImpl
269 *
270 * MenuItem and MenuLink share most of the same functionality captured here.
271 */
272
273
274var MenuItemImpl = /*#__PURE__*/forwardRefWithAs(function MenuItemImpl(_ref3, forwardedRef) {
275 var Comp = _ref3.as,
276 indexProp = _ref3.index,
277 _ref3$isLink = _ref3.isLink,
278 isLink = _ref3$isLink === void 0 ? false : _ref3$isLink,
279 onClick = _ref3.onClick,
280 onDragStart = _ref3.onDragStart,
281 onMouseDown = _ref3.onMouseDown,
282 onMouseEnter = _ref3.onMouseEnter,
283 onMouseLeave = _ref3.onMouseLeave,
284 onMouseMove = _ref3.onMouseMove,
285 onMouseUp = _ref3.onMouseUp,
286 onSelect = _ref3.onSelect,
287 valueTextProp = _ref3.valueText,
288 props = _objectWithoutPropertiesLoose(_ref3, ["as", "index", "isLink", "onClick", "onDragStart", "onMouseDown", "onMouseEnter", "onMouseLeave", "onMouseMove", "onMouseUp", "onSelect", "valueText"]);
289
290 var _useContext2 = useContext(MenuContext),
291 buttonRef = _useContext2.buttonRef,
292 dispatch = _useContext2.dispatch,
293 readyToSelect = _useContext2.readyToSelect,
294 selectCallbacks = _useContext2.selectCallbacks,
295 _useContext2$state = _useContext2.state,
296 selectionIndex = _useContext2$state.selectionIndex,
297 isExpanded = _useContext2$state.isExpanded;
298
299 var ownRef = useRef(null); // After the ref is mounted to the DOM node, we check to see if we have an
300 // explicit valueText prop before looking for the node's textContent for
301 // typeahead functionality.
302
303 var _useState = useState(valueTextProp || ""),
304 valueText = _useState[0],
305 setValueText = _useState[1];
306
307 var setValueTextFromDom = useCallback(function (node) {
308 if (node) {
309 ownRef.current = node;
310
311 if (!valueTextProp || node.textContent && valueText !== node.textContent) {
312 setValueText(node.textContent);
313 }
314 }
315 }, [valueText, valueTextProp]);
316 var ref = useForkedRef(forwardedRef, setValueTextFromDom);
317 var mouseEventStarted = useRef(false);
318 var index = useDescendant({
319 element: ownRef.current,
320 key: valueText,
321 isLink: isLink
322 }, MenuDescendantContext, indexProp);
323 var isSelected = index === selectionIndex; // Update the callback ref array on every render
324
325 selectCallbacks.current[index] = onSelect;
326
327 function select() {
328 focus(buttonRef.current);
329 onSelect && onSelect();
330 dispatch({
331 type: CLICK_MENU_ITEM
332 });
333 }
334
335 function handleClick(event) {
336 if (isLink && !isRightClick(event.nativeEvent)) {
337 select();
338 }
339 }
340
341 function handleDragStart(event) {
342 // Because we don't preventDefault on mousedown for links (we need the
343 // native click event), clicking and holding on a link triggers a
344 // dragstart which we don't want.
345 if (isLink) {
346 event.preventDefault();
347 }
348 }
349
350 function handleMouseDown(event) {
351 if (isRightClick(event.nativeEvent)) return;
352
353 if (isLink) {
354 // Signal that the mouse is down so we can react call the right function
355 // if the user is clicking on a link.
356 mouseEventStarted.current = true;
357 } else {
358 event.preventDefault();
359 }
360 }
361
362 function handleMouseEnter(event) {
363 if (!isSelected && index != null) {
364 dispatch({
365 type: SELECT_ITEM_AT_INDEX,
366 payload: {
367 index: index
368 }
369 });
370 }
371 }
372
373 function handleMouseLeave(event) {
374 // Clear out selection when mouse over a non-menu item child.
375 dispatch({
376 type: CLEAR_SELECTION_INDEX
377 });
378 }
379
380 function handleMouseMove() {
381 readyToSelect.current = true;
382
383 if (!isSelected && index != null) {
384 dispatch({
385 type: SELECT_ITEM_AT_INDEX,
386 payload: {
387 index: index
388 }
389 });
390 }
391 }
392
393 function handleMouseUp(event) {
394 if (!readyToSelect.current) {
395 readyToSelect.current = true;
396 return;
397 }
398
399 if (isRightClick(event.nativeEvent)) return;
400
401 if (isLink) {
402 // If a mousedown event was initiated on a menu link followed by a
403 // mouseup event on the same link, we do nothing; a click event will
404 // come next and handle selection. Otherwise, we trigger a click event.
405 if (mouseEventStarted.current) {
406 mouseEventStarted.current = false;
407 } else if (ownRef.current) {
408 ownRef.current.click();
409 }
410 } else {
411 select();
412 }
413 } // When the menu closes, reset readyToSelect for the next interaction.
414
415
416 useEffect(function () {
417 if (!isExpanded) {
418 readyToSelect.current = false;
419 }
420 }, [isExpanded, readyToSelect]); // Any time a mouseup event occurs anywhere in the document, we reset the
421 // mouseEventStarted ref so we can check it again when needed.
422
423 useEffect(function () {
424 var ownerDocument = getOwnerDocument(ownRef.current) || document;
425
426 var listener = function listener() {
427 return mouseEventStarted.current = false;
428 };
429
430 ownerDocument.addEventListener("mouseup", listener);
431 return function () {
432 return ownerDocument.removeEventListener("mouseup", listener);
433 };
434 }, []);
435 return React.createElement(Comp, Object.assign({
436 role: "menuitem",
437 id: useMenuItemId(index),
438 tabIndex: -1
439 }, props, {
440 ref: ref,
441 "data-reach-menu-item": "",
442 "data-selected": isSelected ? "" : undefined,
443 "data-valuetext": valueText,
444 onClick: wrapEvent(onClick, handleClick),
445 onDragStart: wrapEvent(onDragStart, handleDragStart),
446 onMouseDown: wrapEvent(onMouseDown, handleMouseDown),
447 onMouseEnter: wrapEvent(onMouseEnter, handleMouseEnter),
448 onMouseLeave: wrapEvent(onMouseLeave, handleMouseLeave),
449 onMouseMove: wrapEvent(onMouseMove, handleMouseMove),
450 onMouseUp: wrapEvent(onMouseUp, handleMouseUp)
451 }));
452}); ////////////////////////////////////////////////////////////////////////////////
453
454/**
455 * MenuItem
456 *
457 * Handles menu selection. Must be a direct child of a `<MenuList>`.
458 *
459 * @see Docs https://reacttraining.com/reach-ui/menu-button#menuitem
460 */
461
462var MenuItem = /*#__PURE__*/forwardRefWithAs(function MenuItem(_ref4, forwardedRef) {
463 var _ref4$as = _ref4.as,
464 as = _ref4$as === void 0 ? "div" : _ref4$as,
465 props = _objectWithoutPropertiesLoose(_ref4, ["as"]);
466
467 return React.createElement(MenuItemImpl, Object.assign({}, props, {
468 ref: forwardedRef,
469 as: as
470 }));
471});
472
473if (process.env.NODE_ENV !== "production") {
474 MenuItem.displayName = "MenuItem";
475 MenuItem.propTypes = {
476 as: PropTypes.any,
477 onSelect: PropTypes.func.isRequired
478 };
479} ////////////////////////////////////////////////////////////////////////////////
480
481/**
482 * MenuItems
483 *
484 * A low-level wrapper for menu items. Compose it with `MenuPopover` for more
485 * control over the nested components and their rendered DOM nodes, or if you
486 * need to nest arbitrary components between the outer wrapper and your list.
487 *
488 * @see Docs https://reacttraining.com/reach-ui/menu-button#menuitems
489 */
490
491
492var MenuItems = /*#__PURE__*/forwardRefWithAs(function MenuItems(_ref5, forwardedRef) {
493 var _ref5$as = _ref5.as,
494 Comp = _ref5$as === void 0 ? "div" : _ref5$as,
495 children = _ref5.children,
496 id = _ref5.id,
497 onKeyDown = _ref5.onKeyDown,
498 props = _objectWithoutPropertiesLoose(_ref5, ["as", "children", "id", "onKeyDown"]);
499
500 var _useContext3 = useContext(MenuContext),
501 menuId = _useContext3.menuId,
502 dispatch = _useContext3.dispatch,
503 buttonRef = _useContext3.buttonRef,
504 menuRef = _useContext3.menuRef,
505 selectCallbacks = _useContext3.selectCallbacks,
506 _useContext3$state = _useContext3.state,
507 isExpanded = _useContext3$state.isExpanded,
508 buttonId = _useContext3$state.buttonId,
509 selectionIndex = _useContext3$state.selectionIndex,
510 typeaheadQuery = _useContext3$state.typeaheadQuery;
511
512 var menuItems = useDescendants(MenuDescendantContext);
513 var ref = useForkedRef(menuRef, forwardedRef);
514 useEffect(function () {
515 // Respond to user char key input with typeahead
516 var match = findItemFromTypeahead(menuItems, typeaheadQuery);
517
518 if (typeaheadQuery && match != null) {
519 dispatch({
520 type: SELECT_ITEM_AT_INDEX,
521 payload: {
522 index: match
523 }
524 });
525 }
526
527 var timeout = window.setTimeout(function () {
528 return typeaheadQuery && dispatch({
529 type: SEARCH_FOR_ITEM,
530 payload: ""
531 });
532 }, 1000);
533 return function () {
534 return window.clearTimeout(timeout);
535 };
536 }, [dispatch, menuItems, typeaheadQuery]);
537 var prevMenuItemsLength = usePrevious(menuItems.length);
538 var prevSelected = usePrevious(menuItems[selectionIndex]);
539 var prevSelectionIndex = usePrevious(selectionIndex);
540 useEffect(function () {
541 if (selectionIndex > menuItems.length - 1) {
542 // If for some reason our selection index is larger than our possible
543 // index range (let's say the last item is selected and the list
544 // dynamically updates), we need to select the last item in the list.
545 dispatch({
546 type: SELECT_ITEM_AT_INDEX,
547 payload: {
548 index: menuItems.length - 1
549 }
550 });
551 } else if ( // Checks if
552 // - menu length has changed
553 // - selection index has not changed BUT selected item has changed
554 //
555 // This prevents any dynamic adding/removing of menu items from actually
556 // changing a user's expected selection.
557 prevMenuItemsLength !== menuItems.length && selectionIndex > -1 && prevSelected && prevSelectionIndex === selectionIndex && menuItems[selectionIndex] !== prevSelected) {
558 dispatch({
559 type: SELECT_ITEM_AT_INDEX,
560 payload: {
561 index: menuItems.findIndex(function (i) {
562 return i.key === prevSelected.key;
563 })
564 }
565 });
566 }
567 }, [dispatch, menuItems, prevMenuItemsLength, prevSelected, prevSelectionIndex, selectionIndex]);
568 var handleKeyDown = wrapEvent(function handleKeyDown(event) {
569 var key = event.key;
570
571 if (!isExpanded) {
572 return;
573 }
574
575 switch (key) {
576 case "Enter":
577 case " ":
578 var selected = menuItems.find(function (item) {
579 return item.index === selectionIndex;
580 }); // For links, the Enter key will trigger a click by default, but for
581 // consistent behavior across menu items we'll trigger a click when
582 // the spacebar is pressed.
583
584 if (selected) {
585 if (selected.isLink && selected.element) {
586 selected.element.click();
587 } else {
588 event.preventDefault(); // Focus the button first by default when an item is selected.
589 // We fire the onSelect callback next so the app can manage
590 // focus if needed.
591
592 focus(buttonRef.current);
593 selectCallbacks.current[selected.index] && selectCallbacks.current[selected.index]();
594 dispatch({
595 type: CLICK_MENU_ITEM
596 });
597 }
598 }
599
600 break;
601
602 case "Escape":
603 focus(buttonRef.current);
604 dispatch({
605 type: CLOSE_MENU,
606 payload: {
607 buttonRef: buttonRef
608 }
609 });
610 break;
611
612 case "Tab":
613 // prevent leaving
614 event.preventDefault();
615 break;
616
617 default:
618 // Check if a user is typing some char keys and respond by setting
619 // the query state.
620 if (isString(key) && key.length === 1) {
621 var query = typeaheadQuery + key.toLowerCase();
622 dispatch({
623 type: SEARCH_FOR_ITEM,
624 payload: query
625 });
626 }
627
628 break;
629 }
630 }, useDescendantKeyDown(MenuDescendantContext, {
631 currentIndex: selectionIndex,
632 orientation: "vertical",
633 rotate: false,
634 callback: function callback(index) {
635 dispatch({
636 type: SELECT_ITEM_AT_INDEX,
637 payload: {
638 index: index
639 }
640 });
641 },
642 key: "index"
643 }));
644 return (// TODO: Should probably file a but in jsx-a11y, but this is correct
645 // according to https://www.w3.org/TR/wai-aria-practices-1.2/examples/menu-button/menu-button-actions-active-descendant.html
646 // eslint-disable-next-line jsx-a11y/aria-activedescendant-has-tabindex
647 React.createElement(Comp // Refers to the descendant menuitem element that is visually indicated
648 // as focused.
649 // https://www.w3.org/TR/wai-aria-practices-1.2/examples/menu-button/menu-button-actions-active-descendant.html
650 , Object.assign({
651 "aria-activedescendant": useMenuItemId(selectionIndex) || undefined,
652 "aria-labelledby": buttonId || undefined,
653 // The element that contains the menu items displayed by activating the
654 // button has role menu.
655 // https://www.w3.org/TR/wai-aria-practices-1.2/#menubutton
656 role: "menu",
657 tabIndex: -1
658 }, props, {
659 ref: ref,
660 "data-reach-menu-items": "",
661 id: menuId,
662 onKeyDown: wrapEvent(onKeyDown, handleKeyDown)
663 }), children)
664 );
665});
666
667if (process.env.NODE_ENV !== "production") {
668 MenuItems.displayName = "MenuItems";
669 MenuItems.propTypes = {
670 children: PropTypes.node
671 };
672} ////////////////////////////////////////////////////////////////////////////////
673
674/**
675 * MenuLink
676 *
677 * Handles linking to a different page in the menu. By default it renders `<a>`,
678 * but also accepts any other kind of Link as long as the `Link` uses the
679 * `React.forwardRef` API.
680 *
681 * Must be a direct child of a `<MenuList>`.
682 *
683 * @see Docs https://reacttraining.com/reach-ui/menu-button#menulink
684 */
685
686
687var MenuLink = /*#__PURE__*/forwardRefWithAs(function MenuLink(_ref6, forwardedRef) {
688 var _ref6$as = _ref6.as,
689 as = _ref6$as === void 0 ? "a" : _ref6$as,
690 component = _ref6.component,
691 onSelect = _ref6.onSelect,
692 props = _objectWithoutPropertiesLoose(_ref6, ["as", "component", "onSelect"]);
693
694 if (component) {
695 console.warn("[@reach/menu-button]: Please use the `as` prop instead of `component`.");
696 }
697
698 return React.createElement("div", {
699 role: "none",
700 tabIndex: -1
701 }, React.createElement(MenuItemImpl, Object.assign({}, props, {
702 ref: forwardedRef,
703 "data-reach-menu-link": "",
704 as: as,
705 isLink: true,
706 onSelect: onSelect || noop
707 })));
708});
709
710if (process.env.NODE_ENV !== "production") {
711 MenuLink.displayName = "MenuLink";
712 MenuLink.propTypes = {
713 as: PropTypes.any,
714 component: PropTypes.any
715 };
716} ////////////////////////////////////////////////////////////////////////////////
717
718/**
719 * MenuList
720 *
721 * Wraps a DOM element that renders the menu items. Must be rendered inside of
722 * a `<Menu>`.
723 *
724 * @see Docs https://reacttraining.com/reach-ui/menu-button#menulist
725 */
726
727
728var MenuList = /*#__PURE__*/forwardRef(function MenuList(_ref7, forwardedRef) {
729 var _ref7$portal = _ref7.portal,
730 portal = _ref7$portal === void 0 ? true : _ref7$portal,
731 props = _objectWithoutPropertiesLoose(_ref7, ["portal"]);
732
733 return React.createElement(MenuPopover, {
734 portal: portal
735 }, React.createElement(MenuItems, Object.assign({}, props, {
736 ref: forwardedRef,
737 "data-reach-menu-list": ""
738 })));
739});
740
741if (process.env.NODE_ENV !== "production") {
742 MenuList.displayName = "MenuList";
743 MenuList.propTypes = {
744 children: PropTypes.node.isRequired
745 };
746} ////////////////////////////////////////////////////////////////////////////////
747
748/**
749 * MenuPopover
750 *
751 * A low-level wrapper for the popover that appears when a menu button is open.
752 * You can compose it with `MenuItems` for more control over the nested
753 * components and their rendered DOM nodes, or if you need to nest arbitrary
754 * components between the outer wrapper and your list.
755 *
756 * @see Docs https://reacttraining.com/reach-ui/menu-button#menupopover
757 */
758
759
760var MenuPopover = /*#__PURE__*/forwardRef(function MenuPopover(_ref8, forwardedRef) {
761 var children = _ref8.children,
762 _ref8$portal = _ref8.portal,
763 portal = _ref8$portal === void 0 ? true : _ref8$portal,
764 position = _ref8.position,
765 props = _objectWithoutPropertiesLoose(_ref8, ["children", "portal", "position"]);
766
767 var _useContext4 = useContext(MenuContext),
768 buttonRef = _useContext4.buttonRef,
769 buttonClickedRef = _useContext4.buttonClickedRef,
770 dispatch = _useContext4.dispatch,
771 menuRef = _useContext4.menuRef,
772 popoverRef = _useContext4.popoverRef,
773 isExpanded = _useContext4.state.isExpanded;
774
775 var ref = useForkedRef(popoverRef, forwardedRef);
776 useEffect(function () {
777 function listener(event) {
778 if (buttonClickedRef.current) {
779 buttonClickedRef.current = false;
780 } else {
781 // We on want to close only if focus rests outside the menu
782 if (isExpanded && popoverRef.current) {
783 if (!popoverRef.current.contains(event.target)) {
784 dispatch({
785 type: CLOSE_MENU,
786 payload: {
787 buttonRef: buttonRef
788 }
789 });
790 }
791 }
792 }
793 }
794
795 window.addEventListener("mousedown", listener);
796 return function () {
797 window.removeEventListener("mousedown", listener);
798 };
799 }, [buttonClickedRef, buttonRef, dispatch, isExpanded, menuRef, popoverRef]);
800
801 var commonProps = _extends({
802 ref: ref,
803 // TODO: remove in 1.0
804 "data-reach-menu": "",
805 "data-reach-menu-popover": "",
806 hidden: !isExpanded,
807 children: children
808 }, props);
809
810 return portal ? React.createElement(Popover, Object.assign({}, commonProps, {
811 targetRef: buttonRef,
812 position: position
813 })) : React.createElement("div", Object.assign({}, commonProps));
814});
815
816if (process.env.NODE_ENV !== "production") {
817 MenuPopover.displayName = "MenuPopover";
818 MenuPopover.propTypes = {
819 children: PropTypes.node
820 };
821} ////////////////////////////////////////////////////////////////////////////////
822
823/**
824 * A hook that exposes data for a given `Menu` component to its descendants.
825 *
826 * @see Docs https://reacttraining.com/reach-ui/menu-button#usemenubuttoncontext
827 */
828
829
830function useMenuButtonContext() {
831 var _useContext5 = useContext(MenuContext),
832 isExpanded = _useContext5.state.isExpanded;
833
834 return useMemo(function () {
835 return {
836 isExpanded: isExpanded
837 };
838 }, [isExpanded]);
839} ////////////////////////////////////////////////////////////////////////////////
840
841/**
842 * When a user's typed input matches the string displayed in a menu item, it is
843 * expected that the matching menu item is selected. This is our matching
844 * function.
845 */
846
847function findItemFromTypeahead(items, string) {
848 if (string === void 0) {
849 string = "";
850 }
851
852 if (!string) {
853 return null;
854 }
855
856 var found = items.find(function (_ref9) {
857 var _element$dataset, _element$dataset$valu;
858
859 var element = _ref9.element;
860 return element === null || element === void 0 ? void 0 : (_element$dataset = element.dataset) === null || _element$dataset === void 0 ? void 0 : (_element$dataset$valu = _element$dataset.valuetext) === null || _element$dataset$valu === void 0 ? void 0 : _element$dataset$valu.toLowerCase().startsWith(string);
861 });
862 return found ? items.indexOf(found) : null;
863}
864
865function useMenuItemId(index) {
866 var _useContext6 = useContext(MenuContext),
867 menuId = _useContext6.menuId;
868
869 return index != null && index > -1 ? makeId("option-" + index, menuId) : undefined;
870}
871
872function isRightClick(nativeEvent) {
873 return nativeEvent.which === 3 || nativeEvent.button === 2;
874}
875
876function focus(element) {
877 element && element.focus();
878}
879
880function reducer(state, action) {
881 if (action === void 0) {
882 action = {};
883 }
884
885 switch (action.type) {
886 case CLICK_MENU_ITEM:
887 return _extends({}, state, {
888 isExpanded: false,
889 selectionIndex: -1
890 });
891
892 case CLOSE_MENU:
893 return _extends({}, state, {
894 isExpanded: false,
895 selectionIndex: -1
896 });
897
898 case OPEN_MENU_AT_FIRST_ITEM:
899 return _extends({}, state, {
900 isExpanded: true,
901 selectionIndex: 0
902 });
903
904 case OPEN_MENU_CLEARED:
905 return _extends({}, state, {
906 isExpanded: true,
907 selectionIndex: -1
908 });
909
910 case SELECT_ITEM_AT_INDEX:
911 if (action.payload.index >= 0) {
912 return _extends({}, state, {
913 selectionIndex: action.payload.max != null ? Math.min(Math.max(action.payload.index, 0), action.payload.max) : Math.max(action.payload.index, 0)
914 });
915 }
916
917 return state;
918
919 case CLEAR_SELECTION_INDEX:
920 return _extends({}, state, {
921 selectionIndex: -1
922 });
923
924 case SET_BUTTON_ID:
925 return _extends({}, state, {
926 buttonId: action.payload
927 });
928
929 case SEARCH_FOR_ITEM:
930 if (typeof action.payload !== "undefined") {
931 return _extends({}, state, {
932 typeaheadQuery: action.payload
933 });
934 }
935
936 return state;
937
938 default:
939 return state;
940 }
941}
942
943export { Menu, MenuButton, MenuItem, MenuItems, MenuLink, MenuList, MenuPopover, useMenuButtonContext };
944//# sourceMappingURL=menu-button.esm.js.map