modules_Dialog_index.js

import { React, SimpleBar, PropTypes, classnames } from '@gravityforms/libraries';
import { bodyLock, getClosest, uniqueId } from '@gravityforms/utils';
import { ConditionalWrapper, useStateWithDep, useFocusTrap } from '@gravityforms/react-utils';
import IconIndicator from '../Indicators/IconIndicator';

const { useState, useEffect, useRef, forwardRef } = React;

/**
 * @module Dialog
 * @description A dialog component in react that handle full screen containers, modals, alerts and dialogs.
 *
 * @since 1.1.15
 *
 * @param {object}              props                          Component props.
 * @param {string}              props.alertButtonText          If in alert mode, the text for the ok button below content.
 * @param {string}              props.alignment                Position of message in window.
 * @param {boolean}             props.animateModal             When in modal or dialog mode, animate the dialog in with a fade in and up?
 * @param {number}              props.animationDelay           The css animation delay for the reveal/hide effect. Synchronize with css if modifying the built-in 250 ms delay.
 * @param {string}              props.buttonWidth              The button width, one of  `auto` or `full`.
 * @param {string}              props.cancelButtonHeight       The height for the cancel button.
 * @param {string}              props.cancelButtonText         If in dialog mode, the white cancel buttons text is set here.
 * @param {string}              props.cancelButtonType         The type of the cancel button.
 * @param {JSX.Element}         props.children                 React element children.
 * @param {string}              props.closeButtonSize          The close button size. Default is `xs`.
 * @param {string}              props.closeButtonTitle         The close button title for accessibility purposes.
 * @param {string}              props.closeButtonType          The close button type. Default is `round`. Also supports `noborder` and `unstyled`.
 * @param {boolean}             props.closeOnMaskClick         Whether to close if the background mask is clicked.
 * @param {object}              props.confirmButtonAttributes  Arbitrary additional html attributes for the confirm button if in dialog mode.
 * @param {string}              props.confirmButtonHeight      The height of the confirm button.
 * @param {string}              props.confirmButtonIcon        If in dialog mode, the optional confirmation button icon before the button text.
 * @param {string}              props.confirmButtonText        If in dialog mode, the confirmation button text.
 * @param {string}              props.confirmButtonType        The type of the confirm button.
 * @param {string}              props.content                  Container content. Can only be strings. Use React children for html.
 * @param {string|Array|object} props.customCloseButtonClasses Custom classes for the close button as array.
 * @param {string|Array|object} props.customMaskClasses        Custom classes for the mask as array.
 * @param {string|Array|object} props.customWrapperClasses     Custom classes for the wrapper as array.
 * @param {string|Array|object} props.description              Optional text to show below title.
 * @param {string}              props.id                       The id for the dialog. If not passed auto generated using uniqueId from our utils with a prefix of `dialog`.
 * @param {boolean}             props.isOpen                   Prop to control whether the dialog is currently open.
 * @param {boolean}             props.lockBody                 Whether to lock the body behind the dialog to prevent interaction or scrolling.
 * @param {boolean}             props.maskBlur                 Whether to blur behind the mask for the dialog.
 * @param {string}              props.maskTheme                Mask background color scheme: `none`, `light` or `dark`.
 * @param {string}              props.maxHeight                Max height for the dialog.
 * @param {string}              props.mode                     Container mode: `container`, `modal`, `alert` or `dialog`.
 * @param {Function}            props.onClose                  Function to execute when the dialog closes.
 * @param {Function}            props.onCloseAfterAnimation    Function to execute after the dialog close animation.
 * @param {Function}            props.onOpen                   Function to execute when the dialog opens.
 * @param {Function}            props.onOpenAfterAnimation     Function to execute after the dialog open animation.
 * @param {boolean}             props.padContent               Whether to pad the content on the right or not.
 * @param {string}              props.position                 Position for the mask: `fixed`, `absolute`.
 * @param {boolean}             props.showCancelButton         Whether to show the cancel button or not.
 * @param {boolean}             props.showCloseButton          Whether to show the close button top right or not.
 * @param {boolean}             props.showConfirmButton        Whether to show the confirm button or not.
 * @param {boolean}             props.simplebar                Whether or not to use SimpleBar on the content.
 * @param {string}              props.theme                    Theme for the dialog, one of `gravity-blue` or `cosmos`.
 * @param {string}              props.title                    Title for the dialog. Does not show in container mode.
 * @param {boolean}             props.titleDivider             Whether to show a divider below the title or not.
 * @param {string}              props.titleIndicatorType       Indicator type for the dialog title.
 * @param {string}              props.titleSize                Size for the title, sm or md currently.
 * @param {string}              props.titleTagName             Tagname for the title of the dialog.
 * @param {number}              props.zIndex                   The z-index for the dialog.
 * @param {object|null}         ref                            Ref to the component.
 *
 * @return {JSX.Element} Return the functional dialog component in React.
 *
 * @example
 * import Dialog from '@gravityforms/components/react/admin/modules/Dialog';
 *
 * const Example = () => {
 *     	const dialogArgs = {
 * 		    closeOnMaskClick: false,
 * 		    customCloseButtonClasses: [ 'gform-example--exit-button' ],
 * 		    customWrapperClasses: [
 * 			    'gform-example',
 * 		    ],
 * 		    id: 'test-id',
 * 		    isOpen,
 * 		    lockBody: true,
 * 		    mode: 'container',
 * 		    zIndex: 100001,
 *      };
 *
 *      return (
 *          <Dialog { ...dialogArgs }>
 *              { children }
 *          </Dialog>
 *      );
 * }
 *
 */

