diff --git a/packages/@mantine/core/src/components/NumberInput/NumberInput.story.tsx b/packages/@mantine/core/src/components/NumberInput/NumberInput.story.tsx index 95269b0289..1835c2b457 100644 --- a/packages/@mantine/core/src/components/NumberInput/NumberInput.story.tsx +++ b/packages/@mantine/core/src/components/NumberInput/NumberInput.story.tsx @@ -232,19 +232,27 @@ export function FormValidateOnBlur() { const form = useForm({ validateInputOnBlur: true, validate: { - age: (value) => (value < 18 ? 'Error' : null), + age: (value) => { + if (typeof value === 'string' && value === '') { + return 'Required'; + } + if (typeof value === 'number' && value < 18) { + return 'Error'; + } + return null; + }, name: (value) => (value.length < 2 ? 'Error' : null), }, initialValues: { name: '', - age: 2, + age: '' as string | number, }, }); return (
console.log(values))}> - + diff --git a/packages/@mantine/core/src/components/NumberInput/NumberInput.test.tsx b/packages/@mantine/core/src/components/NumberInput/NumberInput.test.tsx index 5f75c12942..2933cfb0cc 100644 --- a/packages/@mantine/core/src/components/NumberInput/NumberInput.test.tsx +++ b/packages/@mantine/core/src/components/NumberInput/NumberInput.test.tsx @@ -25,6 +25,7 @@ const getInput = () => screen.getByRole('textbox'); const enterText = (text: string) => userEvent.type(getInput(), text); const expectValue = (value: string) => expect(getInput()).toHaveValue(value); const focusInput = () => fireEvent.focus(getInput()); +const blurInput = () => fireEvent.blur(getInput()); describe('@mantine/core/NumberInput', () => { tests.axe([ @@ -190,4 +191,17 @@ describe('@mantine/core/NumberInput', () => { expectValue('0'); expect(spy).toHaveBeenLastCalledWith(0); }); + + it('does not call onChange when nothing has changed after blur', async () => { + const onChangeSpy = jest.fn(); + const onBlurSpy = jest.fn(); + render(); + + focusInput(); + blurInput(); + + expectValue(''); + expect(onChangeSpy).toHaveBeenCalledTimes(0); + expect(onBlurSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/@mantine/core/src/components/NumberInput/NumberInput.tsx b/packages/@mantine/core/src/components/NumberInput/NumberInput.tsx index 42d1179a29..fff146aabf 100644 --- a/packages/@mantine/core/src/components/NumberInput/NumberInput.tsx +++ b/packages/@mantine/core/src/components/NumberInput/NumberInput.tsx @@ -1,4 +1,4 @@ -import { useRef } from 'react'; +import React, { useRef } from 'react'; import cx from 'clsx'; import { NumberFormatValues, NumericFormat, OnValueChange } from 'react-number-format'; import { assignRef, clamp, useMergedRef, useUncontrolled } from '@mantine/hooks'; @@ -382,6 +382,34 @@ export const NumberInput = factory((_props, ref) => { } }; + const handleBlur = (event: React.FocusEvent) => { + let sanitizedValue = _value; + + if (clampBehavior === 'blur' && typeof sanitizedValue === 'number') { + const clampedValue = clamp(sanitizedValue, min, max); + sanitizedValue = clampedValue; + } + + if ( + trimLeadingZeroesOnBlur && + typeof sanitizedValue === 'string' && + getDecimalPlaces(sanitizedValue) < 15 + ) { + const replaced = sanitizedValue.toString().replace(/^0+/, ''); + const parsedValue = parseFloat(replaced); + sanitizedValue = + Number.isNaN(parsedValue) || parsedValue > Number.MAX_SAFE_INTEGER + ? replaced + : clamp(parsedValue, min, max); + } + + if (_value !== sanitizedValue) { + setValue(sanitizedValue); + } + + onBlur?.(event); + }; + assignRef(handlersRef, { increment: incrementRef.current, decrement: decrementRef.current }); const onStepHandleChange = (isIncrement: boolean) => { @@ -485,28 +513,7 @@ export const NumberInput = factory((_props, ref) => { rightSectionPointerEvents={rightSectionPointerEvents ?? (disabled ? 'none' : undefined)} rightSectionWidth={rightSectionWidth ?? `var(--ni-right-section-width-${size || 'sm'})`} allowLeadingZeros={allowLeadingZeros} - onBlur={(event) => { - onBlur?.(event); - if (clampBehavior === 'blur' && typeof _value === 'number') { - const clampedValue = clamp(_value, min, max); - if (clampedValue !== _value) { - setValue(clamp(_value, min, max)); - } - } - if ( - trimLeadingZeroesOnBlur && - typeof _value === 'string' && - getDecimalPlaces(_value) < 15 - ) { - const replaced = _value.replace(/^0+/, ''); - const parsedValue = parseFloat(replaced); - setValue( - Number.isNaN(parsedValue) || parsedValue > Number.MAX_SAFE_INTEGER - ? replaced - : clamp(parsedValue, min, max) - ); - } - }} + onBlur={handleBlur} isAllowed={(val) => { if (clampBehavior === 'strict') { if (isAllowed) {