UNPKG

11.1 kBJavaScriptView Raw
1'use client';
2
3import _extends from "@babel/runtime/helpers/esm/extends";
4import _formatMuiErrorMessage from "@mui/utils/formatMuiErrorMessage";
5import * as React from 'react';
6import { unstable_useForkRef as useForkRef, unstable_useId as useId } from '@mui/utils';
7import { extractEventHandlers } from '../utils/extractEventHandlers';
8import { useControllableReducer } from '../utils/useControllableReducer';
9import { useFormControlContext } from '../FormControl';
10import { NumberInputActionTypes } from './numberInputAction.types';
11import { numberInputReducer } from './numberInputReducer';
12import { isNumber } from './utils';
13const STEP_KEYS = ['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'];
14const SUPPORTED_KEYS = [...STEP_KEYS, 'Home', 'End'];
15export function getInputValueAsString(v) {
16 return v ? String(v.trim()) : String(v);
17}
18
19/**
20 *
21 * Demos:
22 *
23 * - [Number Input](https://mui.com/base-ui/react-number-input/#hook)
24 *
25 * API:
26 *
27 * - [useNumberInput API](https://mui.com/base-ui/react-number-input/hooks-api/#use-number-input)
28 */
29export 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 // TODO: make it work with FormControl
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 // only a blur event will dispatch `numberInput:clamp`
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 // this prevents unintended page scrolling
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 // these are handled by the input slot
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 // onChange from normal props is the custom onChange so we ignore it here
254 onChange: onInputChange
255 };
256 const externalEventHandlers = _extends({}, propsEventHandlers, extractEventHandlers(externalProps, [
257 // onClick is handled by the root slot
258 'onClick'
259 // do not ignore 'onInputChange', we want slotProps.input.onInputChange to enter the DOM and throw
260 ]));
261 const mergedEventHandlers = _extends({}, externalEventHandlers, {
262 onFocus: createHandleFocus(externalEventHandlers),
263 // slotProps.onChange is renamed to onInputChange and passed to createHandleInputChange
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 // get rid of slotProps.input.onInputChange before returning to prevent it from entering the DOM
273 // if it was passed, it will be in mergedEventHandlers and throw
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