wave-ui 2.30.0 → 2.31.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": "2.30.0",
3
+ "version": "2.31.0",
4
4
  "description": "An emerging UI framework for Vue.js & Vue 3 with only the bright side. :sunny:",
5
5
  "author": "Antoni Andre <antoniandre.web@gmail.com>",
6
6
  "main": "./dist/wave-ui.umd.js",
@@ -43,29 +43,32 @@
43
43
  "*.vue"
44
44
  ],
45
45
  "devDependencies": {
46
- "@babel/core": "^7.16.5",
47
- "@babel/eslint-parser": "^7.16.5",
48
- "@babel/plugin-proposal-class-properties": "^7.16.5",
46
+ "@babel/core": "^7.17.0",
47
+ "@babel/eslint-parser": "^7.17.0",
48
+ "@babel/plugin-proposal-class-properties": "^7.16.7",
49
49
  "@mdi/font": "^5.9.55",
50
50
  "@vitejs/plugin-vue": "^1.10.2",
51
51
  "@vue/compiler-sfc": "3.1.5",
52
- "autoprefixer": "^10.4.1",
53
- "axios": "^0.21.4",
52
+ "autoprefixer": "^10.4.2",
53
+ "axios": "^0.25.0",
54
54
  "eslint": "^7.32.0",
55
55
  "font-awesome": "^4.7.0",
56
56
  "gsap": "^3.9.1",
57
57
  "ionicons": "^4.6.3",
58
58
  "material-design-icons": "^3.0.1",
59
59
  "rollup-plugin-delete": "^2.0.0",
60
- "sass": "^1.45.1",
60
+ "sass": "^1.49.7",
61
61
  "simple-syntax-highlighter": "^2.2.0",
62
62
  "splitpanes": "^3.0.6",
63
- "vite": "^2.7.9",
63
+ "vite": "^2.7.13",
64
64
  "vite-plugin-pug": "^0.3.0",
65
- "vue": "^3.2.26",
65
+ "vue": "^3.2.29",
66
66
  "vue-cal": "^4.2.0",
67
67
  "vue-router": "^4.0.12",
68
68
  "vueperslides": "^3.3.2",
69
69
  "vuex": "^4.0.2"
70
+ },
71
+ "dependencies": {
72
+ "postcss": "^8.4.6"
70
73
  }
71
74
  }
@@ -2,7 +2,7 @@
2
2
  .w-confirm
3
3
  w-menu(v-model="showPopup" v-bind="wMenuProps")
4
4
  template(#activator="{ on }")
5
- w-button.w-confirm__button(v-on="{ ...$listeners, ...on }" v-bind="buttonProps")
5
+ w-button.w-confirm__button(v-bind="{ ...$attrs, ...buttonProps, ...on }")
6
6
  slot
7
7
  w-flex(:column="!inline" align-center)
8
8
  div
@@ -12,9 +12,16 @@ component(
12
12
  template(v-else)
13
13
  //- Left label.
14
14
  template(v-if="labelPosition === 'left'")
15
- label.w-input__label.w-input__label--left.w-form-el-shakable(v-if="$slots.default" :for="`w-input--${_.uid}`")
15
+ label.w-input__label.w-input__label--left.w-form-el-shakable(
16
+ v-if="$slots.default"
17
+ :for="`w-input--${_.uid}`"
18
+ :class="validationClasses")
16
19
  slot
17
- label.w-input__label.w-input__label--left.w-form-el-shakable(v-else-if="label" :for="`w-input--${_.uid}`" v-html="label")
20
+ label.w-input__label.w-input__label--left.w-form-el-shakable(
21
+ v-else-if="label"
22
+ :for="`w-input--${_.uid}`"
23
+ :class="validationClasses"
24
+ v-html="label")
18
25
 
19
26
  //- Input wrapper.
20
27
  .w-input__input-wrap(:class="inputWrapClasses")
@@ -58,35 +65,45 @@ component(
58
65
  @blur="onBlur"
59
66
  @change="onFileChange"
60
67
  :multiple="multiple || null"
61
- v-bind="attrs")
62
- transition-group.w-input__input.w-input__input--file(tag="label" name="fade" :for="`w-input--${_.uid}`")
68
+ v-bind="attrs"
69
+ :data-progress="overallFilesProgress /* Needed to emit the overallProgress. */")
70
+ transition-group.w-input__input.w-input__input--file(
71
+ tag="label"
72
+ name="fade"
73
+ :for="`w-input--${_.uid}`")
63
74
  span.w-input__no-file(v-if="!inputFiles.length && isFocused" key="no-file")
64
75
  slot(name="no-file")
65
76
  template(v-if="$slots['no-file'] === undefined") No file
66
77
  span(v-for="(file, i) in inputFiles" :key="file.lastModified")
67
78
  | {{ i ? ', ': '' }}
68
79
  span.filename(:key="`${i}b`") {{ file.base }}
69
- | {{ file.extension }}
80
+ | {{ file.extension ? `.${file.extension}` : '' }}
70
81
 
71
82
  template(v-if="labelPosition === 'inside' && showLabelInside")
72
83
  label.w-input__label.w-input__label--inside.w-form-el-shakable(
73
84
  v-if="$slots.default"
74
85
  :for="`w-input--${_.uid}`"
75
- :class="isFocused && { [valid === false ? 'error' : color]: color || valid === false }")
86
+ :class="validationClasses")
76
87
  slot
