@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
207 lines (193 loc) • 5.34 kB
JavaScript
import { React, classnames, PropTypes } from '@gravityforms/libraries';
import { usePopup } from '@gravityforms/react-utils';
import { spacerClasses } from '@gravityforms/utils';
import DroplistItem from './DroplistItem';
import DroplistList from './DroplistList';
import Popover from '../../elements/Popover';
const { forwardRef, useCallback, useEffect, useRef } = React;
/**
* @module DroplistGroupItem
* @description The DroplistGroupItem component.
*
* @since 4.3.0
*
* @param {object} props Props for the DroplistGroupItem component.
* @param {number} props.depth The depth of the item.
* @param {number} props.index The index of the item.
* @param {object} props.item The item object.
* @param {object} props.propsWithState The props and state object.
* @param {object|null} ref Ref to the component.
*
* @return {JSX.Element|null} The DroplistGroupItem component.
*/
export const DroplistGroupItem = forwardRef( ( {
depth = 0,
index = 0,
item = {},
propsWithState = {},
}, ref ) => {
const containerRef = useRef( null );
const {
align,
itemKey,
openOnHover,
selectedState,
setSelectedState,
stackNestedGroups,
} = propsWithState;
const {
onAfterClose = () => {},
onAfterOpen = () => {},
onClose = () => {},
onOpen = () => {},
} = item;
const {
customClasses: groupTriggerCustomClasses = [],
id: triggerId = `${ itemKey }-trigger`,
onClick = () => {},
...restGroupTriggerAttributes
} = item.triggerAttributes || {};
const {
customClasses: groupListContainerCustomClasses = [],
width = 0,
...restGroupListContainerAttributes
} = item.listContainerAttributes || {};
const {
closePopup,
openPopup,
popupHide,
popupOpen,
popupReveal,
popupRef,
triggerRef,
} = usePopup( {
closeOnClickOutside: false,
onAfterClose,
onAfterOpen,
onClose,
onOpen,
} );
/**
* @function updateSelectedState
* @description Updates the selected state.
*/
const updateSelectedState = () => {
if ( popupOpen ) {
setSelectedStateClosed();
} else {
setSelectedStateOpen();
}
};
/**
* @function setSelectedStateOpen
* @description Sets the selected state to open.
*/
const setSelectedStateOpen = () => {
// If the group is already open, do nothing.
if ( selectedState[ depth ] === triggerId ) {
return;
}
const depthKeys = Object.keys( selectedState );
const filteredState = depthKeys
.filter( ( key ) => key < depth )
.reduce( ( acc, key ) => {
acc[ key ] = selectedState[ key ];
return acc;
}, {} );
const newSelectedState = {
...filteredState,
[ depth ]: triggerId,
};
setSelectedState( newSelectedState );
};
/**
* @function setSelectedStateClosed
* @description Sets the selected state to closed.
*/
const setSelectedStateClosed = () => {
const depthKeys = Object.keys( selectedState );
const filteredState = depthKeys
.filter( ( key ) => key < depth )
.reduce( ( acc, key ) => {
acc[ key ] = selectedState[ key ];
return acc;
}, {} );
setSelectedState( filteredState );
};
useEffect( () => {
if ( selectedState[ depth ] === triggerId && ! popupOpen ) {
openPopup();
} else if ( selectedState[ depth ] !== triggerId && popupOpen ) {
closePopup();
}
}, [ selectedState, triggerId, popupOpen ] );
const groupListItemProps = {
className: classnames( {
'gform-droplist__item': true,
'gform-droplist__item--group': true,
'gform-droplist__item--has-divider': item.hasDivider,
}, item.customClasses || [] ),
};
const groupTriggerProps = {
customAttributes: {
'aria-expanded': popupOpen ? 'true' : 'false',
'aria-haspopup': 'listbox',
id: triggerId,
onClick: ( event ) => {
onClick( event );
updateSelectedState();
},
onMouseEnter: openOnHover && ! stackNestedGroups ? () => {
setSelectedStateOpen();
} : undefined,
},
customClasses: classnames(
'gform-droplist__item-trigger',
groupTriggerCustomClasses,
),
depth,
index,
...restGroupTriggerAttributes,
};
const popoverProps = {
align: 'top',
autoPlacement: true,
className: classnames(
'gform-droplist__popover',
groupListContainerCustomClasses,
),
containerRef,
isHide: popupHide,
isOpen: popupOpen,
isReveal: popupReveal,
placement: align === 'left' ? 'right' : 'left',
popoverClasses: spacerClasses( align === 'left' ? [ 0, 0, 0, 2 ] : [ 0, 2, 0, 0 ] ),
popoverRef: popupRef,
triggerRef: containerRef, // Use container to position the popover rather than the trigger itself.
width,
...restGroupListContainerAttributes,
};
const setRefs = useCallback( ( node ) => {
containerRef.current = node;
if ( ref ) {
ref.current = node;
}
}, [ ref ] );
return (
<li { ...groupListItemProps } ref={ setRefs }>
<DroplistItem { ...groupTriggerProps } ref={ triggerRef } />
{ stackNestedGroups ? null : (
<Popover { ...popoverProps } ref={ popupRef }>
<DroplistList { ...propsWithState } listItems={ item.listItems } depth={ depth + 1 } />
</Popover>
) }
</li>
);
} );
DroplistGroupItem.propTypes = {
depth: PropTypes.number,
item: PropTypes.object,
propsWithState: PropTypes.object,
};
DroplistGroupItem.displayName = 'DroplistGroupItem';
export default DroplistGroupItem;