UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

313 lines (277 loc) 9.39 kB
import { React, classnames } from '@gravityforms/libraries'; import { useStateWithDep, useFocusTrap } from '@gravityforms/react-utils'; import { getClosest } from '@gravityforms/utils'; import Button from '../../elements/Button'; import { ENTER, ESCAPE } from '../../utils/keymap'; const { useCallback, useState, useEffect, useRef, forwardRef } = React; /** * @module Overlay * @description An overlay component. * * @since 5.6.0 * * @param {object} props The props for the component template. * @param {number} props.animationDelay Delay before the animation starts. * @param {JSX.Element} props.children React element children. * @param {object} props.customAttributes Any custom attributes. * @param {string|Array|object} props.customClasses An array of additional classes. * @param {boolean} props.hasConfirm Whether the overlay has a confirm button. * @param {object} props.i18n i18n strings. * @param {Array} props.ignoreOutsideClickClasses Array of selectors to ignore outside click. * @param {boolean} props.isOpen Whether the overlay is open. * @param {Function} props.onCancel Callback function when the overlay is canceled. * @param {Function} props.onClose Callback function when the overlay is closed. * @param {Function} props.onCloseAfterAnimation Callback function when the overlay is closed after animation. * @param {Function} props.onConfirm Callback function when the overlay is confirmed. * @param {Function} props.onOpen Callback function when the overlay is opened. * @param {Function} props.onOpenAfterAnimation Callback function when the overlay is opened after animation. * @param {string} props.placement Placement of the overlay. * @param {string} props.position Position of the overlay. * @param {number} props.verticalOffset Vertical offset of the overlay. * @param {string} props.width Width of the overlay. * @param {number} props.zIndex Z-index of the overlay. * @param {object|null} ref Ref to the component. * * @return {JSX.Element} The overlay component. * * @example * import Overlay from '@gravityforms/components/react/admin/modules/Overlay'; * * return ( * <Overlay> * <div>Overlay Content</div> * </Overlay> * ); * */ const Overlay = forwardRef( ( { animationDelay = 250, cancelButtonAttributes = {}, cancelButtonClasses = [], children = null, confirmButtonAttributes = {}, confirmButtonClasses = [], customAttributes = {}, customClasses = [], hasConfirm = false, i18n = { cancel: 'Needs i18n', confirm: 'Needs i18n', }, ignoreOutsideClickClasses = [], isOpen = false, onCancel = () => {}, onClose = () => {}, onCloseAfterAnimation = () => {}, onConfirm = () => {}, onOpen = () => {}, onOpenAfterAnimation = () => {}, placement = 'auto', position = 'absolute', verticalOffset = 0, width = '100%', zIndex = 10, }, ref ) => { const [ animationReady, setAnimationReady ] = useState( false ); const [ animationActive, setAnimationActive ] = useState( false ); const [ overlayActive, setOverlayActive ] = useStateWithDep( isOpen ); const [ positionAbove, setPositionAbove ] = useState( false ); const overlayRef = useRef( null ); const trapRef = useFocusTrap( overlayActive ); const mountedRef = useRef( true ); const debounceTimerRef = useRef( null ); const checkPosition = () => { if ( ! overlayActive || ! overlayRef.current || placement !== 'auto' ) { return; } const overlayRect = overlayRef.current.getBoundingClientRect(); const spaceAbove = overlayRect.top; const spaceBelow = window.innerHeight - overlayRect.top; const overlayHeight = overlayRect.height; const fitsBelow = spaceBelow >= overlayHeight; const fitsAbove = spaceAbove >= overlayHeight; if ( fitsBelow ) { setPositionAbove( false ); } else if ( fitsAbove ) { setPositionAbove( true ); } else { setPositionAbove( spaceAbove > spaceBelow ); } }; // Debounced event handler for resize const handleViewportChange = () => { if ( debounceTimerRef.current ) { clearTimeout( debounceTimerRef.current ); } debounceTimerRef.current = setTimeout( () => { checkPosition(); debounceTimerRef.current = null; }, 100 ); }; useEffect( () => { if ( ! mountedRef.current ) { return; } if ( overlayActive && ! animationReady && ! animationActive ) { showOverlay(); } else if ( ! overlayActive && animationReady && animationActive ) { closeOverlay(); } }, [ overlayActive ] ); useEffect( () => { document.addEventListener( 'pointerdown', handlePointerDown ); document.addEventListener( 'pointerup', handlePointerUp ); window.addEventListener( 'resize', handleViewportChange ); return () => { document.removeEventListener( 'pointerdown', handlePointerDown ); document.removeEventListener( 'pointerup', handlePointerUp ); window.removeEventListener( 'resize', handleViewportChange ); if ( debounceTimerRef.current ) { clearTimeout( debounceTimerRef.current ); } }; }, [] ); const pointerDownOrigin = useRef( null ); const handlePointerDown = ( event ) => { pointerDownOrigin.current = event.target; }; const handlePointerUp = ( event ) => { if ( ! pointerDownOrigin.current ) { return; } const pointerDownIsOverlay = pointerDownOrigin.current.classList.contains( 'gform-overlay' ); const pointerDownInOverlay = getClosest( pointerDownOrigin.current, '.gform-overlay' ); const pointerDownInIgnore = ignoreOutsideClickClasses.some( ( className ) => ( pointerDownOrigin.current.classList.contains( className ) || getClosest( pointerDownOrigin.current, `.${ className }` ) ) ); const clickIsOverlay = event.target.classList.contains( 'gform-overlay' ); const clickInOverlay = getClosest( event.target, '.gform-overlay' ); const clickInIgnore = ignoreOutsideClickClasses.some( ( className ) => ( event.target.classList.contains( className ) || getClosest( event.target, `.${ className }` ) ) ); if ( ! pointerDownIsOverlay && ! pointerDownInOverlay && ! pointerDownInIgnore && ! clickIsOverlay && ! clickInOverlay && ! clickInIgnore && overlayActive ) { event.stopPropagation(); setOverlayActive( false ); } pointerDownOrigin.current = null; }; const handleKeyboardClose = ( e ) => { if ( e.key !== ESCAPE && e.key !== ENTER ) { return; } e.stopPropagation(); if ( e.key === ESCAPE ) { onCancel(); } if ( e.key === ENTER ) { onConfirm(); } setOverlayActive( false ); }; const closeOverlay = () => { setAnimationActive( false ); setTimeout( () => { setAnimationReady( false ); onCloseAfterAnimation(); }, animationDelay ); onClose(); }; const showOverlay = () => { setAnimationReady( true ); // Check position after a short delay to allow initial rendering setTimeout( () => { if ( overlayRef.current ) { checkPosition(); } }, 25 ); setTimeout( () => { setAnimationActive( true ); setTimeout( () => { onOpenAfterAnimation(); }, animationDelay ); }, 25 ); onOpen(); }; const componentProps = { className: classnames( { 'gform-overlay': true, 'gform-overlay--anim-in-ready': animationReady, 'gform-overlay--anim-in-active': animationReady && animationActive, [ `gform-overlay--placement-${ placement }` ]: true, 'gform-overlay--position-above': positionAbove && placement === 'auto', }, customClasses ), onKeyDown: handleKeyboardClose, style: { marginTop: `-${ verticalOffset }px`, position, width, zIndex, ...( positionAbove && placement === 'auto' && { transform: 'translateY(-100%)' } ), }, ...customAttributes, }; const articleProps = { className: classnames( { 'gform-overlay__inner': true, } ), }; const cancelButtonProps = { label: i18n.cancel, customClasses: classnames( { 'gform-overlay__action': true, 'gform-overlay__cancel': true, }, cancelButtonClasses ), onClick: onCancel, size: 'size-height-s', spacing: [ 0, 2, 0, 0 ], type: 'white', ...cancelButtonAttributes, }; const confirmButtonProps = { label: i18n.confirm, customClasses: classnames( { 'gform-overlay__action': true, 'gform-overlay__confirm': true, }, confirmButtonClasses ), onClick: onConfirm, size: 'size-height-s', ...confirmButtonAttributes, }; const Footer = hasConfirm && ( <div className="gform-overlay__footer"> <Button { ...cancelButtonProps } /> <Button { ...confirmButtonProps } /> </div> ); const setRefs = useCallback( ( node ) => { overlayRef.current = node; if ( ref ) { ref.current = node; } }, [ ref ] ); return ( <div { ...componentProps } ref={ setRefs } > <article { ...articleProps } ref={ trapRef }> { children } { Footer } </article> </div> ); } ); Overlay.displayName = 'Overlay'; export default Overlay;