@fy-/fws-vue 2.2.49 → 2.2.50
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/components/ui/DefaultModal.vue +126 -61
- package/package.json +1 -1
|
@@ -3,8 +3,8 @@ 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
|
+
// Global registry to track all open modals and their z-indexes
|
|
7
|
+
const openModals: Map<string, number> = new Map()
|
|
8
8
|
|
|
9
9
|
const props = withDefaults(
|
|
10
10
|
defineProps<{
|
|
@@ -31,7 +31,8 @@ let previouslyFocusedElement: HTMLElement | null = null
|
|
|
31
31
|
let focusableElements: HTMLElement[] = []
|
|
32
32
|
|
|
33
33
|
// Dynamic z-index to ensure the most recently opened modal is on top
|
|
34
|
-
|
|
34
|
+
// Base z-index between 40 and 60 as required
|
|
35
|
+
const baseZIndex = 40 // Starting z-index value
|
|
35
36
|
const zIndex = ref<number>(baseZIndex)
|
|
36
37
|
|
|
37
38
|
// Trap focus within modal for accessibility
|
|
@@ -49,10 +50,9 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
49
50
|
// Only handle events for the top-most modal
|
|
50
51
|
if (!isOpen.value) return
|
|
51
52
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
if (activeModals.length > 0 && modalRef.value !== activeModals[activeModals.length - 1]) {
|
|
53
|
+
// Check if this modal is the top-most one
|
|
54
|
+
const isTopMost = isTopMostModal(props.id)
|
|
55
|
+
if (!isTopMost) {
|
|
56
56
|
return
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -78,18 +78,36 @@ function handleKeyDown(event: KeyboardEvent) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Check if this modal is the top-most (highest z-index)
|
|
82
|
+
function isTopMostModal(id: string): boolean {
|
|
83
|
+
if (openModals.size === 0) return false
|
|
84
|
+
|
|
85
|
+
// Find the modal with the highest z-index
|
|
86
|
+
const entries = Array.from(openModals.entries())
|
|
87
|
+
const highestEntry = entries.reduce((prev, current) =>
|
|
88
|
+
current[1] > prev[1] ? current : prev,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
// Return true if this modal has the highest z-index
|
|
92
|
+
return highestEntry[0] === id
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
function setModal(value: boolean) {
|
|
82
96
|
if (value === true) {
|
|
83
97
|
if (props.onOpen) props.onOpen()
|
|
84
98
|
previouslyFocusedElement = document.activeElement as HTMLElement
|
|
85
99
|
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
100
|
+
// Calculate the new z-index for this modal
|
|
101
|
+
const highestZIndex = calculateHighestZIndex()
|
|
102
|
+
|
|
103
|
+
// Register this modal in the global registry
|
|
104
|
+
openModals.set(props.id, highestZIndex)
|
|
105
|
+
|
|
106
|
+
// Set this modal's z-index
|
|
107
|
+
zIndex.value = highestZIndex
|
|
89
108
|
|
|
90
109
|
// Only manage body overflow for the first opened modal
|
|
91
|
-
|
|
92
|
-
if (activeModals.length === 0) {
|
|
110
|
+
if (openModals.size === 1) {
|
|
93
111
|
document.body.style.overflow = 'hidden' // Prevent scrolling when modal is open
|
|
94
112
|
}
|
|
95
113
|
|
|
@@ -98,9 +116,11 @@ function setModal(value: boolean) {
|
|
|
98
116
|
if (value === false) {
|
|
99
117
|
if (props.onClose) props.onClose()
|
|
100
118
|
|
|
119
|
+
// Remove this modal from the registry
|
|
120
|
+
openModals.delete(props.id)
|
|
121
|
+
|
|
101
122
|
// Only restore body overflow if this is the last open modal
|
|
102
|
-
|
|
103
|
-
if (activeModals.length <= 1) {
|
|
123
|
+
if (openModals.size === 0) {
|
|
104
124
|
document.body.style.overflow = '' // Restore scrolling
|
|
105
125
|
}
|
|
106
126
|
|
|
@@ -112,6 +132,48 @@ function setModal(value: boolean) {
|
|
|
112
132
|
isOpen.value = value
|
|
113
133
|
}
|
|
114
134
|
|
|
135
|
+
// Calculate the highest z-index for a new modal
|
|
136
|
+
function calculateHighestZIndex(): number {
|
|
137
|
+
// Start with the base z-index
|
|
138
|
+
let newZIndex = baseZIndex
|
|
139
|
+
|
|
140
|
+
// Find the highest z-index currently in use
|
|
141
|
+
if (openModals.size > 0) {
|
|
142
|
+
const values = Array.from(openModals.values())
|
|
143
|
+
newZIndex = Math.max(...values) + 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Ensure we stay within range (40-59)
|
|
147
|
+
if (newZIndex >= 59) {
|
|
148
|
+
// If we're approaching the upper limit, reset all z-indexes
|
|
149
|
+
resetAllModalZIndexes()
|
|
150
|
+
return baseZIndex + 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return newZIndex
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Reset all modal z-indexes when we approach the upper limit
|
|
157
|
+
function resetAllModalZIndexes() {
|
|
158
|
+
// Sort modals by their current z-index to maintain relative ordering
|
|
159
|
+
const entries = Array.from(openModals.entries())
|
|
160
|
+
entries.sort((a, b) => a[1] - b[1])
|
|
161
|
+
|
|
162
|
+
// Reassign z-indexes starting from baseZIndex
|
|
163
|
+
let newIndex = baseZIndex
|
|
164
|
+
entries.forEach(([id, _]) => {
|
|
165
|
+
openModals.set(id, newIndex)
|
|
166
|
+
|
|
167
|
+
// Find the modal element and update its z-index
|
|
168
|
+
const modalElement = document.querySelector(`[data-modal-id="${id}"]`) as HTMLElement
|
|
169
|
+
if (modalElement) {
|
|
170
|
+
modalElement.style.zIndex = newIndex.toString()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
newIndex++
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
115
177
|
// After modal is opened, set focus and collect focusable elements
|
|
116
178
|
watch(isOpen, async (newVal) => {
|
|
117
179
|
if (newVal) {
|
|
@@ -162,63 +224,66 @@ function handleBackdropClick(event: MouseEvent) {
|
|
|
162
224
|
</script>
|
|
163
225
|
|
|
164
226
|
<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"
|
|
227
|
+
<ClientOnly>
|
|
228
|
+
<transition
|
|
229
|
+
enter-active-class="duration-300 ease-out"
|
|
230
|
+
enter-from-class="opacity-0"
|
|
231
|
+
enter-to-class="opacity-100"
|
|
232
|
+
leave-active-class="duration-200 ease-in"
|
|
233
|
+
leave-from-class="opacity-100"
|
|
234
|
+
leave-to-class="opacity-0"
|
|
181
235
|
>
|
|
182
|
-
<!-- Backdrop with click to close functionality -->
|
|
183
236
|
<div
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
237
|
+
v-if="isOpen"
|
|
238
|
+
class="fixed inset-0 overflow-y-auto"
|
|
239
|
+
:style="{ zIndex }"
|
|
240
|
+
role="dialog"
|
|
241
|
+
:aria-labelledby="title ? `${props.id}-title` : undefined"
|
|
242
|
+
aria-modal="true"
|
|
243
|
+
data-modal-active="true"
|
|
244
|
+
:data-modal-id="props.id"
|
|
187
245
|
>
|
|
188
|
-
<!--
|
|
246
|
+
<!-- Backdrop with click to close functionality -->
|
|
189
247
|
<div
|
|
190
|
-
|
|
191
|
-
:
|
|
192
|
-
|
|
193
|
-
tabindex="-1"
|
|
194
|
-
@click.stop
|
|
248
|
+
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]"
|
|
249
|
+
:style="{ zIndex }"
|
|
250
|
+
@click="handleBackdropClick"
|
|
195
251
|
>
|
|
196
|
-
<!--
|
|
252
|
+
<!-- Modal panel -->
|
|
197
253
|
<div
|
|
198
|
-
|
|
199
|
-
class="
|
|
254
|
+
ref="modalRef"
|
|
255
|
+
:class="`relative ${mSize} max-w-6xl max-h-full ${ofy} bg-white rounded-lg shadow dark:bg-fv-neutral-900`"
|
|
256
|
+
:style="{ zIndex }"
|
|
257
|
+
tabindex="-1"
|
|
258
|
+
@click.stop
|
|
200
259
|
>
|
|
201
|
-
|
|
202
|
-
<
|
|
260
|
+
<!-- Header with title if provided -->
|
|
261
|
+
<div
|
|
203
262
|
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)"
|
|
263
|
+
class="flex items-center justify-between p-2 w-full border-b rounded-t dark:border-fv-neutral-700"
|
|
212
264
|
>
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
265
|
+
<slot name="before" />
|
|
266
|
+
<h2
|
|
267
|
+
v-if="title"
|
|
268
|
+
:id="`${props.id}-title`"
|
|
269
|
+
class="text-xl font-semibold text-fv-neutral-900 dark:text-white"
|
|
270
|
+
v-html="title"
|
|
271
|
+
/>
|
|
272
|
+
<button
|
|
273
|
+
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"
|
|
274
|
+
aria-label="Close modal"
|
|
275
|
+
@click="setModal(false)"
|
|
276
|
+
>
|
|
277
|
+
<component :is="closeIcon" class="w-7 h-7" />
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
<!-- Content area -->
|
|
281
|
+
<div class="p-3 space-y-3">
|
|
282
|
+
<slot />
|
|
283
|
+
</div>
|
|
219
284
|
</div>
|
|
220
285
|
</div>
|
|
221
286
|
</div>
|
|
222
|
-
</
|
|
223
|
-
</
|
|
287
|
+
</transition>
|
|
288
|
+
</ClientOnly>
|
|
224
289
|
</template>
|