Custom validation messages using the field name/label (https://github.com/woocommerce/woocommerce-blocks/pull/8143)

* Custom validation strings using a new function named getValidityMessageForInput

* getValidityMessageForInput tests

* Added integration test for error message

* Clear value

* update test strings
This commit is contained in:
Mike Jolley 2023-01-13 15:54:35 +00:00 committed by GitHub
parent 894e02b1a0
commit db96991492
6 changed files with 146 additions and 26 deletions

View File

@ -69,15 +69,11 @@ const Block = (): JSX.Element => {
value={ billingAddress.email } value={ billingAddress.email }
required={ true } required={ true }
onChange={ onChangeEmail } onChange={ onChangeEmail }
requiredMessage={ __(
'Please provide a valid email address',
'woo-gutenberg-products-block'
) }
customValidation={ ( inputObject: HTMLInputElement ) => { customValidation={ ( inputObject: HTMLInputElement ) => {
if ( ! isEmail( inputObject.value ) ) { if ( ! isEmail( inputObject.value ) ) {
inputObject.setCustomValidity( inputObject.setCustomValidity(
__( __(
'Please provide a valid email address', 'Please enter a valid email address',
'woo-gutenberg-products-block' 'woo-gutenberg-products-block'
) )
); );

View File

@ -148,4 +148,24 @@ describe( 'ValidatedTextInput', () => {
select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' ) select( VALIDATION_STORE_KEY ).getValidationError( 'test-input' )
).toBe( undefined ); ).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' );
} );
} ); } );

View File

@ -1,7 +1,6 @@
/** /**
* External dependencies * External dependencies
*/ */
import { __ } from '@wordpress/i18n';
import { import {
useRef, useRef,
useEffect, useEffect,
@ -22,24 +21,36 @@ import { usePrevious } from '@woocommerce/base-hooks';
import TextInput from './text-input'; import TextInput from './text-input';
import './style.scss'; import './style.scss';
import { ValidationInputError } from '../validation-input-error'; import { ValidationInputError } from '../validation-input-error';
import { getValidityMessageForInput } from '../../utils';
interface ValidatedTextInputProps interface ValidatedTextInputProps
extends Omit< extends Omit<
InputHTMLAttributes< HTMLInputElement >, InputHTMLAttributes< HTMLInputElement >,
'onChange' | 'onBlur' 'onChange' | 'onBlur'
> { > {
// id to use for the input. If not provided, an id will be generated.
id?: string; id?: string;
// Unique instance ID. id will be used instead if provided.
instanceId: string; instanceId: string;
// Class name to add to the input.
className?: string | undefined; className?: string | undefined;
// aria-describedby attribute to add to the input.
ariaDescribedBy?: string | undefined; ariaDescribedBy?: string | undefined;
// id to use for the error message. If not provided, an id will be generated.
errorId?: string; errorId?: string;
// if true, the input will be focused on mount.
focusOnMount?: boolean; focusOnMount?: boolean;
showError?: boolean; // Callback to run on change which is passed the updated value.
errorMessage?: string | undefined;
onChange: ( newValue: string ) => void; onChange: ( newValue: string ) => void;
// Optional label for the field.
label?: string | undefined; label?: string | undefined;
// Field value.
value: string; 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?: customValidation?:
| ( ( inputObject: HTMLInputElement ) => boolean ) | ( ( inputObject: HTMLInputElement ) => boolean )
| undefined; | undefined;
@ -56,8 +67,8 @@ const ValidatedTextInput = ( {
showError = true, showError = true,
errorMessage: passedErrorMessage = '', errorMessage: passedErrorMessage = '',
value = '', value = '',
requiredMessage,
customValidation, customValidation,
label,
...rest ...rest
}: ValidatedTextInputProps ): JSX.Element => { }: ValidatedTextInputProps ): JSX.Element => {
const [ isPristine, setIsPristine ] = useState( true ); const [ isPristine, setIsPristine ] = useState( true );
@ -99,17 +110,11 @@ const ValidatedTextInput = ( {
return; return;
} }
const validityState = inputObject.validity;
if ( validityState.valueMissing && requiredMessage ) {
inputObject.setCustomValidity( requiredMessage );
}
setValidationErrors( { setValidationErrors( {
[ errorIdString ]: { [ errorIdString ]: {
message: message: label
inputObject.validationMessage || ? getValidityMessageForInput( label, inputObject )
__( 'Invalid value.', 'woo-gutenberg-products-block' ), : inputObject.validationMessage,
hidden: errorsHidden, hidden: errorsHidden,
}, },
} ); } );
@ -118,8 +123,8 @@ const ValidatedTextInput = ( {
clearValidationError, clearValidationError,
customValidation, customValidation,
errorIdString, errorIdString,
requiredMessage,
setValidationErrors, setValidationErrors,
label,
] ]
); );
@ -211,6 +216,8 @@ const ValidatedTextInput = ( {
} } } }
ariaDescribedBy={ describedBy } ariaDescribedBy={ describedBy }
value={ value } value={ value }
title=""
label={ label }
{ ...rest } { ...rest }
/> />
); );

View File

@ -25,3 +25,34 @@ export const mustContain = (
} }
return true; 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;
};

View File

@ -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' );
} );
} );

View File

@ -145,37 +145,37 @@ describe( 'Shopper → Checkout', () => {
await expect( page ).toMatchElement( await expect( page ).toMatchElement(
'#email ~ .wc-block-components-validation-error p', '#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( await expect( page ).toMatchElement(
'#billing-first_name ~ .wc-block-components-validation-error p', '#billing-first_name ~ .wc-block-components-validation-error p',
{ {
text: 'Please fill', text: 'Please enter',
} }
); );
await expect( page ).toMatchElement( await expect( page ).toMatchElement(
'#billing-last_name ~ .wc-block-components-validation-error p', '#billing-last_name ~ .wc-block-components-validation-error p',
{ {
text: 'Please fill', text: 'Please enter',
} }
); );
await expect( page ).toMatchElement( await expect( page ).toMatchElement(
'#billing-address_1 ~ .wc-block-components-validation-error p', '#billing-address_1 ~ .wc-block-components-validation-error p',
{ {
text: 'Please fill', text: 'Please enter',
} }
); );
await expect( page ).toMatchElement( await expect( page ).toMatchElement(
'#billing-city ~ .wc-block-components-validation-error p', '#billing-city ~ .wc-block-components-validation-error p',
{ {
text: 'Please fill', text: 'Please enter',
} }
); );
await expect( page ).toMatchElement( await expect( page ).toMatchElement(
'#billing-postcode ~ .wc-block-components-validation-error p', '#billing-postcode ~ .wc-block-components-validation-error p',
{ {
text: 'Please fill', text: 'Please enter',
} }
); );
} ); } );