77
88
  label.w-input__label.w-input__label--inside.w-form-el-shakable(
78
89
  v-else-if="label"
79
90
  :for="`w-input--${_.uid}`"
80
91
  v-html="label"
81
- :class="isFocused && { [valid === false ? 'error' : color]: color || valid === false }")
92
+ :class="validationClasses")
82
93
  w-icon.w-input__icon.w-input__icon--inner-right(
83
94
  v-if="innerIconRight"
84
95
  tag="label"
85
96
  :for="`w-input--${_.uid}`"
86
97
  @click="$emit('click:inner-icon-right', $event)") {{ innerIconRight }}
87
98
 
99
+ w-progress.fill-width(
100
+ v-if="hasLoading || (showProgress && (uploadInProgress || uploadComplete))"
101
+ size="2"
102
+ :color="progressColor || color"
103
+ :model-value="showProgress ? (uploadInProgress || uploadComplete) && overallFilesProgress : loadingValue")
104
+
88
105
  //- Files preview.
89
- label.d-flex(v-if="type === 'file' && inputFiles.length" :for="`w-input--${_.uid}`")
106
+ label.d-flex(v-if="type === 'file' && preview && inputFiles.length" :for="`w-input--${_.uid}`")
90
107
  template(v-for="(file, i) in inputFiles")
91
108
  i.w-icon.wi-spinner.w-icon--spin.size--sm.w-input__file-preview.primary(
92
109
  v-if="file.progress < 100"
@@ -96,20 +113,23 @@ component(
96
113
  :key="`${i}b`"
97
114
  :src="file.preview"
98
115
  alt="")
99
- i.w-icon.wi-file.w-input__file-preview.primary(v-else :key="`${i}c`")
116
+ i.w-icon.w-input__file-preview.primary.size--md(
117
+ v-else
118
+ :key="`${i}c`"
119
+ :class="preview && typeof preview === 'string' ? preview : 'wi-file'")
100
120
 
101
121
  //- Right label.
102
122
  template(v-if="labelPosition === 'right'")
103
123
  label.w-input__label.w-input__label--right.w-form-el-shakable(
104
124
  v-if="$slots.default"
105
- :for="`w-input--${_.uid}`")
125
+ :for="`w-input--${_.uid}`"
126
+ :class="validationClasses")
106
127
  slot
107
128
  label.w-input__label.w-input__label--right.w-form-el-shakable(
108
129
  v-else-if="label"
109
130
  :for="`w-input--${_.uid}`"
131
+ :class="validationClasses"
110
132
  v-html="label")
111
-
112
- w-progress.fill-width(v-if="loading" size="2" :color="progressColor || color")
113
133
  </template>
114
134
 
115
135
  <script>
@@ -148,13 +168,17 @@ export default {
148
168
  shadow: { type: Boolean },
149
169
  tile: { type: Boolean },
150
170
  multiple: { type: Boolean }, // Only for file uploads.
151
- preview: { type: Boolean }, // Only for file uploads.
152
- loading: { type: Boolean }
171
+ preview: { type: [Boolean, String], default: true }, // Only for file uploads.
172
+ loading: { type: [Boolean, Number], default: false }, // If a number is given, it will be the value of the progress.
173
+ showProgress: { type: [Boolean] }, // Only for file uploads.
174
+ // Allow syncing the files 1 way: prefilling a file is not possible.
175
+ // https://stackoverflow.com/questions/16365668/pre-populate-html-form-file-input
176
+ files: { type: Array }
153
177
  // Props from mixin: name, disabled, readonly, required, tabindex, validators.
154
178
  // Computed from mixin: inputName, isDisabled & isReadonly.
155
179
  },
