2020-03-23 11:22:00 +00:00
|
|
|
/**
|
|
|
|
* External dependencies
|
|
|
|
*/
|
|
|
|
import { __ } from '@wordpress/i18n';
|
2021-05-20 16:56:56 +00:00
|
|
|
import { useCallback, useRef, useEffect, useState } from 'react';
|
2020-03-23 11:22:00 +00:00
|
|
|
import classnames from 'classnames';
|
2021-05-20 16:56:56 +00:00
|
|
|
import {
|
|
|
|
ValidationInputError,
|
|
|
|
useValidationContext,
|
|
|
|
} from '@woocommerce/base-context';
|
2021-08-05 09:26:00 +00:00
|
|
|
import { withInstanceId } from '@wordpress/compose';
|
2021-06-04 08:44:26 +00:00
|
|
|
import { isString } from '@woocommerce/types';
|
2020-03-23 11:22:00 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Internal dependencies
|
|
|
|
*/
|
2021-01-19 15:55:44 +00:00
|
|
|
import TextInput from './text-input';
|
2020-03-23 11:22:00 +00:00
|
|
|
import './style.scss';
|
|
|
|
|
2021-05-19 09:55:15 +00:00
|
|
|
interface ValidatedTextInputPropsWithId {
|
|
|
|
instanceId?: string;
|
|
|
|
id: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ValidatedTextInputPropsWithInstanceId {
|
|
|
|
instanceId: string;
|
|
|
|
id?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
type ValidatedTextInputProps = (
|
|
|
|
| ValidatedTextInputPropsWithId
|
|
|
|
| ValidatedTextInputPropsWithInstanceId
|
|
|
|
) & {
|
|
|
|
className?: string;
|
|
|
|
ariaDescribedBy?: string;
|
|
|
|
errorId?: string;
|
|
|
|
focusOnMount?: boolean;
|
|
|
|
showError?: boolean;
|
2021-06-04 08:44:26 +00:00
|
|
|
errorMessage?: string;
|
2021-05-19 09:55:15 +00:00
|
|
|
onChange: ( newValue: string ) => void;
|
2021-12-20 12:16:41 +00:00
|
|
|
value: string;
|
2021-05-19 09:55:15 +00:00
|
|
|
};
|
|
|
|
|
2020-03-23 11:22:00 +00:00
|
|
|
const ValidatedTextInput = ( {
|
|
|
|
className,
|
|
|
|
instanceId,
|
|
|
|
id,
|
|
|
|
ariaDescribedBy,
|
|
|
|
errorId,
|
2020-05-06 19:04:00 +00:00
|
|
|
focusOnMount = false,
|
2020-03-23 11:22:00 +00:00
|
|
|
onChange,
|
|
|
|
showError = true,
|
2021-06-04 08:44:26 +00:00
|
|
|
errorMessage: passedErrorMessage = '',
|
2021-12-20 12:16:41 +00:00
|
|
|
value = '',
|
2020-03-23 11:22:00 +00:00
|
|
|
...rest
|
2021-05-19 09:55:15 +00:00
|
|
|
}: ValidatedTextInputProps ) => {
|
2020-10-27 14:37:18 +00:00
|
|
|
const [ isPristine, setIsPristine ] = useState( true );
|
2021-05-19 09:55:15 +00:00
|
|
|
const inputRef = useRef< HTMLInputElement >( null );
|
2021-05-20 16:56:56 +00:00
|
|
|
const {
|
|
|
|
getValidationError,
|
|
|
|
hideValidationError,
|
|
|
|
setValidationErrors,
|
|
|
|
clearValidationError,
|
|
|
|
getValidationErrorId,
|
|
|
|
} = useValidationContext();
|
2021-05-19 09:55:15 +00:00
|
|
|
const textInputId =
|
|
|
|
typeof id !== 'undefined' ? id : 'textinput-' + instanceId;
|
|
|
|
const errorIdString = errorId !== undefined ? errorId : textInputId;
|
2020-05-06 10:21:30 +00:00
|
|
|
|
2020-10-27 14:37:18 +00:00
|
|
|
const validateInput = useCallback(
|
|
|
|
( errorsHidden = true ) => {
|
2020-12-17 14:52:44 +00:00
|
|
|
const inputObject = inputRef.current || null;
|
|
|
|
if ( ! inputObject ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Trim white space before validation.
|
|
|
|
inputObject.value = inputObject.value.trim();
|
|
|
|
const inputIsValid = inputObject.checkValidity();
|
|
|
|
if ( inputIsValid ) {
|
2021-05-19 09:55:15 +00:00
|
|
|
clearValidationError( errorIdString );
|
2020-10-27 14:37:18 +00:00
|
|
|
} else {
|
|
|
|
setValidationErrors( {
|
2021-05-19 09:55:15 +00:00
|
|
|
[ errorIdString ]: {
|
2020-10-27 14:37:18 +00:00
|
|
|
message:
|
2020-12-17 14:52:44 +00:00
|
|
|
inputObject.validationMessage ||
|
2020-10-27 14:37:18 +00:00
|
|
|
__(
|
|
|
|
'Invalid value.',
|
|
|
|
'woo-gutenberg-products-block'
|
|
|
|
),
|
|
|
|
hidden: errorsHidden,
|
|
|
|
},
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
},
|
2021-05-19 09:55:15 +00:00
|
|
|
[ clearValidationError, errorIdString, setValidationErrors ]
|
2020-10-27 14:37:18 +00:00
|
|
|
);
|
2020-03-23 11:22:00 +00:00
|
|
|
|
2021-12-13 16:44:28 +00:00
|
|
|
/**
|
2021-12-20 12:16:41 +00:00
|
|
|
* Focus on mount
|
|
|
|
*
|
|
|
|
* If the input is in pristine state, focus the element.
|
2021-12-13 16:44:28 +00:00
|
|
|
*/
|
2020-05-06 19:04:00 +00:00
|
|
|
useEffect( () => {
|
2021-12-20 12:16:41 +00:00
|
|
|
if ( isPristine && focusOnMount ) {
|
|
|
|
inputRef.current?.focus();
|
2020-05-06 19:04:00 +00:00
|
|
|
}
|
2021-12-20 12:16:41 +00:00
|
|
|
setIsPristine( false );
|
2020-10-27 14:37:18 +00:00
|
|
|
}, [ focusOnMount, isPristine, setIsPristine ] );
|
2020-05-06 19:04:00 +00:00
|
|
|
|
2021-12-20 12:16:41 +00:00
|
|
|
/**
|
|
|
|
* Value Validation
|
|
|
|
*
|
|
|
|
* Runs validation on state change if the current element is not in focus. This is because autofilled elements do not
|
|
|
|
* trigger the blur() event, and so values can be validated in the background if the state changes elsewhere.
|
|
|
|
*/
|
2020-03-23 11:22:00 +00:00
|
|
|
useEffect( () => {
|
2021-12-20 12:16:41 +00:00
|
|
|
if (
|
|
|
|
inputRef.current?.ownerDocument?.activeElement !== inputRef.current
|
|
|
|
) {
|
|
|
|
validateInput( true );
|
2020-03-23 11:22:00 +00:00
|
|
|
}
|
2021-12-20 12:16:41 +00:00
|
|
|
// We need to track value even if it is not directly used so we know when it changes.
|
|
|
|
}, [ value, validateInput ] );
|
2020-03-23 11:22:00 +00:00
|
|
|
|
2020-04-02 09:27:54 +00:00
|
|
|
// Remove validation errors when unmounted.
|
|
|
|
useEffect( () => {
|
|
|
|
return () => {
|
2021-05-19 09:55:15 +00:00
|
|
|
clearValidationError( errorIdString );
|
2020-04-02 09:27:54 +00:00
|
|
|
};
|
2021-05-19 09:55:15 +00:00
|
|
|
}, [ clearValidationError, errorIdString ] );
|
2020-04-02 09:27:54 +00:00
|
|
|
|
2021-05-19 09:55:15 +00:00
|
|
|
// @todo - When useValidationContext is converted to TypeScript, remove this cast and use the correct type.
|
|
|
|
const errorMessage = ( getValidationError( errorIdString ) || {} ) as {
|
|
|
|
message?: string;
|
|
|
|
hidden?: boolean;
|
|
|
|
};
|
2021-12-13 16:44:28 +00:00
|
|
|
|
2021-06-04 08:44:26 +00:00
|
|
|
if ( isString( passedErrorMessage ) && passedErrorMessage !== '' ) {
|
|
|
|
errorMessage.message = passedErrorMessage;
|
|
|
|
}
|
2021-12-13 16:44:28 +00:00
|
|
|
|
2020-03-23 11:22:00 +00:00
|
|
|
const hasError = errorMessage.message && ! errorMessage.hidden;
|
|
|
|
const describedBy =
|
2021-05-19 09:55:15 +00:00
|
|
|
showError && hasError && getValidationErrorId( errorIdString )
|
|
|
|
? getValidationErrorId( errorIdString )
|
2020-03-23 11:22:00 +00:00
|
|
|
: ariaDescribedBy;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<TextInput
|
|
|
|
className={ classnames( className, {
|
|
|
|
'has-error': hasError,
|
|
|
|
} ) }
|
2021-09-14 11:26:41 +00:00
|
|
|
aria-invalid={ hasError === true }
|
2020-03-23 11:22:00 +00:00
|
|
|
id={ textInputId }
|
|
|
|
onBlur={ () => {
|
|
|
|
validateInput( false );
|
|
|
|
} }
|
|
|
|
feedback={
|
2021-05-19 09:55:15 +00:00
|
|
|
showError && (
|
2021-06-07 09:16:47 +00:00
|
|
|
<ValidationInputError
|
|
|
|
errorMessage={ passedErrorMessage }
|
|
|
|
propertyName={ errorIdString }
|
|
|
|
/>
|
2021-05-19 09:55:15 +00:00
|
|
|
)
|
2020-03-23 11:22:00 +00:00
|
|
|
}
|
|
|
|
ref={ inputRef }
|
|
|
|
onChange={ ( val ) => {
|
2021-05-19 09:55:15 +00:00
|
|
|
hideValidationError( errorIdString );
|
2020-03-23 11:22:00 +00:00
|
|
|
onChange( val );
|
|
|
|
} }
|
|
|
|
ariaDescribedBy={ describedBy }
|
2021-12-20 12:16:41 +00:00
|
|
|
value={ value }
|
2020-03-23 11:22:00 +00:00
|
|
|
{ ...rest }
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default withInstanceId( ValidatedTextInput );
|