diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx index 0b42a41b80..a6c8e3d53a 100644 --- a/src/components/Modal/Modal.tsx +++ b/src/components/Modal/Modal.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import type { PropsWithChildren } from 'react'; +import { type PropsWithChildren, useCallback } from 'react'; import React, { useEffect, useRef } from 'react'; import { FocusScope } from '@react-aria/focus'; @@ -7,46 +7,66 @@ import { CloseIconRound } from './icons'; import { useTranslationContext } from '../../context'; +type CloseEvent = + | KeyboardEvent + | React.KeyboardEvent + | React.MouseEvent; +export type ModalCloseSource = 'overlay' | 'button' | 'escape'; + export type ModalProps = { /** If true, modal is opened or visible. */ open: boolean; /** Custom class to be applied to the modal root div */ className?: string; /** Callback handler for closing of modal. */ - onClose?: ( - event: React.KeyboardEvent | React.MouseEvent, - ) => void; + onClose?: (event: CloseEvent) => void; + /** Optional handler to intercept closing logic. Return false to prevent onClose. */ + onCloseAttempt?: (source: ModalCloseSource, event: CloseEvent) => boolean; }; export const Modal = ({ children, className, onClose, + onCloseAttempt, open, }: PropsWithChildren) => { const { t } = useTranslationContext('Modal'); const innerRef = useRef(null); - const closeRef = useRef(null); + const closeButtonRef = useRef(null); + + const maybeClose = useCallback( + (source: ModalCloseSource, event: CloseEvent) => { + const allow = onCloseAttempt?.(source, event); + if (allow !== false) { + onClose?.(event); + } + }, + [onClose, onCloseAttempt], + ); const handleClick = (event: React.MouseEvent) => { const target = event.target as HTMLButtonElement | HTMLDivElement; - if (!innerRef.current || !closeRef.current) return; + if (!innerRef.current || !closeButtonRef.current) return; - if (!innerRef.current.contains(target) || closeRef.current.contains(target)) - onClose?.(event); + if (closeButtonRef.current.contains(target)) { + maybeClose('button', event); + } else if (!innerRef.current.contains(target)) { + maybeClose('overlay', event); + } }; useEffect(() => { if (!open) return; const handleKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') onClose?.(event as unknown as React.KeyboardEvent); + if (event.key === 'Escape') maybeClose('escape', event); }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [onClose, open]); + }, [maybeClose, open]); if (!open) return null; @@ -58,7 +78,7 @@ export const Modal = ({