156
180
 
157
- emits: ['input', 'update:modelValue', 'focus', 'blur', 'click:inner-icon-left', 'click:inner-icon-right'],
181
+ emits: ['input', 'update:modelValue', 'focus', 'blur', 'click:inner-icon-left', 'click:inner-icon-right', 'update:overallProgress'],
158
182
 
159
183
  data () {
160
184
  return {
@@ -183,29 +207,62 @@ export default {
183
207
  const { input, focus, blur, ...listeners } = this.$attrs
184
208
  return listeners
185
209
  },
210
+
186
211
  attrs () {
187
212
  // eslint-disable-next-line no-unused-vars
188
213
  const { class: Class, ...htmlAttrs } = this.$attrs
189
214
  return htmlAttrs
190
215
  },
216
+
191
217
  hasValue () {
192
- return (
193
- this.inputValue ||
194
- this.inputValue === 0 ||
195
- ['date', 'time'].includes(this.type) ||
196
- (this.type === 'number' && this.inputNumberError) ||
197
- (this.type === 'file' && this.inputFiles.length)
198
- )
218
+ switch (this.type) {
219
+ case 'file': return !!this.inputFiles.length
220
+ case 'number': return this.inputNumberError
221
+ case 'date':
222
+ case 'time':
223
+ return true
224
+ default:
225
+ return this.inputValue || this.inputValue === 0
226
+ }
199
227
  },
200
228
 
201
229
  hasLabel () {
202
230
  return this.label || this.$slots.default
203
231
  },
204
232
 
233
+ hasLoading () {
234
+ return ![undefined, false].includes(this.loading)
235
+ },
236
+
237
+ loadingValue () {
238
+ let value
239
+ if (typeof this.loading === 'number') value = this.loading
240
+ else if (this.loading) {
241
+ value = this.type === 'file' && this.overallFilesProgress ? this.overallFilesProgress : undefined
242
+ }
243
+ return value
244
+ },
245
+
205
246
  showLabelInside () {
206
247
  return !this.staticLabel || (!this.hasValue && !this.placeholder)
207
248
  },
208
249
 
250
+ overallFilesProgress () {
251
+ const progress = this.inputFiles.reduce((total, file) => total + file.progress, 0)
252
+ const total = progress / this.inputFiles.length
253
+ this.$emit('update:overallProgress', this.inputFiles.length ? total : undefined)
254
+
255
+ return total
256
+ },
257
+
258
+ uploadInProgress () {
259
+ return this.overallFilesProgress > 0 && this.overallFilesProgress < 100
260
+ },
261
+
262
+ uploadComplete () {
263
+ return this.overallFilesProgress === 100
264
+ },
265
+
209
266
  classes () {
210
267
  return {
211
268
  'w-input': true,
@@ -223,6 +280,12 @@ export default {
223
280
  }
224
281
  },
225
282
 
283
+ validationClasses () {
284
+ return this.isFocused && {
285
+ [this.valid === false ? 'error' : this.color]: this.color || this.valid === false
286
+ }
287
+ },
288
+
226
289
  inputWrapClasses () {
227
290
  return {
228
291
  [this.valid === false ? 'error' : this.color]: this.color || this.valid === false,
@@ -235,7 +298,8 @@ export default {
235
298
  'w-input__input-wrap--underline': !this.outline,
236
299
  'w-input__input-wrap--shadow': this.shadow,
237
300
  'w-input__input-wrap--no-padding': !this.outline && !this.bgColor && !this.shadow && !this.round,
238
- 'w-input__input-wrap--loading': this.loading
301
+ 'w-input__input-wrap--loading': this.loading || (this.showProgress && this.uploadInProgress),
302
+ 'w-input__input-wrap--upload-complete': this.uploadComplete
239
303
  }
240
304
  }
241
305
  },
@@ -260,37 +324,44 @@ export default {
260
324
  // For file input.
261
325
  onFileChange (e) {
262
326
  this.inputFiles = [...e.target.files].map(original => {
263
- const [, base, extension] = original.name.match(/^(.*)(\..*?)$/)
327
+ // `full` if there is no filename but only an extension.
328
+ const [, base = '', extension = '', full = ''] = original.name.match(/^(.*?)\.([^.]*)$|(.*)/)
329
+
264
330
  const file = reactive({
265
331
  name: original.name,
266
- base,
332
+ base: base || full,
267
333
  extension,
268
334
  type: original.type,
269
335
  size: original.size,
270
336
  lastModified: original.lastModified,
271
337
  preview: null,
272
- progress: 0
338
+ progress: 0,
339
+ file: original
273
340
  })
274
341
 
275
- this.filePreview(original, file)
342
+ this.readFile(original, file)
276
343
 
277
344
  return file
278
345
  })
279
346
  this.$emit('update:modelValue', this.inputFiles)
280
347
  this.$emit('input', this.inputFiles)
281
- this.$emit('change', this.inputFiles)
282
348
  },
283
349
 
284
350
  // For file input.
285
- filePreview (original, file) {
351
+ readFile (original, file) {
286
352
  const reader = new FileReader()
287
353
 
354
+ // If the preview prop is a string, the user is setting the preview to an icon and
355
+ // don't need the actual file preview.
356
+ const isPreviewAnIcon = typeof this.preview === 'string'
357
+ const isFileAnImage = original.type && original.type.startsWith('image/')
288
358
  // Check if the file is an image and set a preview image.
289
- if (original.type && original.type.startsWith('image/')) {
359
+ if (this.preview && !isPreviewAnIcon && isFileAnImage) {
290
360
  reader.addEventListener('load', e => {
291
361
  file.preview = e.target.result
292
362
  })
293
363
  }
364
+ else delete file.preview
294
365
 
295
366
  // Used to display a spinner while the file is loading.
296
367
  reader.addEventListener('progress', e => {
@@ -333,6 +404,8 @@ $inactive-color: #777;
333
404
  &--file {
334
405
  flex-wrap: nowrap;
335
406
  align-items: flex-end;
407
+
408
+ span.fade-leave-to {position: absolute;}
336
409
  }
337
410
 
338
411
  // Input field wrapper.
@@ -362,7 +435,10 @@ $inactive-color: #777;
362
435
  &--round {border-radius: 99em;}
363
436
  &--tile {border-radius: initial;}
364
437
  &--shadow {box-shadow: $box-shadow;}
365
- &--loading {border-bottom-color: transparent;}
438
+ &--loading, &--upload-complete {
439
+ border-bottom-color: transparent;
440
+ flex-wrap: wrap;
441
+ }
366
442
  &--loading ~ .w-progress {
367
443
  height: 2px;
368
444
  position: absolute;
@@ -371,7 +447,8 @@ $inactive-color: #777;
371
447
  }
372
448
 
373
449
  .w-input--focused & {border-color: currentColor;}
374
- .w-input--focused &--loading {border-bottom-color: transparent;}
450
+ .w-input--focused &--loading,
451
+ .w-input--focused &--upload-complete {border-bottom-color: transparent;}
375
452
 
376
453
  // Underline.
377
454
  &--underline:after {
@@ -482,6 +559,8 @@ $inactive-color: #777;
482
559
  margin-left: 4px;
483
560
  max-height: 2em;
484
561
  align-self: flex-end;
562
+
563
+ &.w-icon {margin-bottom: 4px;}
485
564
  }
486
565
 
487
566
  // Icons inside.
@@ -74,26 +74,16 @@ export default {
74
74
  menuClass: { type: [String, Object, Array] },
75
75
  titleClass: { type: [String, Object, Array] },
76
76
  contentClass: { type: [String, Object, Array] },
77
- // Position.
78
77
  arrow: { type: Boolean }, // The small triangle pointing toward the activator.
79
- detachTo: { type: [String, Boolean, Object], deprecated: true },
80
- appendTo: { type: [String, Boolean, Object] },
81
- fixed: { type: Boolean },
82
- top: { type: Boolean },
83
- bottom: { type: Boolean },
84
- left: { type: Boolean },
85
- right: { type: Boolean },
86
- alignTop: { type: Boolean },
87
- alignBottom: { type: Boolean },
88
- alignLeft: { type: Boolean },
89
- alignRight: { type: Boolean },
90
- zIndex: { type: [Number, String, Boolean] },
91
78
  minWidth: { type: [Number, String] }, // can be like: `40`, `5em`, `activator`.
92
79
  overlay: { type: Boolean },
93
80
  overlayClass: { type: [String, Object, Array] },
94
81
  overlayProps: { type: Object }, // Allow passing down an object of props to the w-overlay component.
95
82
  persistent: { type: Boolean },
96
- noPosition: { type: Boolean }
83
+ delay: { type: Number }
84
+ // Other props in the detachable mixin:
85
+ // detachTo, appendTo, fixed, top, bottom, left, right, alignTop, alignBottom, alignLeft,
86
+ // alignRight, noPosition, zIndex, activator.
97
87
  },
98
88
 
99
89
  emits: ['input', 'update:modelValue', 'open', 'close'],
@@ -107,7 +97,6 @@ export default {
107
97
  top: 0,
108
98
  left: 0
109
99
  },
110
- activatorEl: null,
111
100
  activatorWidth: 0,
112
101
  detachableEl: null,
113
102
  timeoutId: null
@@ -118,32 +107,15 @@ export default {
118
107
  * Other computed in the detachable mixin:
119
108
  * - `appendToTarget`
120
109
  * - `detachableParentEl`
110
+ * - `activatorEl`
111
+ * - `position`
112
+ * - `alignment`
121
113
  **/
122
114
 
123
115
  transitionName () {
124
116
  return this.transition || 'scale-fade'
125
117
  },
126
118
 
127
- position () {
128
- return (
129
- (this.top && 'top') ||
130
- (this.bottom && 'bottom') ||
131
- (this.left && 'left') ||
132
- (this.right && 'right') ||
133
- 'bottom'
134
- )
135
- },
136
-
137
- alignment () {
138
- return (
139
- (['top', 'bottom'].includes(this.position) && this.alignLeft && 'left') ||
140
- (['top', 'bottom'].includes(this.position) && this.alignRight && 'right') ||
141
- (['left', 'right'].includes(this.position) && this.alignTop && 'top') ||
142
- (['left', 'right'].includes(this.position) && this.alignBottom && 'bottom') ||
143
- ''
144
- )
145
- },
146
-
147
119
  menuMinWidth () {
148
120
  if (this.minWidth === 'activator') return this.activatorWidth ? `${this.activatorWidth}px` : 0
149
121
  else return isNaN(this.minWidth) ? this.minWidth : (this.minWidth ? `${this.minWidth}px` : 0)
@@ -197,8 +169,8 @@ export default {
197
169
 
198
170
  if (this.showOnHover) {
199
171
  handlers = {
200
- focus: this.toggleMenu,
201
- blur: this.toggleMenu,
172
+ focus: this.toggle,
173
+ blur: this.toggle,
202
174
  mouseenter: e => {
203
175
  this.hoveringActivator = true
204
176
  this.open(e)
@@ -213,10 +185,10 @@ export default {
213
185
  }
214
186
  // Check the window exists: SSR-proof.
215
187
  if (typeof window !== 'undefined' && 'ontouchstart' in window) {
216
- handlers.click = this.toggleMenu
188
+ handlers.click = this.toggle
217
189
  }
218
190
  }
219
- else handlers = { click: this.toggleMenu }
191
+ else handlers = { click: this.toggle }
220
192
  return handlers
221
193
  }
222
194
  },
@@ -233,7 +205,7 @@ export default {
233
205
  **/
234
206
 
235
207
  // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
236
- toggleMenu (e) {
208
+ toggle (e) {
237
209
  let shouldShowMenu = this.detachableVisible
238
210
  if ('ontouchstart' in window && this.showOnHover && e.type === 'click') {
239
211
  shouldShowMenu = !shouldShowMenu
@@ -250,19 +222,22 @@ export default {
250
222
 
251
223
  this.timeoutId = clearTimeout(this.timeoutId)
252
224
 
253
- if (shouldShowMenu) {
254
- this.$emit('update:modelValue', (this.detachableVisible = true))
255
- this.$emit('input', true)
256
- this.$emit('open')
257
-
258
- this.open(e)
259
- }
225
+ if (shouldShowMenu) this.open(e)
260
226
  else this.close()
261
227
  },
262
228
 
263
229
  // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
264
230
  async open (e) {
231
+ // A tiny delay may help positioning the detachable correctly in case of multiple activators
232
+ // with different menu contents.
233
+ if (this.delay) await new Promise(resolve => setTimeout(resolve, this.delay))
234
+
265
235
  this.detachableVisible = true
236
+
237
+ // If the activator is external, there might be multiple,
238
+ // so on open, the activator will be set to the event target.
239
+ if (this.activator) this.activatorEl = e.target
240
+
266
241
  await this.insertInDOM()
267
242
 
268
243
  if (this.minWidth === 'activator') this.activatorWidth = this.activatorEl.offsetWidth
@@ -310,43 +285,9 @@ export default {
310
285
  document.removeEventListener('mousedown', this.onOutsideMousedown)
311
286
  window.removeEventListener('resize', this.onResize)
312
287
  }
313
- },
314
-
315
- mounted () {
316
- const wrapper = this.$el
317
- this.activatorEl = wrapper.firstElementChild
318
-
319
- // Unwrap the activator element.
320
- wrapper.parentNode.insertBefore(this.activatorEl, wrapper)
321
-
322
- // Unwrap the overlay.
323
- if (this.overlay) {
324
- this.overlayEl = this.$refs.overlay?.$el
325
- wrapper.parentNode.insertBefore(this.overlayEl, wrapper)
326
- }
327
-
328
- if (this.modelValue) this.toggleMenu({ type: 'click', target: this.activatorEl })
329
- },
330
-
331
- beforeUnmount () {
332
- this.removeFromDOM()
333
- if (this.overlay && this.overlayEl.parentNode) this.overlayEl.remove()
334
- if (this.activatorEl && this.activatorEl.parentNode) this.activatorEl.remove()
335
- },
336
-
337
- watch: {
338
- modelValue (bool) {
339
- if (!!bool !== this.detachableVisible) this.toggleMenu({ type: 'click', target: this.activatorEl })
340
- },
341
- detachTo () {
342
- this.removeFromDOM()
343
- this.insertInDOM()
344
- },
345
- appendTo () {
346
- this.removeFromDOM()
347
- this.insertInDOM()
348
- }
349
288
  }
289
+
290
+ // watch, mounted & beforeDestroy hooks are set in the detachable.js mixin.
350
291
  }
351
292
  </script>
352
293
 
@@ -11,8 +11,8 @@ transition-group(
11
11
  :key="notif._uid"
12
12
  v-model="notif._value"
13
13
  @close="notifManager.dismiss(notif._uid)"
14
- v-bind="notif")
15
- | {{ notif.message }}
14
+ v-bind="notifProps(notif)")
15
+ div(v-html="notif.message")
16
16
  </template>
17
17
 
18
18
  <script>
@@ -42,6 +42,13 @@ export default {
42
42
  }
43
43
  },
44
44
 
45
+ methods: {
46
+ notifProps (notif) {
47
+ const { _value, _uid, message, timeout, ...props } = notif
48
+ return props
49
+ }
50
+ },
51
+
45
52
  created () {
46
53
  this.notifManager = new NotificationManager()
47
54
  },
@@ -3,7 +3,7 @@ component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
5
  v-bind="formRegister && { validators, inputValue: rating, disabled: isDisabled, readonly: isReadonly }"
6
- :valid.sync="valid"
6
+ v-model:valid="valid"
7
7
  @reset="$emit('update:modelValue', rating = null);$emit('input', null)"
8
8
  :class="classes")
9
9
  input(:id="inputName" :name="inputName" type="hidden" :value="rating")
@@ -329,7 +329,7 @@ export default {
329
329
  setTimeout(() => {
330
330
  const itemIndex = this.inputValue.length ? this.inputValue[0].index : 0 // Real index starts at 0.
331
331
  // User visible index starts at 1.
332
- this.$refs['w-list'].$el.querySelector(`#w-select-menu--${this._.uid}_item-${itemIndex + 1}`).focus()
332
+ this.$refs['w-list'].$el.querySelector(`#w-select-menu--${this._.uid}_item-${itemIndex + 1}`)?.focus()
333
333
  }, 100)
334
334
  },
335
335
 
@@ -313,7 +313,7 @@ export default {
313
313
  }
314
314
  )
315
315
 
316
- // Keep external `expanded-rows.sync` updated.
316
+ // Keep external `expanded-rows.sync` (Vue 2) or v-model:expanded-rows (Vue 3) updated.
317
317
  this.$emit('update:expanded-rows', this.expandedRowsInternal)
318
318
  }
319
319
 
@@ -342,7 +342,7 @@ export default {
342
342
  }
343
343
  )
344
344
 
345
- // Keep external `selected-rows.sync` updated.
345
+ // Keep external `selected-rows.sync` (Vue 2) or v-model:selected-rows (Vue 3) updated.
346
346
  this.$emit('update:selected-rows', this.selectedRowsInternal)
347
347
  }
348
348
  }