diff --git a/assets/js/base/components/text-input/index.js b/assets/js/base/components/text-input/index.ts similarity index 100% rename from assets/js/base/components/text-input/index.js rename to assets/js/base/components/text-input/index.ts diff --git a/assets/js/base/components/text-input/text-input.js b/assets/js/base/components/text-input/text-input.tsx similarity index 72% rename from assets/js/base/components/text-input/text-input.js rename to assets/js/base/components/text-input/text-input.tsx index 414777d83f1..44f2782a4b1 100644 --- a/assets/js/base/components/text-input/text-input.js +++ b/assets/js/base/components/text-input/text-input.tsx @@ -1,8 +1,7 @@ /** * External dependencies */ -import { forwardRef } from 'react'; -import PropTypes from 'prop-types'; +import { forwardRef, InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import { useState } from '@wordpress/element'; import { Label } from '@woocommerce/blocks-checkout'; @@ -12,7 +11,24 @@ import { Label } from '@woocommerce/blocks-checkout'; */ import './style.scss'; -const TextInput = forwardRef( +interface TextInputProps + extends Omit< + InputHTMLAttributes< HTMLInputElement >, + 'onChange' | 'onBlur' + > { + id: string; + ariaLabel?: string; + label?: string; + ariaDescribedBy?: string; + screenReaderLabel?: string; + help?: string; + feedback?: boolean | JSX.Element; + autoComplete?: string; + onChange: ( newValue: string ) => void; + onBlur?: ( newValue: string ) => void; +} + +const TextInput = forwardRef< HTMLInputElement, TextInputProps >( ( { className, @@ -29,7 +45,9 @@ const TextInput = forwardRef( value = '', onChange, required = false, - onBlur = () => {}, + onBlur = () => { + /* Do nothing */ + }, feedback, }, ref @@ -57,8 +75,8 @@ const TextInput = forwardRef( onChange( event.target.value ); } } onFocus={ () => setIsActive( true ) } - onBlur={ () => { - onBlur(); + onBlur={ ( event ) => { + onBlur( event.target.value ); setIsActive( false ); } } aria-label={ ariaLabel || label } @@ -93,19 +111,4 @@ const TextInput = forwardRef( } ); -TextInput.propTypes = { - id: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - value: PropTypes.string, - ariaLabel: PropTypes.string, - ariaDescribedBy: PropTypes.string, - label: PropTypes.string, - screenReaderLabel: PropTypes.string, - disabled: PropTypes.bool, - help: PropTypes.string, - autoCapitalize: PropTypes.string, - autoComplete: PropTypes.string, - required: PropTypes.bool, -}; - export default TextInput; diff --git a/assets/js/base/components/text-input/validated-text-input.js b/assets/js/base/components/text-input/validated-text-input.tsx similarity index 62% rename from assets/js/base/components/text-input/validated-text-input.js rename to assets/js/base/components/text-input/validated-text-input.tsx index 67bfe802e61..92448f3d077 100644 --- a/assets/js/base/components/text-input/validated-text-input.js +++ b/assets/js/base/components/text-input/validated-text-input.tsx @@ -3,7 +3,6 @@ */ import { __ } from '@wordpress/i18n'; import { useCallback, useRef, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import classnames from 'classnames'; import { ValidationInputError, @@ -17,6 +16,29 @@ import { withInstanceId } from '@woocommerce/base-hocs/with-instance-id'; import TextInput from './text-input'; import './style.scss'; +interface ValidatedTextInputPropsWithId { + instanceId?: string; + id: string; +} + +interface ValidatedTextInputPropsWithInstanceId { + instanceId: string; + id?: string; +} + +type ValidatedTextInputProps = ( + | ValidatedTextInputPropsWithId + | ValidatedTextInputPropsWithInstanceId + ) & { + className?: string; + ariaDescribedBy?: string; + errorId?: string; + validateOnMount?: boolean; + focusOnMount?: boolean; + showError?: boolean; + onChange: ( newValue: string ) => void; +}; + const ValidatedTextInput = ( { className, instanceId, @@ -28,9 +50,9 @@ const ValidatedTextInput = ( { onChange, showError = true, ...rest -} ) => { +}: ValidatedTextInputProps ) => { const [ isPristine, setIsPristine ] = useState( true ); - const inputRef = useRef(); + const inputRef = useRef< HTMLInputElement >( null ); const { getValidationError, hideValidationError, @@ -39,8 +61,9 @@ const ValidatedTextInput = ( { getValidationErrorId, } = useValidationContext(); - const textInputId = id || 'textinput-' + instanceId; - errorId = errorId || textInputId; + const textInputId = + typeof id !== 'undefined' ? id : 'textinput-' + instanceId; + const errorIdString = errorId !== undefined ? errorId : textInputId; const validateInput = useCallback( ( errorsHidden = true ) => { @@ -52,10 +75,10 @@ const ValidatedTextInput = ( { inputObject.value = inputObject.value.trim(); const inputIsValid = inputObject.checkValidity(); if ( inputIsValid ) { - clearValidationError( errorId ); + clearValidationError( errorIdString ); } else { setValidationErrors( { - [ errorId ]: { + [ errorIdString ]: { message: inputObject.validationMessage || __( @@ -67,13 +90,13 @@ const ValidatedTextInput = ( { } ); } }, - [ clearValidationError, errorId, setValidationErrors ] + [ clearValidationError, errorIdString, setValidationErrors ] ); useEffect( () => { if ( isPristine ) { if ( focusOnMount ) { - inputRef.current.focus(); + inputRef.current?.focus(); } setIsPristine( false ); } @@ -91,15 +114,19 @@ const ValidatedTextInput = ( { // Remove validation errors when unmounted. useEffect( () => { return () => { - clearValidationError( errorId ); + clearValidationError( errorIdString ); }; - }, [ clearValidationError, errorId ] ); + }, [ clearValidationError, errorIdString ] ); - const errorMessage = getValidationError( errorId ) || {}; + // @todo - When useValidationContext is converted to TypeScript, remove this cast and use the correct type. + const errorMessage = ( getValidationError( errorIdString ) || {} ) as { + message?: string; + hidden?: boolean; + }; const hasError = errorMessage.message && ! errorMessage.hidden; const describedBy = - showError && hasError && getValidationErrorId( errorId ) - ? getValidationErrorId( errorId ) + showError && hasError && getValidationErrorId( errorIdString ) + ? getValidationErrorId( errorIdString ) : ariaDescribedBy; return ( @@ -112,11 +139,13 @@ const ValidatedTextInput = ( { validateInput( false ); } } feedback={ - showError && + showError && ( + + ) } ref={ inputRef } onChange={ ( val ) => { - hideValidationError( errorId ); + hideValidationError( errorIdString ); onChange( val ); } } ariaDescribedBy={ describedBy } @@ -125,15 +154,4 @@ const ValidatedTextInput = ( { ); }; -ValidatedTextInput.propTypes = { - onChange: PropTypes.func.isRequired, - id: PropTypes.string, - value: PropTypes.string, - ariaDescribedBy: PropTypes.string, - errorId: PropTypes.string, - validateOnMount: PropTypes.bool, - focusOnMount: PropTypes.bool, - showError: PropTypes.bool, -}; - export default withInstanceId( ValidatedTextInput ); diff --git a/assets/js/base/context/index.js b/assets/js/base/context/index.ts similarity index 100% rename from assets/js/base/context/index.js rename to assets/js/base/context/index.ts diff --git a/packages/checkout/label/index.tsx b/packages/checkout/label/index.tsx index 582a96a5d5f..5a7dcf5d8df 100644 --- a/packages/checkout/label/index.tsx +++ b/packages/checkout/label/index.tsx @@ -3,13 +3,13 @@ */ import { Fragment } from '@wordpress/element'; import classNames from 'classnames'; -import type { ReactElement, HTMLAttributes } from 'react'; +import type { ReactElement, HTMLProps } from 'react'; -interface LabelProps { +interface LabelProps extends HTMLProps< HTMLElement > { label?: string; screenReaderLabel?: string; wrapperElement?: string; - wrapperProps?: HTMLAttributes< HTMLElement >; + wrapperProps?: HTMLProps< HTMLElement >; } /**