@inertiaui/modal-react 1.0.0-beta-5 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CloseButton.d.ts +5 -0
- package/dist/Deferred.d.ts +11 -0
- package/dist/HeadlessModal.d.ts +64 -0
- package/dist/Modal.d.ts +48 -0
- package/dist/ModalContent.d.ts +24 -0
- package/dist/ModalLink.d.ts +29 -0
- package/dist/ModalRenderer.d.ts +4 -0
- package/dist/ModalRoot.d.ts +22 -0
- package/dist/SlideoverContent.d.ts +24 -0
- package/dist/WhenVisible.d.ts +16 -0
- package/dist/config.d.ts +21 -0
- package/dist/constants.d.ts +7 -0
- package/dist/helpers.d.ts +3 -0
- package/dist/inertiaui-modal.d.ts +2 -0
- package/dist/inertiaui-modal.js +1680 -2167
- package/dist/inertiaui-modal.js.map +1 -0
- package/dist/inertiaui-modal.umd.cjs +1854 -21
- package/dist/inertiaui-modal.umd.cjs.map +1 -0
- package/dist/inertiauiModal.d.ts +21 -0
- package/dist/types.d.ts +111 -0
- package/dist/useModal.d.ts +2 -0
- package/package.json +37 -22
- package/src/{CloseButton.jsx → CloseButton.tsx} +5 -1
- package/src/{Deferred.jsx → Deferred.tsx} +10 -3
- package/src/HeadlessModal.tsx +238 -0
- package/src/Modal.tsx +292 -0
- package/src/ModalContent.tsx +311 -0
- package/src/ModalLink.tsx +224 -0
- package/src/ModalRenderer.tsx +33 -0
- package/src/ModalRoot.tsx +880 -0
- package/src/SlideoverContent.tsx +319 -0
- package/src/{WhenVisible.jsx → WhenVisible.tsx} +20 -9
- package/src/config.ts +99 -0
- package/src/constants.ts +22 -0
- package/src/helpers.ts +17 -0
- package/src/inertiauiModal.ts +65 -0
- package/src/types.ts +150 -0
- package/src/useModal.ts +7 -0
- package/src/HeadlessModal.jsx +0 -143
- package/src/Modal.jsx +0 -136
- package/src/ModalContent.jsx +0 -56
- package/src/ModalLink.jsx +0 -123
- package/src/ModalRenderer.jsx +0 -34
- package/src/ModalRoot.jsx +0 -623
- package/src/SlideoverContent.jsx +0 -55
- package/src/config.js +0 -3
- package/src/helpers.js +0 -2
- package/src/inertiauiModal.js +0 -34
- package/src/useModal.js +0 -6
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useMemo, useState, forwardRef, useImperativeHandle, useEffect, useRef, ReactNode } from 'react'
|
|
2
|
+
import { getConfig, getConfigByType } from './config'
|
|
3
|
+
import { useModalIndex } from './ModalRenderer'
|
|
4
|
+
import { useModalStack } from './ModalRoot'
|
|
5
|
+
import ModalRenderer from './ModalRenderer'
|
|
6
|
+
import type { Modal, ModalConfig, ReloadOptions } from './types'
|
|
7
|
+
|
|
8
|
+
interface HeadlessModalConfig {
|
|
9
|
+
slideover: boolean
|
|
10
|
+
closeButton: boolean
|
|
11
|
+
closeExplicitly: boolean
|
|
12
|
+
closeOnClickOutside: boolean
|
|
13
|
+
maxWidth: string
|
|
14
|
+
paddingClasses: string
|
|
15
|
+
panelClasses: string
|
|
16
|
+
position: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface HeadlessModalRenderProps {
|
|
20
|
+
afterLeave: () => void
|
|
21
|
+
close: () => void
|
|
22
|
+
config: HeadlessModalConfig
|
|
23
|
+
emit: (event: string, ...args: unknown[]) => void
|
|
24
|
+
getChildModal: () => Modal | null
|
|
25
|
+
getParentModal: () => Modal | null
|
|
26
|
+
id: string
|
|
27
|
+
index: number
|
|
28
|
+
isOpen: boolean
|
|
29
|
+
modalContext: Modal
|
|
30
|
+
onTopOfStack: boolean
|
|
31
|
+
reload: (options?: ReloadOptions) => void
|
|
32
|
+
setOpen: (open: boolean) => void
|
|
33
|
+
shouldRender: boolean
|
|
34
|
+
// Allow additional props from visitModal
|
|
35
|
+
[key: string]: unknown
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface HeadlessModalBaseProps {
|
|
39
|
+
name?: string
|
|
40
|
+
children: ReactNode | ((props: HeadlessModalRenderProps) => ReactNode)
|
|
41
|
+
onFocus?: () => void
|
|
42
|
+
onBlur?: () => void
|
|
43
|
+
onClose?: () => void
|
|
44
|
+
onSuccess?: () => void
|
|
45
|
+
slideover?: boolean
|
|
46
|
+
closeButton?: boolean
|
|
47
|
+
closeExplicitly?: boolean
|
|
48
|
+
closeOnClickOutside?: boolean
|
|
49
|
+
maxWidth?: string
|
|
50
|
+
paddingClasses?: string
|
|
51
|
+
panelClasses?: string
|
|
52
|
+
position?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type HeadlessModalProps = HeadlessModalBaseProps & Record<string, unknown>
|
|
56
|
+
|
|
57
|
+
export interface HeadlessModalRef {
|
|
58
|
+
afterLeave: () => void
|
|
59
|
+
close: () => void
|
|
60
|
+
emit: (event: string, ...args: unknown[]) => void
|
|
61
|
+
getChildModal: () => Modal | null | undefined
|
|
62
|
+
getParentModal: () => Modal | null | undefined
|
|
63
|
+
reload: (options?: ReloadOptions) => void
|
|
64
|
+
setOpen: (open: boolean) => void
|
|
65
|
+
readonly id: string | undefined
|
|
66
|
+
readonly index: number | undefined
|
|
67
|
+
readonly isOpen: boolean | undefined
|
|
68
|
+
readonly config: ModalConfig | undefined
|
|
69
|
+
readonly modalContext: Modal | null
|
|
70
|
+
readonly onTopOfStack: boolean | undefined
|
|
71
|
+
readonly shouldRender: boolean | undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const HeadlessModal = forwardRef<HeadlessModalRef, HeadlessModalProps>(
|
|
75
|
+
(allProps, ref) => {
|
|
76
|
+
const { name, children, onFocus, onBlur, onClose, onSuccess, ...props } = allProps as HeadlessModalBaseProps & Record<string, unknown>
|
|
77
|
+
const modalIndex = useModalIndex()
|
|
78
|
+
const { stack, registerLocalModal, removeLocalModal } = useModalStack()
|
|
79
|
+
|
|
80
|
+
const [localModalContext, setLocalModalContext] = useState<Modal | null>(null)
|
|
81
|
+
const modalContext = useMemo(
|
|
82
|
+
() => (name ? localModalContext : stack[modalIndex]),
|
|
83
|
+
[name, localModalContext, modalIndex, stack],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const nextIndex = useMemo(() => {
|
|
87
|
+
return stack.find((m) => m.shouldRender && m.index > (modalContext?.index ?? -1))?.index
|
|
88
|
+
}, [modalIndex, stack])
|
|
89
|
+
|
|
90
|
+
const configSlideover = useMemo(
|
|
91
|
+
() => modalContext?.config.slideover ?? props.slideover ?? getConfig('type') === 'slideover',
|
|
92
|
+
[props.slideover, modalContext?.config.slideover],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const config: HeadlessModalConfig = useMemo(
|
|
96
|
+
() => ({
|
|
97
|
+
slideover: configSlideover as boolean,
|
|
98
|
+
closeButton: (props.closeButton ?? getConfigByType(configSlideover as boolean, 'closeButton')) as boolean,
|
|
99
|
+
closeExplicitly: (props.closeExplicitly ?? getConfigByType(configSlideover as boolean, 'closeExplicitly')) as boolean,
|
|
100
|
+
closeOnClickOutside: (props.closeOnClickOutside ?? getConfigByType(configSlideover as boolean, 'closeOnClickOutside')) as boolean,
|
|
101
|
+
maxWidth: (props.maxWidth ?? getConfigByType(configSlideover as boolean, 'maxWidth')) as string,
|
|
102
|
+
paddingClasses: (props.paddingClasses ?? getConfigByType(configSlideover as boolean, 'paddingClasses')) as string,
|
|
103
|
+
panelClasses: (props.panelClasses ?? getConfigByType(configSlideover as boolean, 'panelClasses')) as string,
|
|
104
|
+
position: (props.position ?? getConfigByType(configSlideover as boolean, 'position')) as string,
|
|
105
|
+
...modalContext?.config,
|
|
106
|
+
}),
|
|
107
|
+
[props, modalContext?.config, configSlideover],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (name) {
|
|
112
|
+
let removeListeners: (() => void) | null = null
|
|
113
|
+
|
|
114
|
+
registerLocalModal(name as string, (localContext) => {
|
|
115
|
+
removeListeners = localContext.registerEventListenersFromProps(props as Record<string, unknown>)
|
|
116
|
+
setLocalModalContext(localContext)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
removeListeners?.()
|
|
121
|
+
removeListeners = null
|
|
122
|
+
removeLocalModal(name as string)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return modalContext?.registerEventListenersFromProps(props as Record<string, unknown>)
|
|
127
|
+
}, [name])
|
|
128
|
+
|
|
129
|
+
// Store the latest modalContext in a ref to maintain reference
|
|
130
|
+
const modalContextRef = useRef(modalContext)
|
|
131
|
+
|
|
132
|
+
// Update the ref whenever modalContext changes
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
modalContextRef.current = modalContext
|
|
135
|
+
}, [modalContext])
|
|
136
|
+
|
|
137
|
+
// Track previous isOpen value to only emit close when transitioning from true to false
|
|
138
|
+
const previousIsOpenRef = useRef<boolean | undefined>(undefined)
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (modalContext !== null) {
|
|
142
|
+
if (modalContext.isOpen) {
|
|
143
|
+
onSuccess?.()
|
|
144
|
+
} else if (previousIsOpenRef.current === true) {
|
|
145
|
+
// Only call onClose when transitioning from open to closed,
|
|
146
|
+
// not when the component first mounts with isOpen undefined/false
|
|
147
|
+
onClose?.()
|
|
148
|
+
}
|
|
149
|
+
previousIsOpenRef.current = modalContext.isOpen
|
|
150
|
+
}
|
|
151
|
+
}, [modalContext?.isOpen])
|
|
152
|
+
|
|
153
|
+
const [rendered, setRendered] = useState(false)
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (rendered && modalContext !== null && modalContext.isOpen) {
|
|
157
|
+
if (modalContext.onTopOfStack) {
|
|
158
|
+
onFocus?.()
|
|
159
|
+
} else {
|
|
160
|
+
onBlur?.()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setRendered(true)
|
|
165
|
+
}, [modalContext?.onTopOfStack])
|
|
166
|
+
|
|
167
|
+
useImperativeHandle(
|
|
168
|
+
ref,
|
|
169
|
+
() => ({
|
|
170
|
+
afterLeave: () => modalContextRef.current?.afterLeave(),
|
|
171
|
+
close: () => modalContextRef.current?.close(),
|
|
172
|
+
emit: (...args: [string, ...unknown[]]) => modalContextRef.current?.emit(...args),
|
|
173
|
+
getChildModal: () => modalContextRef.current?.getChildModal(),
|
|
174
|
+
getParentModal: () => modalContextRef.current?.getParentModal(),
|
|
175
|
+
reload: (options?: ReloadOptions) => modalContextRef.current?.reload(options),
|
|
176
|
+
setOpen: (open: boolean) => modalContextRef.current?.setOpen(open),
|
|
177
|
+
|
|
178
|
+
get id() {
|
|
179
|
+
return modalContextRef.current?.id
|
|
180
|
+
},
|
|
181
|
+
get index() {
|
|
182
|
+
return modalContextRef.current?.index
|
|
183
|
+
},
|
|
184
|
+
get isOpen() {
|
|
185
|
+
return modalContextRef.current?.isOpen
|
|
186
|
+
},
|
|
187
|
+
get config() {
|
|
188
|
+
return modalContextRef.current?.config
|
|
189
|
+
},
|
|
190
|
+
get modalContext() {
|
|
191
|
+
return modalContextRef.current
|
|
192
|
+
},
|
|
193
|
+
get onTopOfStack() {
|
|
194
|
+
return modalContextRef.current?.onTopOfStack
|
|
195
|
+
},
|
|
196
|
+
get shouldRender() {
|
|
197
|
+
return modalContextRef.current?.shouldRender
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
[modalContext],
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if (!modalContext?.shouldRender) {
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<>
|
|
209
|
+
{typeof children === 'function'
|
|
210
|
+
? children({
|
|
211
|
+
// Spread props first so they can be overridden by built-in props
|
|
212
|
+
...modalContext.props,
|
|
213
|
+
afterLeave: modalContext.afterLeave,
|
|
214
|
+
close: modalContext.close,
|
|
215
|
+
config,
|
|
216
|
+
emit: modalContext.emit,
|
|
217
|
+
getChildModal: modalContext.getChildModal,
|
|
218
|
+
getParentModal: modalContext.getParentModal,
|
|
219
|
+
id: modalContext.id,
|
|
220
|
+
index: modalContext.index,
|
|
221
|
+
isOpen: modalContext.isOpen,
|
|
222
|
+
modalContext,
|
|
223
|
+
onTopOfStack: modalContext.onTopOfStack,
|
|
224
|
+
reload: modalContext.reload,
|
|
225
|
+
setOpen: modalContext.setOpen,
|
|
226
|
+
shouldRender: modalContext.shouldRender,
|
|
227
|
+
})
|
|
228
|
+
: children}
|
|
229
|
+
|
|
230
|
+
{/* Next modal in the stack */}
|
|
231
|
+
{nextIndex !== undefined && <ModalRenderer index={nextIndex} />}
|
|
232
|
+
</>
|
|
233
|
+
)
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
HeadlessModal.displayName = 'HeadlessModal'
|
|
238
|
+
export default HeadlessModal
|
package/src/Modal.tsx
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { forwardRef, useRef, useImperativeHandle, useState, useEffect, useCallback, useMemo, ReactNode } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import HeadlessModal, { HeadlessModalRef } from './HeadlessModal'
|
|
4
|
+
import ModalContent from './ModalContent'
|
|
5
|
+
import SlideoverContent from './SlideoverContent'
|
|
6
|
+
import { lockScroll, markAriaHidden } from '@inertiaui/vanilla'
|
|
7
|
+
import { getConfig } from './config'
|
|
8
|
+
import type { Modal as ModalType, ReloadOptions } from './types'
|
|
9
|
+
|
|
10
|
+
interface ModalConfig {
|
|
11
|
+
slideover: boolean
|
|
12
|
+
closeButton: boolean
|
|
13
|
+
closeExplicitly: boolean
|
|
14
|
+
maxWidth: string
|
|
15
|
+
paddingClasses: string
|
|
16
|
+
panelClasses: string
|
|
17
|
+
position: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ModalRenderProps {
|
|
21
|
+
afterLeave: () => void
|
|
22
|
+
close: () => void
|
|
23
|
+
config: ModalConfig
|
|
24
|
+
emit: (event: string, ...args: unknown[]) => void
|
|
25
|
+
getChildModal: () => ModalType | null
|
|
26
|
+
getParentModal: () => ModalType | null
|
|
27
|
+
id: string
|
|
28
|
+
index: number
|
|
29
|
+
isOpen: boolean
|
|
30
|
+
modalContext: ModalType
|
|
31
|
+
onTopOfStack: boolean
|
|
32
|
+
reload: (options?: ReloadOptions) => void
|
|
33
|
+
setOpen: (open: boolean) => void
|
|
34
|
+
shouldRender: boolean
|
|
35
|
+
// Allow additional props from visitModal
|
|
36
|
+
[key: string]: unknown
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ModalBaseProps {
|
|
40
|
+
name?: string
|
|
41
|
+
children: ReactNode | ((props: ModalRenderProps) => ReactNode)
|
|
42
|
+
onFocus?: () => void
|
|
43
|
+
onBlur?: () => void
|
|
44
|
+
onClose?: () => void
|
|
45
|
+
onSuccess?: () => void
|
|
46
|
+
onAfterLeave?: () => void
|
|
47
|
+
slideover?: boolean
|
|
48
|
+
closeButton?: boolean
|
|
49
|
+
closeExplicitly?: boolean
|
|
50
|
+
maxWidth?: string
|
|
51
|
+
paddingClasses?: string
|
|
52
|
+
panelClasses?: string
|
|
53
|
+
position?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ModalProps = ModalBaseProps & Record<string, unknown>
|
|
57
|
+
|
|
58
|
+
interface BackdropTransitionProps {
|
|
59
|
+
show: boolean
|
|
60
|
+
appear: boolean
|
|
61
|
+
onAfterAppear?: () => void
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const Modal = forwardRef<HeadlessModalRef, ModalProps>(
|
|
65
|
+
(allProps, ref) => {
|
|
66
|
+
const { name, children, onFocus, onBlur, onClose, onSuccess, onAfterLeave, ...props } = allProps as ModalBaseProps & Record<string, unknown>
|
|
67
|
+
const renderChildren = (contentProps: ModalRenderProps) => {
|
|
68
|
+
if (typeof children === 'function') {
|
|
69
|
+
return children(contentProps)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return children
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const headlessModalRef = useRef<HeadlessModalRef>(null)
|
|
76
|
+
const cleanupScrollLockRef = useRef<(() => void) | null>(null)
|
|
77
|
+
const cleanupAriaHiddenRef = useRef<(() => void) | null>(null)
|
|
78
|
+
const [rendered, setRendered] = useState(false)
|
|
79
|
+
const useNativeDialog = useMemo(() => getConfig('useNativeDialog') as boolean, [])
|
|
80
|
+
|
|
81
|
+
useImperativeHandle(ref, () => headlessModalRef.current!, [headlessModalRef])
|
|
82
|
+
|
|
83
|
+
// Cleanup on unmount
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
return () => {
|
|
86
|
+
cleanupScrollLockRef.current?.()
|
|
87
|
+
cleanupAriaHiddenRef.current?.()
|
|
88
|
+
}
|
|
89
|
+
}, [])
|
|
90
|
+
|
|
91
|
+
const handleSuccess = useCallback(() => {
|
|
92
|
+
onSuccess?.()
|
|
93
|
+
if (!cleanupScrollLockRef.current) {
|
|
94
|
+
cleanupScrollLockRef.current = lockScroll()
|
|
95
|
+
cleanupAriaHiddenRef.current = markAriaHidden(getConfig('appElement') as string)
|
|
96
|
+
}
|
|
97
|
+
}, [onSuccess])
|
|
98
|
+
|
|
99
|
+
const handleClose = useCallback(() => {
|
|
100
|
+
onClose?.()
|
|
101
|
+
cleanupScrollLockRef.current?.()
|
|
102
|
+
cleanupAriaHiddenRef.current?.()
|
|
103
|
+
cleanupScrollLockRef.current = null
|
|
104
|
+
cleanupAriaHiddenRef.current = null
|
|
105
|
+
}, [onClose])
|
|
106
|
+
|
|
107
|
+
const handleAfterLeave = useCallback(() => {
|
|
108
|
+
onAfterLeave?.()
|
|
109
|
+
}, [onAfterLeave])
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<HeadlessModal
|
|
113
|
+
ref={headlessModalRef}
|
|
114
|
+
name={name}
|
|
115
|
+
onFocus={onFocus ?? undefined}
|
|
116
|
+
onBlur={onBlur ?? undefined}
|
|
117
|
+
onClose={handleClose}
|
|
118
|
+
onSuccess={handleSuccess}
|
|
119
|
+
{...props}
|
|
120
|
+
>
|
|
121
|
+
{({
|
|
122
|
+
afterLeave,
|
|
123
|
+
close,
|
|
124
|
+
config,
|
|
125
|
+
emit,
|
|
126
|
+
getChildModal,
|
|
127
|
+
getParentModal,
|
|
128
|
+
id,
|
|
129
|
+
index,
|
|
130
|
+
isOpen,
|
|
131
|
+
modalContext,
|
|
132
|
+
onTopOfStack,
|
|
133
|
+
reload,
|
|
134
|
+
setOpen,
|
|
135
|
+
shouldRender,
|
|
136
|
+
...extraProps
|
|
137
|
+
}) => (
|
|
138
|
+
<ModalPortal>
|
|
139
|
+
<div
|
|
140
|
+
className="im-dialog relative z-20"
|
|
141
|
+
data-inertiaui-modal-id={id}
|
|
142
|
+
data-inertiaui-modal-index={index}
|
|
143
|
+
aria-hidden={!onTopOfStack}
|
|
144
|
+
>
|
|
145
|
+
{/* Only render backdrop for the first modal (non-native dialog mode) */}
|
|
146
|
+
{/* Native dialog uses ::backdrop pseudo-element instead */}
|
|
147
|
+
{index === 0 && !useNativeDialog && (
|
|
148
|
+
<BackdropTransition
|
|
149
|
+
show={isOpen}
|
|
150
|
+
appear={!rendered}
|
|
151
|
+
onAfterAppear={() => setRendered(true)}
|
|
152
|
+
/>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* The modal/slideover content itself */}
|
|
156
|
+
{config.slideover ? (
|
|
157
|
+
<SlideoverContent
|
|
158
|
+
modalContext={modalContext}
|
|
159
|
+
config={config}
|
|
160
|
+
useNativeDialog={useNativeDialog}
|
|
161
|
+
isFirstModal={index === 0}
|
|
162
|
+
onAfterLeave={handleAfterLeave}
|
|
163
|
+
>
|
|
164
|
+
{renderChildren({
|
|
165
|
+
...extraProps,
|
|
166
|
+
afterLeave,
|
|
167
|
+
close,
|
|
168
|
+
config,
|
|
169
|
+
emit,
|
|
170
|
+
getChildModal,
|
|
171
|
+
getParentModal,
|
|
172
|
+
id,
|
|
173
|
+
index,
|
|
174
|
+
isOpen,
|
|
175
|
+
modalContext,
|
|
176
|
+
onTopOfStack,
|
|
177
|
+
reload,
|
|
178
|
+
setOpen,
|
|
179
|
+
shouldRender,
|
|
180
|
+
})}
|
|
181
|
+
</SlideoverContent>
|
|
182
|
+
) : (
|
|
183
|
+
<ModalContent
|
|
184
|
+
modalContext={modalContext}
|
|
185
|
+
config={config}
|
|
186
|
+
useNativeDialog={useNativeDialog}
|
|
187
|
+
isFirstModal={index === 0}
|
|
188
|
+
onAfterLeave={handleAfterLeave}
|
|
189
|
+
>
|
|
190
|
+
{renderChildren({
|
|
191
|
+
...extraProps,
|
|
192
|
+
afterLeave,
|
|
193
|
+
close,
|
|
194
|
+
config,
|
|
195
|
+
emit,
|
|
196
|
+
getChildModal,
|
|
197
|
+
getParentModal,
|
|
198
|
+
id,
|
|
199
|
+
index,
|
|
200
|
+
isOpen,
|
|
201
|
+
modalContext,
|
|
202
|
+
onTopOfStack,
|
|
203
|
+
reload,
|
|
204
|
+
setOpen,
|
|
205
|
+
shouldRender,
|
|
206
|
+
})}
|
|
207
|
+
</ModalContent>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
</ModalPortal>
|
|
211
|
+
)}
|
|
212
|
+
</HeadlessModal>
|
|
213
|
+
)
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Simple portal component
|
|
218
|
+
function ModalPortal({ children }: { children: ReactNode }) {
|
|
219
|
+
const [mounted, setMounted] = useState(false)
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
setMounted(true)
|
|
223
|
+
}, [])
|
|
224
|
+
|
|
225
|
+
if (!mounted) return null
|
|
226
|
+
|
|
227
|
+
return createPortal(children, document.body)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Backdrop with CSS transition
|
|
231
|
+
function BackdropTransition({ show, appear, onAfterAppear }: BackdropTransitionProps) {
|
|
232
|
+
const [state, setState] = useState<'entering' | 'entered' | 'leaving' | 'exited'>(() => {
|
|
233
|
+
if (appear && show) return 'entering'
|
|
234
|
+
return show ? 'entered' : 'exited'
|
|
235
|
+
})
|
|
236
|
+
const initialRender = useRef(true)
|
|
237
|
+
const backdropRef = useRef<HTMLDivElement>(null)
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (initialRender.current) {
|
|
241
|
+
initialRender.current = false
|
|
242
|
+
if (appear && show) {
|
|
243
|
+
requestAnimationFrame(() => {
|
|
244
|
+
setState('entered')
|
|
245
|
+
const backdrop = backdropRef.current
|
|
246
|
+
if (backdrop) {
|
|
247
|
+
const onTransitionEnd = (e: TransitionEvent) => {
|
|
248
|
+
if (e.target !== backdrop) return
|
|
249
|
+
backdrop.removeEventListener('transitionend', onTransitionEnd)
|
|
250
|
+
onAfterAppear?.()
|
|
251
|
+
}
|
|
252
|
+
backdrop.addEventListener('transitionend', onTransitionEnd)
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (show) {
|
|
260
|
+
setState('entering')
|
|
261
|
+
requestAnimationFrame(() => {
|
|
262
|
+
setState('entered')
|
|
263
|
+
})
|
|
264
|
+
} else {
|
|
265
|
+
setState('leaving')
|
|
266
|
+
const backdrop = backdropRef.current
|
|
267
|
+
if (backdrop) {
|
|
268
|
+
const onTransitionEnd = (e: TransitionEvent) => {
|
|
269
|
+
if (e.target !== backdrop) return
|
|
270
|
+
backdrop.removeEventListener('transitionend', onTransitionEnd)
|
|
271
|
+
setState('exited')
|
|
272
|
+
}
|
|
273
|
+
backdrop.addEventListener('transitionend', onTransitionEnd)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}, [show, appear, onAfterAppear])
|
|
277
|
+
|
|
278
|
+
if (state === 'exited') return null
|
|
279
|
+
|
|
280
|
+
const isVisible = state === 'entered'
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div
|
|
284
|
+
ref={backdropRef}
|
|
285
|
+
className={`im-backdrop fixed inset-0 z-30 bg-black/75 transition-opacity duration-300 ease-in-out ${isVisible ? 'opacity-100' : 'opacity-0'}`}
|
|
286
|
+
aria-hidden="true"
|
|
287
|
+
/>
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
Modal.displayName = 'Modal'
|
|
292
|
+
export default Modal
|