UNPKG

@gravityforms/components

Version:

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

441 lines (408 loc) 13.6 kB
import { React, SimpleBar, classnames, PropTypes } from '@gravityforms/libraries'; import { getClosest } from '@gravityforms/utils'; import { useId, useStateWithDep, ConditionalWrapper, useFocusTrap, } from '@gravityforms/react-utils'; import Button from '../../elements/Button'; import Heading from '../../elements/Heading'; import Text from '../../elements/Text'; const { forwardRef, useState, useRef, useEffect } = React; /** * @module Flyout * @description A flyout component in react. * * @since 1.1.18 * * @param {object} props Component props. * @param {JSX.Element} props.afterContent Any custom content to be placed after the body of the flyout. * @param {number} props.animationDelay Total runtime of close animation. Synchronize with css if modifying the built-in 250 ms delay. * @param {JSX.Element} props.beforeContent Any custom content to be placed before the header of the flyout. * @param {JSX.Element} props.children React element children in the flyout body. * @param {object} props.closeButtonCustomAttributes Custom attributes for the close button. * @param {boolean} props.closeOnMaskClick Whether to close if the background mask is clicked. * @param {object} props.customAttributes Custom attributes for the component. * @param {string|Array|object} props.customBodyClasses Custom classes for the flyout body as array. * @param {string|Array|object} props.customClasses Custom classes for the component. * @param {string|Array|object} props.customInnerBodyClasses Custom classes for the flyout inner_body as array. * @param {string|Array|object} props.customMaskClasses Custom classes for the mask as array. * @param {string} props.description Subheading description for the flyout. * @param {string} props.desktopWidth Width for the flyout on desktop. * @param {string} props.direction Flyout direction, left or right. * @param {JSX.Element} props.headerChildrenLeft React element children in the flyout header on the left. * @param {JSX.Element} props.headerChildrenRight React element children in the flyout header on the right. * @param {object} props.headerDescriptionCustomAttributes Custom attributes for the header description. * @param {object} props.headerHeadingCustomAttributes Custom attributes for the header heading. * @param {boolean} props.headerIsFixed Whether or not the header is fixed. * @param {string} props.id Flyout id. * @param {boolean} props.isOpen Prop to control whether the dialog is currently open. * @param {boolean} props.maskBlur Whether to blur behind the mask for the flyout. * @param {string} props.maskTheme Mask background color scheme: `none`, `light` or `dark` * @param {number} props.maxWidth Max width in pixels for the flyout. * @param {number} props.mobileBreakpoint Mobile breakpoint in pixels for the flyout. * @param {string} props.mobileWidth Width for the flyout on mobile. * @param {Function} props.onClose Function to fire on flyout close. * @param {Function} props.onOpen Function to fire on flyout open. * @param {string} props.position The position of the flyout, `absolute` or `fixed`. * @param {boolean} props.showDivider Whether or not to show the divider border below title. * @param {boolean} props.simplebar Whether or not to use SimpleBar on the content. * @param {string} props.title The title of the flyout. * @param {number} props.zIndex z-index of the flyout. * @param {object|null} ref Ref to the component. * * @return {JSX.Element} The Flyout component. * * @example * import Flyout from '@gravityforms/components/react/admin/modules/Flyout'; * * return ( * <Flyout direction="right" title="Flyout title"> * { children } * </Flyout> * ); * */ const Flyout = forwardRef( ( { afterContent = null, animationDelay = 250, beforeContent = null, children = null, closeButtonCustomAttributes = { customClasses: [], icon: 'delete', iconPrefix: 'gravity-component-icon', size: 'size-xs', title: '', type: 'round', }, closeOnMaskClick = true, customAttributes = {}, customBodyClasses = [], customClasses = [], customInnerBodyClasses = [], customMaskClasses = [], description = '', desktopWidth = '0', direction = '', headerChildrenLeft = null, headerChildrenRight = null, headerDescriptionCustomAttributes = {}, headerHeadingCustomAttributes = {}, headerIsFixed = false, id: defaultId = '', isOpen = false, maskBlur = false, maskTheme = 'none', maxWidth = 0, mobileBreakpoint = 0, mobileWidth = '100%', onClose = () => {}, onOpen = () => {}, position = 'fixed', showDivider = true, simplebar = false, title = '', zIndex = 10, }, ref ) => { // eslint-disable-line const [ animationReady, setAnimationReady ] = useState( false ); const [ animationActive, setAnimationActive ] = useState( false ); const [ flyoutActive, setFlyoutActive ] = useStateWithDep( isOpen ); const trapRef = useFocusTrap( flyoutActive ); const closeRef = useRef( true ); const id = useId( defaultId ); useEffect( () => { if ( flyoutActive ) { showFlyout(); } else if ( ! flyoutActive ) { closeFlyout(); } }, [ flyoutActive ] ); useEffect( () => { closeRef.current.addEventListener( 'keydown', handleEscapeRequest ); return () => { if ( ! closeRef.current ) { return; } closeRef.current.removeEventListener( 'keydown', handleEscapeRequest ); }; } ); const handleEscapeRequest = ( e ) => { if ( getClosest( e.target, '.gform-flyout' ) !== closeRef.current ) { return; } if ( e.key !== 'Escape' ) { return; } closeFlyout(); }; const pointerDownOrigin = useRef( null ); const handlePointerDown = ( event ) => { pointerDownOrigin.current = event.target; }; const handlePointerUp = ( event ) => { if ( pointerDownOrigin.current === event.target && event.target.classList.contains( 'gform-flyout__mask' ) && closeOnMaskClick && flyoutActive ) { event.stopPropagation(); setFlyoutActive( false ); } pointerDownOrigin.current = null; }; const maskProps = { className: classnames( { 'gform-flyout__mask': true, 'gform-flyout--anim-in-ready': animationReady, 'gform-flyout--anim-in-active': animationReady && animationActive, [ `gform-flyout__mask--position-${ position }` ]: true, [ `gform-flyout__mask--theme-${ maskTheme }` ]: true, 'gform-flyout__mask--blur': maskBlur, }, customMaskClasses ), onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, style: { zIndex, }, }; const componentProps = { className: classnames( { 'gform-flyout': true, 'gform-flyout--anim-in-ready': animationReady, 'gform-flyout--anim-in-active': animationReady && animationActive, [ `gform-flyout--${ direction }` ]: true, [ `gform-flyout--${ position }` ]: true, 'gform-flyout--divider': showDivider, 'gform-flyout--no-divider': ! showDivider, 'gform-flyout--no-description': ! description, 'gform-flyout--simplebar': simplebar, 'gform-flyout--header-fixed': headerIsFixed, }, customClasses ), id, ...customAttributes, }; /** * @function showFlyout * @description Opens the flyout and fires the `onOpen` function if passed in. * * @since 1.1.18 * * @return {void} */ const showFlyout = () => { setAnimationReady( true ); setTimeout( () => { setAnimationActive( true ); onOpen(); }, 25 ); }; /** * @function closeFlyout * @description Closes the flyout and fires the `onClose` function if passed in. * * @since 1.1.18 * * @return {void} */ const closeFlyout = () => { setAnimationActive( false ); setTimeout( () => { setAnimationReady( false ); onClose(); }, animationDelay ); }; /** * @function getHeader * @description Returns the header of the flyout that contains the title, description, children, and close button. * * @since 1.1.18 * * @return {JSX.Element} */ const getHeader = () => { let buttonType = 'unstyled'; if ( closeButtonCustomAttributes.type === 'round' ) { buttonType = 'white'; } else if ( closeButtonCustomAttributes.type === 'simplified' ) { buttonType = 'simplified'; } const closeButtonProps = { customClasses: classnames( { 'gform-button': true, 'gform-flyout__close': true, }, closeButtonCustomAttributes.customClasses || [], ), circular: closeButtonCustomAttributes.type === 'round', onClick: () => setFlyoutActive( false ), size: 'size-height-s', type: buttonType, ...closeButtonCustomAttributes, }; const headerProps = { className: classnames( { 'gform-flyout__head': true, } ), }; const headingProps = { customClasses: classnames( { 'gform-flyout__title': true, } ), content: title, size: 'display-xs', tagName: 'h3', weight: 'SemiBold', ...headerHeadingCustomAttributes, }; const descriptionProps = { customClasses: classnames( { 'gform-gform-flyout__desc': true, } ), content: description, ...headerDescriptionCustomAttributes, }; return ( <header { ...headerProps }> <div className="gform-flyout__head-left"> { title && <Heading { ...headingProps } /> } { description && <Text { ...descriptionProps } /> } { headerChildrenLeft } </div> <div className="gform-flyout__head-right"> { headerChildrenRight } { <Button { ...closeButtonProps } /> } </div> </header> ); }; /** * @function getBody * @description Returns the body that wrap the children of the flyout. * * @since 1.1.18 * * @return {JSX.Element} */ const getBody = () => { const bodyProps = { className: classnames( { 'gform-flyout__body': true, }, customBodyClasses ), }; const innerBodyProps = { className: classnames( { 'gform-flyout__body-inner': true, }, customInnerBodyClasses ), }; return ( <div { ...bodyProps } > <div { ...innerBodyProps } > { children } </div> </div> ); }; /** * @function getCSS * @description Returns the CSS used to handle the width and mobile media query. * * @since 1.1.18 * * @return {string} The CSS for the flyout. */ const getCSS = () => { let css = `#${ id } { max-width: ${ maxWidth ? `${ maxWidth }px` : 'none' }; width: ${ mobileWidth }; z-index: ${ zIndex } } `; if ( mobileBreakpoint ) { css += ` @media only screen and (min-width: ${ mobileBreakpoint }px) { #${ id } { width: ${ desktopWidth }; } } `; } return css; }; return ( <div { ...maskProps } ref={ trapRef }> <article { ...componentProps } ref={ closeRef }> <ConditionalWrapper condition={ simplebar } wrapper={ ( ch ) => <SimpleBar>{ ch }</SimpleBar> } > { beforeContent } { getHeader() } { getBody() } { afterContent } </ConditionalWrapper> </article> <style> { getCSS() } </style> </div> ); } ); Flyout.propTypes = { afterContent: PropTypes.node, animationDelay: PropTypes.number, beforeContent: PropTypes.node, children: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), closeButtonCustomAttributes: PropTypes.object, closeOnMaskClick: PropTypes.bool, customAttributes: PropTypes.object, customBodyClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), customClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), customInnerBodyClasses: PropTypes.oneOfType( [ PropTypes.string, PropTypes.array, PropTypes.object, ] ), description: PropTypes.string, desktopWidth: PropTypes.string, direction: PropTypes.string, headerChildrenLeft: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), headerChildrenRight: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), headerDescriptionCustomAttributes: PropTypes.object, headerHeadingCustomAttributes: PropTypes.object, headerIsFixed: PropTypes.bool, id: PropTypes.string, isOpen: PropTypes.bool, maskBlur: PropTypes.bool, maskTheme: PropTypes.string, maxWidth: PropTypes.number, mobileBreakpoint: PropTypes.number, mobileWidth: PropTypes.string, onClose: PropTypes.func, onOpen: PropTypes.func, position: PropTypes.oneOf( [ 'absolute', 'fixed' ] ), showDivider: PropTypes.bool, simplebar: PropTypes.bool, title: PropTypes.string, zIndex: PropTypes.number, }; Flyout.displayName = 'Flyout'; export default Flyout;