wave-ui 4.0.0 → 4.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-ui",
3
- "version": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "A UI framework for Vue.js 3 (and 2) with only the bright side. :sunny:",
5
5
  "author": "Antoni Andre <antoniandre.web@gmail.com>",
6
6
  "homepage": "https://antoniandre.github.io/wave-ui",
@@ -45,7 +45,7 @@
45
45
  </template>
46
46
 
47
47
  <script>
48
- import { computed } from 'vue'
48
+ import { computed, Fragment } from 'vue'
49
49
  import { objectifyClasses } from '../../utils/index'
50
50
  import { consoleError } from '../../utils/console'
51
51
  import RippleMixin from '../../mixins/ripple'
@@ -119,7 +119,8 @@ export default {
119
119
 
120
120
  // Detect if the accordion items are directly provided through slot using WAccordionItem.
121
121
  accordionItemsProvided () {
122
- return this.$slots.default?.()?.some(item => item?.type?.name === 'w-accordion-item')
122
+ const slot = this.$slots.default?.() || []
123
+ return this.hasAccordionItemVNodes(slot)
123
124
  },
124
125
 
125
126
  accordionClasses () {
@@ -151,6 +152,18 @@ export default {
151
152
  },
152
153
 
153
154
  methods: {
155
+ hasAccordionItemVNodes (nodes = []) {
156
+ return nodes.some((node) => {
157
+ if (!node) return false
158
+ if (node.type === WAccordionItem) return true
159
+ if (node.type?.name === 'w-accordion-item') return true
160
+ if (node.type === Fragment && Array.isArray(node.children)) {
161
+ return this.hasAccordionItemVNodes(node.children)
162
+ }
163
+ return false
164
+ })
165
+ },
166
+
154
167
  getAccordionItem (cuid) {
155
168
  return this.accordionItemsById[cuid]
156
169
  },
@@ -1,7 +1,7 @@
1
1
  <template lang="pug">
2
2
  slot(name="activator")
3
3
  slot(v-if="!$slots.activator")
4
- teleport(:to="teleportTarget" :disabled="!teleportTarget")
4
+ teleport(v-if="detachableDomReady" :to="teleportTarget" :disabled="!teleportTarget")
5
5
  transition(:name="transitionName" appear @after-leave="onAfterLeave")
6
6
  .w-menu(
7
7
  v-if="custom && detachableVisible"
@@ -183,6 +183,38 @@ export default {
183
183
  },
184
184
 
185
185
  methods: {
186
+ resolveTabUid (value) {
187
+ if (!this.tabs.length) return null
188
+ if (value === undefined || value === null || value === '') return this.tabs[0]._uid
189
+
190
+ if (typeof value === 'string') {
191
+ if (this.tabsByUid[value]?._uid) return value
192
+ const parsed = Number.parseInt(value, 10)
193
+ if (!Number.isNaN(parsed) && `${parsed}` === value.trim()) return this.tabs[parsed]?._uid || null
194
+ return null
195
+ }
196
+
197
+ if (typeof value === 'number' && value >= 0) return this.tabs[value]?._uid || null
198
+ return null
199
+ },
200
+
201
+ syncActiveTabFromModelValue (value = this.modelValue) {
202
+ const uid = this.resolveTabUid(value) || this.tabs[0]?._uid || null
203
+ const tab = uid ? this.tabsByUid[uid] : null
204
+ this.activeTabUid = tab?._uid || null
205
+ this.activeTabIndex = tab?._index || 0
206
+ },
207
+
208
+ shouldEmitUidModelValue () {
209
+ if (typeof this.modelValue !== 'string') return false
210
+ return !/^\d+$/.test(this.modelValue.trim())
211
+ },
212
+
213
+ getModelValueForTab (tab) {
214
+ if (this.shouldEmitUidModelValue()) return tab[this.itemIdKey] ?? tab._uid
215
+ return tab._index
216
+ },
217
+
186
218
  // Adding a tab in the list.
187
219
  addTab (item) {
188
220
  // If there is no unique ID provided, inject one in each tab.
@@ -200,6 +232,7 @@ export default {
200
232
  refreshTabs () {
201
233
  let items = this.items
202
234
  if (typeof items === 'number') items = Array(items).fill().map((_, i) => this.tabs[i] || {})
235
+ else items = items || []
203
236
 
204
237
  this.tabs = items.map((item, _index) => {
205
238
  // If there is no unique ID provided, inject one in each tab.
@@ -258,19 +291,23 @@ export default {
258
291
  openTab (uid) {
259
292
  this.prevTabIndex = this.activeTabIndex // To resolve the transition direction.
260
293
  const tab = this.tabsByUid[uid]
294
+ if (!tab) return
261
295
  this.activeTabIndex = tab._index
262
296
  this.activeTabUid = tab._uid
263
- this.$emit('update:modelValue', tab._index)
264
- this.$emit('input', tab._index)
297
+ const modelValue = this.getModelValueForTab(tab)
298
+ this.$emit('update:modelValue', modelValue)
299
+ this.$emit('input', modelValue)
265
300
 
266
301
  if (!this.noSlider) this.$nextTick(this.updateSlider)
267
302
  },
268
303
 
269
304
  // Updates the slider position.
270
305
  updateSlider (domLookup = true) {
271
- if (domLookup) {
272
- const ref = this.$refs['tabs-bar']
273
- this.activeTabEl = ref?.querySelector('.w-tabs__bar-item--active')
306
+ const ref = this.$refs['tabs-bar']
307
+ if (domLookup || !this.activeTabEl) {
308
+ this.activeTabEl =
309
+ ref?.querySelector('.w-tabs__bar-item--active')
310
+ || ref?.querySelector(`.w-tabs__bar-item:nth-child(${this.activeTabIndex + 1})`)
274
311
  }
275
312
 
276
313
  if (!this.fillBar && this.activeTabEl) {
@@ -281,6 +318,10 @@ export default {
281
318
  this.slider.left = `${left - parentLeft - parseInt(borderLeftWidth) + tabsBar.scrollLeft}px`
282
319
  this.slider.width = `${width}px`
283
320
  }
321
+ else if (!this.fillBar && domLookup && this.tabs.length) {
322
+ // Hydration/layout timing can briefly hide active title lookup; retry once on next tick.
323
+ this.$nextTick(() => this.updateSlider(false))
324
+ }
284
325
  else {
285
326
  this.slider.left = `${this.activeTab._index * 100 / this.tabs.length}%`
286
327
  this.slider.width = `${100 / this.tabs.length}%`
@@ -288,17 +329,17 @@ export default {
288
329
  },
289
330
 
290
331
  updateActiveTab (index) {
291
- if (typeof index === 'string') index = ~~index
292
- else if (isNaN(index) || index < 0) index = 0
332
+ const uid = this.resolveTabUid(index)
333
+ const tab = uid ? this.tabsByUid[uid] : null
293
334
 
294
335
  // Only open the tab if it is found.
295
- if (this.tabs[index]?._uid) {
296
- this.openTab(this.tabs[index]?._uid)
336
+ if (tab?._uid) {
337
+ this.openTab(tab._uid)
297
338
 
298
339
  // Scroll the new active tab item title into view if needed.
299
340
  this.$nextTick(() => {
300
341
  const ref = this.$refs['tabs-bar']
301
- this.activeTabEl = ref?.querySelector(`.w-tabs__bar-item:nth-child(${index + 1})`)
342
+ this.activeTabEl = ref?.querySelector(`.w-tabs__bar-item:nth-child(${tab._index + 1})`)
302
343
  if (this.activeTabEl) {
303
344
  this.activeTabEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
304
345
  }
@@ -312,13 +353,14 @@ export default {
312
353
  }
313
354
  },
314
355
 
315
- beforeMount () {
356
+ created () {
316
357
  this.tabs = [] // Reset for hot-reloading.
317
- const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items
358
+ const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items || []
318
359
  items.forEach(this.addTab)
360
+ this.syncActiveTabFromModelValue(this.modelValue)
361
+ },
319
362
 
320
- if (this.modelValue ?? false) this.updateActiveTab(this.modelValue)
321
-
363
+ beforeMount () {
322
364
  this.$nextTick(() => {
323
365
  this.updateSlider()
324
366
  // Disable the slider transition while loading.
@@ -333,14 +375,15 @@ export default {
333
375
  },
334
376
 
335
377
  watch: {
336
- modelValue (index) {
337
- if (index !== this.activeTabIndex) this.updateActiveTab(index)
378
+ modelValue (value) {
379
+ const uid = this.resolveTabUid(value)
380
+ if (uid && uid !== this.activeTabUid) this.updateActiveTab(value)
338
381
  },
339
382
  items: {
340
383
  handler () {
341
384
  this.refreshTabs()
342
-
343
- if (this.tabs.length) this.reopenTheActiveTab()
385
+ this.syncActiveTabFromModelValue(this.modelValue)
386
+ if (!this.activeTabUid && this.tabs.length) this.reopenTheActiveTab()
344
387
 
345
388
  if (!this.noSlider) this.$nextTick(this.updateSlider)
346
389
  },
@@ -1,7 +1,7 @@
1
1
  <template lang="pug">
2
2
  slot(name="activator")
3
3
  slot(v-if="!$slots.activator")
4
- teleport(:to="teleportTarget" :disabled="!teleportTarget")
4
+ teleport(v-if="detachableDomReady" :to="teleportTarget" :disabled="!teleportTarget")
5
5
  transition(:name="transitionName" appear @after-leave="onAfterLeave")
6
6
  .w-tooltip(
7
7
  v-if="detachableVisible"
@@ -44,6 +44,11 @@ export default {
44
44
  },
45
45
 
46
46
  data: () => ({
47
+ /**
48
+ * When false (SSR + first hydrated paint), the Teleport subtree is not rendered so HTML matches.
49
+ * Set true in mounted(), then the menu/tooltip portal attaches — avoids Nuxt/Vue hydration mismatches.
50
+ */
51
+ detachableDomReady: false,
47
52
  // The event listeners handlers have to be removed the exact same way they have been attached.
48
53
  // Since the handler functions have variables that change after hot-reload, keep them exactly
49
54
  // as is in an array so we can delete them on destroy.
@@ -464,29 +469,32 @@ export default {
464
469
  },
465
470
 
466
471
  mounted () {
467
- if (this.activator) {
468
- // External activator: attach via document-level delegation.
469
- this.bindActivatorEvents()
470
- }
471
- else {
472
- // Slot-based activator: auto-attach DOM listeners to the slot's root element on next tick
473
- // so the slot content is guaranteed to be in the DOM.
474
- this.$nextTick(() => {
475
- // Re-check activator prop (might have resolved from a Vue ref after the tick).
476
- if (this.activator) this.bindActivatorEvents()
477
- else this._attachActivatorListeners()
478
-
479
- if (this.modelValue && !this.disable) this.open({ target: this.activatorEl })
480
- })
481
- }
472
+ this.detachableDomReady = true
473
+ this.$nextTick(() => {
474
+ if (this.activator) {
475
+ // External activator: attach via document-level delegation.
476
+ this.bindActivatorEvents()
477
+ }
478
+ else {
479
+ // Slot-based activator: auto-attach DOM listeners to the slot's root element on next tick
480
+ // so the slot content is guaranteed to be in the DOM.
481
+ this.$nextTick(() => {
482
+ // Re-check activator prop (might have resolved from a Vue ref after the tick).
483
+ if (this.activator) this.bindActivatorEvents()
484
+ else this._attachActivatorListeners()
485
+
486
+ if (this.modelValue && !this.disable) this.open({ target: this.activatorEl })
487
+ })
488
+ }
482
489
 
483
- // Unwrap the overlay if any.
484
- if (this.overlay) this.overlayEl = this.$refs.overlay?.$el
490
+ // Unwrap the overlay if any.
491
+ if (this.overlay) this.overlayEl = this.$refs.overlay?.$el
485
492
 
486
- if (this.modelValue && this.activator && !this.disable) {
487
- this.toggle({ type: this.shouldShowOnClick ? 'click' : 'mouseenter', target: this.activatorEl })
488
- }
489
- else if (this.modelValue && !this.disable) this.open({ target: this.activatorEl })
493
+ if (this.modelValue && this.activator && !this.disable) {
494
+ this.toggle({ type: this.shouldShowOnClick ? 'click' : 'mouseenter', target: this.activatorEl })
495
+ }
496
+ else if (this.modelValue && !this.disable) this.open({ target: this.activatorEl })
497
+ })
490
498
  },
491
499
 
492
500
  unmounted () {