@inertiaui/modal-react 1.0.0-beta-4 → 2.0.0-beta.1
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 -2147
- 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 +33 -23
- 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 -625
- 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,319 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useCallback, useMemo, ReactNode, SyntheticEvent, MouseEvent } from 'react'
|
|
2
|
+
import CloseButton from './CloseButton'
|
|
3
|
+
import clsx from 'clsx'
|
|
4
|
+
import { createFocusTrap, onEscapeKey, animate, cancelAnimations } from '@inertiaui/vanilla'
|
|
5
|
+
import { getMaxWidthClass } from './constants'
|
|
6
|
+
import type { Modal } from './types'
|
|
7
|
+
|
|
8
|
+
interface SlideoverContentConfig {
|
|
9
|
+
maxWidth: string
|
|
10
|
+
paddingClasses: string
|
|
11
|
+
panelClasses: string
|
|
12
|
+
position: string
|
|
13
|
+
closeButton: boolean
|
|
14
|
+
closeExplicitly?: boolean
|
|
15
|
+
closeOnClickOutside?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SlideoverContentProps {
|
|
19
|
+
modalContext: Modal
|
|
20
|
+
config: SlideoverContentConfig
|
|
21
|
+
useNativeDialog: boolean
|
|
22
|
+
isFirstModal: boolean
|
|
23
|
+
onAfterLeave?: () => void
|
|
24
|
+
children: ReactNode | ((props: { modalContext: Modal; config: SlideoverContentConfig }) => ReactNode)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SlideoverContent = ({ modalContext, config, useNativeDialog, isFirstModal, onAfterLeave, children }: SlideoverContentProps) => {
|
|
28
|
+
const [isRendered, setIsRendered] = useState(false)
|
|
29
|
+
const [isVisible, setIsVisible] = useState(false) // For backdrop sync
|
|
30
|
+
const [entered, setEntered] = useState(false) // After animation completes
|
|
31
|
+
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
32
|
+
const dialogRef = useRef<HTMLDialogElement>(null)
|
|
33
|
+
const nativeWrapperRef = useRef<HTMLDivElement>(null)
|
|
34
|
+
const cleanupFocusTrapRef = useRef<(() => void) | null>(null)
|
|
35
|
+
const cleanupEscapeKeyRef = useRef<(() => void) | null>(null)
|
|
36
|
+
|
|
37
|
+
const isLeft = config.position === 'left'
|
|
38
|
+
|
|
39
|
+
const maxWidthClass = useMemo(() => getMaxWidthClass(config.maxWidth), [config.maxWidth])
|
|
40
|
+
|
|
41
|
+
// Get translate value based on position
|
|
42
|
+
const getTranslateX = useCallback(() => (isLeft ? '-100%' : '100%'), [isLeft])
|
|
43
|
+
|
|
44
|
+
// ============ Animation handlers using Web Animations API ============
|
|
45
|
+
|
|
46
|
+
const animateIn = useCallback(
|
|
47
|
+
async (element: HTMLElement | null) => {
|
|
48
|
+
if (!element) return
|
|
49
|
+
|
|
50
|
+
setIsVisible(true) // Trigger backdrop immediately
|
|
51
|
+
const translateX = getTranslateX()
|
|
52
|
+
|
|
53
|
+
await animate(element, [
|
|
54
|
+
{ transform: `translate3d(${translateX}, 0, 0)`, opacity: 0 },
|
|
55
|
+
{ transform: 'translate3d(0, 0, 0)', opacity: 1 },
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
setEntered(true)
|
|
59
|
+
},
|
|
60
|
+
[getTranslateX],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const animateOut = useCallback(
|
|
64
|
+
async (element: HTMLElement | null) => {
|
|
65
|
+
if (!element) return
|
|
66
|
+
|
|
67
|
+
setIsVisible(false) // Trigger backdrop fade out immediately
|
|
68
|
+
const translateX = getTranslateX()
|
|
69
|
+
|
|
70
|
+
await animate(element, [
|
|
71
|
+
{ transform: 'translate3d(0, 0, 0)', opacity: 1 },
|
|
72
|
+
{ transform: `translate3d(${translateX}, 0, 0)`, opacity: 0 },
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
setIsRendered(false)
|
|
76
|
+
if (useNativeDialog && dialogRef.current) {
|
|
77
|
+
dialogRef.current.close()
|
|
78
|
+
}
|
|
79
|
+
onAfterLeave?.()
|
|
80
|
+
modalContext.afterLeave()
|
|
81
|
+
},
|
|
82
|
+
[getTranslateX, useNativeDialog, onAfterLeave, modalContext],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// ============ Non-native dialog handlers ============
|
|
86
|
+
|
|
87
|
+
const setupFocusTrap = useCallback(() => {
|
|
88
|
+
if (useNativeDialog) return
|
|
89
|
+
if (!wrapperRef.current || !modalContext.onTopOfStack) return
|
|
90
|
+
if (cleanupFocusTrapRef.current) return
|
|
91
|
+
|
|
92
|
+
cleanupFocusTrapRef.current = createFocusTrap(wrapperRef.current, {
|
|
93
|
+
initialFocus: true,
|
|
94
|
+
returnFocus: false,
|
|
95
|
+
})
|
|
96
|
+
}, [modalContext.onTopOfStack, useNativeDialog])
|
|
97
|
+
|
|
98
|
+
const cleanupFocusTrap = useCallback(() => {
|
|
99
|
+
if (cleanupFocusTrapRef.current) {
|
|
100
|
+
cleanupFocusTrapRef.current()
|
|
101
|
+
cleanupFocusTrapRef.current = null
|
|
102
|
+
}
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
const setupEscapeKey = useCallback(() => {
|
|
106
|
+
if (useNativeDialog) return
|
|
107
|
+
if (cleanupEscapeKeyRef.current) return
|
|
108
|
+
if (config?.closeExplicitly) return
|
|
109
|
+
|
|
110
|
+
cleanupEscapeKeyRef.current = onEscapeKey(() => {
|
|
111
|
+
if (modalContext.onTopOfStack) {
|
|
112
|
+
modalContext.close()
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
}, [config?.closeExplicitly, modalContext, useNativeDialog])
|
|
116
|
+
|
|
117
|
+
const cleanupEscapeKey = useCallback(() => {
|
|
118
|
+
if (cleanupEscapeKeyRef.current) {
|
|
119
|
+
cleanupEscapeKeyRef.current()
|
|
120
|
+
cleanupEscapeKeyRef.current = null
|
|
121
|
+
}
|
|
122
|
+
}, [])
|
|
123
|
+
|
|
124
|
+
const handleClickOutside = useCallback(
|
|
125
|
+
(event: MouseEvent) => {
|
|
126
|
+
if (useNativeDialog) return
|
|
127
|
+
if (!modalContext.onTopOfStack) return
|
|
128
|
+
if (config?.closeExplicitly) return
|
|
129
|
+
if (config?.closeOnClickOutside === false) return
|
|
130
|
+
if (!wrapperRef.current) return
|
|
131
|
+
|
|
132
|
+
if (!wrapperRef.current.contains(event.target as Node)) {
|
|
133
|
+
modalContext.close()
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[modalContext, config?.closeExplicitly, config?.closeOnClickOutside, useNativeDialog],
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// ============ Native dialog handlers ============
|
|
140
|
+
|
|
141
|
+
const handleCancel = useCallback(
|
|
142
|
+
(event: SyntheticEvent) => {
|
|
143
|
+
event.preventDefault()
|
|
144
|
+
if (modalContext.onTopOfStack && !config?.closeExplicitly) {
|
|
145
|
+
modalContext.close()
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
[modalContext, config?.closeExplicitly],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
const handleDialogClick = useCallback(
|
|
152
|
+
(event: MouseEvent) => {
|
|
153
|
+
if (event.target === dialogRef.current) {
|
|
154
|
+
if (modalContext.onTopOfStack && !config?.closeExplicitly && config?.closeOnClickOutside !== false) {
|
|
155
|
+
modalContext.close()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[modalContext, config?.closeExplicitly, config?.closeOnClickOutside],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// ============ Lifecycle ============
|
|
163
|
+
|
|
164
|
+
// Track previous isOpen state for detecting close
|
|
165
|
+
const prevIsOpenRef = useRef(modalContext.isOpen)
|
|
166
|
+
|
|
167
|
+
// Initial mount and open state changes
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (useNativeDialog) {
|
|
170
|
+
if (modalContext.isOpen && !dialogRef.current?.open) {
|
|
171
|
+
dialogRef.current?.showModal()
|
|
172
|
+
animateIn(nativeWrapperRef.current)
|
|
173
|
+
} else if (!modalContext.isOpen && prevIsOpenRef.current) {
|
|
174
|
+
setEntered(false)
|
|
175
|
+
animateOut(nativeWrapperRef.current)
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
if (modalContext.isOpen && !isRendered) {
|
|
179
|
+
setIsRendered(true)
|
|
180
|
+
} else if (!modalContext.isOpen && prevIsOpenRef.current) {
|
|
181
|
+
setEntered(false)
|
|
182
|
+
animateOut(wrapperRef.current)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
prevIsOpenRef.current = modalContext.isOpen
|
|
186
|
+
}, [modalContext.isOpen, useNativeDialog, animateIn, animateOut, isRendered])
|
|
187
|
+
|
|
188
|
+
// Trigger animation after render (non-native)
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
if (!useNativeDialog && isRendered && !entered && modalContext.isOpen) {
|
|
191
|
+
animateIn(wrapperRef.current).then(() => {
|
|
192
|
+
setupFocusTrap()
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
}, [isRendered, useNativeDialog, entered, modalContext.isOpen, animateIn, setupFocusTrap])
|
|
196
|
+
|
|
197
|
+
// Setup escape key (non-native)
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!useNativeDialog) {
|
|
200
|
+
setupEscapeKey()
|
|
201
|
+
}
|
|
202
|
+
return () => {
|
|
203
|
+
cleanupEscapeKey()
|
|
204
|
+
}
|
|
205
|
+
}, [useNativeDialog, setupEscapeKey, cleanupEscapeKey])
|
|
206
|
+
|
|
207
|
+
// Handle becoming top of stack / losing top of stack (non-native only)
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (useNativeDialog) return
|
|
210
|
+
|
|
211
|
+
if (modalContext.onTopOfStack) {
|
|
212
|
+
setupEscapeKey()
|
|
213
|
+
if (entered) {
|
|
214
|
+
setupFocusTrap()
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
cleanupFocusTrap()
|
|
218
|
+
cleanupEscapeKey()
|
|
219
|
+
}
|
|
220
|
+
}, [modalContext.onTopOfStack, entered, setupEscapeKey, setupFocusTrap, cleanupFocusTrap, cleanupEscapeKey, useNativeDialog])
|
|
221
|
+
|
|
222
|
+
// Cleanup on unmount
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
return () => {
|
|
225
|
+
const wrapper = useNativeDialog ? nativeWrapperRef.current : wrapperRef.current
|
|
226
|
+
if (wrapper) {
|
|
227
|
+
cancelAnimations(wrapper)
|
|
228
|
+
}
|
|
229
|
+
if (useNativeDialog) {
|
|
230
|
+
if (dialogRef.current?.open) {
|
|
231
|
+
dialogRef.current.close()
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
cleanupFocusTrap()
|
|
235
|
+
cleanupEscapeKey()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}, [useNativeDialog, cleanupFocusTrap, cleanupEscapeKey])
|
|
239
|
+
|
|
240
|
+
// ============ Render ============
|
|
241
|
+
|
|
242
|
+
const renderContent = () => (
|
|
243
|
+
<div
|
|
244
|
+
className={`im-slideover-content relative ${config.paddingClasses} ${config.panelClasses}`}
|
|
245
|
+
data-inertiaui-modal-entered={entered}
|
|
246
|
+
>
|
|
247
|
+
{config.closeButton && (
|
|
248
|
+
<div className="absolute right-0 top-0 pr-3 pt-3">
|
|
249
|
+
<CloseButton onClick={modalContext.close} />
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
{typeof children === 'function' ? children({ modalContext, config }) : children}
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
// Native dialog mode
|
|
257
|
+
if (useNativeDialog) {
|
|
258
|
+
return (
|
|
259
|
+
<dialog
|
|
260
|
+
ref={dialogRef}
|
|
261
|
+
className={clsx(
|
|
262
|
+
'im-slideover-dialog m-0 overflow-visible bg-transparent p-0',
|
|
263
|
+
'size-full max-h-none max-w-none',
|
|
264
|
+
'backdrop:bg-black/75 backdrop:transition-opacity backdrop:duration-300',
|
|
265
|
+
isVisible ? 'backdrop:opacity-100' : 'backdrop:opacity-0',
|
|
266
|
+
!isFirstModal && 'backdrop:bg-transparent',
|
|
267
|
+
)}
|
|
268
|
+
onCancel={handleCancel}
|
|
269
|
+
onClick={handleDialogClick}
|
|
270
|
+
>
|
|
271
|
+
<div className="im-slideover-container fixed inset-0 overflow-y-auto overflow-x-hidden">
|
|
272
|
+
<div
|
|
273
|
+
className={clsx('im-slideover-positioner flex min-h-full items-center', {
|
|
274
|
+
'justify-start rtl:justify-end': config?.position === 'left',
|
|
275
|
+
'justify-end rtl:justify-start': config?.position === 'right',
|
|
276
|
+
})}
|
|
277
|
+
>
|
|
278
|
+
<div
|
|
279
|
+
ref={nativeWrapperRef}
|
|
280
|
+
className={clsx('im-slideover-wrapper w-full transition-[filter] duration-300', modalContext.onTopOfStack ? '' : 'blur-xs', maxWidthClass)}
|
|
281
|
+
>
|
|
282
|
+
{renderContent()}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</dialog>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Non-native dialog mode
|
|
291
|
+
if (!isRendered) return null
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<div
|
|
295
|
+
className="im-slideover-container fixed inset-0 z-40 overflow-y-auto overflow-x-hidden"
|
|
296
|
+
onMouseDown={handleClickOutside}
|
|
297
|
+
>
|
|
298
|
+
<div
|
|
299
|
+
className={clsx('im-slideover-positioner flex min-h-full items-center', {
|
|
300
|
+
'justify-start rtl:justify-end': config?.position === 'left',
|
|
301
|
+
'justify-end rtl:justify-start': config?.position === 'right',
|
|
302
|
+
})}
|
|
303
|
+
onMouseDown={handleClickOutside}
|
|
304
|
+
>
|
|
305
|
+
<div
|
|
306
|
+
ref={wrapperRef}
|
|
307
|
+
role="dialog"
|
|
308
|
+
aria-modal="true"
|
|
309
|
+
className={clsx('im-slideover-wrapper w-full transition-[filter] duration-300', modalContext.onTopOfStack ? '' : 'blur-xs', maxWidthClass)}
|
|
310
|
+
>
|
|
311
|
+
<span className="sr-only">Dialog</span>
|
|
312
|
+
{renderContent()}
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default SlideoverContent
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
// See: https://github.com/inertiajs/inertia/blob/48bcd21fb7daf467d0df1bfde2408f161f94a579/packages/react/src/WhenVisible.ts
|
|
2
|
-
import { createElement, useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import { createElement, useCallback, useEffect, useRef, useState, ReactNode, ElementType } from 'react'
|
|
3
3
|
import useModal from './useModal'
|
|
4
|
+
import type { ReloadOptions } from './types'
|
|
5
|
+
|
|
6
|
+
interface WhenVisibleProps {
|
|
7
|
+
children: ReactNode
|
|
8
|
+
data?: string | string[]
|
|
9
|
+
params?: ReloadOptions
|
|
10
|
+
buffer?: number
|
|
11
|
+
as?: ElementType
|
|
12
|
+
always?: boolean
|
|
13
|
+
fallback?: ReactNode
|
|
14
|
+
}
|
|
4
15
|
|
|
5
|
-
const WhenVisible = ({ children, data, params, buffer, as, always, fallback }) => {
|
|
16
|
+
const WhenVisible = ({ children, data, params, buffer, as, always, fallback }: WhenVisibleProps) => {
|
|
6
17
|
always = always ?? false
|
|
7
18
|
as = as ?? 'div'
|
|
8
19
|
fallback = fallback ?? null
|
|
@@ -10,11 +21,11 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }) =
|
|
|
10
21
|
const [loaded, setLoaded] = useState(false)
|
|
11
22
|
const hasFetched = useRef(false)
|
|
12
23
|
const fetching = useRef(false)
|
|
13
|
-
const ref = useRef(null)
|
|
24
|
+
const ref = useRef<HTMLElement>(null)
|
|
14
25
|
|
|
15
26
|
const modal = useModal()
|
|
16
27
|
|
|
17
|
-
const getReloadParams = useCallback(() => {
|
|
28
|
+
const getReloadParams = useCallback((): ReloadOptions => {
|
|
18
29
|
if (data) {
|
|
19
30
|
return {
|
|
20
31
|
only: Array.isArray(data) ? data : [data],
|
|
@@ -52,16 +63,16 @@ const WhenVisible = ({ children, data, params, buffer, as, always, fallback }) =
|
|
|
52
63
|
|
|
53
64
|
const reloadParams = getReloadParams()
|
|
54
65
|
|
|
55
|
-
modal
|
|
66
|
+
modal?.reload({
|
|
56
67
|
...reloadParams,
|
|
57
|
-
onStart: (
|
|
68
|
+
onStart: () => {
|
|
58
69
|
fetching.current = true
|
|
59
|
-
reloadParams.onStart?.(
|
|
70
|
+
reloadParams.onStart?.()
|
|
60
71
|
},
|
|
61
|
-
onFinish: (
|
|
72
|
+
onFinish: () => {
|
|
62
73
|
setLoaded(true)
|
|
63
74
|
fetching.current = false
|
|
64
|
-
reloadParams.onFinish?.(
|
|
75
|
+
reloadParams.onFinish?.()
|
|
65
76
|
|
|
66
77
|
if (!always) {
|
|
67
78
|
observer.disconnect()
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export interface ModalTypeConfig {
|
|
2
|
+
closeButton: boolean
|
|
3
|
+
closeExplicitly: boolean
|
|
4
|
+
closeOnClickOutside: boolean
|
|
5
|
+
maxWidth: string
|
|
6
|
+
paddingClasses: string
|
|
7
|
+
panelClasses: string
|
|
8
|
+
position: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ModalConfig {
|
|
12
|
+
type: 'modal' | 'slideover'
|
|
13
|
+
navigate: boolean
|
|
14
|
+
useNativeDialog: boolean
|
|
15
|
+
appElement: string | null
|
|
16
|
+
modal: ModalTypeConfig
|
|
17
|
+
slideover: ModalTypeConfig
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultConfig: ModalConfig = {
|
|
21
|
+
type: 'modal',
|
|
22
|
+
navigate: false,
|
|
23
|
+
useNativeDialog: true,
|
|
24
|
+
appElement: '#app',
|
|
25
|
+
modal: {
|
|
26
|
+
closeButton: true,
|
|
27
|
+
closeExplicitly: false,
|
|
28
|
+
closeOnClickOutside: true,
|
|
29
|
+
maxWidth: '2xl',
|
|
30
|
+
paddingClasses: 'p-4 sm:p-6',
|
|
31
|
+
panelClasses: 'bg-white rounded',
|
|
32
|
+
position: 'center',
|
|
33
|
+
},
|
|
34
|
+
slideover: {
|
|
35
|
+
closeButton: true,
|
|
36
|
+
closeExplicitly: false,
|
|
37
|
+
closeOnClickOutside: true,
|
|
38
|
+
maxWidth: 'md',
|
|
39
|
+
paddingClasses: 'p-4 sm:p-6',
|
|
40
|
+
panelClasses: 'bg-white min-h-screen',
|
|
41
|
+
position: 'right',
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class Config {
|
|
46
|
+
private config: ModalConfig
|
|
47
|
+
|
|
48
|
+
constructor() {
|
|
49
|
+
this.config = {} as ModalConfig
|
|
50
|
+
this.reset()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reset(): void {
|
|
54
|
+
this.config = JSON.parse(JSON.stringify(defaultConfig))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
put(key: string | Partial<ModalConfig>, value?: unknown): void {
|
|
58
|
+
if (typeof key === 'object') {
|
|
59
|
+
this.config = {
|
|
60
|
+
type: key.type ?? defaultConfig.type,
|
|
61
|
+
navigate: key.navigate ?? defaultConfig.navigate,
|
|
62
|
+
useNativeDialog: key.useNativeDialog ?? defaultConfig.useNativeDialog,
|
|
63
|
+
appElement: key.appElement !== undefined ? key.appElement : defaultConfig.appElement,
|
|
64
|
+
modal: { ...defaultConfig.modal, ...(key.modal ?? {}) },
|
|
65
|
+
slideover: { ...defaultConfig.slideover, ...(key.slideover ?? {}) },
|
|
66
|
+
}
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const keys = key.split('.')
|
|
70
|
+
let current: Record<string, unknown> = this.config as unknown as Record<string, unknown>
|
|
71
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
72
|
+
current = (current[keys[i]] = current[keys[i]] || {}) as Record<string, unknown>
|
|
73
|
+
}
|
|
74
|
+
current[keys[keys.length - 1]] = value
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get(key?: string): unknown {
|
|
78
|
+
if (typeof key === 'undefined') {
|
|
79
|
+
return this.config
|
|
80
|
+
}
|
|
81
|
+
const keys = key.split('.')
|
|
82
|
+
let current: unknown = this.config
|
|
83
|
+
for (const k of keys) {
|
|
84
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
current = (current as Record<string, unknown>)[k]
|
|
88
|
+
}
|
|
89
|
+
return current === undefined ? null : current
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const configInstance = new Config()
|
|
94
|
+
|
|
95
|
+
export const resetConfig = (): void => configInstance.reset()
|
|
96
|
+
export const putConfig = (key: string | Partial<ModalConfig>, value?: unknown): void => configInstance.put(key, value)
|
|
97
|
+
export const getConfig = (key?: string): unknown => configInstance.get(key)
|
|
98
|
+
export const getConfigByType = (isSlideover: boolean, key: string): unknown =>
|
|
99
|
+
configInstance.get(isSlideover ? `slideover.${key}` : `modal.${key}`)
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Max width classes for modals and slideovers.
|
|
3
|
+
* Uses a map lookup for Tailwind 4 compatibility (scanner picks up full class strings).
|
|
4
|
+
*/
|
|
5
|
+
export const maxWidthClasses: Record<string, string> = {
|
|
6
|
+
sm: 'sm:max-w-sm',
|
|
7
|
+
md: 'sm:max-w-md',
|
|
8
|
+
lg: 'sm:max-w-md md:max-w-lg',
|
|
9
|
+
xl: 'sm:max-w-md md:max-w-xl',
|
|
10
|
+
'2xl': 'sm:max-w-md md:max-w-xl lg:max-w-2xl',
|
|
11
|
+
'3xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl',
|
|
12
|
+
'4xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-4xl',
|
|
13
|
+
'5xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl',
|
|
14
|
+
'6xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-6xl',
|
|
15
|
+
'7xl': 'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-7xl',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const defaultMaxWidth = '2xl'
|
|
19
|
+
|
|
20
|
+
export function getMaxWidthClass(maxWidth: string): string {
|
|
21
|
+
return maxWidthClasses[maxWidth] || maxWidthClasses[defaultMaxWidth]
|
|
22
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Re-export helper utilities from vanilla
|
|
2
|
+
export { sameUrlPath, except, only, rejectNullValues, kebabCase, isStandardDomEvent } from '@inertiaui/vanilla'
|
|
3
|
+
import { generateId as vanillaGenerateId } from '@inertiaui/vanilla'
|
|
4
|
+
|
|
5
|
+
// Wrap generateId with custom callback support for testing
|
|
6
|
+
let generateIdUsingCallback: (() => string) | null = null
|
|
7
|
+
|
|
8
|
+
export function generateIdUsing(callback: (() => string) | null): void {
|
|
9
|
+
generateIdUsingCallback = callback
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function generateId(prefix = 'inertiaui_'): string {
|
|
13
|
+
if (generateIdUsingCallback) {
|
|
14
|
+
return generateIdUsingCallback()
|
|
15
|
+
}
|
|
16
|
+
return vanillaGenerateId(prefix)
|
|
17
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createElement } from 'react'
|
|
2
|
+
import { getConfig, putConfig, resetConfig } from './config'
|
|
3
|
+
import { useModalIndex } from './ModalRenderer'
|
|
4
|
+
import { useModalStack, ModalRoot, ModalStackProvider, renderApp, initFromPageProps, modalPropNames, prefetch } from './ModalRoot'
|
|
5
|
+
import useModal from './useModal'
|
|
6
|
+
import Deferred from './Deferred'
|
|
7
|
+
import HeadlessModal from './HeadlessModal'
|
|
8
|
+
import Modal from './Modal'
|
|
9
|
+
import ModalLink from './ModalLink'
|
|
10
|
+
import WhenVisible from './WhenVisible'
|
|
11
|
+
import * as dialogUtils from '@inertiaui/vanilla'
|
|
12
|
+
|
|
13
|
+
// Types
|
|
14
|
+
export type {
|
|
15
|
+
Modal as ModalInstance,
|
|
16
|
+
ModalConfig,
|
|
17
|
+
ModalResponseData,
|
|
18
|
+
ModalStackContextValue,
|
|
19
|
+
VisitOptions,
|
|
20
|
+
ReloadOptions,
|
|
21
|
+
EventCallback,
|
|
22
|
+
ComponentResolver,
|
|
23
|
+
PageProps,
|
|
24
|
+
ModalRootProps,
|
|
25
|
+
ModalRendererProps,
|
|
26
|
+
LocalModal,
|
|
27
|
+
// Prefetch types (#146)
|
|
28
|
+
PrefetchOption,
|
|
29
|
+
PrefetchOptions,
|
|
30
|
+
} from './types'
|
|
31
|
+
|
|
32
|
+
export type { ModalTypeConfig } from './config'
|
|
33
|
+
|
|
34
|
+
export type { CleanupFunction, FocusTrapOptions, EscapeKeyOptions } from '@inertiaui/vanilla'
|
|
35
|
+
|
|
36
|
+
const setPageLayout = <T extends { default: { layout?: (page: React.ReactNode) => React.ReactNode } }>(
|
|
37
|
+
layout: React.ComponentType<{ children?: React.ReactNode }>,
|
|
38
|
+
) => (module: T): T => {
|
|
39
|
+
module.default.layout = (page) => createElement(layout, { children: page })
|
|
40
|
+
return module
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
Deferred,
|
|
45
|
+
HeadlessModal,
|
|
46
|
+
Modal,
|
|
47
|
+
ModalLink,
|
|
48
|
+
ModalRoot,
|
|
49
|
+
ModalStackProvider,
|
|
50
|
+
WhenVisible,
|
|
51
|
+
getConfig,
|
|
52
|
+
initFromPageProps,
|
|
53
|
+
modalPropNames,
|
|
54
|
+
putConfig,
|
|
55
|
+
renderApp,
|
|
56
|
+
resetConfig,
|
|
57
|
+
setPageLayout,
|
|
58
|
+
useModal,
|
|
59
|
+
useModalIndex,
|
|
60
|
+
useModalStack,
|
|
61
|
+
// Prefetch function (#146)
|
|
62
|
+
prefetch,
|
|
63
|
+
// Dialog utilities (framework-agnostic)
|
|
64
|
+
dialogUtils,
|
|
65
|
+
}
|