modules_Dropdown_index.js

import { React, PropTypes, SimpleBar, classnames } from '@gravityforms/libraries';
import { ConditionalWrapper } from '@gravityforms/react-utils';
import { isObject, spacerClasses } from '@gravityforms/utils';
import useDropdownControl from './hooks/control';
import useDropdownBlur from './hooks/blur';
import useDropdownKeyDown from './hooks/key-down';
import useDropdownTypeahead from './hooks/typeahead';
import { getId, getSearchItem, getListItemsState, getComponent, filterListItems } from './utils';
import createStore from './store';
import Input from '../../elements/Input';
import Pill from '../../elements/Pill';
import { getIdProvider, useId } from '../../utils/contexts/id-context';
import { getStoreProvider, useStore } from '../../utils/contexts/store-context';
import { BACKSPACE, DELETE } from '../../utils/keymap';

const { forwardRef, useEffect, useRef } = React;

/**
 * @module DropdownLabel
 * @description The label for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object}              props                 Component props.
 * @param {string}              props.label           The label for the dropdown.
 * @param {object}              props.labelAttributes Custom attributes for the label.
 * @param {string|Array|object} props.labelClasses    Custom classes for the label.
 * @param {object}              ref                   Ref to the component.
 *
 * @return {JSX.Element} The label component.
 */
const DropdownLabel = forwardRef( ( {
	label = '',
	labelAttributes = {},
	labelClasses = [],
}, ref ) => {
	const id = useId();
	const triggerRef = useStore( ( state ) => state.triggerRef );

	if ( ! label ) {
		return null;
	}

	const labelId = getId( id, 'label' );

	const labelProps = {
		className: classnames( [
			'gform-dropdown__label',
			'gform-text',
			'gform-text--color-port',
			'gform-typography--size-text-sm',
			'gform-typography--weight-medium',
		], labelClasses ),
		...labelAttributes,
		id: labelId,
		onClick: () => triggerRef?.current?.focus(),
	};

	return <div { ...labelProps } ref={ ref }>{ label }</div>;
} );

/**
 * @module DropdownTrigger
 * @description The trigger for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object}              props                      Component props.
 * @param {boolean}             props.disabled             Whether the dropdown is disabled.
 * @param {Function}            props.handleBlur           The blur event handler.
 * @param {Function}            props.handleEscKeyDown     The escape keydown event handler.
 * @param {Function}            props.handleKeyDownCapture The keydown capture event handler.
 * @param {Function}            props.handleTriggerKeyDown The trigger keydown event handler.
 * @param {object}              props.i18n                 The i18n object.
 * @param {string}              props.label                The label for the dropdown.
 * @param {boolean}             props.multi                Whether the dropdown is multi-select.
 * @param {Function}            props.open                 The open function.
 * @param {Function}            props.resetAndClose        The reset and close function.
 * @param {object}              props.triggerAttributes    Custom attributes for the trigger.
 * @param {string|Array|object} props.triggerClasses       Custom classes for the trigger.
 * @param {object}              ref                        Ref to the component.
 *
 * @return {JSX.Element} The trigger component.
 */