const Dialog = forwardRef( ( {
	alertButtonText = '',
	alignment = 'center',
	animateModal = false,
	animationDelay = 250,
	buttonWidth = 'auto',
	cancelButtonAttributes = {},
	cancelButtonHeight = 'height-l',
	cancelButtonText = '',
	cancelButtonType = 'white',
	children = null,
	closeButtonSize = 'xs',
	closeButtonTitle = '',
	closeButtonType = 'round',
	closeOnMaskClick = true,
	confirmButtonAttributes = {},
	confirmButtonHeight = 'height-l',
	confirmButtonIcon = '',
	confirmButtonText = '',
	confirmButtonType = 'primary-new',
	content = '',
	customCloseButtonClasses = [],
	customCloseButtonLabelAttributes = {},
	customMaskClasses = [],
	customWrapperClasses = [],
	description = '',
	id = '',
	isOpen = false,
	lockBody = false,
	maskBlur = true,
	maskTheme = 'none',
	maxHeight = '',
	mode = '',
	onClose = () => {},
	onCloseAfterAnimation = () => {},
	onOpen = () => {},
	onOpenAfterAnimation = () => {},
	padContent = true,
	position = 'fixed',
	showCancelButton = true,
	showCloseButton = true,
	showConfirmButton = true,
	simplebar = false,
	theme = 'gravity-blue',
	title = '',
	titleDivider = false,
	titleIndicatorAttributes = {},
	titleIndicatorType = '',
	titleSize = 'sm',
	titleTagName = 'h5',
	zIndex = 10,
}, ref ) => { // eslint-disable-line
	const [ animationReady, setAnimationReady ] = useState( false );
	const [ animationActive, setAnimationActive ] = useState( false );
	const [ modalAnimation, setModalAnimation ] = useState( false );
	const [ dialogActive, setDialogActive ] = useStateWithDep( isOpen );
	const trapRef = useFocusTrap( dialogActive );
	const mountedRef = useRef( true );
	const closeRef = useRef( true );

	useEffect( () => {
		if ( dialogActive ) {
			showDialog();
		} else if ( ! dialogActive && mountedRef.current ) {
			closeDialog();
		}
	}, [ dialogActive ] );

	useEffect( () => {
		if ( animateModal ) {
			setTimeout( () => {
				setModalAnimation( true );
			}, 200 );
		}
		return () => {
			mountedRef.current = false;
		};
	}, [ animateModal ] );

	useEffect( () => {
		closeRef.current.addEventListener( 'keydown', handleEscapeRequest );

		return () => {
			if ( ! closeRef.current ) {
				return;
			}
			closeRef.current.removeEventListener( 'keydown', handleEscapeRequest );
		};
	} );

	const handleEscapeRequest = ( e ) => {
		if ( getClosest( e.target, '.gform-dialog' ) !== closeRef.current ) {
			return;
		}

		if ( e.key !== 'Escape' ) {
			return;
		}

		closeDialog();
	};

	const closeDialog = () => {
		setAnimationActive( false );

		setTimeout( () => {
			setAnimationReady( false );
			onCloseAfterAnimation();
		}, animationDelay );

		if ( lockBody ) {
			bodyLock.unlock();
		}
		onClose();
	};

	const showDialog = () => {
		setAnimationReady( true );

		setTimeout( () => {
			setAnimationActive( true );
			setTimeout( () => onOpenAfterAnimation, animationDelay );
		}, 25 );

		if ( lockBody ) {
			bodyLock.lock();
		}
		onOpen();
	};

	const pointerDownOrigin = useRef( null );

	const handlePointerDown = ( event ) => {
		pointerDownOrigin.current = event.target;
	};

	const handlePointerUp = ( event ) => {
		if ( pointerDownOrigin.current === event.target &&
			event.target.classList.contains( 'gform-dialog__mask' ) &&
			closeOnMaskClick &&
			dialogActive ) {
			event.stopPropagation();
			setDialogActive( false );
		}
		pointerDownOrigin.current = null;
	};

	const maskProps = {
		className: classnames( {
			'gform-dialog__mask': true,
			'gform-dialog--anim-in-ready': animationReady,
			'gform-dialog--anim-in-active': animationReady && animationActive,
			[ `gform-dialog__mask--position-${ position }` ]: true,
			[ `gform-dialog__mask--theme-${ maskTheme }` ]: true,
			[ `gform-dialog--alignment-${ alignment }` ]: mode !== 'container',
			'gform-dialog__mask--blur': maskBlur,
		}, customMaskClasses ),
		onPointerDown: handlePointerDown,
		onPointerUp: handlePointerUp,
		style: {
			zIndex,
		},
	};

	const wrapperProps = {
		id: id || uniqueId( 'dialog-' ),
		className: classnames( {
			'gform-dialog': true,
			'gform-dialog--animated': animateModal,
			'gform-dialog--animate-reveal': modalAnimation,
			[ `gform-dialog--title-size-${ titleSize }` ]: true,
			'gform-dialog--container': mode === 'container',
			'gform-dialog--simplebar': simplebar,
			[ `gform-dialog__theme--${ theme }` ]: true,
		}, customWrapperClasses ),
		style: {
			maxHeight,
		},
	};

	const closeButtonLabelProps = {
		className: classnames( {
			'gform-button__icon': true,
			'gform-common-icon': true,
			'gform-common-icon--x': true,
		} ),
		...customCloseButtonLabelAttributes,
	};

	const contentProps = {
		className: classnames( {
			'gform-dialog__content': true,
			'gform-dialog__content--with-divider': titleDivider,
			'gform-dialog__content--pad-content': padContent,
		} ),
	};

	const closeButtonProps = {
		className: classnames( {
			'gform-dialog__close': true,
			'gform-button': true,
			'gform-button--unstyled': closeButtonType === 'unstyled',
			'gform-button--white': closeButtonType === 'round',
			'gform-button--circular': closeButtonType === 'round' || closeButtonType === 'simplified',
			'gform-button--simplified': closeButtonType === 'simplified',
			[ `gform-button--size-${ closeButtonSize }` ]: true,
		}, customCloseButtonClasses ),
		onClick: () => setDialogActive( false ),
		style: {
			zIndex: zIndex + 1,
		},
		title: closeButtonTitle,
	};

	if ( closeButtonProps.ariaLabel ) {
		closeButtonProps[ 'aria-label' ] = closeButtonProps.ariaLabel;
	} else {
		closeButtonProps[ 'aria-label' ] = closeButtonTitle;
	}

	const getTitleIndicator = () => {
		if ( ! titleIndicatorType ) {
			return null;
		}

		const titleIndicatorProps = {
			type: titleIndicatorType,
			...titleIndicatorAttributes,
		};

		return <IconIndicator { ...titleIndicatorProps } />;
	};

	const getTitle = () => {
		const headerProps = {
			className: classnames( {
				'gform-dialog__head': true,
				'gform-dialog__head--with-divider': titleDivider,
			} ),
			'data-js': 'gform-dialog-header',
		};

		const headingProps = {
			className: classnames( {
				'gform-dialog__title': true,
				'gform-dialog__title--has-icon': !! titleIndicatorType,
				[ `gform-dialog__title--icon-type-${ titleIndicatorType }` ]: !! titleIndicatorType,
			} ),
		};

		const TitleTag = titleTagName;

		return (
			<header { ...headerProps }>
				{ getTitleIndicator() } <TitleTag { ...headingProps }>{ title }</TitleTag>
				{ description && <span className="gform-dialog__description">{ description }</span> }
			</header>
		);
	};

	const getConfirmButtonIcon = () => {
		const iconProps = {
			className: classnames( {
				'gform-button__icon': true,
				[ `gform-icon gform-icon--${ confirmButtonIcon }` ]: true,
			} ),
		};

		return (
			<span { ...iconProps } />
		);
	};

	const getDialogButtons = () => {
		const cancelButtonProps = {
			className: classnames( {
				'gform-dialog__cancel': true,
				'gform-button': true,
				[ `gform-button--size-${ cancelButtonHeight }` ]: true,
				[ `gform-button--${ cancelButtonType }` ]: true,
				[ `gform-button--width-${ buttonWidth }` ]: true,
			} ),
			onClick: () => setDialogActive( false ),
			'data-js': 'gform-dialog-cancel',
			...cancelButtonAttributes,
		};

		const confirmButtonProps = {
			className: classnames( {
				'gform-dialog__confirm': true,
				'gform-button': true,
				[ `gform-button--size-${ confirmButtonHeight }` ]: true,
				[ `gform-button--${ confirmButtonType }` ]: true,
				'gform-button--icon-leading': confirmButtonIcon,
				[ `gform-button--width-${ buttonWidth }` ]: true,
			} ),
			'data-js': 'gform-dialog-confirm',
			...confirmButtonAttributes,
		};

		return (
			<>
				{ showCancelButton && ( <button { ...cancelButtonProps }>
					{ cancelButtonText }
				</button> ) }
				{ showConfirmButton && ( <button { ...confirmButtonProps }>
					{ confirmButtonIcon && getConfirmButtonIcon() }{ confirmButtonText }
				</button> ) }
			</>
		);
	};

	const getAlertButtons = () => {
		const buttonProps = {
			className: classnames( {
				'gform-dialog__alert': true,
				'gform-button': true,
				[ `gform-button--size-${ confirmButtonHeight }` ]: true,
				[ `gform-button--${ confirmButtonType }` ]: true,
				[ `gform-button--width-${ buttonWidth }` ]: true,
			} ),
			onClick: () => setDialogActive( false ),
			'data-js': 'gform-dialog-alert',
		};

		return (
			<>
				<button { ...buttonProps }>
					{ alertButtonText }
				</button>
			</>
		);
	};

	const getFooter = () => {
		const footerProps = {
			className: classnames( {
				'gform-dialog__footer': true,
			} ),
			'data-js': 'gform-dialog-footer',
		};

		return (
			<>
				<footer { ...footerProps }>
					{ mode === 'dialog' && getDialogButtons() }
					{ mode === 'alert' && getAlertButtons() }
				</footer>
			</>
		);
	};

	return (
		<div { ...maskProps } ref={ trapRef }>
			<article { ...wrapperProps } ref={ closeRef }>
				<ConditionalWrapper
					condition={ simplebar }
					wrapper={ ( ch ) => <SimpleBar>{ ch }</SimpleBar> }
				>
					{ showCloseButton && <button { ...closeButtonProps } >
						<span { ...closeButtonLabelProps } />
					</button> }
					{ mode !== 'container' && title && getTitle() }
					<div { ...contentProps }>
						{ mode !== 'container' && content }
						{ children }
					</div>
					{ ( mode === 'dialog' || mode === 'alert' ) && getFooter() }
				</ConditionalWrapper>
			</article>
		</div>
	);
} );

