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;