wave-ui 4.2.3 → 4.2.5
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/wave-ui.cjs.js +3 -3
- package/dist/wave-ui.css +1 -1
- package/dist/wave-ui.esm.js +1227 -1181
- package/dist/wave-ui.umd.js +3 -3
- package/package.json +1 -1
- package/src/wave-ui/components/w-accordion/item.vue +2 -2
- package/src/wave-ui/components/w-button/index.vue +2 -1
- package/src/wave-ui/components/w-checkboxes.vue +2 -2
- package/src/wave-ui/components/w-dialog.vue +5 -0
- package/src/wave-ui/components/w-list.vue +2 -2
- package/src/wave-ui/components/w-radios.vue +2 -2
- package/src/wave-ui/components/w-rating.vue +2 -2
- package/src/wave-ui/components/w-scrollable.vue +3 -0
- package/src/wave-ui/components/w-tabs/index.vue +2 -2
- package/src/wave-ui/components/w-tag.vue +2 -2
- package/src/wave-ui/components/w-tree.vue +2 -2
- package/src/wave-ui/core.js +2 -7
- package/src/wave-ui/mixins/detachable.js +1 -2
- package/src/wave-ui/mixins/focusable.js +2 -1
- package/src/wave-ui/utils/focus.js +111 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-ui",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.5",
|
|
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",
|
|
@@ -90,13 +90,13 @@ import { useId } from 'vue'
|
|
|
90
90
|
import RippleMixin from '../../mixins/ripple'
|
|
91
91
|
import { isRippleEnabled } from '../../utils/ripple'
|
|
92
92
|
import AccordionContent from './accordion-content.vue'
|
|
93
|
-
import { focusElement } from '../../utils/focus'
|
|
93
|
+
import { focusElement, vFocusLifecycleMixin } from '../../utils/focus'
|
|
94
94
|
|
|
95
95
|
export default {
|
|
96
96
|
name: 'w-accordion-item',
|
|
97
97
|
focusable: true,
|
|
98
98
|
expose: ['focus'],
|
|
99
|
-
mixins: [RippleMixin],
|
|
99
|
+
mixins: [RippleMixin, vFocusLifecycleMixin],
|
|
100
100
|
|
|
101
101
|
setup () {
|
|
102
102
|
return { accordionItemUid: useId() }
|
|
@@ -12,13 +12,14 @@ button-partial(v-else ref="button" v-bind="buttonProps")
|
|
|
12
12
|
|
|
13
13
|
<script>
|
|
14
14
|
import ButtonPartial from './button.vue'
|
|
15
|
+
import FocusableMixin from '../../mixins/focusable'
|
|
15
16
|
import { focusElement } from '../../utils/focus'
|
|
16
17
|
|
|
17
18
|
export default {
|
|
18
19
|
name: 'w-button',
|
|
19
|
-
focusable: true,
|
|
20
20
|
expose: ['focus'],
|
|
21
21
|
inheritAttrs: false, // The attrs are only bound to the button-partial, not the root.
|
|
22
|
+
mixins: [FocusableMixin],
|
|
22
23
|
|
|
23
24
|
props: {
|
|
24
25
|
color: { type: String },
|
|
@@ -33,13 +33,13 @@ component(
|
|
|
33
33
|
<script>
|
|
34
34
|
import { reactive } from 'vue'
|
|
35
35
|
import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
|
|
36
|
-
import { guardFocusable } from '../utils/focus'
|
|
36
|
+
import { guardFocusable, vFocusLifecycleMixin } from '../utils/focus'
|
|
37
37
|
|
|
38
38
|
export default {
|
|
39
39
|
name: 'w-checkboxes',
|
|
40
40
|
focusable: true,
|
|
41
41
|
expose: ['focus'],
|
|
42
|
-
mixins: [FormElementMixin],
|
|
42
|
+
mixins: [FormElementMixin, vFocusLifecycleMixin],
|
|
43
43
|
|
|
44
44
|
setup () {
|
|
45
45
|
return useWaveUiFormIds()
|
|
@@ -45,13 +45,13 @@ ul.w-list(:class="classes")
|
|
|
45
45
|
<script>
|
|
46
46
|
import { useId } from 'vue'
|
|
47
47
|
import RippleMixin from '../mixins/ripple'
|
|
48
|
-
import { focusElement } from '../utils/focus'
|
|
48
|
+
import { focusElement, vFocusLifecycleMixin } from '../utils/focus'
|
|
49
49
|
|
|
50
50
|
export default {
|
|
51
51
|
name: 'w-list',
|
|
52
52
|
focusable: true,
|
|
53
53
|
expose: ['focus'],
|
|
54
|
-
mixins: [RippleMixin],
|
|
54
|
+
mixins: [RippleMixin, vFocusLifecycleMixin],
|
|
55
55
|
|
|
56
56
|
setup () {
|
|
57
57
|
return { waveUiUseId: useId() }
|
|
@@ -32,13 +32,13 @@ component(
|
|
|
32
32
|
|
|
33
33
|
<script>
|
|
34
34
|
import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
|
|
35
|
-
import { guardFocusable } from '../utils/focus'
|
|
35
|
+
import { guardFocusable, vFocusLifecycleMixin } from '../utils/focus'
|
|
36
36
|
|
|
37
37
|
export default {
|
|
38
38
|
name: 'w-radios',
|
|
39
39
|
focusable: true,
|
|
40
40
|
expose: ['focus'],
|
|
41
|
-
mixins: [FormElementMixin],
|
|
41
|
+
mixins: [FormElementMixin, vFocusLifecycleMixin],
|
|
42
42
|
|
|
43
43
|
setup () {
|
|
44
44
|
return useWaveUiFormIds()
|
|
@@ -32,13 +32,13 @@ component(
|
|
|
32
32
|
|
|
33
33
|
<script>
|
|
34
34
|
import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
|
|
35
|
-
import { guardFocusable, focusElement } from '../utils/focus'
|
|
35
|
+
import { guardFocusable, focusElement, vFocusLifecycleMixin } from '../utils/focus'
|
|
36
36
|
|
|
37
37
|
export default {
|
|
38
38
|
name: 'w-rating',
|
|
39
39
|
focusable: true,
|
|
40
40
|
expose: ['focus'],
|
|
41
|
-
mixins: [FormElementMixin],
|
|
41
|
+
mixins: [FormElementMixin, vFocusLifecycleMixin],
|
|
42
42
|
|
|
43
43
|
setup () {
|
|
44
44
|
return useWaveUiFormIds()
|
|
@@ -42,9 +42,12 @@
|
|
|
42
42
|
<script setup>
|
|
43
43
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, useId, useAttrs } from 'vue'
|
|
44
44
|
import { objectifyClasses } from '../utils/index'
|
|
45
|
+
import { useVFocus } from '../utils/focus'
|
|
45
46
|
|
|
46
47
|
defineOptions({ name: 'WScrollable', focusable: true })
|
|
47
48
|
|
|
49
|
+
useVFocus()
|
|
50
|
+
|
|
48
51
|
const props = defineProps({
|
|
49
52
|
color: { type: String, default: 'primary' },
|
|
50
53
|
bgColor: { type: String },
|
|
@@ -76,13 +76,13 @@ 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
|
+
import { focusElement, vFocusLifecycleMixin } from '../../utils/focus'
|
|
80
80
|
|
|
81
81
|
export default {
|
|
82
82
|
name: 'w-tabs',
|
|
83
83
|
focusable: true,
|
|
84
84
|
expose: ['focus'],
|
|
85
|
-
mixins: [RippleMixin],
|
|
85
|
+
mixins: [RippleMixin, vFocusLifecycleMixin],
|
|
86
86
|
|
|
87
87
|
setup () {
|
|
88
88
|
return { tabsStableId: useId() }
|
|
@@ -19,13 +19,13 @@ span.w-tag(
|
|
|
19
19
|
|
|
20
20
|
<script>
|
|
21
21
|
import RippleMixin from '../mixins/ripple'
|
|
22
|
-
import { focusElement } from '../utils/focus'
|
|
22
|
+
import { focusElement, vFocusLifecycleMixin } from '../utils/focus'
|
|
23
23
|
|
|
24
24
|
export default {
|
|
25
25
|
name: 'w-tag',
|
|
26
26
|
focusable: true,
|
|
27
27
|
expose: ['focus'],
|
|
28
|
-
mixins: [RippleMixin],
|
|
28
|
+
mixins: [RippleMixin, vFocusLifecycleMixin],
|
|
29
29
|
|
|
30
30
|
props: {
|
|
31
31
|
modelValue: { type: [Boolean, Number], default: -1 },
|
|
@@ -68,14 +68,14 @@ import { consoleWarn } from '../utils/console'
|
|
|
68
68
|
* - option to add a left border.
|
|
69
69
|
**/
|
|
70
70
|
|
|
71
|
-
import { focusElement } from '../utils/focus'
|
|
71
|
+
import { focusElement, vFocusLifecycleMixin } from '../utils/focus'
|
|
72
72
|
|
|
73
73
|
export default {
|
|
74
74
|
name: 'w-tree',
|
|
75
75
|
focusable: true,
|
|
76
76
|
expose: ['focus'],
|
|
77
77
|
|
|
78
|
-
mixins: [RippleMixin],
|
|
78
|
+
mixins: [RippleMixin, vFocusLifecycleMixin],
|
|
79
79
|
|
|
80
80
|
props: {
|
|
81
81
|
modelValue: { type: [Object, Array] },
|
package/src/wave-ui/core.js
CHANGED
|
@@ -5,7 +5,7 @@ import { colorPalette, generateColorShades, flattenColors } from './utils/colors
|
|
|
5
5
|
import { injectColorsCSSInDOM, injectCSSInDOM, generatePaletteVariables, generateColors } from './utils/dynamic-css'
|
|
6
6
|
import { injectNotifManagerInDOM, NotificationManager } from './utils/notification-manager'
|
|
7
7
|
import { waveRippleDirective } from './utils/wave-ripple-directive'
|
|
8
|
-
import {
|
|
8
|
+
import { focusDirective } from './utils/focus'
|
|
9
9
|
import './scss/index.scss'
|
|
10
10
|
|
|
11
11
|
let mounted = false
|
|
@@ -198,12 +198,7 @@ export default class WaveUI {
|
|
|
198
198
|
|
|
199
199
|
static install (app, options = {}) {
|
|
200
200
|
// Register directives.
|
|
201
|
-
app.directive('focus',
|
|
202
|
-
mounted: (el) => {
|
|
203
|
-
if (!registerVFocus(el)) scheduleFocus(el)
|
|
204
|
-
},
|
|
205
|
-
unmounted: (el) => unregisterVFocus(el)
|
|
206
|
-
})
|
|
201
|
+
app.directive('focus', focusDirective)
|
|
207
202
|
app.directive('scroll', {
|
|
208
203
|
mounted: (el, binding) => {
|
|
209
204
|
const f = evt => {
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { consoleWarn } from '../utils/console'
|
|
12
|
-
import { callFocus } from '../utils/focus'
|
|
13
12
|
|
|
14
13
|
// Minimum space (px) from the viewport edge when flipping / nudging.
|
|
15
14
|
const VIEWPORT_MARGIN = 4
|
|
@@ -204,7 +203,7 @@ export default {
|
|
|
204
203
|
const targets = this._autofocusTargets || []
|
|
205
204
|
this._autofocusTargets = []
|
|
206
205
|
if (!targets.length) return
|
|
207
|
-
this.$nextTick(() => targets.forEach(({
|
|
206
|
+
this.$nextTick(() => targets.forEach(({ focus }) => focus()))
|
|
208
207
|
},
|
|
209
208
|
|
|
210
209
|
unbindActivatorDocEvents () {
|
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* v-focus directive and helpers.
|
|
3
|
+
*
|
|
4
|
+
* Two paths:
|
|
5
|
+
* - Native elements (<input v-focus>) → focusDirective
|
|
6
|
+
* - Wave focusable components (<w-input v-focus>) → applyVFocusOnMount in mounted
|
|
7
|
+
* (directive on a component root only hits the wrapper DOM node; calling focus()
|
|
8
|
+
* on the component is required and must not rely on el.__vueParentComponent, which
|
|
9
|
+
* is stripped in production builds).
|
|
10
|
+
*
|
|
11
|
+
* Inside w-menu / w-tooltip, focus is deferred until the floating content is open.
|
|
12
|
+
*/
|
|
2
13
|
|
|
3
|
-
|
|
14
|
+
import { nextTick, getCurrentInstance, onMounted, onUnmounted } from 'vue'
|
|
4
15
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
const DETACHABLE = new Set(['w-menu', 'w-tooltip'])
|
|
17
|
+
|
|
18
|
+
function walk (start, test) {
|
|
19
|
+
let instance = start
|
|
8
20
|
while (instance) {
|
|
9
|
-
if (
|
|
21
|
+
if (test(instance)) return instance
|
|
10
22
|
instance = instance.parent
|
|
11
23
|
}
|
|
12
24
|
return null
|
|
@@ -16,12 +28,59 @@ function componentName (instance) {
|
|
|
16
28
|
return instance?.type?.name ?? instance?.proxy?.$options?.name
|
|
17
29
|
}
|
|
18
30
|
|
|
19
|
-
function
|
|
31
|
+
function isFocusable (instance) {
|
|
20
32
|
return !!(instance?.type?.focusable || instance?.proxy?.$options?.focusable)
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
function
|
|
24
|
-
return
|
|
35
|
+
function isDetachable (instance) {
|
|
36
|
+
return DETACHABLE.has(componentName(instance))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Resolve a component instance from directive hook args (works in production). */
|
|
40
|
+
function fromDirective (vnode, prevVNode, test) {
|
|
41
|
+
// On component roots, prevVNode.component is the host (e.g. w-input).
|
|
42
|
+
return walk(prevVNode?.component, test) || walk(vnode?.component, test)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** From a mounted hook — walk instance.parent (reliable in production). */
|
|
46
|
+
function detachableParent (instance) {
|
|
47
|
+
return walk(instance?.parent, isDetachable)?.proxy ?? null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detachableFromDirective (vnode, prevVNode) {
|
|
51
|
+
return fromDirective(vnode, prevVNode, isDetachable)?.proxy ?? null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveFocusableInstance (vnode, prevVNode) {
|
|
55
|
+
return fromDirective(vnode, prevVNode, isFocusable)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function invokeFocus (instance, el) {
|
|
59
|
+
const focus = instance?.exposed?.focus ?? instance?.proxy?.focus
|
|
60
|
+
if (typeof focus === 'function') focus()
|
|
61
|
+
else focusElement(el)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Queue focus on a detachable host until content is visible (see detachable.js). */
|
|
65
|
+
function deferFocus (el, detachable, focus) {
|
|
66
|
+
el.__waveUiDetachable = detachable
|
|
67
|
+
detachable.registerAutofocus({ el, focus })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unregisterVFocus (el) {
|
|
71
|
+
el.__waveUiDetachable?.unregisterAutofocus(el)
|
|
72
|
+
delete el.__waveUiDetachable
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function scheduleFocus (el, vnode, prevVNode) {
|
|
76
|
+
const instance = resolveFocusableInstance(vnode, prevVNode)
|
|
77
|
+
const focus = () => invokeFocus(instance, el)
|
|
78
|
+
const detachable = detachableFromDirective(vnode, prevVNode)
|
|
79
|
+
if (detachable) {
|
|
80
|
+
deferFocus(el, detachable, focus)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
nextTick(focus)
|
|
25
84
|
}
|
|
26
85
|
|
|
27
86
|
export function focusElement (el) {
|
|
@@ -33,42 +92,57 @@ export function guardFocusable (vm) {
|
|
|
33
92
|
return true
|
|
34
93
|
}
|
|
35
94
|
|
|
36
|
-
/**
|
|
37
|
-
export
|
|
38
|
-
|
|
95
|
+
/** Handles v-focus on native focusable elements only. */
|
|
96
|
+
export const focusDirective = {
|
|
97
|
+
mounted (el, binding, vnode, prevVNode) {
|
|
98
|
+
// Focusable Wave components run applyVFocusOnMount instead.
|
|
99
|
+
if (resolveFocusableInstance(vnode, prevVNode)) return
|
|
100
|
+
scheduleFocus(el, vnode, prevVNode)
|
|
101
|
+
},
|
|
102
|
+
unmounted (el) {
|
|
103
|
+
unregisterVFocus(el)
|
|
104
|
+
}
|
|
39
105
|
}
|
|
40
106
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const instance = walkParentInstances(el, isDetachableInstance)
|
|
44
|
-
return instance?.proxy ?? null
|
|
107
|
+
function hasVFocus (vnode) {
|
|
108
|
+
return !!vnode?.dirs?.some(d => d.dir === focusDirective)
|
|
45
109
|
}
|
|
46
110
|
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
111
|
+
function applyVFocusOnMount (instance) {
|
|
112
|
+
const vnode = instance?.vnode
|
|
113
|
+
if (!hasVFocus(vnode)) return
|
|
114
|
+
const vm = instance?.proxy
|
|
115
|
+
const el = vnode.el ?? vm?.$el
|
|
116
|
+
if (!el) return
|
|
117
|
+
|
|
118
|
+
const focus = () => vm?.focus?.()
|
|
119
|
+
const detachable = detachableParent(instance)
|
|
120
|
+
if (detachable) {
|
|
121
|
+
deferFocus(el, detachable, focus)
|
|
52
122
|
return
|
|
53
123
|
}
|
|
54
|
-
|
|
124
|
+
focus()
|
|
55
125
|
}
|
|
56
126
|
|
|
57
|
-
/**
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!detachable) return false
|
|
66
|
-
el.__waveUiDetachable = detachable
|
|
67
|
-
detachable.registerAutofocus({ el })
|
|
68
|
-
return true
|
|
127
|
+
/** For options-API focusable components (focusable: true). */
|
|
128
|
+
export const vFocusLifecycleMixin = {
|
|
129
|
+
mounted () {
|
|
130
|
+
applyVFocusOnMount(getCurrentInstance())
|
|
131
|
+
},
|
|
132
|
+
unmounted () {
|
|
133
|
+
unregisterVFocus(this.$el)
|
|
134
|
+
}
|
|
69
135
|
}
|
|
70
136
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
137
|
+
/** For script-setup focusable components (focusable: true). */
|
|
138
|
+
export function useVFocus () {
|
|
139
|
+
let el = null
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
const instance = getCurrentInstance()
|
|
142
|
+
el = instance?.vnode?.el ?? instance?.proxy?.$el
|
|
143
|
+
applyVFocusOnMount(instance)
|
|
144
|
+
})
|
|
145
|
+
onUnmounted(() => {
|
|
146
|
+
if (el) unregisterVFocus(el)
|
|
147
|
+
})
|
|
74
148
|
}
|