Skip to content
This repository has been archived by the owner on Feb 23, 2024. It is now read-only.

Custom validation messages using the field name/label #8143

Merged
merged 5 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,11 @@ const Block = (): JSX.Element => {
value={ billingAddress.email }
required={ true }
onChange={ onChangeEmail }
requiredMessage={ __(
'Please provide a valid email address',
'woo-gutenberg-products-block'
) }
customValidation={ ( inputObject: HTMLInputElement ) => {
if ( ! isEmail( inputObject.value ) ) {
inputObject.setCustomValidity(
__(
'Please provide a valid email address',
'Please enter a valid email address',
'woo-gutenberg-products-block'
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,24 @@ describe( 'ValidatedTextInput', () => {
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).toBe( undefined );
} );
it( 'Shows a custom error message for an invalid required input', async () => {
const TestComponent = () => {
const [ inputValue, setInputValue ] = useState( '' );
return (
<ValidatedTextInput
instanceId={ '5' }
id={ 'test-input' }
onChange={ ( value ) => setInputValue( value ) }
value={ inputValue }
label={ 'Test Input' }
/>
);
};
render( <TestComponent /> );
const textInputElement = await screen.getByLabelText( 'Test Input' );
await userEvent.type( textInputElement, '{selectall}{del}' );
await expect(
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).not.toBe( 'Please enter a valid test input' );
} );
} );
37 changes: 22 additions & 15 deletions packages/checkout/components/text-input/validated-text-input.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import {
useRef,
useEffect,
Expand All @@ -22,24 +21,36 @@ import { usePrevious } from '@woocommerce/base-hooks';
import TextInput from './text-input';
import './style.scss';
import { ValidationInputError } from '../validation-input-error';
import { getValidityMessageForInput } from '../../utils';

interface ValidatedTextInputProps
extends Omit<
InputHTMLAttributes< HTMLInputElement >,
'onChange' | 'onBlur'
> {
// id to use for the input. If not provided, an id will be generated.
id?: string;
// Unique instance ID. id will be used instead if provided.
instanceId: string;
// Class name to add to the input.
className?: string | undefined;
// aria-describedby attribute to add to the input.
ariaDescribedBy?: string | undefined;
// id to use for the error message. If not provided, an id will be generated.
errorId?: string;
// if true, the input will be focused on mount.
focusOnMount?: boolean;
showError?: boolean;
errorMessage?: string | undefined;
// Callback to run on change which is passed the updated value.
onChange: ( newValue: string ) => void;
// Optional label for the field.
label?: string | undefined;
// Field value.
value: string;
requiredMessage?: string | undefined;
// If true, validation errors will be shown.
showError?: boolean;
// Error message to display alongside the field regardless of validation.
errorMessage?: string | undefined;
// Custom validation function that is run on change. Use setCustomValidity to set an error message.
customValidation?:
| ( ( inputObject: HTMLInputElement ) => boolean )
| undefined;
Expand All @@ -56,8 +67,8 @@ const ValidatedTextInput = ( {
showError = true,
errorMessage: passedErrorMessage = '',
value = '',
requiredMessage,
customValidation,
label,
...rest
}: ValidatedTextInputProps ): JSX.Element => {
const [ isPristine, setIsPristine ] = useState( true );
Expand Down Expand Up @@ -99,17 +110,11 @@ const ValidatedTextInput = ( {
return;
}

const validityState = inputObject.validity;

if ( validityState.valueMissing && requiredMessage ) {
inputObject.setCustomValidity( requiredMessage );
}

setValidationErrors( {
[ errorIdString ]: {
message:
inputObject.validationMessage ||
__( 'Invalid value.', 'woo-gutenberg-products-block' ),
message: label
? getValidityMessageForInput( label, inputObject )
: inputObject.validationMessage,
hidden: errorsHidden,
},
} );
Expand All @@ -118,8 +123,8 @@ const ValidatedTextInput = ( {
clearValidationError,
customValidation,
errorIdString,
requiredMessage,
setValidationErrors,
label,
]
);

Expand Down Expand Up @@ -211,6 +216,8 @@ const ValidatedTextInput = ( {
} }
ariaDescribedBy={ describedBy }
value={ value }
title=""
opr marked this conversation as resolved.
Show resolved Hide resolved
label={ label }
{ ...rest }
/>
);
Expand Down
31 changes: 31 additions & 0 deletions packages/checkout/utils/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,34 @@ export const mustContain = (
}
return true;
};

/**
* Converts an input's validityState to a string to display on the frontend.
*
* This returns custom messages for invalid/required fields. Other error types use defaults from the browser (these
* could be implemented in the future but are not currently used by the block checkout).
*/
export const getValidityMessageForInput = (
label: string,
inputElement: HTMLInputElement
): string => {
const { valid, customError, valueMissing, badInput, typeMismatch } =
inputElement.validity;

// No errors, or custom error - return early.
if ( valid || customError ) {
return inputElement.validationMessage;
}

const invalidFieldMessage = sprintf(
/* translators: %s field label */
__( 'Please enter a valid %s', 'woo-gutenberg-products-block' ),
label.toLowerCase()
);

if ( valueMissing || badInput || typeMismatch ) {
return invalidFieldMessage;
}

return inputElement.validationMessage || invalidFieldMessage;
};
66 changes: 66 additions & 0 deletions packages/checkout/utils/validation/test/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* External dependencies
*/
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
* Internal dependencies
*/
import { getValidityMessageForInput } from '../index';

describe( 'getValidityMessageForInput', () => {
it( 'Returns nothing if the input is valid', async () => {
render( <input type="text" data-testid="custom-input" /> );

const textInputElement = ( await screen.getByTestId(
'custom-input'
) ) as HTMLInputElement;

const validityMessage = getValidityMessageForInput(
'Test',
textInputElement
);
expect( validityMessage ).toBe( '' );
} );
it( 'Returns error message if a required input is empty', async () => {
render( <input type="text" required data-testid="custom-input" /> );

const textInputElement = ( await screen.getByTestId(
'custom-input'
) ) as HTMLInputElement;

const validityMessage = getValidityMessageForInput(
'Test',
textInputElement
);

expect( validityMessage ).toBe( 'Please enter a valid test' );
} );
it( 'Returns a custom error if set, rather than a new message', async () => {
render(
<input
type="text"
required
onChange={ ( event ) => {
event.target.setCustomValidity( 'Custom error' );
} }
data-testid="custom-input"
/>
);

const textInputElement = ( await screen.getByTestId(
'custom-input'
) ) as HTMLInputElement;

await act( async () => {
await userEvent.type( textInputElement, 'Invalid Value' );
} );

const validityMessage = getValidityMessageForInput(
'Test',
textInputElement
);
expect( validityMessage ).toBe( 'Custom error' );
} );
} );
12 changes: 6 additions & 6 deletions tests/e2e/specs/shopper/cart-checkout/checkout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,37 +145,37 @@ describe( 'Shopper → Checkout', () => {
await expect( page ).toMatchElement(
'#email ~ .wc-block-components-validation-error p',
{
text: 'Please provide a valid email address',
text: 'Please enter a valid email address',
}
);
await expect( page ).toMatchElement(
'#billing-first_name ~ .wc-block-components-validation-error p',
{
text: 'Please fill',
text: 'Please enter',
}
);
await expect( page ).toMatchElement(
'#billing-last_name ~ .wc-block-components-validation-error p',
{
text: 'Please fill',
text: 'Please enter',
}
);
await expect( page ).toMatchElement(
'#billing-address_1 ~ .wc-block-components-validation-error p',
{
text: 'Please fill',
text: 'Please enter',
}
);
await expect( page ).toMatchElement(
'#billing-city ~ .wc-block-components-validation-error p',
{
text: 'Please fill',
text: 'Please enter',
}
);
await expect( page ).toMatchElement(
'#billing-postcode ~ .wc-block-components-validation-error p',
{
text: 'Please fill',
text: 'Please enter',
}
);
} );
Expand Down
Loading