@fy-/fws-vue 2.2.49 → 2.2.51
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.
|
@@ -39,14 +39,23 @@ function showConfirm(data: ConfirmModalData) {
|
|
|
39
39
|
title.value = data.title
|
|
40
40
|
desc.value = data.desc
|
|
41
41
|
onConfirm.value = data.onConfirm
|
|
42
|
-
|
|
42
|
+
|
|
43
|
+
// Emit event first to ensure it's registered before opening the modal
|
|
43
44
|
eventBus.emit('confirmModal', true)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
|
|
46
|
+
// Force this to happen at the end of the event loop
|
|
47
|
+
// to ensure it happens after any other modal operations
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
isOpen.value = true
|
|
50
|
+
eventBus.emit('confirmModal', true)
|
|
51
|
+
|
|
52
|
+
nextTick(() => {
|
|
53
|
+
previouslyFocusedElement = document.activeElement as HTMLElement
|
|
54
|
+
if (modalRef.value) {
|
|
55
|
+
modalRef.value.focus()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}, 0)
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
onMounted(() => {
|
|
@@ -3,8 +3,56 @@ import { XCircleIcon } from '@heroicons/vue/24/solid'
|
|
|
3
3
|
import { h, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
4
4
|
import { useEventBus } from '../../composables/event-bus'
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
|
|
6
|
+
// Use a shared global registry in the window to track all modals across instances
|
|
7
|
+
// This ensures proper z-index stacking even when modals are in different components
|
|
8
|
+
if (typeof window !== 'undefined') {
|
|
9
|
+
// @ts-expect-error: TS doesn't know about the global registry
|
|
10
|
+
window.__FWS_MODAL_REGISTRY = window.__FWS_MODAL_REGISTRY || {
|
|
11
|
+
modals: new Map<string, number>(),
|
|
12
|
+
getNextZIndex() {
|
|
13
|
+
const baseZIndex = 40
|
|
14
|
+
const maxZIndex = 59
|
|
15
|
+
|
|
16
|
+
// If no modals, start at base
|
|
17
|
+
if (this.modals.size === 0) {
|
|
18
|
+
return baseZIndex
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Find highest z-index
|
|
22
|
+
const values = Array.from(this.modals.values())
|
|
23
|
+
// @ts-expect-error: TS doesn't know that values are numbers
|
|
24
|
+
const highestZIndex = Math.max(...values)
|
|
25
|
+
|
|
26
|
+
// Calculate next z-index
|
|
27
|
+
const nextZIndex = highestZIndex + 1
|
|
28
|
+
|
|
29
|
+
// If we're approaching the upper limit, reset all
|
|
30
|
+
if (nextZIndex >= maxZIndex) {
|
|
31
|
+
this.resetAllZIndexes()
|
|
32
|
+
return baseZIndex
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return nextZIndex
|
|
36
|
+
},
|
|
37
|
+
resetAllZIndexes() {
|
|
38
|
+
// Sort by current z-index
|
|
39
|
+
const entries = Array.from(this.modals.entries())
|
|
40
|
+
// @ts-expect-error: TS doesn't know that entries are tuples
|
|
41
|
+
entries.sort((a, b) => a[1] - b[1])
|
|
42
|
+
|
|
43
|
+
// Reassign starting from base
|
|
44
|
+
let newIndex = 40
|
|
45
|
+
// @ts-expect-error: TS doesn't know that entries are tuples
|
|
46
|
+
entries.forEach(([id, _]) => {
|
|
47
|
+
this.modals.set(id, newIndex)
|
|
48
|
+
newIndex++
|
|
49
|
+
})
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// @ts-expect-error: TS doesn't know about the global registry
|
|
55
|
+
const modalRegistry = typeof window !== 'undefined' ? window.__FWS_MODAL_REGISTRY : { modals: new Map() }
|
|
8
56
|
|
|
9
57
|
const props = withDefaults(
|
|
10
58
|
defineProps<{
|
|
@@ -31,7 +79,8 @@ let previouslyFocusedElement: HTMLElement | null = null
|
|
|
31
79
|
let focusableElements: HTMLElement[] = []
|
|
32
80
|
|
|
33
81
|
// Dynamic z-index to ensure the most recently opened modal is on top
|
|
34
|
-
|
|
82
|
+
// Base z-index between 40 and 60 as required
|
|
83
|
+
const baseZIndex = 40 // Starting z-index value
|
|
35
84
|
const zIndex = ref<number>(baseZIndex)
|
|
36
85
|
|
|
37
86
|
// Trap focus within modal for accessibility
|
|
@@ -49,10 +98,9 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
49
98
|
// Only handle events for the top-most modal
|
|
50
99
|
if (!isOpen.value) return
|
|
51
100
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (activeModals.length > 0 && modalRef.value !== activeModals[activeModals.length - 1]) {
|
|
101
|
+
// Check if this modal is the top-most one
|
|
102
|
+
const isTopMost = isTopMostModal(props.id)
|
|
103
|
+
if (!isTopMost) {
|
|
56
104
|
return
|
|
57
105
|
}
|
|
58
106
|
|
|
@@ -78,18 +126,46 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
78
126
|
}
|
|
79
127
|
}
|
|
80
128
|
|
|
129
|
+
// Check if this modal is the top-most (highest z-index)
|
|
130
|
+
function isTopMostModal(id: string): boolean {
|
|
131
|
+
if (modalRegistry.modals.size === 0) return false
|
|
132
|
+
|
|
133
|
+
// Find the modal with the highest z-index
|
|
134
|
+
const entries = Array.from(modalRegistry.modals.entries())
|
|
135
|
+
const highestEntry = entries.reduce((prev, current) =>
|
|
136
|
+
// @ts-expect-error: TS doesn't know that entries are tuples
|
|
137
|
+
current[1] > prev[1] ? current : prev,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// @ts-expect-error: TS doesn't know that entries are tuples
|
|
141
|
+
return highestEntry[0] === id
|
|
142
|
+
}
|
|
143
|
+
|
|
81
144
|
function setModal(value: boolean) {
|
|
82
145
|
if (value === true) {
|
|
83
146
|
if (props.onOpen) props.onOpen()
|
|
84
147
|
previouslyFocusedElement = document.activeElement as HTMLElement
|
|
85
148
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
149
|
+
// Get the next z-index from the global registry
|
|
150
|
+
const newZIndex = modalRegistry.getNextZIndex()
|
|
151
|
+
|
|
152
|
+
// Register this modal in the global registry with a unique ID (combines component id with instance id)
|
|
153
|
+
const uniqueId = `${props.id}-${Date.now()}`
|
|
154
|
+
modalRegistry.modals.set(uniqueId, newZIndex)
|
|
155
|
+
|
|
156
|
+
// Store the unique ID as a data attribute for future reference
|
|
157
|
+
nextTick(() => {
|
|
158
|
+
const modalElement = document.querySelector(`[data-modal-id="${props.id}"]`) as HTMLElement
|
|
159
|
+
if (modalElement) {
|
|
160
|
+
modalElement.setAttribute('data-modal-unique-id', uniqueId)
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// Set this modal's z-index
|
|
165
|
+
zIndex.value = newZIndex
|
|
89
166
|
|
|
90
167
|
// Only manage body overflow for the first opened modal
|
|
91
|
-
|
|
92
|
-
if (activeModals.length === 0) {
|
|
168
|
+
if (modalRegistry.modals.size === 1) {
|
|
93
169
|
document.body.style.overflow = 'hidden' // Prevent scrolling when modal is open
|
|
94
170
|
}
|
|
95
171
|
|
|
@@ -98,9 +174,17 @@ function setModal(value: boolean) {
|
|
|
98
174
|
if (value === false) {
|
|
99
175
|
if (props.onClose) props.onClose()
|
|
100
176
|
|
|
177
|
+
// Find and remove this modal from the registry
|
|
178
|
+
const modalElement = document.querySelector(`[data-modal-id="${props.id}"]`) as HTMLElement
|
|
179
|
+
if (modalElement) {
|
|
180
|
+
const uniqueId = modalElement.getAttribute('data-modal-unique-id')
|
|
181
|
+
if (uniqueId) {
|
|
182
|
+
modalRegistry.modals.delete(uniqueId)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
101
186
|
// Only restore body overflow if this is the last open modal
|
|
102
|
-
|
|
103
|
-
if (activeModals.length <= 1) {
|
|
187
|
+
if (modalRegistry.modals.size === 0) {
|
|
104
188
|
document.body.style.overflow = '' // Restore scrolling
|
|
105
189
|
}
|
|
106
190
|
|
|
@@ -112,6 +196,8 @@ function setModal(value: boolean) {
|
|
|
112
196
|
isOpen.value = value
|
|
113
197
|
}
|
|
114
198
|
|
|
199
|
+
// These functions have been moved to the global registry object
|
|
200
|
+
|
|
115
201
|
// After modal is opened, set focus and collect focusable elements
|
|
116
202
|
watch(isOpen, async (newVal) => {
|
|
117
203
|
if (newVal) {
|
|
@@ -143,10 +229,18 @@ onUnmounted(() => {
|
|
|
143
229
|
eventBus.off(`${props.id}Modal`, setModal)
|
|
144
230
|
document.removeEventListener('keydown', handleKeyDown)
|
|
145
231
|
|
|
146
|
-
//
|
|
232
|
+
// Clean up the modal registry if this modal was open when unmounted
|
|
147
233
|
if (isOpen.value) {
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
234
|
+
const modalElement = document.querySelector(`[data-modal-id="${props.id}"]`) as HTMLElement
|
|
235
|
+
if (modalElement) {
|
|
236
|
+
const uniqueId = modalElement.getAttribute('data-modal-unique-id')
|
|
237
|
+
if (uniqueId) {
|
|
238
|
+
modalRegistry.modals.delete(uniqueId)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Only restore body overflow if this is the last open modal
|
|
243
|
+
if (modalRegistry.modals.size === 0) {
|
|
150
244
|
document.body.style.overflow = '' // Restore scrolling
|
|
151
245
|
}
|
|
152
246
|
}
|
|
@@ -162,63 +256,66 @@ function handleBackdropClick(event: MouseEvent) {
|
|
|
162
256
|
</script>
|
|
163
257
|
|
|
164
258
|
<template>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
<div
|
|
174
|
-
v-if="isOpen"
|
|
175
|
-
class="fixed inset-0 overflow-y-auto"
|
|
176
|
-
:style="{ zIndex }"
|
|
177
|
-
role="dialog"
|
|
178
|
-
:aria-labelledby="title ? `${props.id}-title` : undefined"
|
|
179
|
-
aria-modal="true"
|
|
180
|
-
data-modal-active="true"
|
|
259
|
+
<ClientOnly>
|
|
260
|
+
<transition
|
|
261
|
+
enter-active-class="duration-300 ease-out"
|
|
262
|
+
enter-from-class="opacity-0"
|
|
263
|
+
enter-to-class="opacity-100"
|
|
264
|
+
leave-active-class="duration-200 ease-in"
|
|
265
|
+
leave-from-class="opacity-100"
|
|
266
|
+
leave-to-class="opacity-0"
|
|
181
267
|
>
|
|
182
|
-
<!-- Backdrop with click to close functionality -->
|
|
183
268
|
<div
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
269
|
+
v-if="isOpen"
|
|
270
|
+
class="fixed inset-0 overflow-y-auto"
|
|
271
|
+
:style="{ zIndex }"
|
|
272
|
+
role="dialog"
|
|
273
|
+
:aria-labelledby="title ? `${props.id}-title` : undefined"
|
|
274
|
+
aria-modal="true"
|
|
275
|
+
data-modal-active="true"
|
|
276
|
+
:data-modal-id="props.id"
|
|
187
277
|
>
|
|
188
|
-
<!--
|
|
278
|
+
<!-- Backdrop with click to close functionality -->
|
|
189
279
|
<div
|
|
190
|
-
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
tabindex="-1"
|
|
194
|
-
@click.stop
|
|
280
|
+
class="flex absolute backdrop-blur-[8px] inset-0 flex-col items-center justify-center min-h-screen text-fv-neutral-800 dark:text-fv-neutral-300 bg-fv-neutral-900/[.20] dark:bg-fv-neutral-50/[.20]"
|
|
281
|
+
:style="{ zIndex }"
|
|
282
|
+
@click="handleBackdropClick"
|
|
195
283
|
>
|
|
196
|
-
<!--
|
|
284
|
+
<!-- Modal panel -->
|
|
197
285
|
<div
|
|
198
|
-
|
|
199
|
-
class="
|
|
286
|
+
ref="modalRef"
|
|
287
|
+
:class="`relative ${mSize} max-w-6xl max-h-full ${ofy} bg-white rounded-lg shadow dark:bg-fv-neutral-900`"
|
|
288
|
+
:style="{ zIndex }"
|
|
289
|
+
tabindex="-1"
|
|
290
|
+
@click.stop
|
|
200
291
|
>
|
|
201
|
-
|
|
202
|
-
<
|
|
292
|
+
<!-- Header with title if provided -->
|
|
293
|
+
<div
|
|
203
294
|
v-if="title"
|
|
204
|
-
|
|
205
|
-
class="text-xl font-semibold text-fv-neutral-900 dark:text-white"
|
|
206
|
-
v-html="title"
|
|
207
|
-
/>
|
|
208
|
-
<button
|
|
209
|
-
class="text-fv-neutral-400 bg-transparent hover:bg-fv-neutral-200 hover:text-fv-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center dark:hover:bg-fv-neutral-600 dark:hover:text-white"
|
|
210
|
-
aria-label="Close modal"
|
|
211
|
-
@click="setModal(false)"
|
|
295
|
+
class="flex items-center justify-between p-2 w-full border-b rounded-t dark:border-fv-neutral-700"
|
|
212
296
|
>
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
297
|
+
<slot name="before" />
|
|
298
|
+
<h2
|
|
299
|
+
v-if="title"
|
|
300
|
+
:id="`${props.id}-title`"
|
|
301
|
+
class="text-xl font-semibold text-fv-neutral-900 dark:text-white"
|
|
302
|
+
v-html="title"
|
|
303
|
+
/>
|
|
304
|
+
<button
|
|
305
|
+
class="text-fv-neutral-400 bg-transparent hover:bg-fv-neutral-200 hover:text-fv-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center dark:hover:bg-fv-neutral-600 dark:hover:text-white"
|
|
306
|
+
aria-label="Close modal"
|
|
307
|
+
@click="setModal(false)"
|
|
308
|
+
>
|
|
309
|
+
<component :is="closeIcon" class="w-7 h-7" />
|
|
310
|
+
</button>
|
|
311
|
+
</div>
|
|
312
|
+
<!-- Content area -->
|
|
313
|
+
<div class="p-3 space-y-3">
|
|
314
|
+
<slot />
|
|
315
|
+
</div>
|
|
219
316
|
</div>
|
|
220
317
|
</div>
|
|
221
318
|
</div>
|
|
222
|
-
</
|
|
223
|
-
</
|
|
319
|
+
</transition>
|
|
320
|
+
</ClientOnly>
|
|
224
321
|
</template>
|