modules_Phone_index.js

import { classnames, React } from '@gravityforms/libraries';
import { usePhoneInputFormatUtilsContext as usePhoneInputFormatUtils } from '@gravityforms/react-utils';
import { spacerClasses } from '@gravityforms/utils';
import Box from '../../elements/Box';
import HelpText from '../../elements/HelpText';
import Input from '../../elements/Input';
import Dropdown from '../Dropdown';
import Loader from '../Loaders/RingLoader';

const { forwardRef, lazy, Suspense } = React;

const PhoneComponent = lazy( () => import( './PhoneComponent' ) );

const PhoneComponentPlaceholder = ( {
	countries,
	customAttributes = {},
	customClasses = [],
	dropdownAttributes = {},
	dropdownClasses = [],
	inputAttributes = {},
	inputClasses = [],
	label = '',
	labelAttributes = {},
	labelClasses = [],
	language = 'en',
	required = false,
	requiredLabelAttributes = {},
	requiredLabelClasses = [],
	size = 'r',
	spacing = '',
	width = 300,
} ) => {
	const disabled = true;

	const componentProps = {
		className: classnames( {
			'gform-phone': true,
			'gform-phone--placeholder': true,
			'wp-exclude-emoji': true,
			[ `gform-phone--size-${ size }` ]: true,
			'gform-phone--disabled': disabled,
			...spacerClasses( spacing ),
		}, customClasses ),
		style: {
			width: width ? `${ width }px` : undefined,
		},
		...customAttributes,
	};

	const labelProps = {
		className: classnames( [
			'gform-phone__label',
			'gform-text',
			'gform-text--color-port',
			'gform-typography--size-text-sm',
			'gform-typography--weight-medium',
		], labelClasses ),
		...labelAttributes,
	};

	const requiredLabelProps = {
		size: 'text-sm',
		weight: 'medium',
		...requiredLabelAttributes,
		customClasses: classnames( [ 'gform-phone__required' ], requiredLabelClasses ),
	};

	const dropdownProps = {
		countries,
		hasSearch: true,
		popoverMaxHeight: 300,
		showCallingCode: true,
		...dropdownAttributes,
		customClasses: classnames( [
			'gform-phone__dropdown',
		], dropdownClasses ),
		disabled,
		language,
		searchAttributes: {
			wrapperClasses: [ 'gform-phone__dropdown-search-wrapper' ],
			...( dropdownAttributes?.searchAttributes || {} ),
		},
		searchClasses: [ 'gform-phone__dropdown-search' ],
		size,
		triggerClasses: [ 'gform-phone__dropdown-trigger' ],
	};

	const inputProps = {
		...inputAttributes,
		customClasses: classnames( [
			'gform-phone__input',
		], inputClasses ),
		directControlled: true,
		disabled,
		required,
		size: `size-${ size }`,
		type: 'tel',
		wrapperClasses: classnames( {
			'gform-phone__input-wrapper': true,
		}, inputAttributes?.wrapperClasses || [] ),
	};

	return (
		<div { ...componentProps }>
			{ label && <div { ...labelProps }>{ label }{ required && <HelpText { ...requiredLabelProps } /> }</div> }
			<div className="gform-phone__wrapper">
				<Dropdown { ...dropdownProps } />
				<Input { ...inputProps } />
			</div>
		</div>
	);
};

/**
 * @module Phone
 * @description A phone input component with country code selection.
 *
 * @since 5.5.0
 *
 * @param {object}                     props                         Component props
 * @param {Array}                      props.countries               Array of country codes.
 * @param {object}                     props.customAttributes        Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses           Custom classes for the component.
 * @param {boolean}                    props.disabled                Whether the input is disabled.
 * @param {object}                     props.dropdownAttributes      Custom attributes for the dropdown.
 * @param {string|Array|object}        props.dropdownClasses         Custom classes for the dropdown.
 * @param {object}                     props.helpTextAttributes      Custom attributes for the help text.
 * @param {string|Array|object}        props.helpTextClasses         Custom classes for the help text.
 * @param {object}                     props.i18n                    i18n object for the country dropdown component.
 * @param {string}                     props.id                      Optional id (auto-generated if not provided).
 * @param {string}                     props.inputAttributes         Custom attributes for the input.
 * @param {string|Array|object}        props.inputClasses            Custom classes for the input.
 * @param {boolean}                    props.international           Whether the phone number is international format.
 * @param {object}                     props.label                   Label text.
 * @param {object}                     props.labelAttributes         Custom attributes for the label.
 * @param {string|Array|object}        props.labelClasses            Custom classes for the label.
 * @param {string}                     props.language                Language code for the country dropdown component.
 * @param {Function}                   props.onChange                Change handler function, fires both on input and dropdown change. Receives { country, number, isValid }.
 * @param {Array}                      props.preferredCountries      Array of preferred country codes.
 * @param {boolean}                    props.required                Whether the field is required.
 * @param {object}                     props.requiredLabelAttributes Custom attributes for the required label.
 * @param {string|Array|object}        props.requiredLabelClasses    Custom classes for the required label.
 * @param {string}                     props.size                    Component size: 'r', 'l', or 'xl'.
 * @param {string|number|Array|object} props.spacing                 Component spacing.
 * @param {boolean}                    props.usePlaceholder          Whether to use a placeholder.
 * @param {boolean}                    props.useValidation           Whether to use validation for the phone input.
 * @param {number}                     props.width                   Width of the component in pixels.
 * @param {object}                     ref                           Forwarded ref.
 *
 * @return {JSX.Element} Phone component
 *
 * @example
 * import Phone from '@gravityforms/components/react/admin/modules/Phone';
 *
 * return <Phone
 *     countries={ [ 'US', 'CA', 'GB', 'MX', 'DE', 'JP' ] }
 *     helpTextAttributes={ { content: 'The phone number you entered is not valid.' } }
 *     i18n={ { allCountries: 'All countries' } }
 *     id="contact-phone"
 *     international={ true }
 *     label="Phone Number"
 *     onChange={ ( value, event ) => {
 *         console.log( 'country: ', value.country );
 *         console.log( 'number: ', value.number );
 *         console.log( 'isValid: ', value.isValid );
 *         console.log( 'event: ', event );
 *     } }
 *     preferredCountries={ [ 'US', 'CA', 'GB' ] }
 *     required={ true }
 *     requiredLabelAttributes={ { content: '(Required)' } }
 *     size="xl"
 *     usePlaceholder={ true }
 *     useValidation={ true }
 *     width={ 500 }
 * />;
 *
 */
const Phone = forwardRef( ( props, ref ) => {
	const { width = 300 } = props;
	const { isLoading } = usePhoneInputFormatUtils();

	const fallback = (
		<Box customClasses={ [ 'gform-phone__fallback' ] } x={ width }>
			<PhoneComponentPlaceholder { ...props } />
			<Loader foreground="#9092b2" />
		</Box>
	);

	if ( isLoading ) {
		return fallback;
	}

	return (
		<Suspense fallback={ fallback }>
			<PhoneComponent { ...props } ref={ ref } />
		</Suspense>
	);
} );

export default Phone;