const DropdownTrigger = forwardRef( ( {
	disabled = false,
	handleBlur = () => {},
	handleEscKeyDown = () => {},
	handleKeyDownCapture = () => {},
	handleTriggerKeyDown = () => {},
	i18n = {},
	label = '',
	multi = false,
	open = () => {},
	resetAndClose = () => {},
	triggerAttributes = {},
	triggerClasses = [],
}, ref ) => {
	const id = useId();
	const dropdownOpen = useStore( ( state ) => state.open );
	const selectedItem = useStore( ( state ) => state.selectedItem );
	const triggerHeight = useStore( ( state ) => state.triggerHeight );

	const popoverId = getId( id, 'popover' );
	const labelId = getId( id, 'label' );
	const pillsId = getId( id, 'pills' );

	const triggerProps = {
		className: classnames( [
			'gform-dropdown__trigger',
			'gform-text',
			'gform-text--color-port',
			'gform-typography--size-text-sm',
			'gform-typography--weight-regular',
		], triggerClasses ),
		...triggerAttributes,
		'aria-autocomplete': 'none',
		'aria-controls': popoverId,
		'aria-expanded': dropdownOpen ? 'true' : 'false',
		'aria-haspopup': 'listbox',
		disabled,
		onBlur: handleBlur,
		onClick: ( event ) => {
			const clickHandler = dropdownOpen ? resetAndClose : open;
			clickHandler( event );
		},
		onKeyDown: ( event ) => {
			handleEscKeyDown( event );
			handleTriggerKeyDown( event );
		},
		onKeyDownCapture: ( event ) => handleKeyDownCapture( event ),
		role: 'combobox',
		type: 'button',
	};
	if ( label ) {
		triggerProps[ 'aria-labelledby' ] = labelId;
	}
	if ( multi ) {
		triggerProps[ 'aria-describedby' ] = pillsId;
	}
	if ( triggerHeight ) {
		triggerProps.style = {
			height: `${ triggerHeight }px`,
		};
	}

	/**
	 * @function getSingleTriggerLabel
	 * @description Get the label for the single dropdown trigger.
	 *
	 * @since 4.5.0
	 *
	 * @return {JSX.Element} The label for the single dropdown trigger.
	 */
	const getSingleTriggerLabel = () => (
		<>
			{ selectedItem.beforeLabel && (
				<span className="gform-dropdown__trigger-before-label">
					{ getComponent( selectedItem.beforeLabel ) }
				</span>
			) }
			<span className="gform-dropdown__trigger-label">{ selectedItem.label }</span>
			{ selectedItem.afterLabel && (
				<span className="gform-dropdown__trigger-after-label">
					{ getComponent( selectedItem.afterLabel ) }
				</span>
			) }
		</>
	);

	/**
	 * @function getMultiTriggerLabel
	 * @description Get the label for the multi dropdown trigger.
	 *
	 * @since 4.5.0
	 *
	 * @return {string|JSX.Element} The label for the multi dropdown trigger.
	 */
	const getMultiTriggerLabel = () => {
		if ( selectedItem.length ) {
			return (
				<span className="gform-visually-hidden">
					{ selectedItem.map( ( item ) => item.label ).join( ', ' ) }
				</span>
			);
		}

		return i18n.multiTriggerLabel || 'Select';
	};

	return (
		<button { ...triggerProps } ref={ ref }>
			{ multi ? getMultiTriggerLabel() : getSingleTriggerLabel() }
		</button>
	);
} );

/**
 * @module DropdownPill
 * @description The pill component for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object} props      The component props.
 * @param {object} props.item The item object.
 *
 * @return {JSX.Element} The pill component.
 */
const DropdownPill = ( { item } ) => {
	const selectedItem = useStore( ( state ) => state.selectedItem );
	const setSelectedItem = useStore( ( state ) => state.setSelectedItem );
	const triggerRef = useStore( ( state ) => state.triggerRef );
	const pillRef = useRef( null );

	const removeItem = () => {
		// Find index of removed item.
		const index = selectedItem.findIndex( ( selItem ) => selItem.value === item.value );
		const length = selectedItem.length;

		// Remove item from selected items.
		setSelectedItem( selectedItem.filter( ( selItem ) => selItem.value !== item.value ) );

		// If current item is last one and there are more than 1 item, set focus on previous one.
		if ( pillRef.current && length > 1 && index === length - 1 ) {
			pillRef.current.previousSibling.focus();
		}

		// If current item is last one and there is only 1 item, set focus on trigger.
		if ( length === 1 ) {
			triggerRef.current.focus();
		}
	};

	const pillProps = {
		content: item.label,
		customClasses: [ 'gform-dropdown__pill' ],
		customAttributes: {
			role: 'option',
			tabIndex: '0',
			'aria-keyshortcuts': 'Backspace Delete',
			onKeyDown: ( event ) => {
				// If not backspace or delete, return early.
				if ( ! [ BACKSPACE, DELETE ].includes( event.key ) ) {
					return;
				}

				removeItem();
			},
		},
		tagName: 'div',
		onClick: removeItem,
	};

	return <Pill { ...pillProps } ref={ pillRef } />;
};

