@gravityforms/components
Version:
UI components for use in Gravity Forms development. Both React and vanilla js flavors.
441 lines (408 loc) • 13.6 kB
JavaScript
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;