hooks_use-popup.js

import { React } from '@gravityforms/libraries';

const { useCallback, useEffect, useRef, useState } = React;
const ESCAPE = 'Escape';

/**
 * @module usePopup
 * @description A hook to manage a popup state.
 *
 * @since 4.0.1
 *
 * @param {object}   args                         The arguments for the hook.
 * @param {boolean}  args.closeOnClickOutside     Whether to close the popup when clicking outside.
 * @param {Function} args.customClickOutsideLogic Custom logic to determine if the popup should close when clicking outside.
 * @param {number}   args.duration                The duration of the popup animation.
 * @param {boolean}  args.initialOpen             The initial open state of the popup.
 * @param {boolean}  args.isOpen                  External control of the popup open state. When provided (not undefined), enables external control mode.
 * @param {Function} args.onAfterClose            The callback after the popup is closed.
 * @param {Function} args.onAfterOpen             The callback after the popup is opened.
 * @param {Function} args.onClose                 The callback when the popup is closed.
 * @param {Function} args.onOpen                  The callback when the popup is opened.
 *
 * @return {object} The popup state.
 */
const usePopup = ( {
	closeOnClickOutside = true,
	customClickOutsideLogic = () => {},
	duration = 150,
	initialOpen = false,
	isOpen = undefined,
	onAfterClose = () => {},
	onAfterOpen = () => {},
	onClose = () => {},
	onOpen = () => {},
} = {} ) => {
	// Internal state management for popup visibility and animation states. The popupOpen state also represents the actual open/closed state.
	const [ popupReveal, setPopupReveal ] = useState( false );
	const [ popupHide, setPopupHide ] = useState( false );
	const [ popupOpen, setPopupOpen ] = useState( initialOpen );
	const popupRef = useRef( null );
	const triggerRef = useRef( null );

	/**
	 * @function openPopup
	 * @description Open the popup.
	 *
	 * @since 4.0.1
	 *
	 */
	const openPopup = useCallback( () => {
		onOpen();
		setPopupReveal( true );
		setTimeout( () => {
			setPopupOpen( true );
			setTimeout( () => {
				setPopupReveal( false );
				onAfterOpen();
			}, duration );
		}, 0 );
	}, [ duration, onAfterOpen, onOpen ] );

	/**
	 * @function closePopup
	 * @description Close the popup.
	 *
	 * @since 4.0.1
	 *
	 */
	const closePopup = useCallback( () => {
		onClose();
		setPopupOpen( false );
		setPopupHide( true );
		setTimeout( () => {
			setPopupHide( false );
			onAfterClose();
		}, duration );
	}, [ duration, onAfterClose, onClose ] );

	/**
	 * @function handleEscKeyDown
	 * @description Handles the keydown event for the escape key.
	 *
	 * @since 4.0.1
	 *
	 * @param {Event} event The event object.
	 */
	const handleEscKeyDown = useCallback( ( event ) => {
		if ( event.key !== ESCAPE ) {
			return;
		}
		// Close dropdown if escape is pressed.
		closePopup();
		triggerRef?.current?.focus();
	}, [ closePopup, triggerRef ] );

	// Sync internal popupOpen state with external isOpen prop if provided.
	useEffect( () => {
		if ( isOpen === undefined || isOpen === popupOpen ) {
			return;
		}
		if ( isOpen ) {
			openPopup();
		} else {
			closePopup();
		}
	}, [ isOpen ] ); // eslint-disable-line react-hooks/exhaustive-deps

	useEffect( () => {
		if ( ! closeOnClickOutside ) {
			return;
		}
		const handleClickOutside = ( event ) => {
			// If refs don't exist, return early.
			if ( ! popupOpen || ! triggerRef?.current || ! popupRef?.current ) {
				return;
			}
			if (
				! triggerRef.current.contains( event.target ) &&
				! popupRef.current.contains( event.target ) &&
				! customClickOutsideLogic( event )
			) {
				closePopup();
			}
		};
		document.addEventListener( 'click', handleClickOutside );
		return () => {
			document.removeEventListener( 'click', handleClickOutside );
		};
	}, [ closeOnClickOutside, closePopup, customClickOutsideLogic, popupOpen ] );

	return {
		closePopup,
		openPopup,
		handleEscKeyDown,
		popupHide,
		popupOpen,
		popupReveal,
		setPopupHide,
		setPopupOpen,
		setPopupReveal,
		popupRef,
		triggerRef,
	};
};

export default usePopup;