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.