wave-ui 4.2.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-ui",
3
- "version": "4.2.4",
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] },
@@ -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 { scheduleFocus, registerVFocus, unregisterVFocus } from './utils/focus'
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(({ el }) => callFocus(el)))
206
+ this.$nextTick(() => targets.forEach(({ focus }) => focus()))
208
207
  },
209
208
 
210
209
  unbindActivatorDocEvents () {
@@ -1,6 +1,7 @@
1
- import { guardFocusable, focusElement } from '../utils/focus'
1
+ import { guardFocusable, focusElement, vFocusLifecycleMixin } from '../utils/focus'
2
2
 
3
3
  export default {
4
+ mixins: [vFocusLifecycleMixin],
4
5
  focusable: true,
5
6
 
6
7
  methods: {
@@ -1,12 +1,24 @@
1
- import { nextTick } from 'vue'
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
- const DETACHABLE_COMPONENTS = new Set(['w-menu', 'w-tooltip'])
14
+ import { nextTick, getCurrentInstance, onMounted, onUnmounted } from 'vue'
4
15
 
5
- /** Walk Vue 3 logical parent instances from a directive host element (`__vueParentComponent`). */
6
- function walkParentInstances (el, predicate) {
7
- let instance = el?.__vueParentComponent
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 (predicate(instance)) return instance
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 isFocusableInstance (instance) {
31
+ function isFocusable (instance) {
20
32
  return !!(instance?.type?.focusable || instance?.proxy?.$options?.focusable)
21
33
  }
22
34
 
23
- function isDetachableInstance (instance) {
24
- return DETACHABLE_COMPONENTS.has(componentName(instance))
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
- /** Nearest ancestor component with `focusable: true` (e.g. w-input inside w-form-element). */
37
- export function resolveFocusableInstance (el) {
38
- return walkParentInstances(el, isFocusableInstance)
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
- /** 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
107
+ function hasVFocus (vnode) {
108
+ return !!vnode?.dirs?.some(d => d.dir === focusDirective)
45
109
  }
46
110
 
47
- export function callFocus (el) {
48
- const instance = resolveFocusableInstance(el)
49
- const focusFn = instance?.exposed?.focus ?? instance?.proxy?.focus
50
- if (typeof focusFn === 'function') {
51
- focusFn()
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
- focusElement(el)
124
+ focus()
55
125
  }
56
126
 
57
- /** Schedule focus after mount so template refs exist on the host component. */
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
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
- export function unregisterVFocus (el) {
72
- el.__waveUiDetachable?.unregisterAutofocus(el)
73
- delete el.__waveUiDetachable
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
  }