@inertiaui/modal-react 0.16.0 → 0.17.0
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/inertiaui-modal.js +1899 -1876
- package/dist/inertiaui-modal.umd.cjs +20 -13
- package/package.json +4 -5
- package/src/HeadlessModal.jsx +17 -1
- package/src/Modal.jsx +19 -7
- package/src/ModalContent.jsx +38 -15
- package/src/ModalRoot.jsx +46 -36
- package/src/SlideoverContent.jsx +40 -17
- package/src/focusTrapper.js +23 -0
- package/src/helpers.js +2 -2
package/src/ModalContent.jsx
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
import { TransitionChild
|
|
1
|
+
import { TransitionChild } from '@headlessui/react'
|
|
2
2
|
import CloseButton from './CloseButton'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
|
+
import { focusTrapper } from './focusTrapper'
|
|
5
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
6
|
|
|
5
7
|
const ModalContent = ({ modalContext, config, children }) => {
|
|
8
|
+
const [entered, setEntered] = useState(false)
|
|
9
|
+
const wrapper = useRef(null)
|
|
10
|
+
const [focusTrap, setFocusTrap] = useState(null)
|
|
11
|
+
|
|
12
|
+
function afterEnter() {
|
|
13
|
+
setFocusTrap(focusTrapper(wrapper.current, config?.closeExplicitly, () => modalContext.close()))
|
|
14
|
+
setEntered(true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
useEffect(() => () => focusTrap?.deactivate(), [focusTrap])
|
|
18
|
+
|
|
6
19
|
return (
|
|
7
20
|
<div className="im-modal-container fixed inset-0 z-40 overflow-y-auto p-4">
|
|
8
21
|
<div
|
|
@@ -13,32 +26,42 @@ const ModalContent = ({ modalContext, config, children }) => {
|
|
|
13
26
|
})}
|
|
14
27
|
>
|
|
15
28
|
<TransitionChild
|
|
29
|
+
as="div"
|
|
30
|
+
ref={wrapper}
|
|
16
31
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
17
32
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
|
18
33
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
|
19
34
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
|
35
|
+
afterEnter={afterEnter}
|
|
20
36
|
afterLeave={modalContext.afterLeave}
|
|
21
|
-
className={clsx(
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
className={clsx(
|
|
38
|
+
'im-modal-wrapper pointer-events-auto w-full transition duration-300 ease-in-out',
|
|
39
|
+
modalContext.onTopOfStack ? '' : 'blur-sm',
|
|
40
|
+
{
|
|
41
|
+
'sm:max-w-sm': config.maxWidth === 'sm',
|
|
42
|
+
'sm:max-w-md': config.maxWidth === 'md',
|
|
43
|
+
'sm:max-w-md md:max-w-lg': config.maxWidth === 'lg',
|
|
44
|
+
'sm:max-w-md md:max-w-xl': config.maxWidth === 'xl',
|
|
45
|
+
'sm:max-w-md md:max-w-xl lg:max-w-2xl': config.maxWidth === '2xl',
|
|
46
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl': config.maxWidth === '3xl',
|
|
47
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-4xl': config.maxWidth === '4xl',
|
|
48
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl': config.maxWidth === '5xl',
|
|
49
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-6xl': config.maxWidth === '6xl',
|
|
50
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-7xl': config.maxWidth === '7xl',
|
|
51
|
+
},
|
|
52
|
+
)}
|
|
33
53
|
>
|
|
34
|
-
<
|
|
54
|
+
<div
|
|
55
|
+
className={`im-modal-content relative ${config.paddingClasses} ${config.panelClasses}`}
|
|
56
|
+
data-inertiaui-modal-entered={entered}
|
|
57
|
+
>
|
|
35
58
|
{config.closeButton && (
|
|
36
59
|
<div className="absolute right-0 top-0 pr-3 pt-3">
|
|
37
60
|
<CloseButton onClick={modalContext.close} />
|
|
38
61
|
</div>
|
|
39
62
|
)}
|
|
40
63
|
{typeof children === 'function' ? children({ modalContext, config }) : children}
|
|
41
|
-
</
|
|
64
|
+
</div>
|
|
42
65
|
</TransitionChild>
|
|
43
66
|
</div>
|
|
44
67
|
</div>
|
package/src/ModalRoot.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createElement, useEffect, useState, useRef } from 'react'
|
|
2
2
|
import { default as Axios } from 'axios'
|
|
3
|
-
import { except, only, kebabCase } from './helpers'
|
|
3
|
+
import { except, only, kebabCase, generateId } from './helpers'
|
|
4
4
|
import { router, usePage } from '@inertiajs/react'
|
|
5
5
|
import { mergeDataIntoQueryString } from '@inertiajs/core'
|
|
6
6
|
import { createContext, useContext } from 'react'
|
|
@@ -16,6 +16,7 @@ let resolveComponent = null
|
|
|
16
16
|
let baseUrl = null
|
|
17
17
|
let newModalOnBase = null
|
|
18
18
|
let localStackCopy = []
|
|
19
|
+
let pendingModalUpdates = {}
|
|
19
20
|
|
|
20
21
|
export const ModalStackProvider = ({ children }) => {
|
|
21
22
|
const [stack, setStack] = useState([])
|
|
@@ -73,7 +74,7 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
73
74
|
|
|
74
75
|
class Modal {
|
|
75
76
|
constructor(component, response, config, onClose, afterLeave) {
|
|
76
|
-
this.id =
|
|
77
|
+
this.id = response.id ?? generateId()
|
|
77
78
|
this.isOpen = false
|
|
78
79
|
this.shouldRender = false
|
|
79
80
|
this.listeners = {}
|
|
@@ -81,10 +82,40 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
81
82
|
this.component = component
|
|
82
83
|
this.props = response.props
|
|
83
84
|
this.response = response
|
|
84
|
-
this.config = config
|
|
85
|
+
this.config = config ?? {}
|
|
85
86
|
this.onCloseCallback = onClose
|
|
86
87
|
this.afterLeaveCallback = afterLeave
|
|
87
88
|
|
|
89
|
+
if (pendingModalUpdates[this.id]) {
|
|
90
|
+
this.config = {
|
|
91
|
+
...this.config,
|
|
92
|
+
...(pendingModalUpdates[this.id].config ?? {}),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pendingOnClose = pendingModalUpdates[this.id].onClose
|
|
96
|
+
const pendingOnAfterLeave = pendingModalUpdates[this.id].onAfterLeave
|
|
97
|
+
|
|
98
|
+
if (pendingOnClose) {
|
|
99
|
+
this.onCloseCallback = onClose
|
|
100
|
+
? () => {
|
|
101
|
+
onClose()
|
|
102
|
+
pendingOnClose()
|
|
103
|
+
}
|
|
104
|
+
: pendingOnClose
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (pendingOnAfterLeave) {
|
|
108
|
+
this.afterLeaveCallback = afterLeave
|
|
109
|
+
? () => {
|
|
110
|
+
afterLeave()
|
|
111
|
+
pendingOnAfterLeave()
|
|
112
|
+
}
|
|
113
|
+
: pendingOnAfterLeave
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
delete pendingModalUpdates[this.id]
|
|
117
|
+
}
|
|
118
|
+
|
|
88
119
|
this.index = -1 // Will be set when added to the stack
|
|
89
120
|
this.getParentModal = () => null // Will be set in push()
|
|
90
121
|
this.getChildModal = () => null // Will be set in push()
|
|
@@ -99,19 +130,6 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
99
130
|
return `inertiaui_modal_${Date.now().toString(36)}_${Math.random().toString(36).substr(2, 9)}`
|
|
100
131
|
}
|
|
101
132
|
|
|
102
|
-
update = (config, onClose, afterLeave) => {
|
|
103
|
-
updateStack((prevStack) =>
|
|
104
|
-
prevStack.map((modal) => {
|
|
105
|
-
if (modal.id === this.id) {
|
|
106
|
-
modal.config = config
|
|
107
|
-
modal.onCloseCallback = onClose
|
|
108
|
-
modal.afterLeaveCallback = afterLeave
|
|
109
|
-
}
|
|
110
|
-
return modal
|
|
111
|
-
}),
|
|
112
|
-
)
|
|
113
|
-
}
|
|
114
|
-
|
|
115
133
|
show = () => {
|
|
116
134
|
updateStack((prevStack) =>
|
|
117
135
|
prevStack.map((modal) => {
|
|
@@ -219,7 +237,7 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
219
237
|
'X-Inertia-Partial-Component': this.response.component,
|
|
220
238
|
'X-Inertia-Version': this.response.version,
|
|
221
239
|
'X-Inertia-Partial-Data': keys.join(','),
|
|
222
|
-
'X-InertiaUI-Modal':
|
|
240
|
+
'X-InertiaUI-Modal': generateId(),
|
|
223
241
|
'X-InertiaUI-Modal-Use-Router': 0,
|
|
224
242
|
'X-InertiaUI-Modal-Base-Url': baseUrl,
|
|
225
243
|
},
|
|
@@ -294,6 +312,8 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
294
312
|
queryStringArrayFormat = 'brackets',
|
|
295
313
|
useBrowserHistory = false,
|
|
296
314
|
) => {
|
|
315
|
+
const modalId = generateId()
|
|
316
|
+
|
|
297
317
|
return new Promise((resolve, reject) => {
|
|
298
318
|
if (href.startsWith('#')) {
|
|
299
319
|
resolve(pushLocalModal(href.substring(1), config, onClose, onAfterLeave))
|
|
@@ -314,13 +334,20 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
314
334
|
'X-Requested-With': 'XMLHttpRequest',
|
|
315
335
|
'X-Inertia': true,
|
|
316
336
|
'X-Inertia-Version': pageVersion,
|
|
317
|
-
'X-InertiaUI-Modal':
|
|
337
|
+
'X-InertiaUI-Modal': modalId,
|
|
318
338
|
'X-InertiaUI-Modal-Use-Router': useInertiaRouter ? 1 : 0,
|
|
319
339
|
'X-InertiaUI-Modal-Base-Url': baseUrl,
|
|
320
340
|
}
|
|
321
341
|
|
|
322
342
|
if (useInertiaRouter) {
|
|
323
343
|
newModalOnBase = null
|
|
344
|
+
|
|
345
|
+
pendingModalUpdates[modalId] = {
|
|
346
|
+
config,
|
|
347
|
+
onClose,
|
|
348
|
+
onAfterLeave,
|
|
349
|
+
}
|
|
350
|
+
|
|
324
351
|
// Pushing the modal to the stack will be handled by the ModalRoot...
|
|
325
352
|
return router.visit(url, {
|
|
326
353
|
method,
|
|
@@ -330,24 +357,7 @@ export const ModalStackProvider = ({ children }) => {
|
|
|
330
357
|
preserveState: true,
|
|
331
358
|
onError: reject,
|
|
332
359
|
onFinish: () => {
|
|
333
|
-
waitFor(() => newModalOnBase).then(
|
|
334
|
-
const originalOnClose = modal.onCloseCallback
|
|
335
|
-
const originalAfterLeave = modal.afterLeaveCallback
|
|
336
|
-
|
|
337
|
-
modal.update(
|
|
338
|
-
config,
|
|
339
|
-
() => {
|
|
340
|
-
onClose?.()
|
|
341
|
-
originalOnClose?.()
|
|
342
|
-
},
|
|
343
|
-
() => {
|
|
344
|
-
onAfterLeave?.()
|
|
345
|
-
originalAfterLeave?.()
|
|
346
|
-
},
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
resolve(modal)
|
|
350
|
-
})
|
|
360
|
+
waitFor(() => newModalOnBase).then(resolve)
|
|
351
361
|
},
|
|
352
362
|
})
|
|
353
363
|
}
|
package/src/SlideoverContent.jsx
CHANGED
|
@@ -1,43 +1,66 @@
|
|
|
1
|
-
import { TransitionChild
|
|
1
|
+
import { TransitionChild } from '@headlessui/react'
|
|
2
2
|
import CloseButton from './CloseButton'
|
|
3
3
|
import clsx from 'clsx'
|
|
4
|
+
import { focusTrapper } from './focusTrapper'
|
|
5
|
+
import { useEffect, useRef, useState } from 'react'
|
|
4
6
|
|
|
5
7
|
const SlideoverContent = ({ modalContext, config, children }) => {
|
|
8
|
+
const [entered, setEntered] = useState(false)
|
|
9
|
+
const wrapper = useRef(null)
|
|
10
|
+
const [focusTrap, setFocusTrap] = useState(null)
|
|
11
|
+
|
|
12
|
+
function afterEnter() {
|
|
13
|
+
setFocusTrap(focusTrapper(wrapper.current, config?.closeExplicitly, () => modalContext.close()))
|
|
14
|
+
setEntered(true)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
useEffect(() => () => focusTrap?.deactivate(), [focusTrap])
|
|
18
|
+
|
|
6
19
|
return (
|
|
7
20
|
<div className="im-slideover-container fixed inset-0 z-40 overflow-y-auto overflow-x-hidden">
|
|
8
21
|
<div
|
|
9
22
|
className={clsx('im-slideover-positioner flex min-h-full items-center', {
|
|
10
|
-
'justify-start': config
|
|
11
|
-
'justify-end': config
|
|
23
|
+
'justify-start rtl:justify-end': config?.position === 'left',
|
|
24
|
+
'justify-end rtl:justify-start': config?.position === 'right',
|
|
12
25
|
})}
|
|
13
26
|
>
|
|
14
27
|
<TransitionChild
|
|
28
|
+
as="div"
|
|
29
|
+
ref={wrapper}
|
|
15
30
|
enterFrom={`opacity-0 ${config.position === 'left' ? '-translate-x-full' : 'translate-x-full'}`}
|
|
16
31
|
enterTo="opacity-100 translate-x-0"
|
|
17
32
|
leaveFrom="opacity-100 translate-x-0"
|
|
18
33
|
leaveTo={`opacity-0 ${config.position === 'left' ? '-translate-x-full' : 'translate-x-full'}`}
|
|
34
|
+
afterEnter={afterEnter}
|
|
19
35
|
afterLeave={modalContext.afterLeave}
|
|
20
|
-
className={clsx(
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
className={clsx(
|
|
37
|
+
'im-slideover-wrapper pointer-events-auto w-full transition duration-300 ease-in-out',
|
|
38
|
+
modalContext.onTopOfStack ? '' : 'blur-sm',
|
|
39
|
+
{
|
|
40
|
+
'sm:max-w-sm': config.maxWidth === 'sm',
|
|
41
|
+
'sm:max-w-md': config.maxWidth === 'md',
|
|
42
|
+
'sm:max-w-md md:max-w-lg': config.maxWidth === 'lg',
|
|
43
|
+
'sm:max-w-md md:max-w-xl': config.maxWidth === 'xl',
|
|
44
|
+
'sm:max-w-md md:max-w-xl lg:max-w-2xl': config.maxWidth === '2xl',
|
|
45
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl': config.maxWidth === '3xl',
|
|
46
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-4xl': config.maxWidth === '4xl',
|
|
47
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl': config.maxWidth === '5xl',
|
|
48
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-6xl': config.maxWidth === '6xl',
|
|
49
|
+
'sm:max-w-md md:max-w-xl lg:max-w-3xl xl:max-w-5xl 2xl:max-w-7xl': config.maxWidth === '7xl',
|
|
50
|
+
},
|
|
51
|
+
)}
|
|
32
52
|
>
|
|
33
|
-
<
|
|
53
|
+
<div
|
|
54
|
+
className={`im-slideover-content relative ${config.paddingClasses} ${config.panelClasses}`}
|
|
55
|
+
data-inertiaui-modal-entered={entered}
|
|
56
|
+
>
|
|
34
57
|
{config.closeButton && (
|
|
35
58
|
<div className="absolute right-0 top-0 pr-3 pt-3">
|
|
36
59
|
<CloseButton onClick={modalContext.close} />
|
|
37
60
|
</div>
|
|
38
61
|
)}
|
|
39
62
|
{typeof children === 'function' ? children({ modalContext, config }) : children}
|
|
40
|
-
</
|
|
63
|
+
</div>
|
|
41
64
|
</TransitionChild>
|
|
42
65
|
</div>
|
|
43
66
|
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createFocusTrap } from 'focus-trap'
|
|
2
|
+
|
|
3
|
+
export function focusTrapper(wrapper, closeExplicitly, onDeactivateCallback) {
|
|
4
|
+
let trap = null
|
|
5
|
+
|
|
6
|
+
if (wrapper) {
|
|
7
|
+
trap = createFocusTrap(wrapper, {
|
|
8
|
+
clickOutsideDeactivates: !closeExplicitly,
|
|
9
|
+
escapeDeactivates: !closeExplicitly,
|
|
10
|
+
onDeactivate: () => onDeactivateCallback?.(),
|
|
11
|
+
fallbackFocus: () => wrapper,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
trap.activate()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const deactivate = () => {
|
|
18
|
+
trap?.deactivate()
|
|
19
|
+
trap = null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { deactivate, wrapper }
|
|
23
|
+
}
|
package/src/helpers.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { except, only, rejectNullValues, waitFor, kebabCase } from './../../vue/src/helpers.js'
|
|
2
|
-
export { except, only, rejectNullValues, waitFor, kebabCase }
|
|
1
|
+
import { modalDOMHandler, generateId, except, only, rejectNullValues, waitFor, kebabCase } from './../../vue/src/helpers.js'
|
|
2
|
+
export { modalDOMHandler, generateId, except, only, rejectNullValues, waitFor, kebabCase }
|