elements_Select_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import Label from '../Label';
import HelpText from '../HelpText';
import { uniqueId, isObject, isEmptyObject, slugify, spacerClasses } from '@gravityforms/utils';

const { useState, forwardRef } = React;

/**
 * @module Select
 * @description A select component to display a list of items.
 *
 * @since 1.1.15
 *
 * @param {object}                     props                    Component props.
 * @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 select is disabled.
 * @param {string}                     props.helpTextAttributes Custom attribute for the help text.
 * @param {string}                     props.helpTextPosition   The position of the help text. Above or below.
 * @param {string}                     props.id                 Optional id. Auto generated if not passed.
 * @param {string}                     props.initialValue       Initial value for the select.
 * @param {string}                     props.labelAttributes    Any custom attributes for the label.
 * @param {string}                     props.name               The name attribute for the select.
 * @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 {Array}                      props.options            An array of options, each option as an object in the following structure:
 *                                                              {
 *                                                              customOptionAttrs: {}, // Key-value pairs of custom attributes.
 *                                                              customOptionClasses: [], // Array of strings of custom classes.
 *                                                              label: '', // Label as a string.
 *                                                              value: '', // Value as a string.
 *                                                              }
 * @param {string}                     props.size               The select 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.theme              The theme of the select.
 * @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 textarea wrapper. Defaults to `div`.
 * @param {string}                     props.ariaLabel          The aria-label text for the select element.
 * @param {object|null}                ref                      Ref to the component.
 *
 * @return {JSX.Element} The select component.
 *
 * @example
 * import Select from '@gravityforms/components/react/admin/elements/Select';
 *
 * return (
 *     <Select
 *         name="select-name"
 *         onChange={ () => {} }
 *         options={ [
 *             {
 *                 label: 'Option 1',
 *                 value: 'option-1',
 *             },
 *             {
 *                 label: 'Option 2',
 *                 value: 'option-2',
 *             },
 *             {
 *                 label: 'Option 3',
 *                 value: 'option-3',
 *             },
 *         ] }
 *     />
 * );
 *
 */
const Select = forwardRef( ( {
	customAttributes = {},
	customClasses = [],
	disabled = false,
	helpTextAttributes = {},
	helpTextPosition = 'below',
	id = '',
	initialValue = '',
	labelAttributes = {},
	name = '',
	onBlur = () => {},
	onChange = () => {},
	onFocus = () => {},
	options = [],
	size = 'size-r',
	spacing = '',
	theme = 'cosmos',
	wrapperAttributes = {},
	wrapperClasses = [],
	wrapperTagName = 'div',
	ariaLabel = '',
}, ref ) => {
	const [ selectValue, setSelectValue ] = useState( initialValue );
	const inputId = id || uniqueId( 'gform-select' );
	const helpTextId = `${ inputId }-help-text`;

	const wrapperProps = {
		...wrapperAttributes,
		className: classnames( {
			'gform-input-wrapper': true,
			[ `gform-input-wrapper--theme-${ theme }` ]: true,
			'gform-input-wrapper--select': true,
			'gform-input-wrapper--disabled': disabled,
			[ `gform-input-wrapper--${ size }` ]: true,
			...spacerClasses( spacing ),
		}, wrapperClasses ),
		ref,
	};

	const selectProps = {
		...customAttributes,
		className: classnames( [
			'gform-select',
		], customClasses ),
		disabled: disabled || labelAttributes?.locked === true,
		id: inputId,
		name,
		onBlur,
		onChange: ( e ) => {
			const { value: newSelectValue } = e.target;
			setSelectValue( newSelectValue );
			onChange( newSelectValue, e );
		},
		onFocus,
		value: selectValue,
	};

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

	if ( ariaLabel ) {
		selectProps[ 'aria-label' ] = ariaLabel;
	}

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

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

	const getSubOptions = ( choices = [] ) => {
		if ( isObject( choices ) && ! isEmptyObject( choices ) ) {
			return Object.entries( choices ).map( ( [ key ] ) => choices[ key ] );
		}
		return choices;
	};

	const Container = wrapperTagName;

	const getOption = ( ( {
		customOptionAttributes = {},
		customOptionClasses = [],
		label = '',
		value = '',
	}, index ) => {
		return (
			<option
				className={ classnames( [
					'gform-select__option',
				], customOptionClasses ) }
				key={ `${ slugify( label ) }-${ index }` }
				value={ value }
				{ ...customOptionAttributes }
			>
				{ label }
			</option>
		);
	} );

	const getOptions = options.map( ( data, index ) => {
		const subOptions = getSubOptions( data.choices );
		return subOptions.length ? (
			<optgroup label={ data.label } key={ `${ slugify( data.label ) }-${ index }` }>
				{ subOptions.map( ( subData, subIndex ) => getOption( subData, subIndex ) ) }
			</optgroup>
		) : getOption( data, index );
	} );

	return (
		<Container { ...wrapperProps }>
			<Label { ...labelProps } />
			{ helpTextPosition === 'above' && <HelpText { ...helpTextProps } /> }
			<div className="gform-select__wrapper">
				<select { ...selectProps }>
					{ getOptions }
				</select>
			</div>
			{ helpTextPosition === 'below' && <HelpText { ...helpTextProps } /> }
		</Container>
	);
} );

Select.propTypes = {
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	helpTextAttributes: PropTypes.object,
	helpTextPosition: PropTypes.string,
	id: PropTypes.string,
	initialValue: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
	] ),
	labelAttributes: PropTypes.object,
	name: PropTypes.string,
	onBlur: PropTypes.func,
	onChange: PropTypes.func,
	onFocus: PropTypes.func,
	options: PropTypes.array,
	size: PropTypes.string,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	theme: PropTypes.string,
	wrapperAttributes: PropTypes.object,
	wrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	wrapperTagName: PropTypes.string,
};

Select.displayName = 'Select';

export default Select;