Commit ba6a239ecf for woocommerce

commit ba6a239ecfc09b4c2683e18bfa7a570301dc3505
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date:   Tue Feb 3 10:49:26 2026 +0000

    Prevent showing error messages on empty checkout fields until form submission (#62689)

    * Don't show error message if the input is empty

    * Adjust test to check error doesn't show after blur and only on submit

    * Add changelog

    * Update selector to toBeInTheDocument

    * Add tests to cover non-empty input and pattern mismatch error

    * test that whitepsace also doesn't trigger validation errors

    * Update matchers

    * Don't hide pre-existing errors, e.g. after form submission

diff --git a/plugins/woocommerce/changelog/wooprd-1438-fields-reaction b/plugins/woocommerce/changelog/wooprd-1438-fields-reaction
new file mode 100644
index 0000000000..b603735f06
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooprd-1438-fields-reaction
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Prevent errors showing on empty checkout fields until the form is submitted.
diff --git a/plugins/woocommerce/client/blocks/packages/components/text-input/test/validated-text-input.tsx b/plugins/woocommerce/client/blocks/packages/components/text-input/test/validated-text-input.tsx
index 2a501b21fc..3abbe5862c 100644
--- a/plugins/woocommerce/client/blocks/packages/components/text-input/test/validated-text-input.tsx
+++ b/plugins/woocommerce/client/blocks/packages/components/text-input/test/validated-text-input.tsx
@@ -218,7 +218,64 @@ describe( 'ValidatedTextInput', () => {
 			select( validationStore ).getValidationError( 'test-input' )
 		).toBe( undefined );
 	} );
-	it( 'Shows a custom error message for an invalid required input', async () => {
+	it( 'Shows validation error for non-empty invalid input on blur', async () => {
+		const user = userEvent.setup();
+		const TestComponent = () => {
+			const [ inputValue, setInputValue ] = useState( '' );
+			return (
+				<ValidatedTextInput
+					instanceId={ '5' }
+					id={ 'test-input' }
+					onChange={ ( value ) => setInputValue( value ) }
+					value={ inputValue }
+					label={ 'Test Input' }
+					type="email"
+				/>
+			);
+		};
+		render( <TestComponent /> );
+		const textInputElement = await screen.getByLabelText( 'Test Input' );
+
+		await act( async () => {
+			await user.type( textInputElement, 'invalid-email' );
+			textInputElement.blur();
+		} );
+
+		// Non-empty invalid fields SHOULD show validation errors on blur
+		expect(
+			screen.queryByText( 'Please enter a valid test input' )
+		).toBeInTheDocument();
+	} );
+	it( 'Shows validation error for pattern mismatch on blur', async () => {
+		const user = userEvent.setup();
+		const TestComponent = () => {
+			const [ inputValue, setInputValue ] = useState( '' );
+			return (
+				<ValidatedTextInput
+					instanceId={ '6' }
+					id={ 'pattern-input' }
+					onChange={ ( value ) => setInputValue( value ) }
+					value={ inputValue }
+					label={ 'Pattern Input' }
+					pattern="^[A-Za-z]+$"
+				/>
+			);
+		};
+		render( <TestComponent /> );
+		const textInputElement = await screen.getByLabelText( 'Pattern Input' );
+
+		await act( async () => {
+			await user.type( textInputElement, '123456' );
+			textInputElement.blur();
+		} );
+
+		// Non-empty invalid fields (pattern mismatch) SHOULD show validation errors on blur
+		const validationError =
+			select( validationStore ).getValidationError( 'pattern-input' );
+		expect( validationError ).not.toBeUndefined();
+		expect( validationError?.hidden ).toBe( false );
+	} );
+	it( 'Shows a custom error message for an invalid required input after form submission', async () => {
 		const user = userEvent.setup();
 		const TestComponent = () => {
 			const [ inputValue, setInputValue ] = useState( '' );
@@ -239,13 +296,46 @@ describe( 'ValidatedTextInput', () => {
 		await act( async () => {
 			await user.type( textInputElement, 'test' );
 			await user.clear( textInputElement );
-			await textInputElement.blur();
+			textInputElement.blur();
 		} );

-		await expect(
+		// Empty fields don't show validation errors on blur - they stay hidden
+		// until form submission (showAllValidationErrors is called)
+		expect(
+			screen.queryByText( 'Please enter a valid test input' )
+		).not.toBeInTheDocument();
+
+		// Add whitespace only to verify this also doesn't trigger validation error on blur.
+		await act( async () => {
+			await user.type( textInputElement, ' ' );
+			textInputElement.blur();
+		} );
+
+		// Empty fields, even whitespace don't show validation errors on blur.
+		expect(
+			screen.queryByText( 'Please enter a valid test input' )
+		).not.toBeInTheDocument();
+
+		// Simulate form submission which reveals all hidden validation errors
+		await act( () =>
+			dispatch( validationStore ).showAllValidationErrors()
+		);
+
+		expect(
 			screen.queryByText( 'Please enter a valid test input' )
-		).not.toBeNull();
+		).toBeInTheDocument();
+
+		// After submission, blurring the empty field should NOT hide the error
+		await act( async () => {
+			await user.click( textInputElement );
+			textInputElement.blur();
+		} );
+
+		expect(
+			screen.queryByText( 'Please enter a valid test input' )
+		).toBeInTheDocument();
 	} );
+
 	describe( 'correctly validates on mount', () => {
 		it( 'validates when focusOnMount is true and validateOnMount is not set', async () => {
 			const setValidationErrors = jest.fn();
diff --git a/plugins/woocommerce/client/blocks/packages/components/text-input/validated-text-input.tsx b/plugins/woocommerce/client/blocks/packages/components/text-input/validated-text-input.tsx
index f466ef1d73..56862d1aca 100644
--- a/plugins/woocommerce/client/blocks/packages/components/text-input/validated-text-input.tsx
+++ b/plugins/woocommerce/client/blocks/packages/components/text-input/validated-text-input.tsx
@@ -300,7 +300,21 @@ const ValidatedTextInput = forwardRef<
 						onChange( formattedValue );
 					}
 				} }
-				onBlur={ () => validateInput( false ) }
+				onBlur={ () => {
+					const isEmpty = ! inputRef.current?.value.trim();
+
+					if ( isEmpty ) {
+						// If the error was already shown (e.g. after form
+						// submission), keep it visible. Otherwise, keep it
+						// hidden until the next form submission.
+						validateInput(
+							! validationError?.message ||
+								validationError?.hidden
+						);
+					} else {
+						validateInput( false );
+					}
+				} }
 				aria-describedby={ ariaDescribedBy }
 				value={ value }
 				title="" // This prevents the same error being shown on hover.