/**
 * @module DropdownPills
 * @description The pills component for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object}  props       The component props.
 * @param {boolean} props.multi Whether the dropdown is multi-select.
 * @param {object}  ref         The ref object.
 *
 * @return {JSX.Element} The pills component.
 */
const DropdownPills = forwardRef( ( {
	multi = false,
}, ref ) => {
	const id = useId();
	const selectedItem = useStore( ( state ) => state.selectedItem );

	// If not multi, or selected item is not array, return early.
	if ( ! multi || ! Array.isArray( selectedItem ) ) {
		return null;
	}

	const pillsId = getId( id, 'pills' );
	const pillsProps = {
		className: 'gform-dropdown__pills',
		id: pillsId,
		role: 'listbox',
	};

	return (
		<div { ...pillsProps } ref={ ref }>
			{ selectedItem.map( ( item, index ) => (
				<DropdownPill key={ index } item={ item } />
			) ) }
		</div>
	);
} );

/**
 * @module DropdownPopover
 * @description The popover component for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object}              props                      The component props.
 * @param {JSX.Element}         props.children             The children of the component.
 * @param {Function}            props.handleBlur           The blur event handler.
 * @param {Function}            props.handleEscKeyDown     The escape key down event handler.
 * @param {Function}            props.handleKeyDownCapture The key down capture event handler.
 * @param {Function}            props.handleListKeyDown    The list key down event handler.
 * @param {object}              props.popoverAttributes    The popover attributes.
 * @param {string|Array|object} props.popoverClasses       The popover classes.
 * @param {number}              props.popoverMaxHeight     The popover max height.
 * @param {object}              ref                        The ref object.
 *
 * @return {JSX.Element} The popover component.
 */
const DropdownPopover = forwardRef( ( {
	children = null,
	handleBlur = () => {},
	handleEscKeyDown = () => {},
	handleKeyDownCapture = () => {},
	handleListKeyDown = () => {},
	popoverAttributes = {},
	popoverClasses = [],
	popoverMaxHeight = 0,
}, ref ) => {
	const id = useId();
	const dropdownOpen = useStore( ( state ) => state.open );

	const popoverId = getId( id, 'popover' );

	const popoverProps = {
		className: classnames( {
			'gform-dropdown__popover': true,
		}, popoverClasses ),
		...popoverAttributes,
		'data-dialog': true,
		id: popoverId,
		onBlur: handleBlur,
		onKeyDown: ( event ) => {
			handleEscKeyDown( event );
			handleListKeyDown( event );
		},
		onKeyDownCapture: ( event ) => handleKeyDownCapture( event ),
		role: 'dialog',
		tabIndex: '-1',
	};
	if ( ! dropdownOpen ) {
		popoverProps.hidden = true;
	}
	if ( popoverMaxHeight ) {
		popoverProps.style = {
			maxHeight: `${ popoverMaxHeight }px`,
		};
	}
	return (
		<div className="gform-dropdown__popover-wrapper">
			<div { ...popoverProps } ref={ ref }>
				{ children }
			</div>
		</div>
	);
} );

