import { React, classnames, PropTypes } from '@gravityforms/libraries';
import { useId, usePopup } from '@gravityforms/react-utils';
import { spacerClasses } from '@gravityforms/utils';
import Button from '../index';
import Popover from '../../Popover';
const { forwardRef, useCallback, useRef } = React;
/**
* @description Sets a forwarded ref value.
*
* @since 6.6.2
*
* @param {Function|object|null} forwardedRef The forwarded ref.
* @param {HTMLElement|null} node The node to assign.
*
* @return {void}
*/
const setForwardedRef = ( forwardedRef, node ) => {
if ( typeof forwardedRef === 'function' ) {
forwardedRef( node );
} else if ( forwardedRef ) {
forwardedRef.current = node;
}
};
/**
* @module MultiOptionButton
* @description A split button with a main action and attached action menu.
*
* @since 6.6.2
*
* @param {object} props Component props.
* @param {boolean} props.active Whether the main button is active or not.
* @param {string} props.activeText The active text when the main button is active.
* @param {string} props.activeType The active type, currently supports `loader`.
* @param {string} props.ariaLabel The aria-label text for the main button.
* @param {JSX.Element|null} props.children React element children for the main button.
* @param {boolean} props.circular Whether the main button is a circular shape or not.
* @param {boolean} props.closeOnOptionClick Whether to close the menu when an option is clicked.
* @param {object} props.customAttributes Custom attributes for the wrapper.
* @param {string|Array|object} props.customClasses Custom classes for the wrapper.
* @param {boolean} props.disabled Whether the multi option button is disabled or not.
* @param {boolean} props.disableWhileActive Whether to disable the main button while active.
* @param {string} props.dropdownAriaLabel The aria-label text for the dropdown trigger.
* @param {string} props.dropdownIcon Icon name for the dropdown trigger.
* @param {string} props.dropdownIconPrefix The prefix for the dropdown trigger icon library.
* @param {object} props.iconAttributes Custom attributes for the main button icon.
* @param {string} props.icon Icon name if using an icon button.
* @param {string} props.iconPosition Icon position if using one, `leading` or `trailing`.
* @param {string} props.iconPrefix The prefix for the icon library to be used.
* @param {string} props.label The label for the main button.
* @param {object} props.loaderProps All valid options for the loader component if loader button is active.
* @param {boolean} props.lockSize Whether to lock the main button size when transitioning states.
* @param {object} props.mainButtonAttributes Additional props for the main button.
* @param {string|Array|object} props.mainButtonClasses Custom classes for the main button.
* @param {Function} props.onClick On click handler for the main button.
* @param {Function} props.onDropdownClose Callback fired when the dropdown menu closes.
* @param {Function} props.onDropdownOpen Callback fired when the dropdown menu opens.
* @param {Array} props.options The dropdown action options.
* @param {object} props.popoverAttributes Additional props for the Popover.
* @param {string|Array|object} props.popoverClasses Custom classes for the Popover element.
* @param {string} props.size Size of the main button and dropdown options.
* @param {string|number|Array|object} props.spacing The spacing for the wrapper component.
* @param {object} props.triggerAttributes Additional props for the dropdown trigger.
* @param {string|Array|object} props.triggerClasses Custom classes for the dropdown trigger.
* @param {string} props.type The visual type of the main button and dropdown options.
* @param {string} props.width The main button width, `auto` or `full`.
* @param {object|null} ref Ref to the wrapper.
*
* @return {JSX.Element} The multi option button component.
*/
const MultiOptionButton = forwardRef( ( {
active = false,
activeText = '',
activeType = '',
ariaLabel = '',
children = null,
circular = false,
closeOnOptionClick = true,
customAttributes = {},
customClasses = [],
disabled = false,
disableWhileActive = true,
dropdownAriaLabel = '',
dropdownIcon = 'chevron-down',
dropdownIconPrefix = 'gravity-component-icon',
icon = '',
iconAttributes = {},
iconPosition = '',
iconPrefix = 'gravity-component-icon',
label = '',
loaderProps = {
customClasses: 'gform-button__loader',
lineWeight: 2,
size: 16,
},
lockSize = false,
mainButtonAttributes = {},
mainButtonClasses = [],
onClick = () => {},
onDropdownClose = () => {},
onDropdownOpen = () => {},
options = [],
popoverAttributes = {},
popoverClasses = [],
size = 'size-r',
spacing = '',
triggerAttributes = {},
triggerClasses = [],
type = 'primary-new',
width = 'auto',
}, ref ) => {
const id = useId( customAttributes.id );
const wrapperRef = useRef( null );
const {
closePopup,
handleEscKeyDown,
openPopup,
popupHide,
popupOpen,
popupReveal,
popupRef,
triggerRef,
} = usePopup( {
onClose: onDropdownClose,
onOpen: onDropdownOpen,
} );
/**
* @function handleWrapperRef
* @description Assigns the wrapper node to internal and forwarded refs.
*
* @since 6.6.2
*
* @param {HTMLElement|null} node The wrapper node.
*
* @return {void}
*/
const handleWrapperRef = useCallback( ( node ) => {
wrapperRef.current = node;
setForwardedRef( ref, node );
}, [ ref ] );
const {
customAttributes: mainButtonCustomAttributes = {},
...restMainButtonAttributes
} = mainButtonAttributes;
const {
customAttributes: triggerCustomAttributes = {},
onClick: triggerOnClick = () => {},
...restTriggerAttributes
} = triggerAttributes;
const {
customAttributes: popoverCustomAttributes = {},
...restPopoverAttributes
} = popoverAttributes;
const anyButtonActive = active || options.some( ( option ) => option.active );
const disabledWhileActive = disableWhileActive && anyButtonActive;
const componentDisabled = disabled || disabledWhileActive;
const wrapperProps = {
id,
...customAttributes,
className: classnames( {
'gform-multi-option-button': true,
'gform-multi-option-button--open': popupOpen,
'gform-multi-option-button--disabled': componentDisabled,
[ `gform-multi-option-button--${ type }` ]: type,
[ `gform-multi-option-button--${ size }` ]: size,
[ `gform-multi-option-button--width-${ width }` ]: width,
...spacerClasses( spacing ),
}, customClasses ),
};
const popoverId = `${ id }-popover`;
const triggerId = `${ id }-trigger`;
const optionButtonDefaults = {
circular: false,
disabled: componentDisabled,
disableWhileActive,
iconPrefix,
loaderProps,
lockSize,
size,
type,
width,
};
if ( activeType ) {
optionButtonDefaults.activeType = activeType;
}
const triggerButtonProps = {
ariaLabel: dropdownAriaLabel,
disabled: componentDisabled,
icon: dropdownIcon,
iconPrefix: dropdownIconPrefix,
size,
type,
...restTriggerAttributes,
customAttributes: {
'aria-controls': popoverId,
'aria-expanded': popupOpen ? 'true' : 'false',
'aria-haspopup': 'menu',
id: triggerId,
onKeyDown: handleEscKeyDown,
type: 'button',
...triggerCustomAttributes,
},
customClasses: classnames( {
'gform-multi-option-button__trigger': true,
}, triggerClasses ),
onClick: ( event ) => {
if ( componentDisabled ) {
return;
}
if ( popupOpen ) {
closePopup();
} else {
openPopup();
}
triggerOnClick( event );
},
};
const mainButtonProps = {
active,
activeText,
ariaLabel,
circular,
disabled: componentDisabled,
disableWhileActive,
icon,
iconAttributes,
iconPrefix,
label,
loaderProps,
lockSize,
onClick,
size,
type,
width,
...restMainButtonAttributes,
customAttributes: {
type: 'button',
...mainButtonCustomAttributes,
},
customClasses: classnames( 'gform-multi-option-button__main', mainButtonClasses ),
};
if ( activeType ) {
mainButtonProps.activeType = activeType;
}
if ( iconPosition ) {
mainButtonProps.iconPosition = iconPosition;
}
const popoverProps = {
align: 'right',
autoPlacement: true,
containerRef: wrapperRef,
customClasses: 'gform-multi-option-button__popover',
isHide: popupHide,
isOpen: popupOpen,
isReveal: popupReveal,
placement: 'bottom',
popoverRef: popupRef,
triggerRef,
...restPopoverAttributes,
popoverAttributes: {
...popoverCustomAttributes,
'aria-labelledby': triggerId,
id: popoverId,
onKeyDown: ( event ) => {
popoverCustomAttributes?.onKeyDown?.( event );
handleEscKeyDown( event );
},
},
popoverClasses: classnames( {
'gform-multi-option-button__popover-inner': true,
}, popoverClasses ),
};
/**
* @description Gets props for a dropdown option button.
*
* @since 6.6.2
*
* @param {object} option The option config.
* @param {object} args Additional arguments.
* @param {boolean} args.isSizing Whether the option is being rendered for sizing only.
*
* @return {object} The option button props.
*/
const getOptionButtonProps = ( option, { isSizing = false } = {} ) => {
const {
onClick: optionOnClick = () => {},
customAttributes: optionCustomAttributes = {},
customClasses: optionCustomClasses = [],
...optionProps
} = option;
const optionDisabled = componentDisabled || optionProps.disabled;
const optionAttributes = isSizing ? {
'aria-hidden': 'true',
role: 'presentation',
tabIndex: -1,
type: 'button',
...optionCustomAttributes,
} : {
role: 'menuitem',
type: 'button',
...optionCustomAttributes,
};
return {
...optionButtonDefaults,
...optionProps,
customAttributes: optionAttributes,
customClasses: classnames( {
'gform-multi-option-button__option': true,
'gform-multi-option-button__sizer-option': isSizing,
}, optionCustomClasses ),
disabled: optionDisabled,
onClick: async ( event ) => {
if ( isSizing || optionDisabled ) {
return;
}
try {
await optionOnClick( event, option );
} finally {
if ( closeOnOptionClick ) {
closePopup();
triggerRef?.current?.focus();
}
}
},
};
};
/**
* @description Renders an option button.
*
* @since 6.6.2
*
* @param {object} option The option config.
* @param {object} args Additional arguments.
* @param {boolean} args.isSizing Whether the option is being rendered for sizing only.
*
* @return {JSX.Element} The option button.
*/
const renderOptionButton = ( option, { isSizing = false } = {} ) => (
<Button
{ ...getOptionButtonProps( option, { isSizing } ) }
key={ `${ option.key || option.label }${ isSizing ? '-sizer' : '' }` }
/>
);
return (
<div { ...wrapperProps } ref={ handleWrapperRef }>
<Button { ...mainButtonProps }>
{ children }
</Button>
<Button { ...triggerButtonProps } ref={ triggerRef } />
<div className="gform-multi-option-button__sizer" aria-hidden="true">
{ options.map( ( option ) => renderOptionButton( option, { isSizing: true } ) ) }
</div>
<Popover { ...popoverProps }>
<div className="gform-multi-option-button__options" role="menu">
{ options.map( ( option ) => renderOptionButton( option ) ) }
</div>
</Popover>
</div>
);
} );
MultiOptionButton.propTypes = {
active: PropTypes.bool,
activeText: PropTypes.string,
activeType: PropTypes.oneOf( [ 'loader' ] ),
ariaLabel: PropTypes.string,
children: PropTypes.oneOfType( [
PropTypes.arrayOf( PropTypes.node ),
PropTypes.node,
] ),
circular: PropTypes.bool,
closeOnOptionClick: PropTypes.bool,
customAttributes: PropTypes.object,
customClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
disabled: PropTypes.bool,
disableWhileActive: PropTypes.bool,
dropdownAriaLabel: PropTypes.string,
dropdownIcon: PropTypes.string,
dropdownIconPrefix: PropTypes.string,
icon: PropTypes.string,
iconAttributes: PropTypes.object,
iconPosition: PropTypes.oneOf( [ 'leading', 'trailing' ] ),
iconPrefix: PropTypes.string,
label: PropTypes.string,
loaderProps: PropTypes.object,
lockSize: PropTypes.bool,
mainButtonAttributes: PropTypes.object,
mainButtonClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
onClick: PropTypes.func,
onDropdownClose: PropTypes.func,
onDropdownOpen: PropTypes.func,
options: PropTypes.arrayOf( PropTypes.object ),
popoverAttributes: PropTypes.object,
popoverClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
size: PropTypes.string,
spacing: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.object,
] ),
triggerAttributes: PropTypes.object,
triggerClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
type: PropTypes.string,
width: PropTypes.string,
};
MultiOptionButton.displayName = 'Elements/Button/MultiOptionButton';
export default MultiOptionButton;