From 8d8327b32ba29696c9bdfd92299a14119f08a3b9 Mon Sep 17 00:00:00 2001 From: Kai Gritun Date: Mon, 9 Feb 2026 12:50:26 -0500 Subject: [PATCH] fix(numberfield): preserve validation errors on blur when value unchanged When a NumberField has validation errors (e.g., from Form validationErrors), focusing and blurring the field without changing its value would incorrectly reset the validation state, removing the data-invalid attribute. This fixes the issue by checking if the input value has actually changed from the state's inputValue before calling commitAndAnnounce() on blur. Fixes #9444 --- .../numberfield/src/useNumberField.ts | 6 ++- .../test/NumberField.test.js | 38 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/@react-aria/numberfield/src/useNumberField.ts b/packages/@react-aria/numberfield/src/useNumberField.ts index 5c117bc7767..06e2365e6c9 100644 --- a/packages/@react-aria/numberfield/src/useNumberField.ts +++ b/packages/@react-aria/numberfield/src/useNumberField.ts @@ -110,7 +110,11 @@ export function useNumberField(props: AriaNumberFieldProps, state: NumberFieldSt let inputId = useId(id); let {focusProps} = useFocus({ onBlur() { - commitAndAnnounce(); + // Only commit if the input value has actually changed from the state's input value. + // This prevents validation from being reset when the user focuses and blurs without editing. + if (inputRef.current?.value !== inputValue) { + commitAndAnnounce(); + } } }); 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.'); + }); });