UNPKG

12.4 kBJavaScriptView Raw
1"use strict";
2
3var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
4Object.defineProperty(exports, "__esModule", {
5 value: true
6});
7exports.default = void 0;
8var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
9var React = _interopRequireWildcard(require("react"));
10var _utils = require("@mui/utils");
11var _listActions = require("./listActions.types");
12var _listReducer = _interopRequireDefault(require("./listReducer"));
13var _useListChangeNotifiers = _interopRequireDefault(require("./useListChangeNotifiers"));
14var _useControllableReducer = _interopRequireDefault(require("../utils/useControllableReducer"));
15var _areArraysEqual = _interopRequireDefault(require("../utils/areArraysEqual"));
16var _useLatest = _interopRequireDefault(require("../utils/useLatest"));
17var _useTextNavigation = _interopRequireDefault(require("../utils/useTextNavigation"));
18function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
19function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
20const EMPTY_OBJECT = {};
21const NOOP = () => {};
22const defaultItemComparer = (optionA, optionB) => optionA === optionB;
23const defaultIsItemDisabled = () => false;
24const defaultItemStringifier = item => typeof item === 'string' ? item : String(item);
25const defaultGetInitialState = () => ({
26 highlightedValue: null,
27 selectedValues: []
28});
29
30/**
31 * The useList is a lower-level utility that is used to build list-like components.
32 * It's used to manage the state of the list and its items.
33 *
34 * Supports highlighting a single item and selecting an arbitrary number of items.
35 *
36 * The state of the list is managed by a controllable reducer - that is a reducer that can have its state
37 * controlled from outside.
38 *
39 * By default, the state consists of `selectedValues` and `highlightedValue` but can be extended by the caller of the hook.
40 * Also the actions that can be dispatched and the reducer function can be defined externally.
41 *
42 * @template ItemValue The type of the item values.
43 * @template State The type of the list state. This should be a subtype of `ListState<ItemValue>`.
44 * @template CustomAction The type of the actions that can be dispatched (besides the standard ListAction).
45 * @template CustomActionContext The shape of additional properties that will be added to actions when dispatched.
46 *
47 * @ignore - internal hook.
48 */
49function useList(params) {
50 const {
51 controlledProps = EMPTY_OBJECT,
52 disabledItemsFocusable = false,
53 disableListWrap = false,
54 focusManagement = 'activeDescendant',
55 getInitialState = defaultGetInitialState,
56 getItemDomElement,
57 getItemId,
58 isItemDisabled = defaultIsItemDisabled,
59 rootRef: externalListRef,
60 onStateChange = NOOP,
61 items,
62 itemComparer = defaultItemComparer,
63 getItemAsString = defaultItemStringifier,
64 onChange,
65 onHighlightChange,
66 orientation = 'vertical',
67 pageSize = 5,
68 reducerActionContext = EMPTY_OBJECT,
69 selectionMode = 'single',
70 stateReducer: externalReducer
71 } = params;
72 if (process.env.NODE_ENV !== 'production') {
73 if (focusManagement === 'DOM' && getItemDomElement == null) {
74 throw new Error('useList: The `getItemDomElement` prop is required when using the `DOM` focus management.');
75 }
76 if (focusManagement === 'activeDescendant' && getItemId == null) {
77 throw new Error('useList: The `getItemId` prop is required when using the `activeDescendant` focus management.');
78 }
79 }
80 const listRef = React.useRef(null);
81 const handleRef = (0, _utils.unstable_useForkRef)(externalListRef, listRef);
82 const handleHighlightChange = React.useCallback((event, value, reason) => {
83 onHighlightChange == null ? void 0 : onHighlightChange(event, value, reason);
84 if (focusManagement === 'DOM' && value != null && (reason === _listActions.ListActionTypes.itemClick || reason === _listActions.ListActionTypes.keyDown || reason === _listActions.ListActionTypes.textNavigation)) {
85 var _getItemDomElement;
86 getItemDomElement == null ? void 0 : (_getItemDomElement = getItemDomElement(value)) == null ? void 0 : _getItemDomElement.focus();
87 }
88 }, [getItemDomElement, onHighlightChange, focusManagement]);
89 const stateComparers = React.useMemo(() => ({
90 highlightedValue: itemComparer,
91 selectedValues: (valuesArray1, valuesArray2) => (0, _areArraysEqual.default)(valuesArray1, valuesArray2, itemComparer)
92 }), [itemComparer]);
93
94 // This gets called whenever a reducer changes the state.
95 const handleStateChange = React.useCallback((event, field, value, reason, state) => {
96 onStateChange == null ? void 0 : onStateChange(event, field, value, reason, state);
97 switch (field) {
98 case 'highlightedValue':
99 handleHighlightChange(event, value, reason);
100 break;
101 case 'selectedValues':
102 onChange == null ? void 0 : onChange(event, value, reason);
103 break;
104 default:
105 break;
106 }
107 }, [handleHighlightChange, onChange, onStateChange]);
108
109 // The following object is added to each action when it's dispatched.
110 // It's accessible in the reducer via the `action.context` field.
111 const listActionContext = React.useMemo(() => {
112 return {
113 disabledItemsFocusable,
114 disableListWrap,
115 focusManagement,
116 isItemDisabled,
117 itemComparer,
118 items,
119 getItemAsString,
120 onHighlightChange: handleHighlightChange,
121 orientation,
122 pageSize,
123 selectionMode,
124 stateComparers
125 };
126 }, [disabledItemsFocusable, disableListWrap, focusManagement, isItemDisabled, itemComparer, items, getItemAsString, handleHighlightChange, orientation, pageSize, selectionMode, stateComparers]);
127 const initialState = getInitialState();
128 const reducer = externalReducer != null ? externalReducer : _listReducer.default;
129 const actionContext = React.useMemo(() => (0, _extends2.default)({}, reducerActionContext, listActionContext), [reducerActionContext, listActionContext]);
130 const [state, dispatch] = (0, _useControllableReducer.default)({
131 reducer,
132 actionContext,
133 initialState: initialState,
134 controlledProps,
135 stateComparers,
136 onStateChange: handleStateChange
137 });
138 const {
139 highlightedValue,
140 selectedValues
141 } = state;
142 const handleTextNavigation = (0, _useTextNavigation.default)((searchString, event) => dispatch({
143 type: _listActions.ListActionTypes.textNavigation,
144 event,
145 searchString
146 }));
147
148 // introducing refs to avoid recreating the getItemState function on each change.
149 const latestSelectedValues = (0, _useLatest.default)(selectedValues);
150 const latestHighlightedValue = (0, _useLatest.default)(highlightedValue);
151 const previousItems = React.useRef([]);
152 React.useEffect(() => {
153 // Whenever the `items` object changes, we need to determine if the actual items changed.
154 // If they did, we need to dispatch an `itemsChange` action, so the selected/highlighted state is updated.
155 if ((0, _areArraysEqual.default)(previousItems.current, items, itemComparer)) {
156 return;
157 }
158 dispatch({
159 type: _listActions.ListActionTypes.itemsChange,
160 event: null,
161 items,
162 previousItems: previousItems.current
163 });
164 previousItems.current = items;
165 }, [items, itemComparer, dispatch]);
166
167 // Subitems are notified of changes to the highlighted and selected values.
168 // This is not done via context because we don't want to trigger a re-render of all the subitems each time any of them changes state.
169 // Instead, we use a custom message bus to publish messages about changes.
170 // On the child component, we use a custom hook to subscribe to these messages and re-render only when the value they care about changes.
171 const {
172 notifySelectionChanged,
173 notifyHighlightChanged,
174 registerHighlightChangeHandler,
175 registerSelectionChangeHandler
176 } = (0, _useListChangeNotifiers.default)();
177 React.useEffect(() => {
178 notifySelectionChanged(selectedValues);
179 }, [selectedValues, notifySelectionChanged]);
180 React.useEffect(() => {
181 notifyHighlightChanged(highlightedValue);
182 }, [highlightedValue, notifyHighlightChanged]);
183 const createHandleKeyDown = other => event => {
184 var _other$onKeyDown;
185 (_other$onKeyDown = other.onKeyDown) == null ? void 0 : _other$onKeyDown.call(other, event);
186 if (event.defaultMuiPrevented) {
187 return;
188 }
189 const keysToPreventDefault = ['Home', 'End', 'PageUp', 'PageDown'];
190 if (orientation === 'vertical') {
191 keysToPreventDefault.push('ArrowUp', 'ArrowDown');
192 } else {
193 keysToPreventDefault.push('ArrowLeft', 'ArrowRight');
194 }
195 if (focusManagement === 'activeDescendant') {
196 // When the child element is focused using the activeDescendant attribute,
197 // the list handles keyboard events on its behalf.
198 // We have to `preventDefault()` is this case to prevent the browser from
199 // scrolling the view when space is pressed or submitting forms when enter is pressed.
200 keysToPreventDefault.push(' ', 'Enter');
201 }
202 if (keysToPreventDefault.includes(event.key)) {
203 event.preventDefault();
204 }
205 dispatch({
206 type: _listActions.ListActionTypes.keyDown,
207 key: event.key,
208 event
209 });
210 handleTextNavigation(event);
211 };
212 const createHandleBlur = other => event => {
213 var _other$onBlur, _listRef$current;
214 (_other$onBlur = other.onBlur) == null ? void 0 : _other$onBlur.call(other, event);
215 if (event.defaultMuiPrevented) {
216 return;
217 }
218 if ((_listRef$current = listRef.current) != null && _listRef$current.contains(event.relatedTarget)) {
219 // focus remains within the list
220 return;
221 }
222 dispatch({
223 type: _listActions.ListActionTypes.blur,
224 event
225 });
226 };
227 const getRootProps = (otherHandlers = {}) => {
228 return (0, _extends2.default)({}, otherHandlers, {
229 'aria-activedescendant': focusManagement === 'activeDescendant' && highlightedValue != null ? getItemId(highlightedValue) : undefined,
230 onBlur: createHandleBlur(otherHandlers),
231 onKeyDown: createHandleKeyDown(otherHandlers),
232 tabIndex: focusManagement === 'DOM' ? -1 : 0,
233 ref: handleRef
234 });
235 };
236 const getItemState = React.useCallback(item => {
237 var _latestSelectedValues;
238 const index = items.findIndex(i => itemComparer(i, item));
239 const selected = ((_latestSelectedValues = latestSelectedValues.current) != null ? _latestSelectedValues : []).some(value => value != null && itemComparer(item, value));
240 const disabled = isItemDisabled(item, index);
241 const highlighted = latestHighlightedValue.current != null && itemComparer(item, latestHighlightedValue.current);
242 const focusable = focusManagement === 'DOM';
243 return {
244 disabled,
245 focusable,
246 highlighted,
247 index,
248 selected
249 };
250 }, [items, isItemDisabled, itemComparer, latestSelectedValues, latestHighlightedValue, focusManagement]);
251 const contextValue = React.useMemo(() => ({
252 dispatch,
253 getItemState,
254 registerHighlightChangeHandler,
255 registerSelectionChangeHandler
256 }), [dispatch, getItemState, registerHighlightChangeHandler, registerSelectionChangeHandler]);
257 React.useDebugValue({
258 state
259 });
260 return {
261 contextValue,
262 dispatch,
263 getRootProps,
264 rootRef: handleRef,
265 state
266 };
267}
268var _default = useList;
269exports.default = _default;
\No newline at end of file