wave-ui 4.0.2 → 4.1.0

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.0.2",
3
+ "version": "4.1.0",
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,11 @@ 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
+ expose: ['focus'],
97
98
  mixins: [RippleMixin],
98
99
 
99
100
  setup () {
@@ -161,6 +162,11 @@ export default {
161
162
  },
162
163
 
163
164
  methods: {
165
+ focus () {
166
+ if (this.accordionItem._disabled) return
167
+ focusElement(this.$el.querySelector('.w-accordion__item-title'))
168
+ },
169
+
164
170
  onAccordionTitlePointerDown (e) {
165
171
  if (this.accordionItem._disabled) return
166
172
  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,11 @@ 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
+ expose: ['focus'],
18
20
  inheritAttrs: false, // The attrs are only bound to the button-partial, not the root.
19
21
 
20
22
  props: {
@@ -66,6 +68,13 @@ export default {
66
68
  const { tooltip, tooltipProps = {}, ...props } = this.$props
67
69
  return { ...props, ...this.$attrs }
68
70
  }
71
+ },
72
+
73
+ methods: {
74
+ focus () {
75
+ if (this.disabled) return
76
+ focusElement(this.$refs.button?.$el)
77
+ }
69
78
  }
70
79
  }
71
80
  </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,11 @@ 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
+ expose: ['focus'],
38
41
  mixins: [FormElementMixin],
39
42
 
40
43
  setup () {
@@ -104,6 +107,12 @@ export default {
104
107
  },
105
108
 
106
109
  methods: {
110
+ focus () {
111
+ if (!guardFocusable(this)) return
112
+ const items = [].concat(this.$refs.item || []).filter(Boolean)
113
+ items.find(c => !c.isDisabled && !c.isReadonly)?.focus?.()
114
+ },
115
+
107
116
  reset () {
108
117
  this.checkboxItems.forEach(item => (item._isChecked = null))
109
118
  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,11 @@ 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
+ expose: ['focus'],
52
53
  mixins: [RippleMixin],
53
54
 
54
55
  setup () {
@@ -138,6 +139,14 @@ export default {
138
139
  },
139
140
 
140
141
  methods: {
142
+ focus () {
143
+ const selected = this.listItems.find(li => li._selected && !li.disabled)
144
+ const target = selected || this.listItems.find(li => !li.disabled)
145
+ if (!target) return
146
+ if (this.listId) focusElement(document.getElementById(`${this.listId}_item-${target._index + 1}`))
147
+ else focusElement(this.$el.querySelector('[role="option"][tabindex="0"]'))
148
+ },
149
+
141
150
  // If object, get the item value, if none, get the item label, if none get the id.
142
151
  // If simple value, return as is.
143
152
  getItemValue (item) {
@@ -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,11 @@ 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
+ expose: ['focus'],
37
40
  mixins: [FormElementMixin],
38
41
 
39
42
  setup () {
@@ -97,6 +100,12 @@ export default {
97
100
  },
98
101
 
99
102
  methods: {
103
+ focus () {
104
+ if (!guardFocusable(this)) return
105
+ const items = [].concat(this.$refs.item || []).filter(Boolean)
106
+ items.find(c => !c.isDisabled && !c.isReadonly)?.focus?.()
107
+ },
108
+
100
109
  onInput (item) {
101
110
  this.inputValue = true
102
111
  this.$emit('update:modelValue', item.value)
@@ -30,9 +30,11 @@ 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
+ expose: ['focus'],
36
38
  mixins: [FormElementMixin],
37
39
 
38
40
  setup () {
@@ -104,6 +106,11 @@ export default {
104
106
  },
105
107
 
106
108
  methods: {
109
+ focus () {
110
+ if (!guardFocusable(this)) return
111
+ focusElement(this.$el.querySelector('.w-rating__button:not([disabled])'))
112
+ },
113
+
107
114
  onButtonClick (i) {
108
115
  this.rating = i
109
116
  this.$emit('update:modelValue', this.rating)
@@ -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,11 @@ 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
+ expose: ['focus'],
83
84
  mixins: [RippleMixin],
84
85
 
85
86
  setup () {
@@ -183,6 +184,14 @@ export default {
183
184
  },
184
185
 
185
186
  methods: {
187
+ focus () {
188
+ const bar = this.$refs['tabs-bar']
189
+ if (!bar) return
190
+ const tab = bar.querySelector('[role="tab"][aria-selected="true"]:not([tabindex="-1"])')
191
+ || bar.querySelector('[role="tab"][tabindex="0"]')
192
+ focusElement(tab)
193
+ },
194
+
186
195
  resolveTabUid (value) {
187
196
  if (!this.tabs.length) return null
188
197
  if (value === undefined || value === null || value === '') return this.tabs[0]._uid
@@ -455,6 +464,12 @@ export default {
455
464
  margin-right: -1px;
456
465
  }
457
466
  .w-tabs--card &--active {border-bottom-color: transparent;}
467
+ .w-tabs--pill-slider & {
468
+ padding: calc(var(--w-base-increment) * 1) calc(var(--w-base-increment) * 2);
469
+ margin: calc(var(--w-base-increment) * 1) calc(var(--w-base-increment) * 1);
470
+ border-radius: 99em;
471
+ font-size: round(nearest, calc(1.1 * var(--w-base-font-size)), 1px);
472
+ }
458
473
 
459
474
  &--disabled {
460
475
  cursor: not-allowed;
@@ -19,10 +19,11 @@ 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
+ expose: ['focus'],
26
27
  mixins: [RippleMixin],
27
28
 
28
29
  props: {
@@ -48,6 +49,13 @@ export default {
48
49
 
49
50
  emits: ['input', 'update:modelValue'],
50
51
 
52
+ methods: {
53
+ focus () {
54
+ if (this.modelValue === -1) return
55
+ focusElement(this.$el)
56
+ }
57
+ },
58
+
51
59
  computed: {
52
60
  presetSize () {
53
61
  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 () {
@@ -68,8 +68,11 @@ 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
+ expose: ['focus'],
73
76
 
74
77
  mixins: [RippleMixin],
75
78
 
@@ -122,6 +125,11 @@ export default {
122
125
  },
123
126
 
124
127
  methods: {
128
+ focus () {
129
+ if (this.disabled) return
130
+ focusElement(this.$el.querySelector('.w-tree__item-label[tabindex="0"]'))
131
+ },
132
+
125
133
  // From data watcher, retain the oldItems open state.
126
134
  updateCurrentDepthTree (items, oldItems = []) {
127
135
  this.currentDepthItems = []
@@ -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 } from './utils/focus'
8
9
  import './scss/index.scss'
9
10
 
10
11
  let mounted = false
@@ -112,8 +113,7 @@ 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, binding, vnode) => scheduleFocus(vnode, el)
117
117
  })
118
118
  app.directive('scroll', {
119
119
  mounted: (el, binding) => {
@@ -0,0 +1,13 @@
1
+ import { guardFocusable, focusElement } from '../utils/focus'
2
+
3
+ export default {
4
+ methods: {
5
+ focus () {
6
+ if (!guardFocusable(this)) return
7
+ const refName = this.$options.focusTargetRef || 'input'
8
+ const target = this.$refs[refName]
9
+ if (target) focusElement(target)
10
+ else this.$nextTick(() => focusElement(this.$refs[refName]))
11
+ }
12
+ }
13
+ }