1 | 'use client';
|
2 |
|
3 | import _extends from "@babel/runtime/helpers/esm/extends";
|
4 | import _formatMuiErrorMessage from "@mui/utils/formatMuiErrorMessage";
|
5 | import * as React from 'react';
|
6 | import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils';
|
7 | import { extractEventHandlers } from '../utils/extractEventHandlers';
|
8 | import { useControllableReducer } from '../utils/useControllableReducer';
|
9 | import { useFormControlContext } from '../FormControl';
|
10 | import { NumberInputActionTypes } from './numberInputAction.types';
|
11 | import { numberInputReducer } from './numberInputReducer';
|
12 | import { isNumber } from './utils';
|
13 | const STEP_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'];
|
14 | const SUPPORTED_KEYS = [...STEP_KEYS, 'Home', 'End'];
|
15 | export function getInputValueAsString(v) {
|
16 | return v ? String(v.trim()) : String(v);
|
17 | }
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | export function useNumberInput(parameters) {
|
30 | const {
|
31 | min,
|
32 | max,
|
33 | step,
|
34 | shiftMultiplier = 10,
|
35 | defaultValue: defaultValueProp,
|
36 | disabled: disabledProp = false,
|
37 | error: errorProp = false,
|
38 | onBlur,
|
39 | onInputChange,
|
40 | onFocus,
|
41 | onChange,
|
42 | required: requiredProp = false,
|
43 | readOnly: readOnlyProp = false,
|
44 | value: valueProp,
|
45 | inputRef: inputRefProp,
|
46 | inputId: inputIdProp,
|
47 | componentName = 'useNumberInput'
|
48 | } = parameters;
|
49 |
|
50 |
|
51 | const formControlContext = useFormControlContext();
|
52 | const {
|
53 | current: isControlled
|
54 | } = React.useRef(valueProp != null);
|
55 | const handleInputRefWarning = React.useCallback(instance => {
|
56 | if (process.env.NODE_ENV !== 'production') {
|
57 | if (instance && instance.nodeName !== 'INPUT' && !instance.focus) {
|
58 | console.error(['MUI: You have provided a `slots.input` to the input component', 'that does not correctly handle the `ref` prop.', 'Make sure the `ref` prop is called with a HTMLInputElement.'].join('\n'));
|
59 | }
|
60 | }
|
61 | }, []);
|
62 | const inputRef = React.useRef(null);
|
63 | const handleInputRef = useForkRef(inputRef, inputRefProp, handleInputRefWarning);
|
64 | const inputId = useId(inputIdProp);
|
65 | const [focused, setFocused] = React.useState(false);
|
66 | const handleStateChange = React.useCallback((event, field, fieldValue, reason) => {
|
67 | if (field === 'value' && typeof fieldValue !== 'string') {
|
68 | switch (reason) {
|
69 |
|
70 | case 'numberInput:clamp':
|
71 | onChange?.(event, fieldValue);
|
72 | break;
|
73 | case 'numberInput:increment':
|
74 | case 'numberInput:decrement':
|
75 | case 'numberInput:incrementToMax':
|
76 | case 'numberInput:decrementToMin':
|
77 | onChange?.(event, fieldValue);
|
78 | break;
|
79 | default:
|
80 | break;
|
81 | }
|
82 | }
|
83 | }, [onChange]);
|
84 | const numberInputActionContext = React.useMemo(() => {
|
85 | return {
|
86 | min,
|
87 | max,
|
88 | step,
|
89 | shiftMultiplier,
|
90 | getInputValueAsString
|
91 | };
|
92 | }, [min, max, step, shiftMultiplier]);
|
93 | const initialValue = valueProp ?? defaultValueProp ?? null;
|
94 | const initialState = {
|
95 | value: initialValue,
|
96 | inputValue: initialValue ? String(initialValue) : ''
|
97 | };
|
98 | const controlledState = React.useMemo(() => ({
|
99 | value: valueProp
|
100 | }), [valueProp]);
|
101 | const [state, dispatch] = useControllableReducer({
|
102 | reducer: numberInputReducer,
|
103 | controlledProps: controlledState,
|
104 | initialState,
|
105 | onStateChange: handleStateChange,
|
106 | actionContext: React.useMemo(() => numberInputActionContext, [numberInputActionContext]),
|
107 | componentName
|
108 | });
|
109 | const {
|
110 | value,
|
111 | inputValue
|
112 | } = state;
|
113 | React.useEffect(() => {
|
114 | if (!formControlContext && disabledProp && focused) {
|
115 | setFocused(false);
|
116 | onBlur?.();
|
117 | }
|
118 | }, [formControlContext, disabledProp, focused, onBlur]);
|
119 | React.useEffect(() => {
|
120 | if (isControlled && isNumber(value)) {
|
121 | dispatch({
|
122 | type: NumberInputActionTypes.resetInputValue
|
123 | });
|
124 | }
|
125 | }, [value, dispatch, isControlled]);
|
126 | const createHandleFocus = otherHandlers => event => {
|
127 | otherHandlers.onFocus?.(event);
|
128 | if (event.defaultMuiPrevented || event.defaultPrevented) {
|
129 | return;
|
130 | }
|
131 | if (formControlContext && formControlContext.onFocus) {
|
132 | formControlContext?.onFocus?.();
|
133 | }
|
134 | setFocused(true);
|
135 | };
|
136 | const createHandleInputChange = otherHandlers => event => {
|
137 | if (!isControlled && event.target === null) {
|
138 | throw new Error(process.env.NODE_ENV !== "production" ? `MUI: Expected valid input target. Did you use a custom \`slots.input\` and forget to forward refs? See https://mui.com/r/input-component-ref-interface for more info.` : _formatMuiErrorMessage(17));
|
139 | }
|
140 | formControlContext?.onChange?.(event);
|
141 | otherHandlers.onInputChange?.(event);
|
142 | if (event.defaultMuiPrevented || event.defaultPrevented) {
|
143 | return;
|
144 | }
|
145 | dispatch({
|
146 | type: NumberInputActionTypes.inputChange,
|
147 | event,
|
148 | inputValue: event.currentTarget.value
|
149 | });
|
150 | };
|
151 | const createHandleBlur = otherHandlers => event => {
|
152 | formControlContext?.onBlur();
|
153 | otherHandlers.onBlur?.(event);
|
154 | if (event.defaultMuiPrevented || event.defaultPrevented) {
|
155 | return;
|
156 | }
|
157 | dispatch({
|
158 | type: NumberInputActionTypes.clamp,
|
159 | event,
|
160 | inputValue: event.currentTarget.value
|
161 | });
|
162 | setFocused(false);
|
163 | };
|
164 | const createHandleClick = otherHandlers => event => {
|
165 | otherHandlers.onClick?.(event);
|
166 | if (event.defaultMuiPrevented || event.defaultPrevented) {
|
167 | return;
|
168 | }
|
169 | if (inputRef.current && event.currentTarget === event.target) {
|
170 | inputRef.current.focus();
|
171 | }
|
172 | };
|
173 | const handleStep = direction => event => {
|
174 | const applyMultiplier = Boolean(event.shiftKey);
|
175 | const actionType = {
|
176 | up: NumberInputActionTypes.increment,
|
177 | down: NumberInputActionTypes.decrement
|
178 | }[direction];
|
179 | dispatch({
|
180 | type: actionType,
|
181 | event,
|
182 | applyMultiplier
|
183 | });
|
184 | };
|
185 | const createHandleKeyDown = otherHandlers => event => {
|
186 | otherHandlers.onKeyDown?.(event);
|
187 | if (event.defaultMuiPrevented || event.defaultPrevented) {
|
188 | return;
|
189 | }
|
190 |
|
191 |
|
192 | if (SUPPORTED_KEYS.includes(event.key)) {
|
193 | event.preventDefault();
|
194 | }
|
195 | switch (event.key) {
|
196 | case 'ArrowUp':
|
197 | dispatch({
|
198 | type: NumberInputActionTypes.increment,
|
199 | event,
|
200 | applyMultiplier: !!event.shiftKey
|
201 | });
|
202 | break;
|
203 | case 'ArrowDown':
|
204 | dispatch({
|
205 | type: NumberInputActionTypes.decrement,
|
206 | event,
|
207 | applyMultiplier: !!event.shiftKey
|
208 | });
|
209 | break;
|
210 | case 'PageUp':
|
211 | dispatch({
|
212 | type: NumberInputActionTypes.increment,
|
213 | event,
|
214 | applyMultiplier: true
|
215 | });
|
216 | break;
|
217 | case 'PageDown':
|
218 | dispatch({
|
219 | type: NumberInputActionTypes.decrement,
|
220 | event,
|
221 | applyMultiplier: true
|
222 | });
|
223 | break;
|
224 | case 'Home':
|
225 | dispatch({
|
226 | type: NumberInputActionTypes.incrementToMax,
|
227 | event
|
228 | });
|
229 | break;
|
230 | case 'End':
|
231 | dispatch({
|
232 | type: NumberInputActionTypes.decrementToMin,
|
233 | event
|
234 | });
|
235 | break;
|
236 | default:
|
237 | break;
|
238 | }
|
239 | };
|
240 | const getRootProps = (externalProps = {}) => {
|
241 | const propsEventHandlers = extractEventHandlers(parameters, [
|
242 |
|
243 | 'onBlur', 'onInputChange', 'onFocus', 'onChange']);
|
244 | const externalEventHandlers = _extends({}, propsEventHandlers, extractEventHandlers(externalProps));
|
245 | return _extends({}, externalProps, externalEventHandlers, {
|
246 | onClick: createHandleClick(externalEventHandlers)
|
247 | });
|
248 | };
|
249 | const getInputProps = (externalProps = {}) => {
|
250 | const propsEventHandlers = {
|
251 | onBlur,
|
252 | onFocus,
|
253 |
|
254 | onChange: onInputChange
|
255 | };
|
256 | const externalEventHandlers = _extends({}, propsEventHandlers, extractEventHandlers(externalProps, [
|
257 |
|
258 | 'onClick'
|
259 |
|
260 | ]));
|
261 | const mergedEventHandlers = _extends({}, externalEventHandlers, {
|
262 | onFocus: createHandleFocus(externalEventHandlers),
|
263 |
|
264 | onChange: createHandleInputChange(_extends({}, externalEventHandlers, {
|
265 | onInputChange: externalEventHandlers.onChange
|
266 | })),
|
267 | onBlur: createHandleBlur(externalEventHandlers),
|
268 | onKeyDown: createHandleKeyDown(externalEventHandlers)
|
269 | });
|
270 | const displayValue = (focused ? inputValue : value) ?? '';
|
271 |
|
272 |
|
273 |
|
274 | delete externalProps.onInputChange;
|
275 | return _extends({
|
276 | type: 'text',
|
277 | id: inputId,
|
278 | 'aria-invalid': errorProp || undefined,
|
279 | defaultValue: undefined,
|
280 | value: displayValue,
|
281 | 'aria-valuenow': displayValue,
|
282 | 'aria-valuetext': String(displayValue),
|
283 | 'aria-valuemin': min,
|
284 | 'aria-valuemax': max,
|
285 | autoComplete: 'off',
|
286 | autoCorrect: 'off',
|
287 | spellCheck: 'false',
|
288 | required: requiredProp,
|
289 | readOnly: readOnlyProp,
|
290 | 'aria-disabled': disabledProp,
|
291 | disabled: disabledProp
|
292 | }, externalProps, {
|
293 | ref: handleInputRef
|
294 | }, mergedEventHandlers);
|
295 | };
|
296 | const handleStepperButtonMouseDown = event => {
|
297 | event.preventDefault();
|
298 | if (inputRef.current) {
|
299 | inputRef.current.focus();
|
300 | }
|
301 | };
|
302 | const stepperButtonCommonProps = {
|
303 | 'aria-controls': inputId,
|
304 | tabIndex: -1
|
305 | };
|
306 | const isIncrementDisabled = disabledProp || (isNumber(value) ? value >= (max ?? Number.MAX_SAFE_INTEGER) : false);
|
307 | const getIncrementButtonProps = (externalProps = {}) => {
|
308 | return _extends({}, externalProps, stepperButtonCommonProps, {
|
309 | disabled: isIncrementDisabled,
|
310 | 'aria-disabled': isIncrementDisabled,
|
311 | onMouseDown: handleStepperButtonMouseDown,
|
312 | onClick: handleStep('up')
|
313 | });
|
314 | };
|
315 | const isDecrementDisabled = disabledProp || (isNumber(value) ? value <= (min ?? Number.MIN_SAFE_INTEGER) : false);
|
316 | const getDecrementButtonProps = (externalProps = {}) => {
|
317 | return _extends({}, externalProps, stepperButtonCommonProps, {
|
318 | disabled: isDecrementDisabled,
|
319 | 'aria-disabled': isDecrementDisabled,
|
320 | onMouseDown: handleStepperButtonMouseDown,
|
321 | onClick: handleStep('down')
|
322 | });
|
323 | };
|
324 | return {
|
325 | disabled: disabledProp,
|
326 | error: errorProp,
|
327 | focused,
|
328 | formControlContext,
|
329 | getInputProps,
|
330 | getIncrementButtonProps,
|
331 | getDecrementButtonProps,
|
332 | getRootProps,
|
333 | required: requiredProp,
|
334 | value,
|
335 | inputValue,
|
336 | isIncrementDisabled,
|
337 | isDecrementDisabled
|
338 | };
|
339 | } |
\ | No newline at end of file |