@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.
@@ -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
- // Static counter for managing z-index across all modals
7
- let modalCounter = 0
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
- const baseZIndex = 100 // Use a higher base z-index to avoid conflicts
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
- // 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]) {
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
- // 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
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
- const activeModals = document.querySelectorAll('[data-modal-active="true"]')
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
- const activeModals = document.querySelectorAll('[data-modal-active="true"]')
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
- <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"
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
- 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"
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
- <!-- Modal panel -->
246
+ <!-- Backdrop with click to close functionality -->
189
247
  <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
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
- <!-- Header with title if provided -->
252
+ <!-- Modal panel -->
197
253
  <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"
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
- <slot name="before" />
202
- <h2
260
+ <!-- Header with title if provided -->
261
+ <div
203
262
  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)"
263
+ class="flex items-center justify-between p-2 w-full border-b rounded-t dark:border-fv-neutral-700"
212
264
  >
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 />
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
- </div>
223
- </transition>
287
+ </transition>
288
+ </ClientOnly>
224
289
  </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.50",
4
4
  "author": "Florian 'Fy' Gasquez <m@fy.to>",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/fy-to/FWJS#readme",