/**
 * @module DropdownList
 * @description The list component for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object}              props                      The component props.
 * @param {Function}            props.handleBlur           The blur event handler.
 * @param {Function}            props.handleKeyDownCapture The key down capture event handler.
 * @param {Function}            props.handleListKeyDown    The list key down event handler.
 * @param {boolean}             props.hasSearch            Whether the dropdown has search.
 * @param {string}              props.label                The label of the dropdown.
 * @param {Array}               props.listAttributes       Custom attributes for the list.
 * @param {string|Array|object} props.listClasses          Custom classes for the list.
 * @param {Array}               props.listItems            The list items.
 * @param {boolean}             props.multi                Whether the dropdown is multi-select.
 * @param {number}              props.popoverMaxHeight     The popover max height.
 * @param {string}              props.selectedIcon         The selected icon.
 * @param {string}              props.selectedIconPrefix   The selected icon prefix.
 * @param {Function}            props.selectItem           The select item event handler.
 * @param {object}              ref                        The ref object.
 *
 * @return {JSX.Element} The list component.
 */
const DropdownList = forwardRef( ( {
	handleBlur = () => {},
	handleKeyDownCapture = () => {},
	handleListKeyDown = () => {},
	hasSearch = false,
	label = '',
	listAttributes = {},
	listClasses = [],
	listItems = [],
	multi = false,
	popoverMaxHeight = 0,
	selectedIcon = 'check',
	selectedIconPrefix = 'gform-icon',
	selectItem = () => {},
}, ref ) => {
	const id = useId();
	const activeItem = useStore( ( state ) => state.activeItem );
	const listItemsState = useStore( ( state ) => state.listItems );
	const selectedItem = useStore( ( state ) => state.selectedItem );
	const setActiveItem = useStore( ( state ) => state.setActiveItem );
	const baseElRef = useStore( ( state ) => state.baseElRef );

	/**
	 * @function getListItems
	 * @description Gets the list items for the dropdown.
	 *
	 * @since 4.5.0
	 *
	 * @return {Array} An array of list items.
	 */
	const getListItems = () => {
		return listItemsState.items.map( ( item ) => {
			if ( item.component === 'search' ) {
				return null;
			}
			const selected = multi
				? selectedItem.some( ( selItem ) => selItem.value === item.value )
				: selectedItem.value === item.value;
			const itemProps = {
				className: 'gform-dropdown__list-item',
				id: item.id,
				'aria-selected': selected ? 'true' : 'false',
				tabIndex: '-1',
				role: 'option',
				onMouseMove: () => {
					setActiveItem( item );
				},
				onClick: selectItem( item ),
				onFocus: () => baseElRef?.current?.focus(),
			};
			if ( activeItem.value === item.value ) {
				itemProps[ 'data-active-item' ] = 'true';
			}
			const itemInnerProps = {
				className: classnames( [
					'gform-dropdown__list-item-inner',
					'gform-text',
					'gform-text--color-port',
					'gform-typography--size-text-sm',
					'gform-typography--weight-regular',
				] ),
			};

			return (
				<div { ...itemProps } key={ item.id }>
					<div { ...itemInnerProps }>
						{ item.beforeLabel && (
							<span className="gform-dropdown__list-item-before-label">
								{ getComponent( item.beforeLabel ) }
							</span>
						) }
						<span className="gform-dropdown__list-item-label">{ item.label }</span>
						{ ( ( item.afterLabel && ! multi ) || ( multi && selected ) ) && (
							<span className="gform-dropdown__list-item-after-label">
								{ multi && selected
									? getComponent( {
										component: 'Icon',
										props: {
											iconPrefix: selectedIconPrefix,
											icon: selectedIcon,
										},
									} )
									: getComponent( item.afterLabel )
								}
							</span>
						) }
					</div>
				</div>
			);
		} );
	};

	const listId = getId( id, 'list' );

	const listProps = {
		className: classnames( [ 'gform-dropdown__list' ], listClasses ),
		...listAttributes,
		id: listId,
		onBlur: handleBlur,
		onKeyDown: ( event ) => {
			handleListKeyDown( event );
		},
		onKeyDownCapture: ( event ) => handleKeyDownCapture( event ),
		role: 'listbox',
	};
	if ( ! hasSearch ) {
		listProps.tabIndex = '0';
		if ( label ) {
			const labelId = getId( id, 'label' );
			listProps[ 'aria-labelledby' ] = labelId;
		}
		if ( listItems.length ) {
			listProps[ 'aria-activedescendant' ] = activeItem.id;
		}
	}
	if ( popoverMaxHeight ) {
		const listMaxHeight = hasSearch ? popoverMaxHeight - 58 : popoverMaxHeight;

		if ( listMaxHeight > 0 ) {
			listProps.style = {
				maxHeight: `${ listMaxHeight }px`,
			};
		}
	}

	return (
		<div { ...listProps } ref={ ref }>
			{ getListItems() }
		</div>
	);
} );

