UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

439 lines (422 loc) 14.1 kB
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;