import { createContext, useCallback, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { noop } from '../utils'; import { useLastFocusedElement } from './LastFocusedElement'; import useContextHook from './useContextHook'; import usePreviousFocus from '../hooks/usePreviousFocus'; import useStateWithCallback from '../hooks/useStateWithCallback'; const Context = createContext(null); Context.displayName = 'Modal'; export const MODAL_TYPE = { CONFIRMATION: 'CONFIRMATION', PRODUCT_DESCRIPTION: 'PRODUCT_DESCRIPTION', STREAM_BROADCAST_SETTINGS: 'STREAM_BROADCAST_SETTINGS', STREAM_MANAGER_ACTION: 'STREAM_MANAGER_ACTION' }; /** * @typedef {Object} Modal The data object that is passed from the openModal function in order to open a new Modal * @property {Function} [onCancel=()=>{}] Called when the Modal is cancelled (i.e. clicking the "Cancel" button, pressing the Escape key, or clicking anywhere outside the Modal) * @property {Function} [onConfirm=()=>{}] Called when the Modal is submitted through user input * @property {Function} [onSave=()=>{}] Saves the Modal data (only used for STREAM_MANAGER_ACTION modals) * @property {string} [type=CONFIRMATION] The type of Modal (defaults to CONFIRMATION) * @property {object} [lastFocusedElement] A ref pointing to the element that you wish to refocus when the modal closes * @property {object} content The data that the Modal component receives and uses to render the Modal */ /** * The Modal context acts the orchestrator for all the Modals in the app. * * It receives data via the openModal function, which it then uses to provide the * necessary content properties and handlers to the respective Modal component. * It also ensures that only 1 Modal component is rendered at any given time. */ export const Provider = ({ children }) => { /** @type {[Modal, Function]} */ const [modal, setModal] = useStateWithCallback(null); const [isModalOpen, setIsModalOpen] = useState(false); const { setLastFocusedElement } = useLastFocusedElement(); const { onCancel = noop, onConfirm = noop, onSave = noop, type } = modal || {}; const { refocus } = usePreviousFocus({ isActive: isModalOpen, onRefocus: (isKeyEvent) => { setIsModalOpen(false); isKeyEvent && onCancel(); } }); const openModal = useCallback( (modalData) => { const { lastFocusedElement, type = MODAL_TYPE.CONFIRMATION } = modalData; if (lastFocusedElement) setLastFocusedElement(lastFocusedElement.current); setModal({ ...modalData, type }, () => setIsModalOpen(true)); }, [setLastFocusedElement, setModal] ); const closeModal = useCallback( ({ shouldCancel = true, shouldRefocus = true } = {}) => { setIsModalOpen(false); if (shouldRefocus) refocus(); if (shouldCancel) onCancel(); }, [onCancel, refocus] ); const handleSave = useCallback((data) => onSave(data), [onSave]); const handleConfirm = useCallback( async (data) => { const shouldCloseModal = await onConfirm(data); if (shouldCloseModal === false) return; closeModal({ shouldCancel: false, shouldRefocus: false }); }, [closeModal, onConfirm] ); const value = useMemo( () => ({ ...modal, closeModal, handleConfirm, handleSave, isModalOpen, openModal, type }), [modal, closeModal, handleConfirm, handleSave, isModalOpen, openModal, type] ); return {children}; }; Provider.propTypes = { children: PropTypes.node.isRequired }; export const useModal = () => useContextHook(Context);