/**
 * @module DropdownSearch
 * @description The search component for the dropdown.
 *
 * @since 4.5.0
 *
 * @param {object}              props                  The component props.
 * @param {boolean}             props.hasSearch        Whether the dropdown has a search component.
 * @param {object}              props.searchAttributes The search component attributes.
 * @param {string|Array|object} props.searchClasses    The search component classes.
 * @param {object}              ref                    The ref object.
 *
 * @return {JSX.Element} The dropdown search component.
 */
const DropdownSearch = forwardRef( ( {
	hasSearch = false,
	searchAttributes = {},
	searchClasses = [],
}, ref ) => {
	const id = useId();
	const dropdownOpen = useStore( ( state ) => state.dropdownOpen );
	const activeItem = useStore( ( state ) => state.activeItem );
	const searchValue = useStore( ( state ) => state.searchValue );
	const setActiveItem = useStore( ( state ) => state.setActiveItem );
	const setSearchValue = useStore( ( state ) => state.setSearchValue );

	if ( ! hasSearch ) {
		return null;
	}

	const listId = getId( id, 'list' );
	const searchId = getId( id, 'search' );

	const {
		customAttributes: searchCustomAttributes = {},
		wrapperClasses: searchWrapperClasses = [],
		...restSearchAttributes
	} = searchAttributes;

	const searchProps = {
		customClasses: classnames( [
			'gform-dropdown__search',
		], searchClasses ),
		...restSearchAttributes,
		controlled: true,
		customAttributes: {
			...searchCustomAttributes,
			autoComplete: 'off',
			role: 'combobox',
			'aria-autocomplete': 'list',
			'aria-haspopup': 'listbox',
			'aria-controls': listId,
			'aria-expanded': dropdownOpen ? 'true' : 'false',
			onMouseMove: () => {
				setActiveItem( getSearchItem( id ) );
			},
			ref,
		},
		id: searchId,
		wrapperClasses: classnames( [
			'gform-dropdown__search-wrapper',
		], searchWrapperClasses ),
		onChange: ( value ) => {
			setSearchValue( value );
		},
		value: searchValue,
	};

	if ( activeItem.component === 'search' ) {
		searchProps.customAttributes[ 'data-active-item' ] = 'true';
	} else {
		searchProps.customAttributes[ 'aria-activedescendant' ] = activeItem.id;
	}

	return <Input { ...searchProps } />;
} );

/**
 * @module DropdownComponent
 * @description The dropdown component.
 *
 * @since 4.5.0
 *
 * @param {object} props The component props.
 * @param {object} ref   The ref object.
 *
 * @return {JSX.Element} The dropdown component.
 */
