wave-ui 4.0.1 → 4.1.0
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/dist/types/types/components/WAccordion.d.ts +6 -6
- package/dist/types/types/components/WTabs.d.ts +6 -5
- package/dist/wave-ui.cjs.js +3 -3
- package/dist/wave-ui.css +1 -1
- package/dist/wave-ui.esm.js +1332 -1165
- package/dist/wave-ui.umd.js +3 -3
- package/package.json +1 -1
- package/src/wave-ui/components/w-accordion/index.vue +15 -2
- package/src/wave-ui/components/w-accordion/item.vue +7 -1
- package/src/wave-ui/components/w-autocomplete.vue +3 -1
- package/src/wave-ui/components/w-button/index.vue +11 -2
- package/src/wave-ui/components/w-checkbox.vue +3 -1
- package/src/wave-ui/components/w-checkboxes.vue +9 -0
- package/src/wave-ui/components/w-dialog.vue +6 -0
- package/src/wave-ui/components/w-drawer.vue +15 -2
- package/src/wave-ui/components/w-input.vue +3 -1
- package/src/wave-ui/components/w-list.vue +10 -1
- package/src/wave-ui/components/w-menu.vue +7 -0
- package/src/wave-ui/components/w-overlay.vue +44 -8
- package/src/wave-ui/components/w-radio.vue +3 -1
- package/src/wave-ui/components/w-radios.vue +9 -0
- package/src/wave-ui/components/w-rating.vue +7 -0
- package/src/wave-ui/components/w-select.vue +10 -1
- package/src/wave-ui/components/w-slider.vue +4 -1
- package/src/wave-ui/components/w-switch.vue +3 -1
- package/src/wave-ui/components/w-tabs/index.vue +77 -19
- package/src/wave-ui/components/w-tag.vue +9 -1
- package/src/wave-ui/components/w-textarea.vue +4 -1
- package/src/wave-ui/components/w-tree.vue +8 -0
- package/src/wave-ui/core.js +2 -2
- package/src/wave-ui/mixins/focusable.js +13 -0
- package/src/wave-ui/scss/_base.scss +4 -0
- package/src/wave-ui/utils/focus.js +39 -0
|
@@ -76,10 +76,11 @@ import { useId } from 'vue'
|
|
|
76
76
|
import { objectifyClasses } from '../../utils/index'
|
|
77
77
|
import RippleMixin from '../../mixins/ripple'
|
|
78
78
|
import TabContent from './tab-content.vue'
|
|
79
|
+
import { focusElement } from '../../utils/focus'
|
|
79
80
|
|
|
80
81
|
export default {
|
|
81
82
|
name: 'w-tabs',
|
|
82
|
-
|
|
83
|
+
expose: ['focus'],
|
|
83
84
|
mixins: [RippleMixin],
|
|
84
85
|
|
|
85
86
|
setup () {
|
|
@@ -183,6 +184,46 @@ export default {
|
|
|
183
184
|
},
|
|
184
185
|
|
|
185
186
|
methods: {
|
|
187
|
+
focus () {
|
|
188
|
+
const bar = this.$refs['tabs-bar']
|
|
189
|
+
if (!bar) return
|
|
190
|
+
const tab = bar.querySelector('[role="tab"][aria-selected="true"]:not([tabindex="-1"])')
|
|
191
|
+
|| bar.querySelector('[role="tab"][tabindex="0"]')
|
|
192
|
+
focusElement(tab)
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
resolveTabUid (value) {
|
|
196
|
+
if (!this.tabs.length) return null
|
|
197
|
+
if (value === undefined || value === null || value === '') return this.tabs[0]._uid
|
|
198
|
+
|
|
199
|
+
if (typeof value === 'string') {
|
|
200
|
+
if (this.tabsByUid[value]?._uid) return value
|
|
201
|
+
const parsed = Number.parseInt(value, 10)
|
|
202
|
+
if (!Number.isNaN(parsed) && `${parsed}` === value.trim()) return this.tabs[parsed]?._uid || null
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (typeof value === 'number' && value >= 0) return this.tabs[value]?._uid || null
|
|
207
|
+
return null
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
syncActiveTabFromModelValue (value = this.modelValue) {
|
|
211
|
+
const uid = this.resolveTabUid(value) || this.tabs[0]?._uid || null
|
|
212
|
+
const tab = uid ? this.tabsByUid[uid] : null
|
|
213
|
+
this.activeTabUid = tab?._uid || null
|
|
214
|
+
this.activeTabIndex = tab?._index || 0
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
shouldEmitUidModelValue () {
|
|
218
|
+
if (typeof this.modelValue !== 'string') return false
|
|
219
|
+
return !/^\d+$/.test(this.modelValue.trim())
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
getModelValueForTab (tab) {
|
|
223
|
+
if (this.shouldEmitUidModelValue()) return tab[this.itemIdKey] ?? tab._uid
|
|
224
|
+
return tab._index
|
|
225
|
+
},
|
|
226
|
+
|
|
186
227
|
// Adding a tab in the list.
|
|
187
228
|
addTab (item) {
|
|
188
229
|
// If there is no unique ID provided, inject one in each tab.
|
|
@@ -200,6 +241,7 @@ export default {
|
|
|
200
241
|
refreshTabs () {
|
|
201
242
|
let items = this.items
|
|
202
243
|
if (typeof items === 'number') items = Array(items).fill().map((_, i) => this.tabs[i] || {})
|
|
244
|
+
else items = items || []
|
|
203
245
|
|
|
204
246
|
this.tabs = items.map((item, _index) => {
|
|
205
247
|
// If there is no unique ID provided, inject one in each tab.
|
|
@@ -258,19 +300,23 @@ export default {
|
|
|
258
300
|
openTab (uid) {
|
|
259
301
|
this.prevTabIndex = this.activeTabIndex // To resolve the transition direction.
|
|
260
302
|
const tab = this.tabsByUid[uid]
|
|
303
|
+
if (!tab) return
|
|
261
304
|
this.activeTabIndex = tab._index
|
|
262
305
|
this.activeTabUid = tab._uid
|
|
263
|
-
this
|
|
264
|
-
this.$emit('
|
|
306
|
+
const modelValue = this.getModelValueForTab(tab)
|
|
307
|
+
this.$emit('update:modelValue', modelValue)
|
|
308
|
+
this.$emit('input', modelValue)
|
|
265
309
|
|
|
266
310
|
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
267
311
|
},
|
|
268
312
|
|
|
269
313
|
// Updates the slider position.
|
|
270
314
|
updateSlider (domLookup = true) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
this.activeTabEl =
|
|
315
|
+
const ref = this.$refs['tabs-bar']
|
|
316
|
+
if (domLookup || !this.activeTabEl) {
|
|
317
|
+
this.activeTabEl =
|
|
318
|
+
ref?.querySelector('.w-tabs__bar-item--active')
|
|
319
|
+
|| ref?.querySelector(`.w-tabs__bar-item:nth-child(${this.activeTabIndex + 1})`)
|
|
274
320
|
}
|
|
275
321
|
|
|
276
322
|
if (!this.fillBar && this.activeTabEl) {
|
|
@@ -281,6 +327,10 @@ export default {
|
|
|
281
327
|
this.slider.left = `${left - parentLeft - parseInt(borderLeftWidth) + tabsBar.scrollLeft}px`
|
|
282
328
|
this.slider.width = `${width}px`
|
|
283
329
|
}
|
|
330
|
+
else if (!this.fillBar && domLookup && this.tabs.length) {
|
|
331
|
+
// Hydration/layout timing can briefly hide active title lookup; retry once on next tick.
|
|
332
|
+
this.$nextTick(() => this.updateSlider(false))
|
|
333
|
+
}
|
|
284
334
|
else {
|
|
285
335
|
this.slider.left = `${this.activeTab._index * 100 / this.tabs.length}%`
|
|
286
336
|
this.slider.width = `${100 / this.tabs.length}%`
|
|
@@ -288,17 +338,17 @@ export default {
|
|
|
288
338
|
},
|
|
289
339
|
|
|
290
340
|
updateActiveTab (index) {
|
|
291
|
-
|
|
292
|
-
|
|
341
|
+
const uid = this.resolveTabUid(index)
|
|
342
|
+
const tab = uid ? this.tabsByUid[uid] : null
|
|
293
343
|
|
|
294
344
|
// Only open the tab if it is found.
|
|
295
|
-
if (
|
|
296
|
-
this.openTab(
|
|
345
|
+
if (tab?._uid) {
|
|
346
|
+
this.openTab(tab._uid)
|
|
297
347
|
|
|
298
348
|
// Scroll the new active tab item title into view if needed.
|
|
299
349
|
this.$nextTick(() => {
|
|
300
350
|
const ref = this.$refs['tabs-bar']
|
|
301
|
-
this.activeTabEl = ref?.querySelector(`.w-tabs__bar-item:nth-child(${
|
|
351
|
+
this.activeTabEl = ref?.querySelector(`.w-tabs__bar-item:nth-child(${tab._index + 1})`)
|
|
302
352
|
if (this.activeTabEl) {
|
|
303
353
|
this.activeTabEl.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' })
|
|
304
354
|
}
|
|
@@ -312,13 +362,14 @@ export default {
|
|
|
312
362
|
}
|
|
313
363
|
},
|
|
314
364
|
|
|
315
|
-
|
|
365
|
+
created () {
|
|
316
366
|
this.tabs = [] // Reset for hot-reloading.
|
|
317
|
-
const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items
|
|
367
|
+
const items = typeof this.items === 'number' ? Array(this.items).fill().map(Object) : this.items || []
|
|
318
368
|
items.forEach(this.addTab)
|
|
369
|
+
this.syncActiveTabFromModelValue(this.modelValue)
|
|
370
|
+
},
|
|
319
371
|
|
|
320
|
-
|
|
321
|
-
|
|
372
|
+
beforeMount () {
|
|
322
373
|
this.$nextTick(() => {
|
|
323
374
|
this.updateSlider()
|
|
324
375
|
// Disable the slider transition while loading.
|
|
@@ -333,14 +384,15 @@ export default {
|
|
|
333
384
|
},
|
|
334
385
|
|
|
335
386
|
watch: {
|
|
336
|
-
modelValue (
|
|
337
|
-
|
|
387
|
+
modelValue (value) {
|
|
388
|
+
const uid = this.resolveTabUid(value)
|
|
389
|
+
if (uid && uid !== this.activeTabUid) this.updateActiveTab(value)
|
|
338
390
|
},
|
|
339
391
|
items: {
|
|
340
392
|
handler () {
|
|
341
393
|
this.refreshTabs()
|
|
342
|
-
|
|
343
|
-
if (this.tabs.length) this.reopenTheActiveTab()
|
|
394
|
+
this.syncActiveTabFromModelValue(this.modelValue)
|
|
395
|
+
if (!this.activeTabUid && this.tabs.length) this.reopenTheActiveTab()
|
|
344
396
|
|
|
345
397
|
if (!this.noSlider) this.$nextTick(this.updateSlider)
|
|
346
398
|
},
|
|
@@ -412,6 +464,12 @@ export default {
|
|
|
412
464
|
margin-right: -1px;
|
|
413
465
|
}
|
|
414
466
|
.w-tabs--card &--active {border-bottom-color: transparent;}
|
|
467
|
+
.w-tabs--pill-slider & {
|
|
468
|
+
padding: calc(var(--w-base-increment) * 1) calc(var(--w-base-increment) * 2);
|
|
469
|
+
margin: calc(var(--w-base-increment) * 1) calc(var(--w-base-increment) * 1);
|
|
470
|
+
border-radius: 99em;
|
|
471
|
+
font-size: round(nearest, calc(1.1 * var(--w-base-font-size)), 1px);
|
|
472
|
+
}
|
|
415
473
|
|
|
416
474
|
&--disabled {
|
|
417
475
|
cursor: not-allowed;
|
|
@@ -19,10 +19,11 @@ span.w-tag(
|
|
|
19
19
|
|
|
20
20
|
<script>
|
|
21
21
|
import RippleMixin from '../mixins/ripple'
|
|
22
|
+
import { focusElement } from '../utils/focus'
|
|
22
23
|
|
|
23
24
|
export default {
|
|
24
25
|
name: 'w-tag',
|
|
25
|
-
|
|
26
|
+
expose: ['focus'],
|
|
26
27
|
mixins: [RippleMixin],
|
|
27
28
|
|
|
28
29
|
props: {
|
|
@@ -48,6 +49,13 @@ export default {
|
|
|
48
49
|
|
|
49
50
|
emits: ['input', 'update:modelValue'],
|
|
50
51
|
|
|
52
|
+
methods: {
|
|
53
|
+
focus () {
|
|
54
|
+
if (this.modelValue === -1) return
|
|
55
|
+
focusElement(this.$el)
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
|
|
51
59
|
computed: {
|
|
52
60
|
presetSize () {
|
|
53
61
|
return (
|
|
@@ -68,10 +68,13 @@ component(
|
|
|
68
68
|
**/
|
|
69
69
|
|
|
70
70
|
import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
|
|
71
|
+
import FocusableMixin from '../mixins/focusable'
|
|
71
72
|
|
|
72
73
|
export default {
|
|
73
74
|
name: 'w-textarea',
|
|
74
|
-
|
|
75
|
+
expose: ['focus'],
|
|
76
|
+
focusTargetRef: 'textarea',
|
|
77
|
+
mixins: [FormElementMixin, FocusableMixin],
|
|
75
78
|
inheritAttrs: false, // The attrs should only be added to the textarea not the wrapper.
|
|
76
79
|
|
|
77
80
|
setup () {
|
|
@@ -68,8 +68,11 @@ import { consoleWarn } from '../utils/console'
|
|
|
68
68
|
* - option to add a left border.
|
|
69
69
|
**/
|
|
70
70
|
|
|
71
|
+
import { focusElement } from '../utils/focus'
|
|
72
|
+
|
|
71
73
|
export default {
|
|
72
74
|
name: 'w-tree',
|
|
75
|
+
expose: ['focus'],
|
|
73
76
|
|
|
74
77
|
mixins: [RippleMixin],
|
|
75
78
|
|
|
@@ -122,6 +125,11 @@ export default {
|
|
|
122
125
|
},
|
|
123
126
|
|
|
124
127
|
methods: {
|
|
128
|
+
focus () {
|
|
129
|
+
if (this.disabled) return
|
|
130
|
+
focusElement(this.$el.querySelector('.w-tree__item-label[tabindex="0"]'))
|
|
131
|
+
},
|
|
132
|
+
|
|
125
133
|
// From data watcher, retain the oldItems open state.
|
|
126
134
|
updateCurrentDepthTree (items, oldItems = []) {
|
|
127
135
|
this.currentDepthItems = []
|
package/src/wave-ui/core.js
CHANGED
|
@@ -5,6 +5,7 @@ import { colorPalette, generateColorShades, flattenColors } from './utils/colors
|
|
|
5
5
|
import { injectColorsCSSInDOM, injectCSSInDOM } from './utils/dynamic-css'
|
|
6
6
|
import { injectNotifManagerInDOM, NotificationManager } from './utils/notification-manager'
|
|
7
7
|
import { waveRippleDirective } from './utils/wave-ripple-directive'
|
|
8
|
+
import { scheduleFocus } from './utils/focus'
|
|
8
9
|
import './scss/index.scss'
|
|
9
10
|
|
|
10
11
|
let mounted = false
|
|
@@ -112,8 +113,7 @@ export default class WaveUI {
|
|
|
112
113
|
static install (app, options = {}) {
|
|
113
114
|
// Register directives.
|
|
114
115
|
app.directive('focus', {
|
|
115
|
-
|
|
116
|
-
mounted: el => setTimeout(() => el.focus(), 0)
|
|
116
|
+
mounted: (el, binding, vnode) => scheduleFocus(vnode, el)
|
|
117
117
|
})
|
|
118
118
|
app.directive('scroll', {
|
|
119
119
|
mounted: (el, binding) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { guardFocusable, focusElement } from '../utils/focus'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
methods: {
|
|
5
|
+
focus () {
|
|
6
|
+
if (!guardFocusable(this)) return
|
|
7
|
+
const refName = this.$options.focusTargetRef || 'input'
|
|
8
|
+
const target = this.$refs[refName]
|
|
9
|
+
if (target) focusElement(target)
|
|
10
|
+
else this.$nextTick(() => focusElement(this.$refs[refName]))
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
:root[data-theme="light"] {
|
|
91
|
+
color-scheme: light;
|
|
92
|
+
|
|
91
93
|
--w-base-bg-color: #fff;
|
|
92
94
|
--w-base-color: #000;
|
|
93
95
|
--w-contrast-bg-color: #000;
|
|
@@ -97,6 +99,8 @@
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
:root[data-theme="dark"] {
|
|
102
|
+
color-scheme: dark;
|
|
103
|
+
|
|
100
104
|
--w-base-bg-color: #222;
|
|
101
105
|
--w-base-color: #fff;
|
|
102
106
|
--w-contrast-bg-color: #fff;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { nextTick } from 'vue'
|
|
2
|
+
|
|
3
|
+
export function focusElement (el) {
|
|
4
|
+
el?.focus?.()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function guardFocusable (vm) {
|
|
8
|
+
if (vm.isDisabled || vm.isReadonly) return false
|
|
9
|
+
return true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve focus() from a directive host. On components, `vnode` is the root element
|
|
14
|
+
* VNode (no `vnode.component`); the DOM owner is on `el.__vueParentComponent`.
|
|
15
|
+
* Walk up when the root is a wrapper (e.g. `w-form-element` around `w-input`).
|
|
16
|
+
*/
|
|
17
|
+
export function resolveFocusFn (vnode, el) {
|
|
18
|
+
let instance = vnode?.component ?? el?.__vueParentComponent
|
|
19
|
+
while (instance) {
|
|
20
|
+
const focusFn = instance.exposed?.focus ?? instance.proxy?.focus
|
|
21
|
+
if (typeof focusFn === 'function') return focusFn
|
|
22
|
+
instance = instance.parent
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function callFocus (vnode, el) {
|
|
28
|
+
const focusFn = resolveFocusFn(vnode, el)
|
|
29
|
+
if (typeof focusFn === 'function') {
|
|
30
|
+
focusFn()
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
focusElement(el)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Schedule focus after mount so template refs exist on the host component. */
|
|
37
|
+
export function scheduleFocus (vnode, el) {
|
|
38
|
+
nextTick(() => callFocus(vnode, el))
|
|
39
|
+
}
|