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