From fc369d3a573a00848630fd07fbbe0478386cf03c Mon Sep 17 00:00:00 2001 From: Taylor Jones Date: Fri, 18 Aug 2023 12:48:31 -0500 Subject: [PATCH 1/2] fix(numberinput): use native methods for stepping the value up/down --- .../components/NumberInput/NumberInput.tsx | 484 +++++++++--------- 1 file changed, 238 insertions(+), 246 deletions(-) diff --git a/packages/react/src/components/NumberInput/NumberInput.tsx b/packages/react/src/components/NumberInput/NumberInput.tsx index e1465b5d5eeb..1e497c25b261 100644 --- a/packages/react/src/components/NumberInput/NumberInput.tsx +++ b/packages/react/src/components/NumberInput/NumberInput.tsx @@ -190,270 +190,262 @@ export interface NumberInputProps warnText?: ReactNode; } -const NumberInput = React.forwardRef(function NumberInput( - props: NumberInputProps, - forwardRef -) { - const { - allowEmpty = false, - className: customClassName, - disabled = false, - disableWheel: disableWheelProp = false, - defaultValue, - helperText = '', - hideLabel = false, - hideSteppers, - iconDescription, - id, - label, - invalid = false, - invalidText, - light, - max, - min, - onChange, - onClick, - onKeyUp, - readOnly, - size = 'md', - step = 1, - translateWithId: t = (id) => defaultTranslations[id], - warn = false, - warnText = '', - value: controlledValue, - ...rest - } = props; - const prefix = usePrefix(); - const { isFluid } = useContext(FormContext); - const [isFocused, setIsFocused] = useState(false); - const [value, setValue] = useState(() => { - if (controlledValue !== undefined) { - return controlledValue; +const NumberInput = React.forwardRef( + function NumberInput(props: NumberInputProps, forwardRef) { + const { + allowEmpty = false, + className: customClassName, + disabled = false, + disableWheel: disableWheelProp = false, + defaultValue, + helperText = '', + hideLabel = false, + hideSteppers, + iconDescription, + id, + label, + invalid = false, + invalidText, + light, + max, + min, + onChange, + onClick, + onKeyUp, + readOnly, + size = 'md', + step = 1, + translateWithId: t = (id) => defaultTranslations[id], + warn = false, + warnText = '', + value: controlledValue, + ...rest + } = props; + const prefix = usePrefix(); + const { isFluid } = useContext(FormContext); + const [isFocused, setIsFocused] = useState(false); + const [value, setValue] = useState(() => { + if (controlledValue !== undefined) { + return controlledValue; + } + if (defaultValue !== undefined) { + return defaultValue; + } + return 0; + }); + const [prevControlledValue, setPrevControlledValue] = + useState(controlledValue); + const inputRef = useRef(null); + const ref = useMergedRefs([forwardRef, inputRef]); + const numberInputClasses = cx({ + [`${prefix}--number`]: true, + [`${prefix}--number--helpertext`]: true, + [`${prefix}--number--readonly`]: readOnly, + [`${prefix}--number--light`]: light, + [`${prefix}--number--nolabel`]: hideLabel, + [`${prefix}--number--nosteppers`]: hideSteppers, + [`${prefix}--number--${size}`]: size, + }); + const isInputValid = getInputValidity({ + allowEmpty, + invalid, + value, + max, + min, + }); + const normalizedProps = normalize({ + id, + readOnly, + disabled, + invalid: !isInputValid, + invalidText, + warn, + warnText, + }); + const [incrementNumLabel, decrementNumLabel] = [ + t('increment.number'), + t('decrement.number'), + ]; + const wrapperClasses = cx(`${prefix}--number__input-wrapper`, { + [`${prefix}--number__input-wrapper--warning`]: normalizedProps.warn, + }); + const iconClasses = cx({ + [`${prefix}--number__invalid`]: + normalizedProps.invalid || normalizedProps.warn, + [`${prefix}--number__invalid--warning`]: normalizedProps.warn, + }); + + if (controlledValue !== prevControlledValue) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + setValue(controlledValue!); + setPrevControlledValue(controlledValue); } - if (defaultValue !== undefined) { - return defaultValue; + + let ariaDescribedBy: string | undefined = undefined; + if (normalizedProps.invalid) { + ariaDescribedBy = normalizedProps.invalidId; + } + if (normalizedProps.warn) { + ariaDescribedBy = normalizedProps.warnId; + } + if (!normalizedProps.validation) { + ariaDescribedBy = helperText ? normalizedProps.helperId : undefined; } - return 0; - }); - const [prevControlledValue, setPrevControlledValue] = - useState(controlledValue); - const inputRef = useRef(null); - const ref = useMergedRefs([forwardRef, inputRef]) as - | LegacyRef - | undefined; - const numberInputClasses = cx({ - [`${prefix}--number`]: true, - [`${prefix}--number--helpertext`]: true, - [`${prefix}--number--readonly`]: readOnly, - [`${prefix}--number--light`]: light, - [`${prefix}--number--nolabel`]: hideLabel, - [`${prefix}--number--nosteppers`]: hideSteppers, - [`${prefix}--number--${size}`]: size, - }); - const isInputValid = getInputValidity({ - allowEmpty, - invalid, - value, - max, - min, - }); - const normalizedProps = normalize({ - id, - readOnly, - disabled, - invalid: !isInputValid, - invalidText, - warn, - warnText, - }); - const [incrementNumLabel, decrementNumLabel] = [ - t('increment.number'), - t('decrement.number'), - ]; - const wrapperClasses = cx(`${prefix}--number__input-wrapper`, { - [`${prefix}--number__input-wrapper--warning`]: normalizedProps.warn, - }); - const iconClasses = cx({ - [`${prefix}--number__invalid`]: - normalizedProps.invalid || normalizedProps.warn, - [`${prefix}--number__invalid--warning`]: normalizedProps.warn, - }); - if (controlledValue !== prevControlledValue) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - setValue(controlledValue!); - setPrevControlledValue(controlledValue); - } + function handleOnChange(event) { + if (disabled) { + return; + } - let ariaDescribedBy: string | undefined = undefined; - if (normalizedProps.invalid) { - ariaDescribedBy = normalizedProps.invalidId; - } - if (normalizedProps.warn) { - ariaDescribedBy = normalizedProps.warnId; - } - if (!normalizedProps.validation) { - ariaDescribedBy = helperText ? normalizedProps.helperId : undefined; - } + const state = { + value: event.target.value, + direction: value < event.target.value ? 'up' : 'down', + }; + setValue(state.value); - function handleOnChange(event) { - if (disabled) { - return; + if (onChange) { + onChange(event, state); + } } - const state = { - value: event.target.value, - direction: value < event.target.value ? 'up' : 'down', + const handleFocus: React.FocusEventHandler< + HTMLInputElement | HTMLDivElement + > = (evt) => { + if ('type' in evt.target && evt.target.type === 'button') { + setIsFocused(false); + } else { + setIsFocused(evt.type === 'focus' ? true : false); + } }; - setValue(state.value); - if (onChange) { - onChange(event, state); + const outerElementClasses = cx(`${prefix}--form-item`, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + [customClassName!]: !!customClassName, + [`${prefix}--number-input--fluid--invalid`]: + isFluid && normalizedProps.invalid, + [`${prefix}--number-input--fluid--focus`]: isFluid && isFocused, + [`${prefix}--number-input--fluid--disabled`]: isFluid && disabled, + }); + + const Icon = normalizedProps.icon as any; + + function handleStepperClick(event, direction) { + if (inputRef.current) { + direction === 'up' + ? inputRef.current.stepUp() + : inputRef.current.stepDown(); + + const state = { + value: Number(inputRef.current.value), + direction: direction, + }; + setValue(state.value); + + if (onChange) { + onChange(event, state); + } + + if (onClick) { + onClick(event, state); + } + } } - } - const handleFocus: React.FocusEventHandler< - HTMLInputElement | HTMLDivElement - > = (evt) => { - if ('type' in evt.target && evt.target.type === 'button') { - setIsFocused(false); - } else { - setIsFocused(evt.type === 'focus' ? true : false); - } - }; - - const outerElementClasses = cx(`${prefix}--form-item`, { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - [customClassName!]: !!customClassName, - [`${prefix}--number-input--fluid--invalid`]: - isFluid && normalizedProps.invalid, - [`${prefix}--number-input--fluid--focus`]: isFluid && isFocused, - [`${prefix}--number-input--fluid--disabled`]: isFluid && disabled, - }); - - const Icon = normalizedProps.icon as any; - return ( -
+ return (
-