wave-ui 4.1.0 → 4.1.1
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.esm.js +651 -579
- package/dist/wave-ui.umd.js +3 -3
- package/package.json +1 -1
- package/src/wave-ui/components/w-accordion/item.vue +1 -0
- package/src/wave-ui/components/w-button/index.vue +1 -0
- package/src/wave-ui/components/w-checkboxes.vue +1 -0
- package/src/wave-ui/components/w-list.vue +1 -0
- package/src/wave-ui/components/w-menu.vue +1 -1
- package/src/wave-ui/components/w-radios.vue +1 -0
- package/src/wave-ui/components/w-rating.vue +1 -0
- package/src/wave-ui/components/w-scrollable.vue +1 -1
- package/src/wave-ui/components/w-tabs/index.vue +1 -0
- package/src/wave-ui/components/w-tag.vue +1 -0
- package/src/wave-ui/components/w-tooltip.vue +1 -1
- package/src/wave-ui/components/w-tree.vue +1 -0
- package/src/wave-ui/core.js +5 -2
- package/src/wave-ui/mixins/detachable.js +41 -2
- package/src/wave-ui/mixins/focusable.js +12 -3
- package/src/wave-ui/utils/focus.js +52 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-ui",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.1",
|
|
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",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
slot(name="activator")
|
|
3
3
|
slot(v-if="!$slots.activator")
|
|
4
4
|
teleport(v-if="detachableDomReady" :to="teleportTarget" :disabled="!teleportTarget")
|
|
5
|
-
transition(:name="transitionName" appear @after-leave="onAfterLeave")
|
|
5
|
+
transition(:name="transitionName" appear @after-enter="onDetachableAfterEnter" @after-leave="onAfterLeave")
|
|
6
6
|
.w-menu(
|
|
7
7
|
v-if="custom && detachableVisible"
|
|
8
8
|
ref="detachable"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, useId, useAttrs } from 'vue'
|
|
44
44
|
import { objectifyClasses } from '../utils/index'
|
|
45
45
|
|
|
46
|
-
defineOptions({ name: 'WScrollable' })
|
|
46
|
+
defineOptions({ name: 'WScrollable', focusable: true })
|
|
47
47
|
|
|
48
48
|
const props = defineProps({
|
|
49
49
|
color: { type: String, default: 'primary' },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
slot(name="activator")
|
|
3
3
|
slot(v-if="!$slots.activator")
|
|
4
4
|
teleport(v-if="detachableDomReady" :to="teleportTarget" :disabled="!teleportTarget")
|
|
5
|
-
transition(:name="transitionName" appear @after-leave="onAfterLeave")
|
|
5
|
+
transition(:name="transitionName" appear @after-enter="onDetachableAfterEnter" @after-leave="onAfterLeave")
|
|
6
6
|
.w-tooltip(
|
|
7
7
|
v-if="detachableVisible"
|
|
8
8
|
ref="detachable"
|
package/src/wave-ui/core.js
CHANGED
|
@@ -5,7 +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
|
+
import { scheduleFocus, registerVFocus, unregisterVFocus } from './utils/focus'
|
|
9
9
|
import './scss/index.scss'
|
|
10
10
|
|
|
11
11
|
let mounted = false
|
|
@@ -113,7 +113,10 @@ export default class WaveUI {
|
|
|
113
113
|
static install (app, options = {}) {
|
|
114
114
|
// Register directives.
|
|
115
115
|
app.directive('focus', {
|
|
116
|
-
mounted: (el
|
|
116
|
+
mounted: (el) => {
|
|
117
|
+
if (!registerVFocus(el)) scheduleFocus(el)
|
|
118
|
+
},
|
|
119
|
+
unmounted: (el) => unregisterVFocus(el)
|
|
117
120
|
})
|
|
118
121
|
app.directive('scroll', {
|
|
119
122
|
mounted: (el, binding) => {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { consoleWarn } from '../utils/console'
|
|
12
|
+
import { callFocus } from '../utils/focus'
|
|
12
13
|
|
|
13
14
|
// Minimum space (px) from the viewport edge when flipping / nudging.
|
|
14
15
|
const VIEWPORT_MARGIN = 4
|
|
@@ -65,6 +66,10 @@ export default {
|
|
|
65
66
|
// Components use this to bind visibility:hidden, so the element is never visible at the
|
|
66
67
|
// wrong position before its coordinates are calculated.
|
|
67
68
|
detachableReady: false,
|
|
69
|
+
// Transition @after-enter has run for the current open cycle.
|
|
70
|
+
_contentEntered: false,
|
|
71
|
+
// v-focus entries waiting for detachableReady + _contentEntered.
|
|
72
|
+
_autofocusTargets: [],
|
|
68
73
|
// The Vue Teleport target. Stored as data (not computed) so it is resolved lazily at
|
|
69
74
|
// open()-time — after the DOM is committed — rather than during VNode creation where
|
|
70
75
|
// document.querySelector() may return null for elements that are part of the same render batch.
|
|
@@ -171,6 +176,35 @@ export default {
|
|
|
171
176
|
onAfterLeave () {
|
|
172
177
|
this.detachableReady = false
|
|
173
178
|
this.detachableEl = null
|
|
179
|
+
this._contentEntered = false
|
|
180
|
+
this._autofocusTargets = []
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
onDetachableAfterEnter () {
|
|
184
|
+
this._contentEntered = true
|
|
185
|
+
this._maybeFlushAutofocus()
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
registerAutofocus (entry) {
|
|
189
|
+
if (!this._autofocusTargets) this._autofocusTargets = []
|
|
190
|
+
this._autofocusTargets.push(entry)
|
|
191
|
+
this._maybeFlushAutofocus()
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
unregisterAutofocus (el) {
|
|
195
|
+
if (!this._autofocusTargets?.length) return
|
|
196
|
+
this._autofocusTargets = this._autofocusTargets.filter(entry => entry.el !== el)
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
_maybeFlushAutofocus () {
|
|
200
|
+
if (this.detachableReady && this._contentEntered) this.flushAutofocus()
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
flushAutofocus () {
|
|
204
|
+
const targets = this._autofocusTargets || []
|
|
205
|
+
this._autofocusTargets = []
|
|
206
|
+
if (!targets.length) return
|
|
207
|
+
this.$nextTick(() => targets.forEach(({ el }) => callFocus(el)))
|
|
174
208
|
},
|
|
175
209
|
|
|
176
210
|
unbindActivatorDocEvents () {
|
|
@@ -253,6 +287,7 @@ export default {
|
|
|
253
287
|
|
|
254
288
|
// Hide before entering the DOM; handles rapid re-opens where detachableReady is still true.
|
|
255
289
|
this.detachableReady = false
|
|
290
|
+
this._contentEntered = false
|
|
256
291
|
|
|
257
292
|
// Resolve the teleport target here, at open()-time, so the DOM is fully committed and
|
|
258
293
|
// detachableDefaultRoot() (for nested menus/tooltips) returns the correct element.
|
|
@@ -274,8 +309,11 @@ export default {
|
|
|
274
309
|
this.activatorWidth = this.activatorEl.offsetWidth
|
|
275
310
|
}
|
|
276
311
|
|
|
277
|
-
if (!this.noPosition) this.computeDetachableCoords()
|
|
278
|
-
else
|
|
312
|
+
if (!this.noPosition) await this.computeDetachableCoords()
|
|
313
|
+
else {
|
|
314
|
+
this.detachableReady = true
|
|
315
|
+
this._maybeFlushAutofocus()
|
|
316
|
+
}
|
|
279
317
|
|
|
280
318
|
// In `getActivatorCoordinates` accessing the menu computed styles takes a few ms (less than 10ms),
|
|
281
319
|
// if we don't postpone the Menu apparition it will start transition from a visible menu and
|
|
@@ -412,6 +450,7 @@ export default {
|
|
|
412
450
|
// Always await $nextTick so coordinates, placement class, and visibility are flushed atomically.
|
|
413
451
|
this.detachableReady = true
|
|
414
452
|
await this.$nextTick()
|
|
453
|
+
this._maybeFlushAutofocus()
|
|
415
454
|
|
|
416
455
|
// Guard against the component being unmounted while awaiting.
|
|
417
456
|
if (!this.detachableEl) return
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { guardFocusable, focusElement } from '../utils/focus'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
|
+
focusable: true,
|
|
5
|
+
|
|
4
6
|
methods: {
|
|
5
7
|
focus () {
|
|
6
8
|
if (!guardFocusable(this)) return
|
|
7
9
|
const refName = this.$options.focusTargetRef || 'input'
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const tryFocus = () => {
|
|
11
|
+
const target = this.$refs[refName]
|
|
12
|
+
if (target) focusElement(target)
|
|
13
|
+
return !!target
|
|
14
|
+
}
|
|
15
|
+
if (tryFocus()) return
|
|
16
|
+
this.$nextTick(() => {
|
|
17
|
+
if (tryFocus()) return
|
|
18
|
+
this.$nextTick(() => tryFocus())
|
|
19
|
+
})
|
|
11
20
|
}
|
|
12
21
|
}
|
|
13
22
|
}
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { nextTick } from 'vue'
|
|
2
2
|
|
|
3
|
+
const DETACHABLE_COMPONENTS = new Set(['w-menu', 'w-tooltip'])
|
|
4
|
+
|
|
5
|
+
/** Walk Vue 3 logical parent instances from a directive host element (`__vueParentComponent`). */
|
|
6
|
+
function walkParentInstances (el, predicate) {
|
|
7
|
+
let instance = el?.__vueParentComponent
|
|
8
|
+
while (instance) {
|
|
9
|
+
if (predicate(instance)) return instance
|
|
10
|
+
instance = instance.parent
|
|
11
|
+
}
|
|
12
|
+
return null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function componentName (instance) {
|
|
16
|
+
return instance?.type?.name ?? instance?.proxy?.$options?.name
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isFocusableInstance (instance) {
|
|
20
|
+
return !!(instance?.type?.focusable || instance?.proxy?.$options?.focusable)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isDetachableInstance (instance) {
|
|
24
|
+
return DETACHABLE_COMPONENTS.has(componentName(instance))
|
|
25
|
+
}
|
|
26
|
+
|
|
3
27
|
export function focusElement (el) {
|
|
4
28
|
el?.focus?.()
|
|
5
29
|
}
|
|
@@ -9,23 +33,20 @@ export function guardFocusable (vm) {
|
|
|
9
33
|
return true
|
|
10
34
|
}
|
|
11
35
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (typeof focusFn === 'function') return focusFn
|
|
22
|
-
instance = instance.parent
|
|
23
|
-
}
|
|
24
|
-
return null
|
|
36
|
+
/** Nearest ancestor component with `focusable: true` (e.g. w-input inside w-form-element). */
|
|
37
|
+
export function resolveFocusableInstance (el) {
|
|
38
|
+
return walkParentInstances(el, isFocusableInstance)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** w-menu / w-tooltip host for deferred v-focus while floating content opens. */
|
|
42
|
+
export function findDetachableInstance (el) {
|
|
43
|
+
const instance = walkParentInstances(el, isDetachableInstance)
|
|
44
|
+
return instance?.proxy ?? null
|
|
25
45
|
}
|
|
26
46
|
|
|
27
|
-
export function callFocus (
|
|
28
|
-
const
|
|
47
|
+
export function callFocus (el) {
|
|
48
|
+
const instance = resolveFocusableInstance(el)
|
|
49
|
+
const focusFn = instance?.exposed?.focus ?? instance?.proxy?.focus
|
|
29
50
|
if (typeof focusFn === 'function') {
|
|
30
51
|
focusFn()
|
|
31
52
|
return
|
|
@@ -34,6 +55,20 @@ export function callFocus (vnode, el) {
|
|
|
34
55
|
}
|
|
35
56
|
|
|
36
57
|
/** Schedule focus after mount so template refs exist on the host component. */
|
|
37
|
-
export function scheduleFocus (
|
|
38
|
-
nextTick(() => callFocus(
|
|
58
|
+
export function scheduleFocus (el) {
|
|
59
|
+
nextTick(() => callFocus(el))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Register v-focus on a detachable host; returns false when not inside w-menu/w-tooltip. */
|
|
63
|
+
export function registerVFocus (el) {
|
|
64
|
+
const detachable = findDetachableInstance(el)
|
|
65
|
+
if (!detachable) return false
|
|
66
|
+
el.__waveUiDetachable = detachable
|
|
67
|
+
detachable.registerAutofocus({ el })
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function unregisterVFocus (el) {
|
|
72
|
+
el.__waveUiDetachable?.unregisterAutofocus(el)
|
|
73
|
+
delete el.__waveUiDetachable
|
|
39
74
|
}
|