11import clsx from 'clsx' ;
2- import type { PropsWithChildren } from 'react' ;
2+ import { type PropsWithChildren , useCallback } from 'react' ;
33import React , { useEffect , useRef } from 'react' ;
44import { FocusScope } from '@react-aria/focus' ;
55
66import { CloseIconRound } from './icons' ;
77
88import { useTranslationContext } from '../../context' ;
99
10+ type CloseEvent =
11+ | KeyboardEvent
12+ | React . KeyboardEvent
13+ | React . MouseEvent < HTMLButtonElement | HTMLDivElement > ;
14+ export type ModalCloseSource = 'overlay' | 'button' | 'escape' ;
15+
1016export type ModalProps = {
1117 /** If true, modal is opened or visible. */
1218 open : boolean ;
1319 /** Custom class to be applied to the modal root div */
1420 className ?: string ;
1521 /** Callback handler for closing of modal. */
16- onClose ?: (
17- event : React . KeyboardEvent | React . MouseEvent < HTMLButtonElement | HTMLDivElement > ,
18- ) => void ;
22+ onClose ?: ( event : CloseEvent ) => void ;
23+ /** Optional handler to intercept closing logic. Return false to prevent onClose. */
24+ onCloseAttempt ?: ( source : ModalCloseSource , event : CloseEvent ) => boolean ;
1925} ;
2026
2127export const Modal = ( {
2228 children,
2329 className,
2430 onClose,
31+ onCloseAttempt,
2532 open,
2633} : PropsWithChildren < ModalProps > ) => {
2734 const { t } = useTranslationContext ( 'Modal' ) ;
2835
2936 const innerRef = useRef < HTMLDivElement | null > ( null ) ;
30- const closeRef = useRef < HTMLButtonElement | null > ( null ) ;
37+ const closeButtonRef = useRef < HTMLButtonElement | null > ( null ) ;
38+
39+ const maybeClose = useCallback (
40+ ( source : ModalCloseSource , event : CloseEvent ) => {
41+ const allow = onCloseAttempt ?.( source , event ) ;
42+ if ( allow !== false ) {
43+ onClose ?.( event ) ;
44+ }
45+ } ,
46+ [ onClose , onCloseAttempt ] ,
47+ ) ;
3148
3249 const handleClick = ( event : React . MouseEvent < HTMLButtonElement | HTMLDivElement > ) => {
3350 const target = event . target as HTMLButtonElement | HTMLDivElement ;
34- if ( ! innerRef . current || ! closeRef . current ) return ;
51+ if ( ! innerRef . current || ! closeButtonRef . current ) return ;
3552
36- if ( ! innerRef . current . contains ( target ) || closeRef . current . contains ( target ) )
37- onClose ?.( event ) ;
53+ if ( closeButtonRef . current . contains ( target ) ) {
54+ maybeClose ( 'button' , event ) ;
55+ } else if ( ! innerRef . current . contains ( target ) ) {
56+ maybeClose ( 'overlay' , event ) ;
57+ }
3858 } ;
3959
4060 useEffect ( ( ) => {
4161 if ( ! open ) return ;
4262
4363 const handleKeyDown = ( event : KeyboardEvent ) => {
44- if ( event . key === 'Escape' ) onClose ?. ( event as unknown as React . KeyboardEvent ) ;
64+ if ( event . key === 'Escape' ) maybeClose ( 'escape' , event ) ;
4565 } ;
4666
4767 document . addEventListener ( 'keydown' , handleKeyDown ) ;
4868 return ( ) => document . removeEventListener ( 'keydown' , handleKeyDown ) ;
49- } , [ onClose , open ] ) ;
69+ } , [ maybeClose , open ] ) ;
5070
5171 if ( ! open ) return null ;
5272
@@ -58,7 +78,7 @@ export const Modal = ({
5878 < FocusScope autoFocus contain >
5979 < button
6080 className = 'str-chat__modal__close-button'
61- ref = { closeRef }
81+ ref = { closeButtonRef }
6282 title = { t ( 'Close' ) }
6383 >
6484 < CloseIconRound />
0 commit comments