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:
parent
894e02b1a0
commit
db96991492
|
@ -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'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -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' );
|
||||||
|
} );
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -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 }
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
|
@ -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' );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -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',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in New Issue