const DropdownComponent = forwardRef( ( props, ref ) => {
	const id = useId();
	const dropdownOpen = useStore( ( state ) => state.open );
	const dropdownReveal = useStore( ( state ) => state.reveal );
	const dropdownHide = useStore( ( state ) => state.hide );
	const listItemsState = useStore( ( state ) => state.listItems );
	const searchValue = useStore( ( state ) => state.searchValue );
	const selectedItem = useStore( ( state ) => state.selectedItem );
	const initialTriggerHeight = useStore( ( state ) => state.initialTriggerHeight );
	const setListItems = useStore( ( state ) => state.setListItems );
	const setActiveItem = useStore( ( state ) => state.setActiveItem );
	const setSelectedItem = useStore( ( state ) => state.setSelectedItem );
	const setTriggerHeight = useStore( ( state ) => state.setTriggerHeight );
	const setInitialTriggerHeight = useStore( ( state ) => state.setInitialTriggerHeight );
	const listItemsMounted = useRef( false );

	const triggerRef = useStore( ( state ) => state.triggerRef );
	const popoverRef = useStore( ( state ) => state.popoverRef );
	const listRef = useStore( ( state ) => state.listRef );
	const searchRef = useStore( ( state ) => state.searchRef );
	const baseElRef = useStore( ( state ) => state.baseElRef );
	const pillsRef = useStore( ( state ) => state.pillsRef );

	const {
		customAttributes = {},
		customClasses = [],
		hasSearch = false,
		listItems = [],
		multi = false,
		popoverMaxHeight = 0,
		simplebar = true,
		size = 'r',
		spacing = '',
		width = 0,
	} = props;

	/**
	 * @function convertSingleToMultiValue
	 * @description Converts a single value to a multi value.
	 *
	 * @since 4.5.0
	 *
	 */
	const convertSingleToMultiValue = () => {
		if ( ! isObject( selectedItem ) ) {
			// Selected item is not object, something is wrong here, return early.
			return;
		}
		if ( Object.keys( selectedItem ).length === 0 ) {
			// Selected item is empty, set selected item to empty array.
			setSelectedItem( [] );
			return;
		}
		// Set selected item as multi value.
		setSelectedItem( [ selectedItem ] );
	};
	/**
	 * @function convertMultiToSingleValue
	 * @description Converts a multi value to a single value.
	 *
	 * @since 4.5.0
	 *
	 */
	const convertMultiToSingleValue = () => {
		if ( ! Array.isArray( selectedItem ) ) {
			// Selected item is not array, something is wrong here, return early.
			return;
		}
		if ( selectedItem.length === 0 ) {
			// Selected item is empty, set selected item to first item in list items.
			setSelectedItem( listItemsState.items[ 0 ] || {} );
			return;
		}
		// Set selected item to first item in multi value.
		setSelectedItem( selectedItem[ 0 ] );
	};

	/* Set active item and list items state when id, list items, or search value changes. */
	useEffect( () => {
		if ( ! listItemsMounted.current ) {
			listItemsMounted.current = true;
			return;
		}
		const newListItems = getListItemsState(
			filterListItems( listItems, searchValue ),
			{ hasSearch, id },
		);
		setActiveItem( newListItems.items[ 0 ] );
		setListItems( newListItems );
	}, [ id, listItems, searchValue ] );
	/* Focus on base element when dropdown opens. */
	useEffect( () => {
		if ( ! dropdownOpen ) {
			return;
		}
		baseElRef?.current?.focus();
	}, [ dropdownOpen, baseElRef ] );
	/* Convert single to multi value and multi to single value when multi changes. */
	useEffect( () => {
		if ( multi ) {
			convertSingleToMultiValue();
		} else {
			convertMultiToSingleValue();
		}
	}, [ multi ] );
	/* Set initial trigger height when size changes. */
	useEffect( () => {
		if ( ! triggerRef.current ) {
			return;
		}
		setInitialTriggerHeight( triggerRef.current.offsetHeight );
	}, [ size, triggerRef ] );
	/* Set trigger height when selected item changes in multi. */
	useEffect( () => {
		if ( ! multi ) {
			setTriggerHeight( 0 );
			return;
		}

		if ( ! pillsRef.current ) {
			return;
		}

		if ( ! initialTriggerHeight ) {
			return;
		}

		const pillsHeight = pillsRef.current.offsetHeight;
		if ( pillsHeight <= initialTriggerHeight ) {
			setTriggerHeight( 0 );
			return;
		}

		setTriggerHeight( pillsHeight );
	}, [ multi, selectedItem, pillsRef, initialTriggerHeight ] );

	const dropdownProps = {
		className: classnames( {
			'gform-dropdown': true,
			'gform-dropdown--react': true,
			[ `gform-dropdown--size-${ size }` ]: size,
			'gform-dropdown--open': dropdownOpen,
			'gform-dropdown--reveal': dropdownReveal,
			'gform-dropdown--hide': dropdownHide,
			'gform-dropdown--multi': multi,
			'gform-dropdown--has-simplebar': simplebar,
			'gform-dropdown--has-search': hasSearch,
			...spacerClasses( spacing ),
		}, customClasses ),
		style: {
			width: width ? `${ width }px` : undefined,
		},
		...customAttributes,
	};

	const listMaxHeight = hasSearch ? popoverMaxHeight - 58 : popoverMaxHeight;

	return (
		<div { ...dropdownProps } ref={ ref }>
			<DropdownLabel { ...props } />
			<div className="gform-dropdown__trigger-wrapper">
				<DropdownTrigger { ...props } ref={ triggerRef } />
				<DropdownPills { ...props } ref={ pillsRef } />
			</div>
			<DropdownPopover { ...props } ref={ popoverRef }>
				<DropdownSearch { ...props } ref={ searchRef } />
				<ConditionalWrapper
					condition={ simplebar && popoverMaxHeight > 0 }
					wrapper={ ( ch ) => <div className="gform-dropdown__popover-simplebar" style={ { height: `${ listMaxHeight }px` } } ><SimpleBar>{ ch }</SimpleBar></div> }
				>
					<DropdownList { ...props } ref={ listRef } />
				</ConditionalWrapper>
			</DropdownPopover>
		</div>
	);
} );

