elements_Input_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses, uniqueId } from '@gravityforms/utils';
import { useStateWithDep, ConditionalWrapper } from '@gravityforms/react-utils';
import Button from '../Button';
import Label from '../Label';
import HelpText from '../HelpText';
import Icon from '../Icon';

const { useState, forwardRef } = React;

/**
 * @module Input
 * @description A multi type input component that supports standard text, email, tel.
 *
 * @since 1.1.15
 *
 * @param {object}                     props                           Component props.
 * @param {string}                     props.borderStyle               The border style, one of `default`, `error`, or `correct`.
 * @param {boolean}                    props.clearable                 If input is clearable.
 * @param {object}                     props.clearableButtonAttributes Custom attributes for the clearable button.
 * @param {boolean}                    props.controlled                Whether the input is controlled or not.
 * @param {object}                     props.customAttributes          Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses             Custom classes for the component.
 * @param {boolean}                    props.disabled                  If input is disabled.
 * @param {object}                     props.helpTextAttributes        Custom attribute for the help text.
 * @param {string}                     props.helpTextPosition          The position of the help text.
 * @param {string}                     props.iconAttributes            Custom attributes for the icon.
 * @param {string}                     props.id                        Optional id. Auto generated if not passed.
 * @param {object}                     props.labelAttributes           Any custom attributes for the label.
 * @param {string}                     props.name                      The name attribute for the input.
 * @param {Function}                   props.onBlur                    On blur function handler.
 * @param {Function}                   props.onChange                  On change function handler.
 * @param {Function}                   props.onClear                   On clear function handler. When clearable true, this function is called when it occurs.
 * @param {Function}                   props.onFocus                   On focus function handler.
 * @param {string}                     props.placeholder               The optional placeholder attribute for the input.
 * @param {boolean}                    props.required                  Whether the field is required or not.
 * @param {object}                     props.requiredLabel             Required label properties.
 * @param {string}                     props.size                      The input size: regular (`size-r`), large (`size-l`), or extra large (`size-xl`).
 * @param {string|number|Array|object} props.spacing                   The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.textSecurity              The text security of the input, one of `circle`, `disc`, `square`, or `none`.
 * @param {string}                     props.theme                     The theme of the input.
 * @param {string}                     props.type                      The input type.
 * @param {string}                     props.value                     The input value.
 * @param {object}                     props.wrapperAttributes         Custom attributes for the wrapper element.
 * @param {string|Array|object}        props.wrapperClasses            Custom classes for the wrapper element.
 * @param {object}                     props.wrapperTagName            Tag to use for the input wrapper. Defaults to `div`.
 * @param {object|null}                ref                             Ref to the component.
 *
 * @return {JSX.Element} The input component.
 *
 * @example
 * import Input from '@gravityforms/components/react/admin/elements/Input';
 *
 * return (
 *     <Input
 *         onChange={ () => {} }
 *         placeholder="I am a placeholder"
 *     />
 * );
 *
 */
