1 | 'use client';
|
2 |
|
3 | import _extends from "@babel/runtime/helpers/esm/extends";
|
4 | import * as React from 'react';
|
5 | import { unstable_useForkRef as useForkRef } from '@mui/utils';
|
6 | import { ListActionTypes } from './listActions.types';
|
7 | import { listReducer as defaultReducer } from './listReducer';
|
8 | import { useControllableReducer } from '../utils/useControllableReducer';
|
9 | import { areArraysEqual } from '../utils/areArraysEqual';
|
10 | import { useTextNavigation } from '../utils/useTextNavigation';
|
11 | import { extractEventHandlers } from '../utils/extractEventHandlers';
|
12 | const EMPTY_OBJECT = {};
|
13 | const NOOP = () => {};
|
14 | const defaultItemComparer = (optionA, optionB) => optionA === optionB;
|
15 | const defaultIsItemDisabled = () => false;
|
16 | const defaultItemStringifier = item => typeof item === 'string' ? item : String(item);
|
17 | const defaultGetInitialState = () => ({
|
18 | highlightedValue: null,
|
19 | selectedValues: []
|
20 | });
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | function useList(params) {
|
42 | const {
|
43 | controlledProps = EMPTY_OBJECT,
|
44 | disabledItemsFocusable = false,
|
45 | disableListWrap = false,
|
46 | focusManagement = 'activeDescendant',
|
47 | getInitialState = defaultGetInitialState,
|
48 | getItemDomElement,
|
49 | getItemId,
|
50 | isItemDisabled = defaultIsItemDisabled,
|
51 | rootRef: externalListRef,
|
52 | onStateChange = NOOP,
|
53 | items,
|
54 | itemComparer = defaultItemComparer,
|
55 | getItemAsString = defaultItemStringifier,
|
56 | onChange,
|
57 | onHighlightChange,
|
58 | onItemsChange,
|
59 | orientation = 'vertical',
|
60 | pageSize = 5,
|
61 | reducerActionContext = EMPTY_OBJECT,
|
62 | selectionMode = 'single',
|
63 | stateReducer: externalReducer,
|
64 | componentName = 'useList'
|
65 | } = params;
|
66 | if (process.env.NODE_ENV !== 'production') {
|
67 | if (focusManagement === 'DOM' && getItemDomElement == null) {
|
68 | throw new Error('useList: The `getItemDomElement` prop is required when using the `DOM` focus management.');
|
69 | }
|
70 | if (focusManagement === 'activeDescendant' && getItemId == null) {
|
71 | throw new Error('useList: The `getItemId` prop is required when using the `activeDescendant` focus management.');
|
72 | }
|
73 | }
|
74 | const listRef = React.useRef(null);
|
75 | const handleRef = useForkRef(externalListRef, listRef);
|
76 | const handleHighlightChange = React.useCallback((event, value, reason) => {
|
77 | onHighlightChange?.(event, value, reason);
|
78 | if (focusManagement === 'DOM' && value != null && (reason === ListActionTypes.itemClick || reason === ListActionTypes.keyDown || reason === ListActionTypes.textNavigation)) {
|
79 | getItemDomElement?.(value)?.focus();
|
80 | }
|
81 | }, [getItemDomElement, onHighlightChange, focusManagement]);
|
82 | const stateComparers = React.useMemo(() => ({
|
83 | highlightedValue: itemComparer,
|
84 | selectedValues: (valuesArray1, valuesArray2) => areArraysEqual(valuesArray1, valuesArray2, itemComparer)
|
85 | }), [itemComparer]);
|
86 |
|
87 |
|
88 | const handleStateChange = React.useCallback((event, field, value, reason, state) => {
|
89 | onStateChange?.(event, field, value, reason, state);
|
90 | switch (field) {
|
91 | case 'highlightedValue':
|
92 | handleHighlightChange(event, value, reason);
|
93 | break;
|
94 | case 'selectedValues':
|
95 | onChange?.(event, value, reason);
|
96 | break;
|
97 | default:
|
98 | break;
|
99 | }
|
100 | }, [handleHighlightChange, onChange, onStateChange]);
|
101 |
|
102 |
|
103 |
|
104 | const listActionContext = React.useMemo(() => {
|
105 | return {
|
106 | disabledItemsFocusable,
|
107 | disableListWrap,
|
108 | focusManagement,
|
109 | isItemDisabled,
|
110 | itemComparer,
|
111 | items,
|
112 | getItemAsString,
|
113 | onHighlightChange: handleHighlightChange,
|
114 | orientation,
|
115 | pageSize,
|
116 | selectionMode,
|
117 | stateComparers
|
118 | };
|
119 | }, [disabledItemsFocusable, disableListWrap, focusManagement, isItemDisabled, itemComparer, items, getItemAsString, handleHighlightChange, orientation, pageSize, selectionMode, stateComparers]);
|
120 | const initialState = getInitialState();
|
121 | const reducer = externalReducer ?? defaultReducer;
|
122 | const actionContext = React.useMemo(() => _extends({}, reducerActionContext, listActionContext), [reducerActionContext, listActionContext]);
|
123 | const [state, dispatch] = useControllableReducer({
|
124 | reducer,
|
125 | actionContext,
|
126 | initialState: initialState,
|
127 | controlledProps,
|
128 | stateComparers,
|
129 | onStateChange: handleStateChange,
|
130 | componentName
|
131 | });
|
132 | const {
|
133 | highlightedValue,
|
134 | selectedValues
|
135 | } = state;
|
136 | const handleTextNavigation = useTextNavigation((searchString, event) => dispatch({
|
137 | type: ListActionTypes.textNavigation,
|
138 | event,
|
139 | searchString
|
140 | }));
|
141 | const previousItems = React.useRef([]);
|
142 | React.useEffect(() => {
|
143 |
|
144 |
|
145 | if (areArraysEqual(previousItems.current, items, itemComparer)) {
|
146 | return;
|
147 | }
|
148 | dispatch({
|
149 | type: ListActionTypes.itemsChange,
|
150 | event: null,
|
151 | items,
|
152 | previousItems: previousItems.current
|
153 | });
|
154 | previousItems.current = items;
|
155 | onItemsChange?.(items);
|
156 | }, [items, itemComparer, dispatch, onItemsChange]);
|
157 | const createHandleKeyDown = externalHandlers => event => {
|
158 | externalHandlers.onKeyDown?.(event);
|
159 | if (event.defaultMuiPrevented) {
|
160 | return;
|
161 | }
|
162 | const keysToPreventDefault = ['Home', 'End', 'PageUp', 'PageDown'];
|
163 | if (orientation === 'vertical') {
|
164 | keysToPreventDefault.push('ArrowUp', 'ArrowDown');
|
165 | } else {
|
166 | keysToPreventDefault.push('ArrowLeft', 'ArrowRight');
|
167 | }
|
168 | if (focusManagement === 'activeDescendant') {
|
169 |
|
170 |
|
171 |
|
172 |
|
173 | keysToPreventDefault.push(' ', 'Enter');
|
174 | }
|
175 | if (keysToPreventDefault.includes(event.key)) {
|
176 | event.preventDefault();
|
177 | }
|
178 | dispatch({
|
179 | type: ListActionTypes.keyDown,
|
180 | key: event.key,
|
181 | event
|
182 | });
|
183 | handleTextNavigation(event);
|
184 | };
|
185 | const createHandleBlur = externalHandlers => event => {
|
186 | externalHandlers.onBlur?.(event);
|
187 | if (event.defaultMuiPrevented) {
|
188 | return;
|
189 | }
|
190 | if (listRef.current?.contains(event.relatedTarget)) {
|
191 |
|
192 | return;
|
193 | }
|
194 | dispatch({
|
195 | type: ListActionTypes.blur,
|
196 | event
|
197 | });
|
198 | };
|
199 | const getRootProps = (externalProps = {}) => {
|
200 | const externalEventHandlers = extractEventHandlers(externalProps);
|
201 | return _extends({}, externalProps, {
|
202 | 'aria-activedescendant': focusManagement === 'activeDescendant' && highlightedValue != null ? getItemId(highlightedValue) : undefined,
|
203 | tabIndex: focusManagement === 'DOM' ? -1 : 0,
|
204 | ref: handleRef
|
205 | }, externalEventHandlers, {
|
206 | onBlur: createHandleBlur(externalEventHandlers),
|
207 | onKeyDown: createHandleKeyDown(externalEventHandlers)
|
208 | });
|
209 | };
|
210 | const getItemState = React.useCallback(item => {
|
211 | const selected = (selectedValues ?? []).some(value => value != null && itemComparer(item, value));
|
212 | const highlighted = highlightedValue != null && itemComparer(item, highlightedValue);
|
213 | const focusable = focusManagement === 'DOM';
|
214 | return {
|
215 | focusable,
|
216 | highlighted,
|
217 | selected
|
218 | };
|
219 | }, [itemComparer, selectedValues, highlightedValue, focusManagement]);
|
220 | const contextValue = React.useMemo(() => ({
|
221 | dispatch,
|
222 | getItemState
|
223 | }), [dispatch, getItemState]);
|
224 | React.useDebugValue({
|
225 | state
|
226 | });
|
227 | return {
|
228 | contextValue,
|
229 | dispatch,
|
230 | getRootProps,
|
231 | rootRef: handleRef,
|
232 | state
|
233 | };
|
234 | }
|
235 | export { useList }; |
\ | No newline at end of file |