UNPKG

5.52 kBJavaScriptView Raw
1'use client';
2
3import _extends from "@babel/runtime/helpers/esm/extends";
4import * as React from 'react';
5import { unstable_useForkRef as useForkRef, unstable_useId as useId, unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils';
6import { menuReducer } from './menuReducer';
7import { DropdownContext } from '../useDropdown/DropdownContext';
8import { ListActionTypes, useList } from '../useList';
9import { DropdownActionTypes } from '../useDropdown';
10import { useCompoundParent } from '../useCompound';
11import { combineHooksSlotProps } from '../utils/combineHooksSlotProps';
12import { extractEventHandlers } from '../utils/extractEventHandlers';
13const FALLBACK_MENU_CONTEXT = {
14 dispatch: () => {},
15 popupId: '',
16 registerPopup: () => {},
17 registerTrigger: () => {},
18 state: {
19 open: true,
20 changeReason: null
21 },
22 triggerElement: null
23};
24
25/**
26 *
27 * Demos:
28 *
29 * - [Menu](https://mui.com/base-ui/react-menu/#hooks)
30 *
31 * API:
32 *
33 * - [useMenu API](https://mui.com/base-ui/react-menu/hooks-api/#use-menu)
34 */
35export function useMenu(parameters = {}) {
36 const {
37 listboxRef: listboxRefProp,
38 onItemsChange,
39 id: idParam,
40 disabledItemsFocusable = true,
41 disableListWrap = false,
42 autoFocus = true,
43 componentName = 'useMenu'
44 } = parameters;
45 const rootRef = React.useRef(null);
46 const handleRef = useForkRef(rootRef, listboxRefProp);
47 const listboxId = useId(idParam) ?? '';
48 const {
49 state: {
50 open,
51 changeReason
52 },
53 dispatch: menuDispatch,
54 triggerElement,
55 registerPopup
56 } = React.useContext(DropdownContext) ?? FALLBACK_MENU_CONTEXT;
57
58 // store the initial open state to prevent focus stealing
59 // (the first menu items gets focued only when the menu is opened by the user)
60 const isInitiallyOpen = React.useRef(open);
61 const {
62 subitems,
63 contextValue: compoundComponentContextValue
64 } = useCompoundParent();
65 const subitemKeys = React.useMemo(() => Array.from(subitems.keys()), [subitems]);
66 const getItemDomElement = React.useCallback(itemId => {
67 if (itemId == null) {
68 return null;
69 }
70 return subitems.get(itemId)?.ref.current ?? null;
71 }, [subitems]);
72 const isItemDisabled = React.useCallback(id => subitems?.get(id)?.disabled || false, [subitems]);
73 const getItemAsString = React.useCallback(id => subitems.get(id)?.label || subitems.get(id)?.ref.current?.innerText, [subitems]);
74 const reducerActionContext = React.useMemo(() => ({
75 listboxRef: rootRef
76 }), [rootRef]);
77 const {
78 dispatch: listDispatch,
79 getRootProps: getListRootProps,
80 contextValue: listContextValue,
81 state: {
82 highlightedValue
83 },
84 rootRef: mergedListRef
85 } = useList({
86 disabledItemsFocusable,
87 disableListWrap,
88 focusManagement: 'DOM',
89 getItemDomElement,
90 getInitialState: () => ({
91 selectedValues: [],
92 highlightedValue: null
93 }),
94 isItemDisabled,
95 items: subitemKeys,
96 getItemAsString,
97 rootRef: handleRef,
98 onItemsChange,
99 reducerActionContext,
100 selectionMode: 'none',
101 stateReducer: menuReducer,
102 componentName
103 });
104 useEnhancedEffect(() => {
105 registerPopup(listboxId);
106 }, [listboxId, registerPopup]);
107 useEnhancedEffect(() => {
108 if (open && changeReason?.type === 'keydown' && changeReason.key === 'ArrowUp') {
109 listDispatch({
110 type: ListActionTypes.highlightLast,
111 event: changeReason
112 });
113 }
114 }, [open, changeReason, listDispatch]);
115 React.useEffect(() => {
116 if (open && autoFocus && highlightedValue && !isInitiallyOpen.current) {
117 subitems.get(highlightedValue)?.ref?.current?.focus();
118 }
119 }, [open, autoFocus, highlightedValue, subitems, subitemKeys]);
120 React.useEffect(() => {
121 // set focus to the highlighted item (but prevent stealing focus from other elements on the page)
122 if (rootRef.current?.contains(document.activeElement) && highlightedValue !== null) {
123 subitems?.get(highlightedValue)?.ref.current?.focus();
124 }
125 }, [highlightedValue, subitems]);
126 const createHandleBlur = otherHandlers => event => {
127 otherHandlers.onBlur?.(event);
128 if (event.defaultMuiPrevented) {
129 return;
130 }
131 if (rootRef.current?.contains(event.relatedTarget) || event.relatedTarget === triggerElement) {
132 return;
133 }
134 menuDispatch({
135 type: DropdownActionTypes.blur,
136 event
137 });
138 };
139 const createHandleKeyDown = otherHandlers => event => {
140 otherHandlers.onKeyDown?.(event);
141 if (event.defaultMuiPrevented) {
142 return;
143 }
144 if (event.key === 'Escape') {
145 menuDispatch({
146 type: DropdownActionTypes.escapeKeyDown,
147 event
148 });
149 }
150 };
151 const getOwnListboxHandlers = (otherHandlers = {}) => ({
152 onBlur: createHandleBlur(otherHandlers),
153 onKeyDown: createHandleKeyDown(otherHandlers)
154 });
155 const getListboxProps = (externalProps = {}) => {
156 const getCombinedRootProps = combineHooksSlotProps(getOwnListboxHandlers, getListRootProps);
157 const externalEventHandlers = extractEventHandlers(externalProps);
158 return _extends({}, externalProps, externalEventHandlers, getCombinedRootProps(externalEventHandlers), {
159 id: listboxId,
160 role: 'menu'
161 });
162 };
163 React.useDebugValue({
164 subitems,
165 highlightedValue
166 });
167 return {
168 contextValue: _extends({}, compoundComponentContextValue, listContextValue),
169 dispatch: listDispatch,
170 getListboxProps,
171 highlightedValue,
172 listboxRef: mergedListRef,
173 menuItems: subitems,
174 open,
175 triggerElement
176 };
177}
\No newline at end of file