const Input = forwardRef( ( {
	actionButtonAttributes = {
		label: '',
	},
	borderStyle = 'default',
	clearable = false,
	clearableButtonAttributes = {},
	controlled = false,
	customAttributes = {},
	customClasses = [],
	disabled = false,
	helpTextAttributes = {
		size: 'text-xs',
		weight: 'regular',
	},
	helpTextPosition = 'below',
	iconAttributes = {},
	id = '',
	labelAttributes = {
		size: 'text-sm',
		weight: 'medium',
	},
	name = '',
	onBlur = () => {},
	onChange = () => {},
	onClear = () => {},
	onKeyDown = () => {},
	onFocus = () => {},
	placeholder = '',
	required = false,
	requiredLabel = {
		size: 'text-sm',
		weight: 'medium',
	},
	size = 'size-r',
	spacing = '',
	textSecurity = 'none',
	theme = 'cosmos',
	type = 'text',
	value = '',
	wrapperAttributes = {},
	wrapperClasses = [],
	wrapperTagName = 'div',
}, ref ) => {
	const useStateFxn = controlled ? useStateWithDep : useState;
	const [ inputValue, setInputValue ] = useStateFxn( value );

	const inputId = id || uniqueId( 'input' );
	const helpTextId = `${ inputId }-help-text`;
	const withIcon = !! ( iconAttributes.icon || iconAttributes.preset );
	const withAction = actionButtonAttributes.label && actionButtonAttributes.label.length > 0;

	const wrapperProps = {
		...wrapperAttributes,
		className: classnames( {
			'gform-input-wrapper': true,
			[ `gform-input-wrapper--theme-${ theme }` ]: true,
			'gform-input-wrapper--input': true,
			'gform-input-wrapper--with-action': withAction,
			'gform-input-wrapper--clearable': clearable,
			'gform-input-wrapper--disabled': disabled,
			...spacerClasses( spacing ),
			'gform-input-wrapper--required': required,
			[ `gform-input-wrapper--border-${ borderStyle }` ]: true,
			'gform-input-wrapper--with-icon': withIcon,
			[ `gform-input-wrapper--text-security-${ textSecurity }` ]: type !== 'password' && textSecurity !== 'none',
		}, wrapperClasses ),
		ref,
	};

	const inputProps = {
		...customAttributes,
		className: classnames( {
			'gform-input': true,
			'gform-typography--size-text-sm': 'cosmos' === theme,
			[ `gform-input--${ size }` ]: true,
			[ `gform-input--${ type }` ]: true,
		}, customClasses ),
		disabled: disabled || labelAttributes?.locked === true,
		id: inputId,
		name,
		onBlur,
		onKeyDown,
		onChange: ( e ) => {
			const { value: newInputValue } = e.target;
			setInputValue( newInputValue );
			onChange( newInputValue, e );
		},
		onFocus,
		type,
		value: inputValue,
	};

	if ( placeholder ) {
		inputProps.placeholder = placeholder;
	}

	if ( helpTextAttributes.content ) {
		inputProps[ 'aria-describedby' ] = helpTextId;
	}

	if ( required ) {
		inputProps.required = true;
	}

	const labelProps = {
		...labelAttributes,
		htmlFor: inputId,
	};

	const helpTextProps = {
		...helpTextAttributes,
		id: helpTextId,
	};

	const requiredLabelProps = {
		customClasses: classnames(
			[ 'gform-input-help-text--required' ],
		),
		id: helpTextId,
		...requiredLabel,
	};

	const iconProps = {
		...iconAttributes,
		customClasses: classnames(
			[ 'gform-input__icon' ],
			iconAttributes.customClasses || [],
		),
	};

	const clearableButtonProps = {
		icon: 'circle-close',
		type: 'unstyled',
		...clearableButtonAttributes,
		customClasses: classnames(
			[ 'gform-input__clearable-button' ],
			clearableButtonAttributes.customClasses || [],
		),
	};

	const actionButtonProps = {
		iconPosition: 'leading',
		type: 'white',
		...actionButtonAttributes,
		customClasses: classnames(
			[ 'gform-input__action-button' ],
			actionButtonAttributes.customClasses || [],
		),
		onClick: ( e ) => {
			if ( actionButtonAttributes.onClick ) {
				actionButtonAttributes.onClick( e, inputValue );
			}
		},
	};

	const Container = wrapperTagName;

	return (
		<Container { ...wrapperProps }>
			<Label { ...labelProps } />{ required && <HelpText { ...requiredLabelProps } /> }
			{ helpTextPosition === 'above' && <HelpText { ...helpTextProps } /> }
			<ConditionalWrapper
				condition={ withAction }
				wrapper={ ( ch ) => <div className="gform-input__action-wrapper">{ ch }{ <Button { ...actionButtonProps } /> }</div> }
			>
				<ConditionalWrapper
					condition={ withIcon }
					wrapper={ ( ch ) => <div className="gform-input__wrapper">{ ch }</div> }
				>
					<input { ...inputProps } />
					{ withIcon && <Icon { ...iconProps } /> }
					{ clearable && inputValue && (
						<Button
							{ ...clearableButtonProps }
							onClick={ () => {
								setInputValue( '' );
								onClear();
							} }
						/>
					) }
				</ConditionalWrapper>
			</ConditionalWrapper>
			{ helpTextPosition === 'below' && <HelpText { ...helpTextProps } /> }
		</Container>
	);
} );

Input.propTypes = {
	borderStyle: PropTypes.string,
	clearable: PropTypes.bool,
	clearableButtonAttributes: PropTypes.object,
	controlled: PropTypes.bool,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	helpTextAttributes: PropTypes.object,
	helpTextPosition: PropTypes.string,
	iconAttributes: PropTypes.object,
	id: PropTypes.string,
	labelAttributes: PropTypes.object,
	name: PropTypes.string,
	onBlur: PropTypes.func,
	onChange: PropTypes.func,
	onClear: PropTypes.func,
	onKeyDown: PropTypes.func,
	onFocus: PropTypes.func,
	placeholder: PropTypes.string,
	required: PropTypes.bool,
	requiredLabel: PropTypes.object,
	size: PropTypes.string,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	textSecurity: PropTypes.string,
	theme: PropTypes.string,
	type: PropTypes.string,
	value: PropTypes.string,
	wrapperAttributes: PropTypes.object,
	wrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	wrapperTagName: PropTypes.string,
};

Input.displayName = 'Input';

export default Input;