UNPKG

@gravityforms/components

Version:

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

516 lines (483 loc) 16.7 kB
import { React, SimpleBar, classnames, PropTypes } from '@gravityforms/libraries'; import { useId, useStateWithDep, ConditionalWrapper, useFocusTrap, } from '@gravityforms/react-utils'; import Button from '../../elements/Button'; import Heading from '../../elements/Heading'; import Text from '../../elements/Text'; import { ESCAPE } from '../../utils/keymap'; 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.footerChildrenLeft React element children in the flyout footer on the left. * @param {JSX.Element} props.footerChildrenRight React element children in the flyout footer on the right. * @param {boolean} props.footerIsFixed Whether or not the footer is fixed. * @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 {Array} props.maskClickExcludeButtons Array of button codes that will not close the flyout on mask click, 0 = left click, 1 = middle click, 2 = right click, 3 = back button, 4 = forward button, 5 = X1, 6 = X2. * @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 {boolean} props.offsetWPAdminBar Whether to offset the flyout from the WordPress admin bar. * @param {boolean} props.offsetWPAdminMenu Whether to offset the flyout from the WordPress admin menu. * @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.resetScrollOnOpen Whether to reset scroll on open. * @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 = '', footerChildrenLeft = null, footerChildrenRight = null, footerIsFixed = false, headerChildrenLeft = null, headerChildrenRight = null, headerDescriptionCustomAttributes = {}, headerHeadingCustomAttributes = {}, headerIsFixed = false, id: defaultId = '', isOpen = false, maskBlur = false, maskClickExcludeButtons = [], maskTheme = 'none', maxWidth = 0, mobileBreakpoint = 0, mobileWidth = '100%', offsetWPAdminBar = false, offsetWPAdminMenu = false, onClose = () => {}, onOpen = () => {}, position = 'fixed', resetScrollOnOpen = false, 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 simplebarRef = useRef( null ); const bodyRef = useRef( null ); const id = useId( defaultId ); useEffect( () => { if ( flyoutActive ) { showFlyout(); } else if ( ! flyoutActive ) { closeFlyout(); } }, [ flyoutActive ] ); const handleEscapeRequest = ( e ) => { if ( e.key !== ESCAPE ) { return; } e.stopPropagation(); 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 && ! maskClickExcludeButtons.includes( event.button ) && 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, 'gform-flyout--offset-wpadmin-bar': offsetWPAdminBar, 'gform-flyout--offset-wpadmin-menu': offsetWPAdminMenu, }, 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--scroll-simplebar': simplebar, 'gform-flyout--scroll-native': ! simplebar, 'gform-flyout--header-footer-fixed': headerIsFixed && footerIsFixed, 'gform-flyout--header-fixed': headerIsFixed, 'gform-flyout--footer-fixed': footerIsFixed, }, customClasses ), id, onKeyDown: handleEscapeRequest, ...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 ); if ( resetScrollOnOpen ) { if ( simplebar && simplebarRef.current ) { const scrollElement = simplebarRef.current.getScrollElement(); if ( scrollElement ) { scrollElement.scrollTop = 0; } } else if ( headerIsFixed && bodyRef.current ) { bodyRef.current.scrollTop = 0; } else if ( ref && ref.current ) { ref.current.scrollTop = 0; } } 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 = { circular: closeButtonCustomAttributes.type === 'round', onClick: () => setFlyoutActive( false ), size: 'size-height-s', type: buttonType, ...closeButtonCustomAttributes, customClasses: classnames( { 'gform-flyout__close': true, }, closeButtonCustomAttributes.customClasses || [], ), }; 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-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 ( <ConditionalWrapper condition={ simplebar && headerIsFixed && footerIsFixed } wrapper={ ( ch ) => <SimpleBar className="gform-flyout__simplebar" ref={ simplebarRef }>{ ch }</SimpleBar> } > <div { ...bodyProps } ref={ bodyRef } > <div { ...innerBodyProps } > { children } </div> </div> </ConditionalWrapper> ); }; /** * @function getFooter * @description Returns the footer of the flyout that contains optional children. * * @since 5.7.4 * * @return {JSX.Element} */ const getFooter = () => { if ( ! footerChildrenLeft && ! footerChildrenRight ) { return null; } const footerProps = { className: classnames( { 'gform-flyout__footer': true, } ), }; return ( <footer { ...footerProps }> { footerChildrenLeft && ( <div className="gform-flyout__footer-left"> { footerChildrenLeft } </div> ) } { footerChildrenRight && ( <div className="gform-flyout__footer-right"> { footerChildrenRight } </div> ) } </footer> ); }; /** * @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={ ref } > <ConditionalWrapper condition={ simplebar && ! ( headerIsFixed && footerIsFixed ) } wrapper={ ( ch ) => <SimpleBar className="gform-flyout__simplebar" ref={ simplebarRef }>{ ch }</SimpleBar> } > { beforeContent } { getHeader() } { getBody() } { getFooter() } { 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, footerChildrenLeft: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), footerChildrenRight: PropTypes.oneOfType( [ PropTypes.arrayOf( PropTypes.node ), PropTypes.node, ] ), footerIsFixed: PropTypes.bool, 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, maskClickExcludeButtons: PropTypes.array, maskTheme: PropTypes.string, maxWidth: PropTypes.number, mobileBreakpoint: PropTypes.number, mobileWidth: PropTypes.string, offsetWPAdminBar: PropTypes.bool, offsetWPAdminMenu: PropTypes.bool, onClose: PropTypes.func, onOpen: PropTypes.func, position: PropTypes.oneOf( [ 'absolute', 'fixed' ] ), resetScrollOnOpen: PropTypes.bool, showDivider: PropTypes.bool, simplebar: PropTypes.bool, title: PropTypes.string, zIndex: PropTypes.number, }; Flyout.displayName = 'Flyout'; export default Flyout;