modules_Flyout_index.js

import { React, SimpleBar, classnames, PropTypes } from '@gravityforms/libraries';
import { uniqueId } from '@gravityforms/utils';
import {
	useStateWithDep,
	ConditionalWrapper,
} from '@gravityforms/react-utils';
import Button from '../../elements/Button';
import Heading from '../../elements/Heading';
import Text from '../../elements/Text';

const { Fragment, forwardRef, useState, 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 170 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.
 * @param {object}              props.closeButtonCustomAttributes       Custom attributes for the close button.
 * @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}              props.description                       Subheading description for the flyout.
 * @param {number}              props.desktopWidth                      Width in % for the flyout on desktop.
 * @param {string}              props.direction                         Flyout direction, left or right.
 * @param {object}              props.headerHeadingCustomAttributes     Custom attributes for the header heading.
 * @param {object}              props.headerDescriptionCustomAttributes Custom attributes for the header description.
 * @param {string}              props.id                                Flyout id.
 * @param {boolean}             props.isOpen                            Prop to control whether the dialog is currently open.
 * @param {number}              props.maxWidth                          Max width in pixels for the flyout.
 * @param {number}              props.mobileBreakpoint                  Mobile breakpoint in pixels for the flyout.
 * @param {number}              props.mobileWidth                       Width in % 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 = 170,
	beforeContent = null,
	children = null,
	closeButtonCustomAttributes = {
		customClasses: [],
		icon: 'delete',
		iconPrefix: 'gform-icon',
		size: 'size-xs',
		title: '',
		type: 'round',
	},
	customAttributes = {},
	customBodyClasses = [],
	customClasses = [],
	customInnerBodyClasses = [],
	description = '',
	desktopWidth = 0,
	direction = '',
	headerHeadingCustomAttributes = {},
	headerDescriptionCustomAttributes = {},
	id = uniqueId(),
	isOpen = false,
	maxWidth = 0,
	mobileBreakpoint = 0,
	mobileWidth = 0,
	onClose = () => {},
	onOpen = () => {},
	position = 'fixed',
	showDivider = true,
	simplebar = false,
	title = '',
	zIndex = 10,
}, ref ) => {
	const [ animationReady, setAnimationReady ] = useState( false );
	const [ animationActive, setAnimationActive ] = useState( false );
	const [ flyoutActive, setFlyoutActive ] = useStateWithDep( isOpen );

	const componentProps = {
		className: classnames( {
			'gform-flyout': true,
			'gform-flyout--anim-in-ready': animationReady,
			'gform-flyout--anim-in-active': animationActive,
			[ `gform-flyout--${ direction }` ]: true,
			[ `gform-flyout--${ position }` ]: true,
			'gform-flyout--divider': showDivider,
			'gform-flyout--no-divider': ! showDivider,
			'gform-flyout--no-description': ! description,
		}, customClasses ),
		...customAttributes,
	};

	useEffect( () => {
		if ( flyoutActive ) {
			showFlyout();
		} else if ( ! flyoutActive ) {
			closeFlyout();
		}
	}, [ flyoutActive ] );

	/**
	 * @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 and description.
	 *
	 * @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 = {
			...closeButtonCustomAttributes,
			customClasses: classnames(
				{
					'gform-button': true,
					'gform-flyout__close': true,
				},
				closeButtonCustomAttributes.customClasses || [],
			),
			circular: closeButtonCustomAttributes.type === 'round' || closeButtonCustomAttributes.type === 'simplified',
			onClick: () => setFlyoutActive( false ),
			type: buttonType,
		};

		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 (
			<>
				<ConditionalWrapper
					condition={ title || description }
					wrapper={ ( ch ) => <header { ...headerProps }>{ ch }</header> }
				>
					<Heading { ...headingProps } />
					{ description && <Text { ...descriptionProps } /> }
				</ConditionalWrapper>
				<Button	{ ...closeButtonProps } />
			</>
		);
	};

	/**
	 * @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 (
		<Fragment ref={ ref }>
			<article id={ id } { ...componentProps } >
				<ConditionalWrapper
					condition={ simplebar }
					wrapper={ ( ch ) => <SimpleBar>{ ch }</SimpleBar> }
				>
					{ beforeContent }
					{ getHeader() }
					{ getBody() }
					{ afterContent }
				</ConditionalWrapper>
			</article>
			<style>
				{ getCSS() }
			</style>
		</Fragment>
	);
} );

Flyout.propTypes = {
	afterContent: PropTypes.node,
	animationDelay: PropTypes.number,
	beforeContent: PropTypes.node,
	children: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	closeButtonCustomAttributes: PropTypes.object,
	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.number,
	direction: PropTypes.string,
	headerHeadingCustomAttributes: PropTypes.object,
	headerDescriptionCustomAttributes: PropTypes.object,
	id: PropTypes.string,
	isOpen: PropTypes.bool,
	maxWidth: PropTypes.number,
	mobileBreakpoint: PropTypes.number,
	mobileWidth: PropTypes.number,
	onClose: PropTypes.func,
	onOpen: PropTypes.func,
	position: PropTypes.oneOf( [ 'absolute', 'fixed' ] ),
	showDivider: PropTypes.bool,
	title: PropTypes.string,
	zIndex: PropTypes.number,
};

Flyout.displayName = 'Flyout';

export default Flyout;