const useDropdown = ( props ) => {
	const hooks = [
		useDropdownControl,
		useDropdownBlur,
		useDropdownKeyDown,
		useDropdownTypeahead,
	];

	return hooks.reduce( ( carryProps, hook ) => hook( carryProps, useStore ), props );
};

/**
 * @module Dropdown
 * @description Dropdown component with store and id wrapper.
 *
 * @since 4.5.0
 *
 * @param {object}                     props                    Component props.
 * @param {object}                     props.customAttributes   Custom attributes for the component.
 * @param {object}                     props.customClasses      Custom classes for the component.
 * @param {boolean}                    props.disabled           Whether the dropdown is disabled or not.
 * @param {boolean}                    props.hasSearch          Whether the dropdown has search or not.
 * @param {object}                     props.i18n               i18n strings.
 * @param {string}                     props.id                 The ID of the dropdown.
 * @param {string}                     props.label              The label text.
 * @param {object}                     props.labelAttributes    Custom attributes for the label.
 * @param {string|Array|object}        props.labelClasses       Custom classes for the label.
 * @param {object}                     props.listAttributes     Custom attributes for the list.
 * @param {string|Array|object}        props.listClasses        Custom classes for the list.
 * @param {Array}                      props.listItems          The list items for the dropdown.
 * @param {boolean}                    props.multi              Whether the dropdown is a multi dropdown or not.
 * @param {Function}                   props.onAfterClose       Callback for after the dropdown closes.
 * @param {Function}                   props.onAfterOpen        Callback for after the dropdown opens.
 * @param {Function}                   props.onClose            Callback for when the dropdown closes.
 * @param {Function}                   props.onOpen             Callback for when the dropdown opens.
 * @param {object}                     props.popoverAttributes  Custom attributes for the popover.
 * @param {string|Array|object}        props.popoverClasses     Custom classes for the popover.
 * @param {number}                     props.popoverMaxHeight   The maximum height of the popover.
 * @param {object}                     props.searchAttributes   Custom attributes for the search.
 * @param {string|Array|object}        props.searchClasses      Custom classes for the search.
 * @param {string}                     props.selectedIcon       The icon for the selected state in multi dropdown.
 * @param {string}                     props.selectedIconPrefix The prefix for the icon library to be used in multi dropdown.
 * @param {boolean}                    props.simplebar          Whether to use simplebar for the dropdown.
 * @param {string}                     props.size               The size of the dropdown, one of `r`, `l`, `xl`.
 * @param {string|number|Array|object} props.spacing            The spacing for the component, as a string, number, array, or object.
 * @param {object}                     props.triggerAttributes  Custom attributes for the trigger.
 * @param {string|Array|object}        props.triggerClasses     Custom classes for the trigger.
 * @param {number}                     props.width              The width of the dropdown.
 *
 * @return {JSX.Element} The dropdown component.
 */
