UNPKG

7.79 kBJavaScriptView Raw
1import _extends from "@babel/runtime/helpers/esm/extends";
2import * as React from 'react';
3import { unstable_useForkRef as useForkRef, unstable_useId as useId, unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils';
4import useButton from '../useButton';
5import { SelectActionTypes } from './useSelect.types';
6import useList from '../useList';
7import defaultOptionStringifier from './defaultOptionStringifier';
8import { useCompoundParent } from '../utils/useCompound';
9import selectReducer from './selectReducer';
10import combineHooksSlotProps from '../utils/combineHooksSlotProps';
11function preventDefault(event) {
12 event.preventDefault();
13}
14
15/**
16 *
17 * Demos:
18 *
19 * - [Select](https://mui.com/base/react-select/#hooks)
20 *
21 * API:
22 *
23 * - [useSelect API](https://mui.com/base/react-select/hooks-api/#use-select)
24 */
25function useSelect(props) {
26 const {
27 buttonRef: buttonRefProp,
28 defaultOpen = false,
29 defaultValue: defaultValueProp,
30 disabled = false,
31 listboxId: listboxIdProp,
32 listboxRef: listboxRefProp,
33 multiple = false,
34 onChange,
35 onHighlightChange,
36 onOpenChange,
37 open: openProp,
38 options: optionsParam,
39 getOptionAsString = defaultOptionStringifier,
40 value: valueProp
41 } = props;
42 const buttonRef = React.useRef(null);
43 const handleButtonRef = useForkRef(buttonRefProp, buttonRef);
44 const listboxRef = React.useRef(null);
45 const listboxId = useId(listboxIdProp);
46 let defaultValue;
47 if (valueProp === undefined && defaultValueProp === undefined) {
48 defaultValue = [];
49 } else if (defaultValueProp !== undefined) {
50 if (multiple) {
51 defaultValue = defaultValueProp;
52 } else {
53 defaultValue = defaultValueProp == null ? [] : [defaultValueProp];
54 }
55 }
56 const value = React.useMemo(() => {
57 if (valueProp !== undefined) {
58 if (multiple) {
59 return valueProp;
60 }
61 return valueProp == null ? [] : [valueProp];
62 }
63 return undefined;
64 }, [valueProp, multiple]);
65 const {
66 subitems,
67 contextValue: compoundComponentContextValue
68 } = useCompoundParent();
69 const options = React.useMemo(() => {
70 if (optionsParam != null) {
71 return new Map(optionsParam.map((option, index) => [option.value, {
72 value: option.value,
73 label: option.label,
74 disabled: option.disabled,
75 ref: /*#__PURE__*/React.createRef(),
76 id: `${listboxId}_${index}`
77 }]));
78 }
79 return subitems;
80 }, [optionsParam, subitems, listboxId]);
81 const handleListboxRef = useForkRef(listboxRefProp, listboxRef);
82 const {
83 getRootProps: getButtonRootProps,
84 active: buttonActive,
85 focusVisible: buttonFocusVisible,
86 rootRef: mergedButtonRef
87 } = useButton({
88 disabled,
89 rootRef: handleButtonRef
90 });
91 const optionValues = React.useMemo(() => Array.from(options.keys()), [options]);
92 const isItemDisabled = React.useCallback(valueToCheck => {
93 const option = options.get(valueToCheck);
94 return option?.disabled ?? false;
95 }, [options]);
96 const stringifyOption = React.useCallback(valueToCheck => {
97 const option = options.get(valueToCheck);
98 if (!option) {
99 return '';
100 }
101 return getOptionAsString(option);
102 }, [options, getOptionAsString]);
103 const controlledState = React.useMemo(() => ({
104 selectedValues: value,
105 open: openProp
106 }), [value, openProp]);
107 const getItemId = React.useCallback(itemValue => options.get(itemValue)?.id, [options]);
108 const handleSelectionChange = React.useCallback((event, newValues) => {
109 if (multiple) {
110 onChange?.(event, newValues);
111 } else {
112 onChange?.(event, newValues[0] ?? null);
113 }
114 }, [multiple, onChange]);
115 const handleHighlightChange = React.useCallback((event, newValue) => {
116 onHighlightChange?.(event, newValue ?? null);
117 }, [onHighlightChange]);
118 const handleStateChange = React.useCallback((event, field, fieldValue) => {
119 if (field === 'open') {
120 onOpenChange?.(fieldValue);
121 if (fieldValue === false && event?.type !== 'blur') {
122 buttonRef.current?.focus();
123 }
124 }
125 }, [onOpenChange]);
126 const useListParameters = {
127 getInitialState: () => ({
128 highlightedValue: null,
129 selectedValues: defaultValue ?? [],
130 open: defaultOpen
131 }),
132 getItemId,
133 controlledProps: controlledState,
134 isItemDisabled,
135 rootRef: mergedButtonRef,
136 onChange: handleSelectionChange,
137 onHighlightChange: handleHighlightChange,
138 onStateChange: handleStateChange,
139 reducerActionContext: React.useMemo(() => ({
140 multiple
141 }), [multiple]),
142 items: optionValues,
143 getItemAsString: stringifyOption,
144 selectionMode: multiple ? 'multiple' : 'single',
145 stateReducer: selectReducer
146 };
147 const {
148 dispatch,
149 getRootProps: getListboxRootProps,
150 contextValue: listContextValue,
151 state: {
152 open,
153 highlightedValue: highlightedOption,
154 selectedValues: selectedOptions
155 },
156 rootRef: mergedListRootRef
157 } = useList(useListParameters);
158 const createHandleButtonClick = otherHandlers => event => {
159 otherHandlers?.onClick?.(event);
160 if (!event.defaultMuiPrevented) {
161 const action = {
162 type: SelectActionTypes.buttonClick,
163 event
164 };
165 dispatch(action);
166 }
167 };
168 useEnhancedEffect(() => {
169 // Scroll to the currently highlighted option.
170 if (highlightedOption != null) {
171 const optionRef = options.get(highlightedOption)?.ref;
172 if (!listboxRef.current || !optionRef?.current) {
173 return;
174 }
175 const listboxClientRect = listboxRef.current.getBoundingClientRect();
176 const optionClientRect = optionRef.current.getBoundingClientRect();
177 if (optionClientRect.top < listboxClientRect.top) {
178 listboxRef.current.scrollTop -= listboxClientRect.top - optionClientRect.top;
179 } else if (optionClientRect.bottom > listboxClientRect.bottom) {
180 listboxRef.current.scrollTop += optionClientRect.bottom - listboxClientRect.bottom;
181 }
182 }
183 }, [highlightedOption, options]);
184 const getOptionMetadata = React.useCallback(optionValue => options.get(optionValue), [options]);
185 const getSelectTriggerProps = (otherHandlers = {}) => {
186 return _extends({}, otherHandlers, {
187 onClick: createHandleButtonClick(otherHandlers),
188 ref: mergedListRootRef,
189 role: 'combobox',
190 'aria-expanded': open,
191 'aria-controls': listboxId
192 });
193 };
194 const getButtonProps = (otherHandlers = {}) => {
195 const listboxAndButtonProps = combineHooksSlotProps(getButtonRootProps, getListboxRootProps);
196 const combinedProps = combineHooksSlotProps(listboxAndButtonProps, getSelectTriggerProps);
197 return combinedProps(otherHandlers);
198 };
199 const getListboxProps = (otherHandlers = {}) => {
200 return _extends({}, otherHandlers, {
201 id: listboxId,
202 role: 'listbox',
203 'aria-multiselectable': multiple ? 'true' : undefined,
204 ref: handleListboxRef,
205 onMouseDown: preventDefault // to prevent the button from losing focus when interacting with the listbox
206 });
207 };
208
209 React.useDebugValue({
210 selectedOptions,
211 highlightedOption,
212 open
213 });
214 const contextValue = React.useMemo(() => _extends({}, listContextValue, compoundComponentContextValue), [listContextValue, compoundComponentContextValue]);
215 let selectValue;
216 if (props.multiple) {
217 selectValue = selectedOptions;
218 } else {
219 selectValue = selectedOptions.length > 0 ? selectedOptions[0] : null;
220 }
221 return {
222 buttonActive,
223 buttonFocusVisible,
224 buttonRef: mergedButtonRef,
225 contextValue,
226 disabled,
227 dispatch,
228 getButtonProps,
229 getListboxProps,
230 getOptionMetadata,
231 listboxRef: mergedListRootRef,
232 open,
233 options: optionValues,
234 value: selectValue,
235 highlightedOption
236 };
237}
238export default useSelect;
\No newline at end of file