diff --git a/packages/@react-stately/numberfield/src/useNumberFieldState.ts b/packages/@react-stately/numberfield/src/useNumberFieldState.ts index 5bc374a4313..ad71558b3c9 100644 --- a/packages/@react-stately/numberfield/src/useNumberFieldState.ts +++ b/packages/@react-stately/numberfield/src/useNumberFieldState.ts @@ -175,11 +175,14 @@ export function useNumberFieldState( } clampedValue = numberParser.parse(format(clampedValue)); + let shouldValidate = clampedValue !== numberValue; setNumberValue(clampedValue); // in a controlled state, the numberValue won't change, so we won't go back to our old input without help setInputValue(format(value === undefined ? clampedValue : numberValue)); - validation.commitValidation(); + if (shouldValidate) { + validation.commitValidation(); + } }; let safeNextStep = (operation: '+' | '-', minMax: number = 0) => { diff --git a/packages/react-aria-components/test/NumberField.test.js b/packages/react-aria-components/test/NumberField.test.js index ae7fb7c4130..cd3516072e1 100644 --- a/packages/react-aria-components/test/NumberField.test.js +++ b/packages/react-aria-components/test/NumberField.test.js @@ -13,7 +13,7 @@ jest.mock('@react-aria/live-announcer'); import {act, pointerMap, render} from '@react-spectrum/test-utils-internal'; import {announce} from '@react-aria/live-announcer'; -import {Button, FieldError, Group, Input, Label, NumberField, NumberFieldContext, Text} from '../'; +import {Button, FieldError, Form, Group, Input, Label, NumberField, NumberFieldContext, Text} from '../'; import React from 'react'; import userEvent from '@testing-library/user-event'; @@ -250,4 +250,40 @@ describe('NumberField', () => { await user.keyboard('{Enter}'); expect(input).toHaveValue('200'); }); + + it('should not reset validation errors on blur when value has not changed', async () => { + let {getByRole} = render( +
+ ); + + let input = getByRole('textbox'); + let numberfield = input.closest('.react-aria-NumberField'); + + // Validation error should be displayed + expect(numberfield).toHaveAttribute('data-invalid'); + expect(input).toHaveAttribute('aria-describedby'); + expect(document.getElementById(input.getAttribute('aria-describedby').split(' ')[0])).toHaveTextContent('This field has an error.'); + + // Focus the field without changing the value + act(() => { input.focus(); }); + expect(numberfield).toHaveAttribute('data-invalid'); + + // Blur the field without changing the value + act(() => { input.blur(); }); + + // Validation error should still be displayed because the value didn't change + expect(numberfield).toHaveAttribute('data-invalid'); + expect(input).toHaveAttribute('aria-describedby'); + expect(document.getElementById(input.getAttribute('aria-describedby').split(' ')[0])).toHaveTextContent('This field has an error.'); + }); });