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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-ui",
3
- "version": "4.0.2",
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",
@@ -90,10 +90,12 @@ 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
94
 
94
95
  export default {
95
96
  name: 'w-accordion-item',
96
-
97
+ focusable: true,
98
+ expose: ['focus'],
97
99
  mixins: [RippleMixin],
98
100
 
99
101
  setup () {
@@ -161,6 +163,11 @@ export default {
161
163
  },
162
164
 
163
165
  methods: {
166
+ focus () {
167
+ if (this.accordionItem._disabled) return
168
+ focusElement(this.$el.querySelector('.w-accordion__item-title'))
169
+ },
170
+
164
171
  onAccordionTitlePointerDown (e) {
165
172
  if (this.accordionItem._disabled) return
166
173
  if (e.target.closest?.('.w-accordion__expand-icon')) return
@@ -113,10 +113,12 @@ component(
113
113
  <script>
114
114
  import { computed } from 'vue'
115
115
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
116
+ import FocusableMixin from '../mixins/focusable'
116
117
 
117
118
  export default {
118
119
  name: 'w-autocomplete',
119
- mixins: [FormElementMixin],
120
+ expose: ['focus'],
121
+ mixins: [FormElementMixin, FocusableMixin],
120
122
  inheritAttrs: false,
121
123
 
122
124
  setup () {
@@ -1,10 +1,10 @@
1
1
  <template lang="pug">
2
2
  component(v-if="tooltip" is="w-tooltip" v-bind="tooltipProps")
3
- button-partial(v-bind="buttonProps")
3
+ button-partial(ref="button" v-bind="buttonProps")
4
4
  slot
5
5
  template(#tooltip)
6
6
  div(v-html="tooltip")
7
- button-partial(v-else v-bind="buttonProps")
7
+ button-partial(v-else ref="button" v-bind="buttonProps")
8
8
  slot
9
9
  template(#loading)
10
10
  slot(name="loading")
@@ -12,9 +12,12 @@ button-partial(v-else v-bind="buttonProps")
12
12
 
13
13
  <script>
14
14
  import ButtonPartial from './button.vue'
15
+ import { focusElement } from '../../utils/focus'
15
16
 
16
17
  export default {
17
18
  name: 'w-button',
19
+ focusable: true,
20
+ expose: ['focus'],
18
21
  inheritAttrs: false, // The attrs are only bound to the button-partial, not the root.
19
22
 
20
23
  props: {
@@ -66,6 +69,13 @@ export default {
66
69
  const { tooltip, tooltipProps = {}, ...props } = this.$props
67
70
  return { ...props, ...this.$attrs }
68
71
  }
72
+ },
73
+
74
+ methods: {
75
+ focus () {
76
+ if (this.disabled) return
77
+ focusElement(this.$refs.button?.$el)
78
+ }
69
79
  }
70
80
  }
71
81
  </script>
@@ -50,10 +50,12 @@ component(
50
50
 
51
51
  <script>
52
52
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
53
+ import FocusableMixin from '../mixins/focusable'
53
54
 
54
55
  export default {
55
56
  name: 'w-checkbox',
56
- mixins: [FormElementMixin],
57
+ expose: ['focus'],
58
+ mixins: [FormElementMixin, FocusableMixin],
57
59
 
58
60
  setup () {
59
61
  return useWaveUiFormIds()
@@ -9,6 +9,7 @@ component(
9
9
  :wrap="inline"
10
10
  :class="classes")
11
11
  w-checkbox(
12
+ ref="item"
12
13
  v-for="(item, i) in checkboxItems"
13
14
  :key="i"
14
15
  :model-value="item._isChecked"
@@ -32,9 +33,12 @@ component(
32
33
  <script>
33
34
  import { reactive } from 'vue'
34
35
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
36
+ import { guardFocusable } from '../utils/focus'
35
37
 
36
38
  export default {
37
39
  name: 'w-checkboxes',
40
+ focusable: true,
41
+ expose: ['focus'],
38
42
  mixins: [FormElementMixin],
39
43
 
40
44
  setup () {
@@ -104,6 +108,12 @@ export default {
104
108
  },
105
109
 
106
110
  methods: {
111
+ focus () {
112
+ if (!guardFocusable(this)) return
113
+ const items = [].concat(this.$refs.item || []).filter(Boolean)
114
+ items.find(c => !c.isDisabled && !c.isReadonly)?.focus?.()
115
+ },
116
+
107
117
  reset () {
108
118
  this.checkboxItems.forEach(item => (item._isChecked = null))
109
119
  this.$emit('update:modelValue', [])
@@ -1,5 +1,6 @@
1
1
  <template lang="pug">
2
2
  w-overlay.w-dialog(
3
+ ref="overlay"
3
4
  :model-value="showWrapper"
4
5
  :persistent="persistent"
5
6
  :persistent-no-animation="persistentNoAnimation"
@@ -32,6 +33,7 @@ import { objectifyClasses } from '../utils/index'
32
33
 
33
34
  export default {
34
35
  name: 'w-dialog',
36
+ expose: ['focus'],
35
37
 
36
38
  props: {
37
39
  modelValue: { default: true },
@@ -103,6 +105,10 @@ export default {
103
105
  },
104
106
 
105
107
  methods: {
108
+ focus () {
109
+ this.$refs.overlay?.focus?.()
110
+ },
111
+
106
112
  onOutsideClick () {
107
113
  if (!this.persistent) {
108
114
  this.showContent = false
@@ -5,6 +5,7 @@
5
5
  .w-drawer-wrap__pushable
6
6
  w-overlay(
7
7
  v-if="!noOverlay"
8
+ ref="overlay"
8
9
  v-model="showDrawer"
9
10
  @click="onOutsideClick"
10
11
  :persistent="persistent"
@@ -21,12 +22,15 @@
21
22
  ref="drawer"
22
23
  :is="tag || 'aside'"
23
24
  :class="drawerClasses"
24
- :style="styles")
25
+ :style="styles"
26
+ :tabindex="noOverlay ? 0 : null"
27
+ @keydown.escape.stop="noOverlay && !persistent && onOutsideClick()")
25
28
  slot
26
29
  //- Other cases.
27
30
  template(v-else)
28
31
  w-overlay(
29
32
  v-if="!noOverlay"
33
+ ref="overlay"
30
34
  v-model="showDrawer"
31
35
  @click="onOutsideClick"
32
36
  :persistent="persistent"
@@ -43,11 +47,14 @@
43
47
  ref="drawer"
44
48
  :is="tag || 'aside'"
45
49
  :class="drawerClasses"
46
- :style="styles")
50
+ :style="styles"
51
+ :tabindex="noOverlay ? 0 : null"
52
+ @keydown.escape.stop="noOverlay && !persistent && onOutsideClick()")
47
53
  slot
48
54
  </template>
49
55
 
50
56
  <script>
57
+ import { focusElement } from '../utils/focus'
51
58
  // The complexity in this component is on close:
52
59
  // we must keep the wrapper in the DOM until the drawer transition is finished.
53
60
  // Then emit the modelValue update that will trigger the removal of the wrapper from the DOM.
@@ -56,6 +63,7 @@ const oppositeSides = { left: 'right', right: 'left', top: 'down', bottom: 'up'
56
63
 
57
64
  export default {
58
65
  name: 'w-drawer',
66
+ expose: ['focus'],
59
67
 
60
68
  props: {
61
69
  modelValue: { default: true },
@@ -164,6 +172,11 @@ export default {
164
172
  },
165
173
 
166
174
  methods: {
175
+ focus () {
176
+ if (this.$refs.overlay) this.$refs.overlay.focus()
177
+ else focusElement(this.$refs.drawer?.$el || this.$refs.drawer)
178
+ },
179
+
167
180
  onBeforeClose () {
168
181
  this.$emit('before-close')
169
182
  },
@@ -129,11 +129,13 @@ component(
129
129
  **/
130
130
 
131
131
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
132
+ import FocusableMixin from '../mixins/focusable'
132
133
  import { reactive } from 'vue'
133
134
 
134
135
  export default {
135
136
  name: 'w-input',
136
- mixins: [FormElementMixin],
137
+ expose: ['focus'],
138
+ mixins: [FormElementMixin, FocusableMixin],
137
139
  inheritAttrs: false, // The attrs should only be added to the input not the wrapper.
138
140
 
139
141
  setup () {
@@ -45,10 +45,12 @@ 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
49
 
49
50
  export default {
50
51
  name: 'w-list',
51
-
52
+ focusable: true,
53
+ expose: ['focus'],
52
54
  mixins: [RippleMixin],
53
55
 
54
56
  setup () {
@@ -138,6 +140,14 @@ export default {
138
140
  },
139
141
 
140
142
  methods: {
143
+ focus () {
144
+ const selected = this.listItems.find(li => li._selected && !li.disabled)
145
+ const target = selected || this.listItems.find(li => !li.disabled)
146
+ if (!target) return
147
+ if (this.listId) focusElement(document.getElementById(`${this.listId}_item-${target._index + 1}`))
148
+ else focusElement(this.$el.querySelector('[role="option"][tabindex="0"]'))
149
+ },
150
+
141
151
  // If object, get the item value, if none, get the item label, if none get the id.
142
152
  // If simple value, return as is.
143
153
  getItemValue (item) {
@@ -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"
@@ -48,9 +48,11 @@ teleport(v-if="detachableDomReady" :to="teleportTarget" :disabled="!teleportTarg
48
48
  <script>
49
49
  import { objectifyClasses } from '../utils/index'
50
50
  import DetachableMixin from '../mixins/detachable'
51
+ import { focusElement } from '../utils/focus'
51
52
 
52
53
  export default {
53
54
  name: 'w-menu',
55
+ expose: ['focus'],
54
56
  mixins: [DetachableMixin],
55
57
  inheritAttrs: false, // The attrs are only bound to the button-partial, not the root.
56
58
 
@@ -215,6 +217,11 @@ export default {
215
217
  },
216
218
 
217
219
  methods: {
220
+ focus () {
221
+ if (this.overlay && this.$refs.overlay) this.$refs.overlay.focus()
222
+ else focusElement(this.activatorEl)
223
+ },
224
+
218
225
  /**
219
226
  * Other methods in the `detachable` mixin:
220
227
  * - `open`
@@ -5,17 +5,18 @@ transition(name="fade" appear @after-leave="onClose")
5
5
  v-show="showOverlay"
6
6
  ref="overlay"
7
7
  :style="(modelValue && styles) || null"
8
- @keydown.escape.stop="onClick"
9
8
  @click="onClick"
10
- v-focus
11
9
  tabindex="0"
12
10
  :class="classes")
13
11
  slot
14
12
  </template>
15
13
 
16
14
  <script>
15
+ import { focusElement } from '../utils/focus'
16
+
17
17
  export default {
18
18
  name: 'w-overlay',
19
+ expose: ['focus'],
19
20
 
20
21
  props: {
21
22
  modelValue: {},
@@ -39,7 +40,8 @@ export default {
39
40
 
40
41
  data: () => ({
41
42
  persistentAnimate: false,
42
- showOverlay: false
43
+ showOverlay: false,
44
+ documentEscapeBound: false
43
45
  }),
44
46
 
45
47
  computed: {
@@ -61,11 +63,29 @@ export default {
61
63
  },
62
64
 
63
65
  methods: {
64
- onClick (e) {
65
- // Don't react to a click inside the content (event bubbling).
66
- if (!e.target.classList.contains('w-overlay')) return
66
+ focus () {
67
+ focusElement(this.$refs.overlay)
68
+ },
67
69
 
68
- // Quickly add the animation class and remove it.
70
+ bindDocumentEscape () {
71
+ if (this.documentEscapeBound) return
72
+ document.addEventListener('keydown', this.onDocumentEscape)
73
+ this.documentEscapeBound = true
74
+ },
75
+
76
+ unbindDocumentEscape () {
77
+ if (!this.documentEscapeBound) return
78
+ document.removeEventListener('keydown', this.onDocumentEscape)
79
+ this.documentEscapeBound = false
80
+ },
81
+
82
+ onDocumentEscape (e) {
83
+ if (e.key !== 'Escape') return
84
+ if (!this.showOverlay || !this.modelValue) return
85
+ this.dismiss(e)
86
+ },
87
+
88
+ dismiss (e) {
69
89
  if (this.persistent && !this.persistentNoAnimation) {
70
90
  this.persistentAnimate = true
71
91
  setTimeout(() => (this.persistentAnimate = false), 150) // Must match CSS animation duration.
@@ -78,9 +98,16 @@ export default {
78
98
  this.$emit('click', e)
79
99
  },
80
100
 
101
+ onClick (e) {
102
+ // Don't react to a click inside the content (event bubbling).
103
+ if (!e.target.classList.contains('w-overlay')) return
104
+ this.dismiss(e)
105
+ },
106
+
81
107
  // Wait until the end of the closing transition (v-show) to completely unmount (v-if).
82
108
  // The onClose method is called twice from the transition: once for the v-show, and once for the v-if.
83
109
  onClose () {
110
+ this.unbindDocumentEscape()
84
111
  this.$emit('update:modelValue', false)
85
112
  this.$emit('input', false)
86
113
  if (!this.modelValue) this.$emit('close') // Only emit once.
@@ -89,11 +116,20 @@ export default {
89
116
 
90
117
  created () {
91
118
  this.showOverlay = this.modelValue
119
+ if (this.modelValue) this.bindDocumentEscape()
120
+ },
121
+
122
+ unmounted () {
123
+ this.unbindDocumentEscape()
92
124
  },
93
125
 
94
126
  watch: {
95
127
  modelValue (bool) {
96
- if (bool) this.showOverlay = true
128
+ if (bool) {
129
+ this.showOverlay = true
130
+ this.bindDocumentEscape()
131
+ }
132
+ else this.unbindDocumentEscape()
97
133
  }
98
134
  }
99
135
  }
@@ -48,10 +48,12 @@ component(
48
48
 
49
49
  <script>
50
50
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
51
+ import FocusableMixin from '../mixins/focusable'
51
52
 
52
53
  export default {
53
54
  name: 'w-radio',
54
- mixins: [FormElementMixin],
55
+ expose: ['focus'],
56
+ mixins: [FormElementMixin, FocusableMixin],
55
57
 
56
58
  setup () {
57
59
  return useWaveUiFormIds()
@@ -9,6 +9,7 @@ component(
9
9
  :wrap="inline"
10
10
  :class="classes")
11
11
  w-radio(
12
+ ref="item"
12
13
  v-for="(item, i) in radioItems"
13
14
  :key="i"
14
15
  :model-value="item.value === modelValue"
@@ -31,9 +32,12 @@ component(
31
32
 
32
33
  <script>
33
34
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
35
+ import { guardFocusable } from '../utils/focus'
34
36
 
35
37
  export default {
36
38
  name: 'w-radios',
39
+ focusable: true,
40
+ expose: ['focus'],
37
41
  mixins: [FormElementMixin],
38
42
 
39
43
  setup () {
@@ -97,6 +101,12 @@ export default {
97
101
  },
98
102
 
99
103
  methods: {
104
+ focus () {
105
+ if (!guardFocusable(this)) return
106
+ const items = [].concat(this.$refs.item || []).filter(Boolean)
107
+ items.find(c => !c.isDisabled && !c.isReadonly)?.focus?.()
108
+ },
109
+
100
110
  onInput (item) {
101
111
  this.inputValue = true
102
112
  this.$emit('update:modelValue', item.value)
@@ -30,9 +30,12 @@ component(
30
30
 
31
31
  <script>
32
32
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
33
+ import { guardFocusable, focusElement } from '../utils/focus'
33
34
 
34
35
  export default {
35
36
  name: 'w-rating',
37
+ focusable: true,
38
+ expose: ['focus'],
36
39
  mixins: [FormElementMixin],
37
40
 
38
41
  setup () {
@@ -104,6 +107,11 @@ export default {
104
107
  },
105
108
 
106
109
  methods: {
110
+ focus () {
111
+ if (!guardFocusable(this)) return
112
+ focusElement(this.$el.querySelector('.w-rating__button:not([disabled])'))
113
+ },
114
+
107
115
  onButtonClick (i) {
108
116
  this.rating = i
109
117
  this.$emit('update:modelValue', this.rating)
@@ -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' },
@@ -110,10 +110,13 @@ component(
110
110
 
111
111
  import { computed } from 'vue'
112
112
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
113
+ import FocusableMixin from '../mixins/focusable'
114
+ import { guardFocusable, focusElement } from '../utils/focus'
113
115
 
114
116
  export default {
115
117
  name: 'w-select',
116
- mixins: [FormElementMixin],
118
+ expose: ['focus'],
119
+ mixins: [FormElementMixin, FocusableMixin],
117
120
 
118
121
  setup (props) {
119
122
  const { waveUiUseId } = useWaveUiFormIds()
@@ -259,6 +262,12 @@ export default {
259
262
  },
260
263
 
261
264
  methods: {
265
+ focus () {
266
+ if (!guardFocusable(this)) return
267
+ focusElement(this.$refs['selection-input'])
268
+ if (!this.showMenu) this.openMenu()
269
+ },
270
+
262
271
  onFocus (e) {
263
272
  if (this.isFocused) return
264
273
 
@@ -79,10 +79,13 @@ component(
79
79
 
80
80
  <script>
81
81
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
82
+ import FocusableMixin from '../mixins/focusable'
82
83
 
83
84
  export default {
84
85
  name: 'w-slider',
85
- mixins: [FormElementMixin],
86
+ expose: ['focus'],
87
+ focusTargetRef: 'thumb',
88
+ mixins: [FormElementMixin, FocusableMixin],
86
89
 
87
90
  setup () {
88
91
  return useWaveUiFormIds()
@@ -55,10 +55,12 @@ component(
55
55
 
56
56
  <script>
57
57
  import FormElementMixin, { useWaveUiFormIds } from '../mixins/form-elements'
58
+ import FocusableMixin from '../mixins/focusable'
58
59
 
59
60
  export default {
60
61
  name: 'w-switch',
61
- mixins: [FormElementMixin],
62
+ expose: ['focus'],
63
+ mixins: [FormElementMixin, FocusableMixin],
62
64
  inheritAttrs: false, // The attrs should only be added to the input not the wrapper.
63
65
 
64
66
  setup () {
@@ -76,10 +76,12 @@ 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
+ focusable: true,
84
+ expose: ['focus'],
83
85
  mixins: [RippleMixin],
84
86
 
85
87
  setup () {
@@ -183,6 +185,14 @@ export default {
183
185
  },
184
186
 
185
187
  methods: {
188
+ focus () {
189
+ const bar = this.$refs['tabs-bar']
190
+ if (!bar) return
191
+ const tab = bar.querySelector('[role="tab"][aria-selected="true"]:not([tabindex="-1"])')
192
+ || bar.querySelector('[role="tab"][tabindex="0"]')
193
+ focusElement(tab)
194
+ },
195
+
186
196
  resolveTabUid (value) {
187
197
  if (!this.tabs.length) return null
188
198
  if (value === undefined || value === null || value === '') return this.tabs[0]._uid
@@ -455,6 +465,12 @@ export default {
455
465
  margin-right: -1px;
456
466
  }
457
467
  .w-tabs--card &--active {border-bottom-color: transparent;}
468
+ .w-tabs--pill-slider & {
469
+ padding: calc(var(--w-base-increment) * 1) calc(var(--w-base-increment) * 2);
470
+ margin: calc(var(--w-base-increment) * 1) calc(var(--w-base-increment) * 1);
471
+ border-radius: 99em;
472
+ font-size: round(nearest, calc(1.1 * var(--w-base-font-size)), 1px);
473
+ }
458
474
 
459
475
  &--disabled {
460
476
  cursor: not-allowed;
@@ -19,10 +19,12 @@ 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
+ focusable: true,
27
+ expose: ['focus'],
26
28
  mixins: [RippleMixin],
27
29
 
28
30
  props: {
@@ -48,6 +50,13 @@ export default {
48
50
 
49
51
  emits: ['input', 'update:modelValue'],
50
52
 
53
+ methods: {
54
+ focus () {
55
+ if (this.modelValue === -1) return
56
+ focusElement(this.$el)
57
+ }
58
+ },
59
+
51
60
  computed: {
52
61
  presetSize () {
53
62
  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
- mixins: [FormElementMixin],
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 () {
@@ -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"
@@ -68,8 +68,12 @@ 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
+ focusable: true,
76
+ expose: ['focus'],
73
77
 
74
78
  mixins: [RippleMixin],
75
79
 
@@ -122,6 +126,11 @@ export default {
122
126
  },
123
127
 
124
128
  methods: {
129
+ focus () {
130
+ if (this.disabled) return
131
+ focusElement(this.$el.querySelector('.w-tree__item-label[tabindex="0"]'))
132
+ },
133
+
125
134
  // From data watcher, retain the oldItems open state.
126
135
  updateCurrentDepthTree (items, oldItems = []) {
127
136
  this.currentDepthItems = []