Skip to content

Commit

Permalink
[@mantine/core]: NumberInput: Fix onChange being called in onBlur
Browse files Browse the repository at this point in the history
… if the value has not been changed (#7383)
  • Loading branch information
btmnk authored Jan 19, 2025
1 parent 12ee02f commit 678eb00
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div style={{ padding: 40, maxWidth: 340 }}>
<form onSubmit={form.onSubmit((values) => console.log(values))}>
<NumberInput label="Age" {...form.getInputProps('age')} />
<NumberInput label="Age" required {...form.getInputProps('age')} />
<TextInput label="Name" {...form.getInputProps('name')} />
<Group justify="flex-end" mt="xl">
<Button type="submit">Submit</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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(<NumberInput onChange={onChangeSpy} onBlur={onBlurSpy} value="" />);

focusInput();
blurInput();

expectValue('');
expect(onChangeSpy).toHaveBeenCalledTimes(0);
expect(onBlurSpy).toHaveBeenCalledTimes(1);
});
});
53 changes: 30 additions & 23 deletions packages/@mantine/core/src/components/NumberInput/NumberInput.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -382,6 +382,34 @@ export const NumberInput = factory<NumberInputFactory>((_props, ref) => {
}
};

const handleBlur = (event: React.FocusEvent<HTMLInputElement>) => {
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) => {
Expand Down Expand Up @@ -485,28 +513,7 @@ export const NumberInput = factory<NumberInputFactory>((_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) {
Expand Down

0 comments on commit 678eb00

Please sign in to comment.