Dialog.propTypes = {
	alertButtonText: PropTypes.string,
	alignment: PropTypes.string,
	animateModal: PropTypes.bool,
	animationDelay: PropTypes.number,
	buttonWidth: PropTypes.string,
	cancelButtonHeight: PropTypes.string,
	cancelButtonText: PropTypes.string,
	cancelButtonType: PropTypes.string,
	children: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	closeButtonSize: PropTypes.string,
	closeButtonTitle: PropTypes.string,
	closeButtonType: PropTypes.string,
	closeOnMaskClick: PropTypes.bool,
	confirmButtonAttributes: PropTypes.object,
	confirmButtonHeight: PropTypes.string,
	confirmButtonIcon: PropTypes.string,
	confirmButtonText: PropTypes.string,
	confirmButtonType: PropTypes.string,
	content: PropTypes.string,
	customCloseButtonClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	customMaskClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	customWrapperClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	id: PropTypes.string,
	isOpen: PropTypes.bool,
	lockBody: PropTypes.bool,
	maskBlur: PropTypes.bool,
	maskTheme: PropTypes.string,
	mode: PropTypes.string,
	onClose: PropTypes.func,
	onCloseAfterAnimation: PropTypes.func,
	onOpen: PropTypes.func,
	onOpenAfterAnimation: PropTypes.func,
	position: PropTypes.string,
	showCancelButton: PropTypes.bool,
	showCloseButton: PropTypes.bool,
	showConfirmButton: PropTypes.bool,
	theme: PropTypes.string,
	title: PropTypes.string,
	titleIndicatorType: PropTypes.string,
	zIndex: PropTypes.number,
};

Dialog.displayName = 'Dialog';

export default Dialog;