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;