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;