const Dropdown = forwardRef( ( props, ref ) => {
	const defaultProps = {
		customAttributes: {},
		customClasses: [],
		disabled: false,
		hasSearch: false,
		i18n: {},
		id: '',
		label: '',
		labelAttributes: {},
		labelClasses: [],
		listAttributes: {},
		listClasses: [],
		listItems: [],
		multi: false,
		onAfterClose: () => {},
		onAfterOpen: () => {},
		onClose: () => {},
		onOpen: () => {},
		popoverAttributes: {},
		popoverClasses: [],
		popoverMaxHeight: 0,
		searchAttributes: {},
		searchClasses: [],
		selectedIcon: 'check',
		selectedIconPrefix: 'gform-icon',
		simplebar: true,
		size: 'r',
		spacing: '',
		triggerAttributes: {},
		triggerClasses: [],
		width: 0,
	};
	const combinedProps = { ...defaultProps, ...props };
	const { hasSearch, id: idProp, listItems: listItemsProp, multi } = combinedProps;

	const IdProvider = getIdProvider( { id: idProp } );
	const StoreProvider = ( { children } ) => {
		const id = useId();
		const triggerRef = useRef();
		const popoverRef = useRef();
		const listRef = useRef();
		const searchRef = useRef();
		const pillsRef = useRef();
		const listItems = getListItemsState( listItemsProp, { hasSearch, id } );
		const firstItem = listItems.items[ 0 ] || {};
		const selectedItem = multi ? [] : firstItem;
		const activeItem = firstItem;
		const StoreProviderWrapper = getStoreProvider( {
			initialState: {
				listItems,
				selectedItem,
				activeItem,
				triggerRef,
				popoverRef,
				listRef,
				searchRef,
				baseElRef: hasSearch ? searchRef : listRef,
				pillsRef,
			},
			createStore,
		} );
		return (
			<StoreProviderWrapper>
				{ children }
			</StoreProviderWrapper>
		);
	};
	const DropdownWrapper = ( wrapperProps ) => {
		const componentProps = useDropdown( wrapperProps );
		return <DropdownComponent { ...componentProps } ref={ ref } />;
	};

	return (
		<IdProvider>
			<StoreProvider>
				<DropdownWrapper { ...combinedProps } />
			</StoreProvider>
		</IdProvider>
	);
} );

Dropdown.propTypes = {
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	hasSearch: PropTypes.bool,
	i18n: PropTypes.object,
	id: PropTypes.string,
	label: PropTypes.string,
	labelAttributes: PropTypes.object,
	labelClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	listAttributes: PropTypes.object,
	listClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	listItems: PropTypes.array,
	multi: PropTypes.bool,
	onAfterClose: PropTypes.func,
	onAfterOpen: PropTypes.func,
	onClose: PropTypes.func,
	onOpen: PropTypes.func,
	popoverAttributes: PropTypes.object,
	popoverClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	popoverMaxHeight: PropTypes.number,
	searchAttributes: PropTypes.object,
	searchClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	selectedIcon: PropTypes.string,
	selectedIconPrefix: PropTypes.string,
	simplebar: PropTypes.bool,
	size: PropTypes.oneOf( [ 'r', 'l', 'xl' ] ),
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	triggerAttributes: PropTypes.object,
	triggerClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	width: PropTypes.number,
};

export default Dropdown;