@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
- isOpen.value = true
42
+
43
+ // Emit event first to ensure it's registered before opening the modal
43
44
  eventBus.emit('confirmModal', true)
44
- nextTick(() => {
45
- previouslyFocusedElement = document.activeElement as HTMLElement
46
- if (modalRef.value) {
47
- modalRef.value.focus()
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
- // Static counter for managing z-index across all modals
7
- let modalCounter = 0
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
- const baseZIndex = 100 // Use a higher base z-index to avoid conflicts
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
- // Get all active modals
53
- const activeModals = document.querySelectorAll('[data-modal-active="true"]')
54
- // If this is not the top-most modal, don't handle keyboard events
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
- // Set this modal's z-index higher than any existing modal
87
- modalCounter += 3 // Increase by 3 to handle backdrop, container and content
88
- zIndex.value = baseZIndex + modalCounter
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
- const activeModals = document.querySelectorAll('[data-modal-active="true"]')
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
- const activeModals = document.querySelectorAll('[data-modal-active="true"]')
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
- // Only restore body overflow if this modal was open when unmounted
232
+ // Clean up the modal registry if this modal was open when unmounted
147
233
  if (isOpen.value) {
148
- const activeModals = document.querySelectorAll('[data-modal-active="true"]')
149
- if (activeModals.length <= 1) {
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
- <transition
166
- enter-active-class="duration-300 ease-out"
167
- enter-from-class="opacity-0"
168
- enter-to-class="opacity-100"
169
- leave-active-class="duration-200 ease-in"
170
- leave-from-class="opacity-100"
171
- leave-to-class="opacity-0"
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
- 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]"
185
- :style="{ zIndex: zIndex + 1 }"
186
- @click="handleBackdropClick"
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
- <!-- Modal panel -->
278
+ <!-- Backdrop with click to close functionality -->
189
279
  <div
190
- ref="modalRef"
191
- :class="`relative ${mSize} max-w-6xl max-h-full ${ofy} bg-white rounded-lg shadow dark:bg-fv-neutral-900`"
192
- :style="{ zIndex: zIndex + 2 }"
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
- <!-- Header with title if provided -->
284
+ <!-- Modal panel -->
197
285
  <div
198
- v-if="title"
199
- class="flex items-center justify-between p-2 w-full border-b rounded-t dark:border-fv-neutral-700"
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
- <slot name="before" />
202
- <h2
292
+ <!-- Header with title if provided -->
293
+ <div
203
294
  v-if="title"
204
- :id="`${props.id}-title`"
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
- <component :is="closeIcon" class="w-7 h-7" />
214
- </button>
215
- </div>
216
- <!-- Content area -->
217
- <div class="p-3 space-y-3">
218
- <slot />
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
- </div>
223
- </transition>
319
+ </transition>
320
+ </ClientOnly>
224
321
  </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fy-/fws-vue",
3
- "version": "2.2.49",
3
+ "version": "2.2.51",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",