wave-ui 4.0.2 → 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.
Files changed (33) hide show
  1. package/dist/wave-ui.cjs.js +3 -3
  2. package/dist/wave-ui.css +1 -1
  3. package/dist/wave-ui.esm.js +1355 -1145
  4. package/dist/wave-ui.umd.js +3 -3
  5. package/package.json +1 -1
  6. package/src/wave-ui/components/w-accordion/item.vue +8 -1
  7. package/src/wave-ui/components/w-autocomplete.vue +3 -1
  8. package/src/wave-ui/components/w-button/index.vue +12 -2
  9. package/src/wave-ui/components/w-checkbox.vue +3 -1
  10. package/src/wave-ui/components/w-checkboxes.vue +10 -0
  11. package/src/wave-ui/components/w-dialog.vue +6 -0
  12. package/src/wave-ui/components/w-drawer.vue +15 -2
  13. package/src/wave-ui/components/w-input.vue +3 -1
  14. package/src/wave-ui/components/w-list.vue +11 -1
  15. package/src/wave-ui/components/w-menu.vue +8 -1
  16. package/src/wave-ui/components/w-overlay.vue +44 -8
  17. package/src/wave-ui/components/w-radio.vue +3 -1
  18. package/src/wave-ui/components/w-radios.vue +10 -0
  19. package/src/wave-ui/components/w-rating.vue +8 -0
  20. package/src/wave-ui/components/w-scrollable.vue +1 -1
  21. package/src/wave-ui/components/w-select.vue +10 -1
  22. package/src/wave-ui/components/w-slider.vue +4 -1
  23. package/src/wave-ui/components/w-switch.vue +3 -1
  24. package/src/wave-ui/components/w-tabs/index.vue +17 -1
  25. package/src/wave-ui/components/w-tag.vue +10 -1
  26. package/src/wave-ui/components/w-textarea.vue +4 -1
  27. package/src/wave-ui/components/w-tooltip.vue +1 -1
  28. package/src/wave-ui/components/w-tree.vue +9 -0
  29. package/src/wave-ui/core.js +5 -2
  30. package/src/wave-ui/mixins/detachable.js +41 -2
  31. package/src/wave-ui/mixins/focusable.js +22 -0
  32. package/src/wave-ui/scss/_base.scss +4 -0
  33. package/src/wave-ui/utils/focus.js +74 -0
@@ -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, registerVFocus, unregisterVFocus } from './utils/focus'
8
9
  import './scss/index.scss'
9
10
 
10
11
  let mounted = false
@@ -112,8 +113,10 @@ export default class WaveUI {
112
113
  static install (app, options = {}) {
113
114
  // Register directives.
114
115
  app.directive('focus', {
115
- // Wait for the next tick to focus the newly mounted element.
116
- mounted: el => setTimeout(() => el.focus(), 0)
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 this.detachableReady = true
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
@@ -0,0 +1,22 @@
1
+ import { guardFocusable, focusElement } from '../utils/focus'
2
+
3
+ export default {
4
+ focusable: true,
5
+
6
+ methods: {
7
+ focus () {
8
+ if (!guardFocusable(this)) return
9
+ const refName = this.$options.focusTargetRef || 'input'
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
+ })
20
+ }
21
+ }
22
+ }
@@ -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,74 @@
1
+ import { nextTick } from 'vue'
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
+
27
+ export function focusElement (el) {
28
+ el?.focus?.()
29
+ }
30
+
31
+ export function guardFocusable (vm) {
32
+ if (vm.isDisabled || vm.isReadonly) return false
33
+ return true
34
+ }
35
+
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
45
+ }
46
+
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()
52
+ return
53
+ }
54
+ focusElement(el)
55
+ }
56
+
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
69
+ }
70
+
71
+ export function unregisterVFocus (el) {
72
+ el.__waveUiDetachable?.unregisterAutofocus(el)
73
+ delete el.__waveUiDetachable
74
+ }