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, unstable_useId as useId, unstable_useEnhancedEffect as useEnhancedEffect, visuallyHidden as visuallyHiddenStyle } from '@mui/utils';
|
6 | import { useButton } from '../useButton';
|
7 | import { SelectActionTypes } from './useSelect.types';
|
8 | import { ListActionTypes, useList } from '../useList';
|
9 | import { defaultOptionStringifier } from './defaultOptionStringifier';
|
10 | import { useCompoundParent } from '../useCompound';
|
11 | import { extractEventHandlers } from '../utils/extractEventHandlers';
|
12 | import { selectReducer } from './selectReducer';
|
13 | import { combineHooksSlotProps } from '../utils/combineHooksSlotProps';
|
14 | function 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 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | function 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: 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 |
|
114 |
|
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 |
|
199 |
|
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 |
|
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 | }
|
352 | export { useSelect }; |
\ | No newline at end of file |