@@ -6,51 +6,111 @@ export interface ModalProviderProps {
6
6
children ?: React . ReactNode ;
7
7
}
8
8
9
+ export interface ModalComponentV2 < P extends CommonModalProps = CommonModalProps > extends React . FC < P > {
10
+ unmountDelayMS ?: number ;
11
+ }
12
+
9
13
export interface ModalState {
10
- component : React . FC ;
14
+ key : string ;
15
+ component : ModalComponentV2 ;
11
16
props : object ;
12
17
}
13
18
19
+ export interface CommonModalProps {
20
+ isClosing ?: boolean ;
21
+ }
22
+
14
23
export interface ModalContextTypeV2 {
15
24
activeModal : ModalState | null ;
16
- showModal : < T > ( component : React . FC < T > | null , props ?: T ) => void ;
25
+ closingModals : { [ key : string ] : ModalState } ;
26
+ showModal : < P extends CommonModalProps > ( key : string , component : ModalComponentV2 < P > | null , props ?: P ) => void ;
27
+ hideCurrentModal : ( ) => void ;
28
+ hideModal : ( key : string ) => void ;
17
29
}
18
30
19
31
const defaultModalContext : ModalContextTypeV2 = {
20
32
activeModal : null ,
33
+ closingModals : { } ,
21
34
showModal : ( ) => { } ,
35
+ hideCurrentModal : ( ) => { } ,
36
+ hideModal : ( ) => { } ,
22
37
} ;
23
38
export const ModalContextV2 = React . createContext ( defaultModalContext ) ;
24
39
25
40
function newModalState ( ) : ModalState | null {
26
41
return null ;
27
42
}
28
43
29
- export function ModalProvider ( { children } : ModalProviderProps ) {
44
+ /** Provides a context that allows any of its descendants to control modals via
45
+ * `useModalV2()`. Must be paired with `<ModalsV2 />` in order for modals to
46
+ * show up. */
47
+ export function ModalProviderV2 ( { children } : ModalProviderProps ) {
30
48
const [ activeModal , setActiveModal ] = React . useState ( newModalState ) ;
49
+ const [ closingModals , setClosingModals ] = React . useState < { [ key : string ] : ModalState } > ( { } ) ;
50
+
51
+ const showModal = ( key , component , props ) => {
52
+ // If a modal was previously active, remove it now, or set a timeout for it
53
+ // if there is an unmount delay.
54
+ hideCurrentModal ( ) ;
55
+
56
+ if ( component ) {
57
+ setActiveModal ( {
58
+ key,
59
+ component,
60
+ props : props || { } ,
61
+ } ) ;
62
+ } else {
63
+ setActiveModal ( null ) ;
64
+ }
65
+ } ;
66
+
67
+ const hideCurrentModal = ( ) => {
68
+ if ( activeModal ) {
69
+ setActiveModal ( null ) ;
70
+ setClosingModals ( ( prev ) => ( { ...prev , [ activeModal . key ] : activeModal } ) ) ;
31
71
32
- const showModal = ( component , props ) => {
33
- setActiveModal ( {
34
- component,
35
- props : props || { } ,
36
- } ) ;
72
+ const timeout = activeModal . component . unmountDelayMS || 0 ;
73
+ const callback = ( ) => {
74
+ setClosingModals ( ( prev ) => {
75
+ const { [ activeModal . key ] : _ , ...filtered } = prev ;
76
+ return filtered ;
77
+ } ) ;
78
+ } ;
79
+
80
+ if ( timeout ) {
81
+ setTimeout ( callback , timeout ) ;
82
+ } else {
83
+ callback ( ) ;
84
+ }
85
+ }
86
+ } ;
87
+
88
+ const hideModal = ( key ) => {
89
+ if ( activeModal && activeModal . key === key ) {
90
+ hideCurrentModal ( ) ;
91
+ }
37
92
} ;
38
93
39
- return < ModalContextV2 . Provider value = { { activeModal, showModal } } > { children } </ ModalContextV2 . Provider > ;
94
+ return < ModalContextV2 . Provider value = { { activeModal, closingModals , showModal, hideCurrentModal , hideModal } } > { children } </ ModalContextV2 . Provider > ;
40
95
}
41
96
97
+ /** Displays the active modal. Without this component, the modal context does
98
+ * nothing. */
42
99
export function ModalsV2 ( ) {
43
- const { activeModal } = React . useContext ( ModalContextV2 ) ;
100
+ const { activeModal, closingModals } = React . useContext ( ModalContextV2 ) ;
101
+
102
+ const renderModal = ( state : ModalState , isClosing : boolean ) => {
103
+ const Component : React . FC < CommonModalProps > = state ?. component ;
104
+ const props = state ?. props || { } ;
44
105
45
- const Component = activeModal ?. component ;
46
- const props = activeModal ?. props || { } ;
106
+ return < Component key = { state . key } isClosing = { isClosing } { ... props } /> ;
107
+ } ;
47
108
48
109
return (
49
- Component && (
50
- < div className = { styles . modalBackground } >
51
- < Component { ...props } />
52
- </ div >
53
- )
110
+ < div className = { styles . modalBackground } >
111
+ { activeModal && renderModal ( activeModal , false ) }
112
+ { Object . values ( closingModals ) . map ( ( closingModal ) => renderModal ( closingModal , true ) ) }
113
+ </ div >
54
114
) ;
55
115
}
56
116
@@ -60,12 +120,18 @@ export interface ModalHandleV2<T> {
60
120
close : ( ) => void ;
61
121
}
62
122
63
- export function useModalV2 < T > ( component : React . FC < T > ) : ModalHandleV2 < T > {
64
- const { showModal, activeModal } = React . useContext ( ModalContextV2 ) ;
123
+ export function useModalV2 < T extends CommonModalProps > ( component : React . FC < T > ) : ModalHandleV2 < T > {
124
+ const { showModal, hideModal, hideCurrentModal, activeModal } = React . useContext ( ModalContextV2 ) ;
125
+
126
+ const key = React . useMemo ( ( ) => uniqueModalKey ( ) , [ ] ) ;
65
127
66
128
return {
67
129
isActive : component === activeModal ?. component ,
68
- open : ( props ) => showModal ( component , props ) ,
69
- close : ( ) => showModal ( null ) ,
130
+ open : ( props ) => showModal ( key , component , props ) ,
131
+ close : ( ) => hideModal ( key ) ,
70
132
} ;
71
133
}
134
+
135
+ function uniqueModalKey ( ) : string {
136
+ return `modal-${ Math . floor ( Math . random ( ) * 999999999 ) } ` ;
137
+ }
0 commit comments