UNPKG

13.4 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, visuallyHidden as visuallyHiddenStyle } from '@mui/utils';
6import { useButton } from '../useButton';
7import { SelectActionTypes } from './useSelect.types';
8import { ListActionTypes, useList } from '../useList';
9import { defaultOptionStringifier } from './defaultOptionStringifier';
10import { useCompoundParent } from '../useCompound';
11import { extractEventHandlers } from '../utils/extractEventHandlers';
12import { selectReducer } from './selectReducer';
13import { combineHooksSlotProps } from '../utils/combineHooksSlotProps';
14function defaultFormValueProvider(selectedOption) {
15 if (Array.isArray(selectedOption)) {
16 if (selectedOption.length === 0) {
17 return '';
18 }
19 return JSON.stringify(selectedOption.map(o => o.value));
20 }
21 if ((selectedOption == null ? void 0 : selectedOption.value) == null) {
22 return '';
23 }
24 if (typeof selectedOption.value === 'string' || typeof selectedOption.value === 'number') {
25 return selectedOption.value;
26 }
27 return JSON.stringify(selectedOption.value);
28}
29
30/**
31 *
32 * Demos:
33 *
34 * - [Select](https://mui.com/base-ui/react-select/#hooks)
35 *
36 * API:
37 *
38 * - [useSelect API](https://mui.com/base-ui/react-select/hooks-api/#use-select)
39 */
40function useSelect(props) {
41 const {
42 areOptionsEqual,
43 buttonRef: buttonRefProp,
44 defaultOpen = false,
45 defaultValue: defaultValueProp,
46 disabled = false,
47 listboxId: listboxIdProp,
48 listboxRef: listboxRefProp,
49 multiple = false,
50 name,
51 required,
52 onChange,
53 onHighlightChange,
54 onOpenChange,
55 open: openProp,
56 options: optionsParam,
57 getOptionAsString = defaultOptionStringifier,
58 getSerializedValue = defaultFormValueProvider,
59 value: valueProp,
60 componentName = 'useSelect'
61 } = props;
62 const buttonRef = React.useRef(null);
63 const handleButtonRef = useForkRef(buttonRefProp, buttonRef);
64 const listboxRef = React.useRef(null);
65 const listboxId = useId(listboxIdProp);
66 let defaultValue;
67 if (valueProp === undefined && defaultValueProp === undefined) {
68 defaultValue = [];
69 } else if (defaultValueProp !== undefined) {
70 if (multiple) {
71 defaultValue = defaultValueProp;
72 } else {
73 defaultValue = defaultValueProp == null ? [] : [defaultValueProp];
74 }
75 }
76 const value = React.useMemo(() => {
77 if (valueProp !== undefined) {
78 if (multiple) {
79 return valueProp;
80 }
81 return valueProp == null ? [] : [valueProp];
82 }
83 return undefined;
84 }, [valueProp, multiple]);
85 const {
86 subitems,
87 contextValue: compoundComponentContextValue
88 } = useCompoundParent();
89 const options = React.useMemo(() => {
90 if (optionsParam != null) {
91 return new Map(optionsParam.map((option, index) => [option.value, {
92 value: option.value,
93 label: option.label,
94 disabled: option.disabled,
95 ref: /*#__PURE__*/React.createRef(),
96 id: `${listboxId}_${index}`
97 }]));
98 }
99 return subitems;
100 }, [optionsParam, subitems, listboxId]);
101 const handleListboxRef = useForkRef(listboxRefProp, listboxRef);
102 const {
103 getRootProps: getButtonRootProps,
104 active: buttonActive,
105 focusVisible: buttonFocusVisible,
106 rootRef: mergedButtonRef
107 } = useButton({
108 disabled,
109 rootRef: handleButtonRef
110 });
111 const optionValues = React.useMemo(() => Array.from(options.keys()), [options]);
112 const getOptionByValue = React.useCallback(valueToGet => {
113 // This can't be simply `options.get(valueToGet)` because of the `areOptionsEqual` prop.
114 // If it's provided, we assume that the user wants to compare the options by value.
115 if (areOptionsEqual !== undefined) {
116 const similarValue = optionValues.find(optionValue => areOptionsEqual(optionValue, valueToGet));
117 return options.get(similarValue);
118 }
119 return options.get(valueToGet);
120 }, [options, areOptionsEqual, optionValues]);
121 const isItemDisabled = React.useCallback(valueToCheck => {
122 var _option$disabled;
123 const option = getOptionByValue(valueToCheck);
124 return (_option$disabled = option == null ? void 0 : option.disabled) != null ? _option$disabled : false;
125 }, [getOptionByValue]);
126 const stringifyOption = React.useCallback(valueToCheck => {
127 const option = getOptionByValue(valueToCheck);
128 if (!option) {
129 return '';
130 }
131 return getOptionAsString(option);
132 }, [getOptionByValue, getOptionAsString]);
133 const controlledState = React.useMemo(() => ({
134 selectedValues: value,
135 open: openProp
136 }), [value, openProp]);
137 const getItemId = React.useCallback(itemValue => {
138 var _options$get;
139 return (_options$get = options.get(itemValue)) == null ? void 0 : _options$get.id;
140 }, [options]);
141 const handleSelectionChange = React.useCallback((event, newValues) => {
142 if (multiple) {
143 onChange == null || onChange(event, newValues);
144 } else {
145 var _newValues$;
146 onChange == null || onChange(event, (_newValues$ = newValues[0]) != null ? _newValues$ : null);
147 }
148 }, [multiple, onChange]);
149 const handleHighlightChange = React.useCallback((event, newValue) => {
150 onHighlightChange == null || onHighlightChange(event, newValue != null ? newValue : null);
151 }, [onHighlightChange]);
152 const handleStateChange = React.useCallback((event, field, fieldValue) => {
153 if (field === 'open') {
154 onOpenChange == null || onOpenChange(fieldValue);
155 if (fieldValue === false && (event == null ? void 0 : event.type) !== 'blur') {
156 var _buttonRef$current;
157 (_buttonRef$current = buttonRef.current) == null || _buttonRef$current.focus();
158 }
159 }
160 }, [onOpenChange]);
161 const getItemDomElement = React.useCallback(itemId => {
162 var _subitems$get$ref$cur, _subitems$get;
163 if (itemId == null) {
164 return null;
165 }
166 return (_subitems$get$ref$cur = (_subitems$get = subitems.get(itemId)) == null ? void 0 : _subitems$get.ref.current) != null ? _subitems$get$ref$cur : null;
167 }, [subitems]);
168 const useListParameters = {
169 getInitialState: () => {
170 var _defaultValue;
171 return {
172 highlightedValue: null,
173 selectedValues: (_defaultValue = defaultValue) != null ? _defaultValue : [],
174 open: defaultOpen
175 };
176 },
177 getItemId,
178 controlledProps: controlledState,
179 focusManagement: 'DOM',
180 getItemDomElement,
181 itemComparer: areOptionsEqual,
182 isItemDisabled,
183 rootRef: handleListboxRef,
184 onChange: handleSelectionChange,
185 onHighlightChange: handleHighlightChange,
186 onStateChange: handleStateChange,
187 reducerActionContext: React.useMemo(() => ({
188 multiple
189 }), [multiple]),
190 items: optionValues,
191 getItemAsString: stringifyOption,
192 selectionMode: multiple ? 'multiple' : 'single',
193 stateReducer: selectReducer,
194 componentName
195 };
196 const {
197 dispatch,
198 getRootProps: getListboxRootProps,
199 contextValue: listContextValue,
200 state: {
201 open,
202 highlightedValue: highlightedOption,
203 selectedValues: selectedOptions
204 },
205 rootRef: mergedListRootRef
206 } = useList(useListParameters);
207
208 // store the initial open state to prevent focus stealing
209 // (the first option gets focused only when the select is opened by the user)
210 const isInitiallyOpen = React.useRef(open);
211 useEnhancedEffect(() => {
212 if (open && highlightedOption !== null) {
213 var _getOptionByValue;
214 const optionRef = (_getOptionByValue = getOptionByValue(highlightedOption)) == null ? void 0 : _getOptionByValue.ref;
215 if (!listboxRef.current || !(optionRef != null && optionRef.current)) {
216 return;
217 }
218 if (!isInitiallyOpen.current) {
219 optionRef.current.focus({
220 preventScroll: true
221 });
222 }
223 const listboxClientRect = listboxRef.current.getBoundingClientRect();
224 const optionClientRect = optionRef.current.getBoundingClientRect();
225 if (optionClientRect.top < listboxClientRect.top) {
226 listboxRef.current.scrollTop -= listboxClientRect.top - optionClientRect.top;
227 } else if (optionClientRect.bottom > listboxClientRect.bottom) {
228 listboxRef.current.scrollTop += optionClientRect.bottom - listboxClientRect.bottom;
229 }
230 }
231 }, [open, highlightedOption, getOptionByValue]);
232 const getOptionMetadata = React.useCallback(optionValue => getOptionByValue(optionValue), [getOptionByValue]);
233 const createHandleButtonClick = externalEventHandlers => event => {
234 var _externalEventHandler;
235 externalEventHandlers == null || (_externalEventHandler = externalEventHandlers.onClick) == null || _externalEventHandler.call(externalEventHandlers, event);
236 if (!event.defaultMuiPrevented) {
237 const action = {
238 type: SelectActionTypes.buttonClick,
239 event
240 };
241 dispatch(action);
242 }
243 };
244 const createHandleButtonKeyDown = otherHandlers => event => {
245 var _otherHandlers$onKeyD;
246 (_otherHandlers$onKeyD = otherHandlers.onKeyDown) == null || _otherHandlers$onKeyD.call(otherHandlers, event);
247 if (event.defaultMuiPrevented) {
248 return;
249 }
250 if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
251 event.preventDefault();
252 dispatch({
253 type: ListActionTypes.keyDown,
254 key: event.key,
255 event
256 });
257 }
258 };
259 const getButtonOwnRootProps = (otherHandlers = {}) => ({
260 onClick: createHandleButtonClick(otherHandlers),
261 onKeyDown: createHandleButtonKeyDown(otherHandlers)
262 });
263 const getSelectTriggerProps = (otherHandlers = {}) => {
264 return _extends({}, otherHandlers, getButtonOwnRootProps(otherHandlers), {
265 role: 'combobox',
266 'aria-expanded': open,
267 'aria-controls': listboxId
268 });
269 };
270 const getButtonProps = (externalProps = {}) => {
271 const externalEventHandlers = extractEventHandlers(externalProps);
272 const combinedProps = combineHooksSlotProps(getSelectTriggerProps, getButtonRootProps);
273 return _extends({}, externalProps, combinedProps(externalEventHandlers));
274 };
275 const createListboxHandleBlur = otherHandlers => event => {
276 var _otherHandlers$onBlur, _listboxRef$current;
277 (_otherHandlers$onBlur = otherHandlers.onBlur) == null || _otherHandlers$onBlur.call(otherHandlers, event);
278 if (event.defaultMuiPrevented) {
279 return;
280 }
281 if ((_listboxRef$current = listboxRef.current) != null && _listboxRef$current.contains(event.relatedTarget) || event.relatedTarget === buttonRef.current) {
282 event.defaultMuiPrevented = true;
283 }
284 };
285 const getOwnListboxHandlers = (otherHandlers = {}) => ({
286 onBlur: createListboxHandleBlur(otherHandlers)
287 });
288 const getListboxProps = (externalProps = {}) => {
289 const externalEventHandlers = extractEventHandlers(externalProps);
290 const getCombinedRootProps = combineHooksSlotProps(getOwnListboxHandlers, getListboxRootProps);
291 return _extends({
292 id: listboxId,
293 role: 'listbox',
294 'aria-multiselectable': multiple ? 'true' : undefined
295 }, externalProps, getCombinedRootProps(externalEventHandlers));
296 };
297 React.useDebugValue({
298 selectedOptions,
299 highlightedOption,
300 open
301 });
302 const contextValue = React.useMemo(() => _extends({}, listContextValue, compoundComponentContextValue), [listContextValue, compoundComponentContextValue]);
303 let selectValue;
304 if (props.multiple) {
305 selectValue = selectedOptions;
306 } else {
307 selectValue = selectedOptions.length > 0 ? selectedOptions[0] : null;
308 }
309 let selectedOptionsMetadata;
310 if (multiple) {
311 selectedOptionsMetadata = selectValue.map(v => getOptionMetadata(v)).filter(o => o !== undefined);
312 } else {
313 var _getOptionMetadata;
314 selectedOptionsMetadata = (_getOptionMetadata = getOptionMetadata(selectValue)) != null ? _getOptionMetadata : null;
315 }
316 const createHandleHiddenInputChange = externalEventHandlers => event => {
317 var _externalEventHandler2;
318 externalEventHandlers == null || (_externalEventHandler2 = externalEventHandlers.onChange) == null || _externalEventHandler2.call(externalEventHandlers, event);
319 if (event.defaultMuiPrevented) {
320 return;
321 }
322 const option = options.get(event.target.value);
323
324 // support autofill
325 if (event.target.value === '') {
326 dispatch({
327 type: ListActionTypes.clearSelection
328 });
329 } else if (option !== undefined) {
330 dispatch({
331 type: SelectActionTypes.browserAutoFill,
332 item: option.value,
333 event
334 });
335 }
336 };
337 const getHiddenInputProps = (externalProps = {}) => {
338 const externalEventHandlers = extractEventHandlers(externalProps);
339 return _extends({
340 name,
341 tabIndex: -1,
342 'aria-hidden': true,
343 required: required ? true : undefined,
344 value: getSerializedValue(selectedOptionsMetadata),
345 style: visuallyHiddenStyle
346 }, externalProps, {
347 onChange: createHandleHiddenInputChange(externalEventHandlers)
348 });
349 };
350 return {
351 buttonActive,
352 buttonFocusVisible,
353 buttonRef: mergedButtonRef,
354 contextValue,
355 disabled,
356 dispatch,
357 getButtonProps,
358 getHiddenInputProps,
359 getListboxProps,
360 getOptionMetadata,
361 listboxRef: mergedListRootRef,
362 open,
363 options: optionValues,
364 value: selectValue,
365 highlightedOption
366 };
367}
368export { useSelect };
\No newline at end of file