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 == null || onHighlightChange(event, value, reason);
|
78 | if (focusManagement === 'DOM' && value != null && (reason === ListActionTypes.itemClick || reason === ListActionTypes.keyDown || reason === ListActionTypes.textNavigation)) {
|
79 | var _getItemDomElement;
|
80 | getItemDomElement == null || (_getItemDomElement = getItemDomElement(value)) == null || _getItemDomElement.focus();
|
81 | }
|
82 | }, [getItemDomElement, onHighlightChange, focusManagement]);
|
83 | const stateComparers = React.useMemo(() => ({
|
84 | highlightedValue: itemComparer,
|
85 | selectedValues: (valuesArray1, valuesArray2) => areArraysEqual(valuesArray1, valuesArray2, itemComparer)
|
86 | }), [itemComparer]);
|
87 |
|
88 |
|
89 | const handleStateChange = React.useCallback((event, field, value, reason, state) => {
|
90 | onStateChange == null || onStateChange(event, field, value, reason, state);
|
91 | switch (field) {
|
92 | case 'highlightedValue':
|
93 | handleHighlightChange(event, value, reason);
|
94 | break;
|
95 | case 'selectedValues':
|
96 | onChange == null || onChange(event, value, reason);
|
97 | break;
|
98 | default:
|
99 | break;
|
100 | }
|
101 | }, [handleHighlightChange, onChange, onStateChange]);
|
102 |
|
103 |
|
104 |
|
105 | const listActionContext = React.useMemo(() => {
|
106 | return {
|
107 | disabledItemsFocusable,
|
108 | disableListWrap,
|
109 | focusManagement,
|
110 | isItemDisabled,
|
111 | itemComparer,
|
112 | items,
|
113 | getItemAsString,
|
114 | onHighlightChange: handleHighlightChange,
|
115 | orientation,
|
116 | pageSize,
|
117 | selectionMode,
|
118 | stateComparers
|
119 | };
|
120 | }, [disabledItemsFocusable, disableListWrap, focusManagement, isItemDisabled, itemComparer, items, getItemAsString, handleHighlightChange, orientation, pageSize, selectionMode, stateComparers]);
|
121 | const initialState = getInitialState();
|
122 | const reducer = externalReducer != null ? externalReducer : defaultReducer;
|
123 | const actionContext = React.useMemo(() => _extends({}, reducerActionContext, listActionContext), [reducerActionContext, listActionContext]);
|
124 | const [state, dispatch] = useControllableReducer({
|
125 | reducer,
|
126 | actionContext,
|
127 | initialState: initialState,
|
128 | controlledProps,
|
129 | stateComparers,
|
130 | onStateChange: handleStateChange,
|
131 | componentName
|
132 | });
|
133 | const {
|
134 | highlightedValue,
|
135 | selectedValues
|
136 | } = state;
|
137 | const handleTextNavigation = useTextNavigation((searchString, event) => dispatch({
|
138 | type: ListActionTypes.textNavigation,
|
139 | event,
|
140 | searchString
|
141 | }));
|
142 | const previousItems = React.useRef([]);
|
143 | React.useEffect(() => {
|
144 |
|
145 |
|
146 | if (areArraysEqual(previousItems.current, items, itemComparer)) {
|
147 | return;
|
148 | }
|
149 | dispatch({
|
150 | type: ListActionTypes.itemsChange,
|
151 | event: null,
|
152 | items,
|
153 | previousItems: previousItems.current
|
154 | });
|
155 | previousItems.current = items;
|
156 | onItemsChange == null || onItemsChange(items);
|
157 | }, [items, itemComparer, dispatch, onItemsChange]);
|
158 | const createHandleKeyDown = externalHandlers => event => {
|
159 | var _externalHandlers$onK;
|
160 | (_externalHandlers$onK = externalHandlers.onKeyDown) == null || _externalHandlers$onK.call(externalHandlers, event);
|
161 | if (event.defaultMuiPrevented) {
|
162 | return;
|
163 | }
|
164 | const keysToPreventDefault = ['Home', 'End', 'PageUp', 'PageDown'];
|
165 | if (orientation === 'vertical') {
|
166 | keysToPreventDefault.push('ArrowUp', 'ArrowDown');
|
167 | } else {
|
168 | keysToPreventDefault.push('ArrowLeft', 'ArrowRight');
|
169 | }
|
170 | if (focusManagement === 'activeDescendant') {
|
171 |
|
172 |
|
173 |
|
174 |
|
175 | keysToPreventDefault.push(' ', 'Enter');
|
176 | }
|
177 | if (keysToPreventDefault.includes(event.key)) {
|
178 | event.preventDefault();
|
179 | }
|
180 | dispatch({
|
181 | type: ListActionTypes.keyDown,
|
182 | key: event.key,
|
183 | event
|
184 | });
|
185 | handleTextNavigation(event);
|
186 | };
|
187 | const createHandleBlur = externalHandlers => event => {
|
188 | var _externalHandlers$onB, _listRef$current;
|
189 | (_externalHandlers$onB = externalHandlers.onBlur) == null || _externalHandlers$onB.call(externalHandlers, event);
|
190 | if (event.defaultMuiPrevented) {
|
191 | return;
|
192 | }
|
193 | if ((_listRef$current = listRef.current) != null && _listRef$current.contains(event.relatedTarget)) {
|
194 |
|
195 | return;
|
196 | }
|
197 | dispatch({
|
198 | type: ListActionTypes.blur,
|
199 | event
|
200 | });
|
201 | };
|
202 | const getRootProps = (externalProps = {}) => {
|
203 | const externalEventHandlers = extractEventHandlers(externalProps);
|
204 | return _extends({}, externalProps, {
|
205 | 'aria-activedescendant': focusManagement === 'activeDescendant' && highlightedValue != null ? getItemId(highlightedValue) : undefined,
|
206 | tabIndex: focusManagement === 'DOM' ? -1 : 0,
|
207 | ref: handleRef
|
208 | }, externalEventHandlers, {
|
209 | onBlur: createHandleBlur(externalEventHandlers),
|
210 | onKeyDown: createHandleKeyDown(externalEventHandlers)
|
211 | });
|
212 | };
|
213 | const getItemState = React.useCallback(item => {
|
214 | const selected = (selectedValues != null ? selectedValues : []).some(value => value != null && itemComparer(item, value));
|
215 | const highlighted = highlightedValue != null && itemComparer(item, highlightedValue);
|
216 | const focusable = focusManagement === 'DOM';
|
217 | return {
|
218 | focusable,
|
219 | highlighted,
|
220 | selected
|
221 | };
|
222 | }, [itemComparer, selectedValues, highlightedValue, focusManagement]);
|
223 | const contextValue = React.useMemo(() => ({
|
224 | dispatch,
|
225 | getItemState
|
226 | }), [dispatch, getItemState]);
|
227 | React.useDebugValue({
|
228 | state
|
229 | });
|
230 | return {
|
231 | contextValue,
|
232 | dispatch,
|
233 | getRootProps,
|
234 | rootRef: handleRef,
|
235 | state
|
236 | };
|
237 | }
|
238 | export { useList }; |
\ | No newline at end of file |