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;