UNPKG

11.9 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?.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 const option = getOptionByValue(valueToCheck);
123 return option?.disabled ?? false;
124 }, [getOptionByValue]);
125 const stringifyOption = React.useCallback(valueToCheck => {
126 const option = getOptionByValue(valueToCheck);
127 if (!option) {
128 return '';
129 }
130 return getOptionAsString(option);
131 }, [getOptionByValue, getOptionAsString]);
132 const controlledState = React.useMemo(() => ({
133 selectedValues: value,
134 open: openProp
135 }), [value, openProp]);
136 const getItemId = React.useCallback(itemValue => options.get(itemValue)?.id, [options]);
137 const handleSelectionChange = React.useCallback((event, newValues) => {
138 if (multiple) {
139 onChange?.(event, newValues);
140 } else {
141 onChange?.(event, newValues[0] ?? null);
142 }
143 }, [multiple, onChange]);
144 const handleHighlightChange = React.useCallback((event, newValue) => {
145 onHighlightChange?.(event, newValue ?? null);
146 }, [onHighlightChange]);
147 const handleStateChange = React.useCallback((event, field, fieldValue) => {
148 if (field === 'open') {
149 onOpenChange?.(fieldValue);
150 if (fieldValue === false && event?.type !== 'blur') {
151 buttonRef.current?.focus();
152 }
153 }
154 }, [onOpenChange]);
155 const getItemDomElement = React.useCallback(itemId => {
156 if (itemId == null) {
157 return null;
158 }
159 return subitems.get(itemId)?.ref.current ?? null;
160 }, [subitems]);
161 const useListParameters = {
162 getInitialState: () => ({
163 highlightedValue: null,
164 selectedValues: defaultValue ?? [],
165 open: defaultOpen
166 }),
167 getItemId,
168 controlledProps: controlledState,
169 focusManagement: 'DOM',
170 getItemDomElement,
171 itemComparer: areOptionsEqual,
172 isItemDisabled,
173 rootRef: handleListboxRef,
174 onChange: handleSelectionChange,
175 onHighlightChange: handleHighlightChange,
176 onStateChange: handleStateChange,
177 reducerActionContext: React.useMemo(() => ({
178 multiple
179 }), [multiple]),
180 items: optionValues,
181 getItemAsString: stringifyOption,
182 selectionMode: multiple ? 'multiple' : 'single',
183 stateReducer: selectReducer,
184 componentName
185 };
186 const {
187 dispatch,
188 getRootProps: getListboxRootProps,
189 contextValue: listContextValue,
190 state: {
191 open,
192 highlightedValue: highlightedOption,
193 selectedValues: selectedOptions
194 },
195 rootRef: mergedListRootRef
196 } = useList(useListParameters);
197
198 // store the initial open state to prevent focus stealing
199 // (the first option gets focused only when the select is opened by the user)
200 const isInitiallyOpen = React.useRef(open);
201 useEnhancedEffect(() => {
202 if (open && highlightedOption !== null) {
203 const optionRef = getOptionByValue(highlightedOption)?.ref;
204 if (!listboxRef.current || !optionRef?.current) {
205 return;
206 }
207 if (!isInitiallyOpen.current) {
208 optionRef.current.focus({
209 preventScroll: true
210 });
211 }
212 const listboxClientRect = listboxRef.current.getBoundingClientRect();
213 const optionClientRect = optionRef.current.getBoundingClientRect();
214 if (optionClientRect.top < listboxClientRect.top) {
215 listboxRef.current.scrollTop -= listboxClientRect.top - optionClientRect.top;
216 } else if (optionClientRect.bottom > listboxClientRect.bottom) {
217 listboxRef.current.scrollTop += optionClientRect.bottom - listboxClientRect.bottom;
218 }
219 }
220 }, [open, highlightedOption, getOptionByValue]);
221 const getOptionMetadata = React.useCallback(optionValue => getOptionByValue(optionValue), [getOptionByValue]);
222 const createHandleButtonClick = externalEventHandlers => event => {
223 externalEventHandlers?.onClick?.(event);
224 if (!event.defaultMuiPrevented) {
225 const action = {
226 type: SelectActionTypes.buttonClick,
227 event
228 };
229 dispatch(action);
230 }
231 };
232 const createHandleButtonKeyDown = otherHandlers => event => {
233 otherHandlers.onKeyDown?.(event);
234 if (event.defaultMuiPrevented) {
235 return;
236 }
237 if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
238 event.preventDefault();
239 dispatch({
240 type: ListActionTypes.keyDown,
241 key: event.key,
242 event
243 });
244 }
245 };
246 const getButtonOwnRootProps = (otherHandlers = {}) => ({
247 onClick: createHandleButtonClick(otherHandlers),
248 onKeyDown: createHandleButtonKeyDown(otherHandlers)
249 });
250 const getSelectTriggerProps = (otherHandlers = {}) => {
251 return _extends({}, otherHandlers, getButtonOwnRootProps(otherHandlers), {
252 role: 'combobox',
253 'aria-expanded': open,
254 'aria-controls': listboxId
255 });
256 };
257 const getButtonProps = (externalProps = {}) => {
258 const externalEventHandlers = extractEventHandlers(externalProps);
259 const combinedProps = combineHooksSlotProps(getSelectTriggerProps, getButtonRootProps);
260 return _extends({}, externalProps, combinedProps(externalEventHandlers));
261 };
262 const createListboxHandleBlur = otherHandlers => event => {
263 otherHandlers.onBlur?.(event);
264 if (event.defaultMuiPrevented) {
265 return;
266 }
267 if (listboxRef.current?.contains(event.relatedTarget) || event.relatedTarget === buttonRef.current) {
268 event.defaultMuiPrevented = true;
269 }
270 };
271 const getOwnListboxHandlers = (otherHandlers = {}) => ({
272 onBlur: createListboxHandleBlur(otherHandlers)
273 });
274 const getListboxProps = (externalProps = {}) => {
275 const externalEventHandlers = extractEventHandlers(externalProps);
276 const getCombinedRootProps = combineHooksSlotProps(getOwnListboxHandlers, getListboxRootProps);
277 return _extends({
278 id: listboxId,
279 role: 'listbox',
280 'aria-multiselectable': multiple ? 'true' : undefined
281 }, externalProps, getCombinedRootProps(externalEventHandlers));
282 };
283 React.useDebugValue({
284 selectedOptions,
285 highlightedOption,
286 open
287 });
288 const contextValue = React.useMemo(() => _extends({}, listContextValue, compoundComponentContextValue), [listContextValue, compoundComponentContextValue]);
289 let selectValue;
290 if (props.multiple) {
291 selectValue = selectedOptions;
292 } else {
293 selectValue = selectedOptions.length > 0 ? selectedOptions[0] : null;
294 }
295 let selectedOptionsMetadata;
296 if (multiple) {
297 selectedOptionsMetadata = selectValue.map(v => getOptionMetadata(v)).filter(o => o !== undefined);
298 } else {
299 selectedOptionsMetadata = getOptionMetadata(selectValue) ?? null;
300 }
301 const createHandleHiddenInputChange = externalEventHandlers => event => {
302 externalEventHandlers?.onChange?.(event);
303 if (event.defaultMuiPrevented) {
304 return;
305 }
306 const option = options.get(event.target.value);
307
308 // support autofill
309 if (event.target.value === '') {
310 dispatch({
311 type: ListActionTypes.clearSelection
312 });
313 } else if (option !== undefined) {
314 dispatch({
315 type: SelectActionTypes.browserAutoFill,
316 item: option.value,
317 event
318 });
319 }
320 };
321 const getHiddenInputProps = (externalProps = {}) => {
322 const externalEventHandlers = extractEventHandlers(externalProps);
323 return _extends({
324 name,
325 tabIndex: -1,
326 'aria-hidden': true,
327 required: required ? true : undefined,
328 value: getSerializedValue(selectedOptionsMetadata),
329 style: visuallyHiddenStyle
330 }, externalProps, {
331 onChange: createHandleHiddenInputChange(externalEventHandlers)
332 });
333 };
334 return {
335 buttonActive,
336 buttonFocusVisible,
337 buttonRef: mergedButtonRef,
338 contextValue,
339 disabled,
340 dispatch,
341 getButtonProps,
342 getHiddenInputProps,
343 getListboxProps,
344 getOptionMetadata,
345 listboxRef: mergedListRootRef,
346 open,
347 options: optionValues,
348 value: selectValue,
349 highlightedOption
350 };
351}
352export { useSelect };
\No newline at end of file