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 == 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 |
|
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 | 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 |
|
209 |
|
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 |
|
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 | }
|
368 | export { useSelect }; |
\ | No newline at end of file |