elements_Toggle_index.js

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

const { useState, forwardRef } = React;

/**
 * @module Toggle
 * @description A toggle component to use wherever boolean based form fields are needed.
 *
 * @since 1.1.15
 *
 * @param {object}                     props                    Component props.
 * @param {string}                     props.ariaLabel          The aria-label text for the toggle.
 * @param {JSX.Element}                props.children           React element children.
 * @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 toggle is disabled.
 * @param {boolean}                    props.externalChecked    If toggle is checked or not, can change current checked state if changed.
 * @param {boolean}                    props.externalControl    If toggle can be controlled externally.
 * @param {object}                     props.helpTextAttributes Attributes for the help text component.
 * @param {string}                     props.helpTextWidth      Width of the help text, one of `auto` or `full`.
 * @param {boolean}                    props.icons              Do we use the checkmark and x icons for this instance.
 * @param {string}                     props.iconPrefix         The icon prefix to use for the icons.
 * @param {string}                     props.iconBefore         The icon to use when the toggle is unchecked.
 * @param {string}                     props.iconAfter          The icon to use when the toggle is checked.
 * @param {string}                     props.id                 Id for the toggle, auto generated if not passed.
 * @param {boolean}                    props.initialChecked     Is it checked on render?
 * @param {object}                     props.labelAttributes    Attributes for the label component.
 * @param {string}                     props.labelPosition      Position of the label, one of `left` or `right`.
 * @param {string}                     props.name               Name for the input.
 * @param {Function}                   props.onBlur             On blur function handler.
 * @param {Function}                   props.onChange           On change function handler.
 * @param {Function}                   props.onFocus            On focus function handler.
 * @param {string}                     props.size               Size for the toggle. Small (`size-s`), medium (`size-m`), or large (`size-l`).
 * @param {string|number|Array|object} props.spacing            The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.theme              Theme for the toggle, one of `primary` or `cosmos`.
 * @param {string}                     props.width              Width for the toggle, one of `auto` or `full`.
 * @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 toggle wrapper. Defaults to `div`.
 * @param {object|null}                ref                      Ref to the component.
 *
 * @return {JSX.Element} The toggle component.
 *
 * @example
 * import Toggle from '@gravityforms/components/react/admin/elements/Toggle';
 *
 * return <Toggle disabled={ true } initialChecked={ true } onChange={ () => {} } />;
 *
 */

const Toggle = forwardRef( ( {
	ariaLabel = '',
	children = null,
	customAttributes = {},
	customClasses = [],
	disabled = false,
	externalChecked = false,
	externalControl = false,
	helpTextAttributes = {},
	helpTextWidth = 'auto',
	icons = true,
	id = '',
	initialChecked = false,
	labelAttributes = {},
	labelPosition = 'right',
	name = '',
	onBlur = () => {},
	onChange = () => {},
	onFocus = () => {},
	size = 'size-s',
	spacing = '',
	theme = 'cosmos',
	width = 'auto',
	wrapperAttributes = {},
	wrapperClasses = [],
	wrapperTagName = 'div',
}, ref ) => {
	const [ checked, setChecked ] = useState( initialChecked );
	const [ controlChecked, setControlChecked ] = useStateWithDep( externalChecked ); // eslint-disable-line no-unused-vars
	const inputId = id || uniqueId( 'toggle' );
	const helpTextId = `${ inputId }-help-text`;

	const wrapperProps = {
		...wrapperAttributes,
		className: classnames( {
			'gform-toggle': true,
			[ `gform-toggle--theme-${ theme }` ]: true,
			[ `gform-toggle--${ size }` ]: true,
			[ `gform-toggle--label-${ labelPosition }` ]: true,
			[ `gform-toggle--width-${ width }` ]: true,
			[ `gform-toggle--help-text-width-${ helpTextWidth }` ]: helpTextAttributes.content,
			'gform-toggle--disabled': disabled,
			...spacerClasses( spacing ),
		}, wrapperClasses ),
		ref,
	};

	const inputProps = {
		...customAttributes,
		checked: externalControl ? controlChecked : checked,
		className: classnames( {
			'gform-toggle__toggle': true,
			'gform-toggle__toggle--has-icons': icons,
		}, customClasses ),
		disabled: disabled || labelAttributes?.locked === true,
		id: inputId,
		name,
		onBlur,
		onChange: ( event ) => {
			const { checked: toggleChecked } = event.target;
			if ( ! externalControl ) {
				setChecked( toggleChecked );
			}
			setTimeout( () => {
				onChange( toggleChecked );
			}, 150 );
		},
		onFocus,
		type: 'checkbox',
	};

	if ( ariaLabel ) {
		inputProps[ 'aria-label' ] = ariaLabel;
	}
	if ( helpTextAttributes.content ) {
		inputProps[ 'aria-describedby' ] = helpTextId;
	}
	if ( ! helpTextAttributes.spacing ) {
		helpTextAttributes.spacing = [ 4, 0, 0, 0 ];
	}

	const labelProps = {
		...labelAttributes,
		customClasses: classnames( [
			'gform-toggle__label',
		], labelAttributes.customClasses ),
		htmlFor: inputId,
	};

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

	const Container = wrapperTagName;

	return (
		<Container { ...wrapperProps }>
			<input { ...inputProps } />
			<ConditionalWrapper
				condition={ helpTextAttributes.content && labelAttributes.label }
				wrapper={ ( ch ) => <div className="gform-toggle__label-wrapper">{ ch }</div> }
			>
				<Label { ...labelProps } />
				{ helpTextAttributes.content && <HelpText { ...helpTextProps } /> }
				{ children }
			</ConditionalWrapper>
		</Container>
	);
} );

Toggle.propTypes = {
	ariaLabel: PropTypes.string,
	children: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	externalChecked: PropTypes.bool,
	externalControl: PropTypes.bool,
	helpTextAttributes: PropTypes.object,
	helpTextWidth: PropTypes.oneOf( [ 'auto', 'full' ] ),
	icons: PropTypes.bool,
	id: PropTypes.string,
	initialChecked: PropTypes.bool,
	labelAttributes: PropTypes.object,
	labelPosition: PropTypes.string,
	name: PropTypes.string,
	onBlur: PropTypes.func,
	onChange: PropTypes.func,
	onFocus: PropTypes.func,
	size: PropTypes.string,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	theme: PropTypes.string,
	width: PropTypes.oneOf( [ 'auto', 'full' ] ),
	wrapperAttributes: PropTypes.object,
	wrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	wrapperTagName: PropTypes.string,
};

Toggle.displayName = 'Toggle';

export default Toggle;