wave-ui 3.26.1 → 3.27.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": "3.26.1",
3
+ "version": "3.27.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",
@@ -77,7 +77,7 @@
77
77
  "vuex": "^4.1.0"
78
78
  },
79
79
  "peerDependencies": {
80
- "vue": "^3.2.0"
80
+ "vue": "^3.5.0"
81
81
  },
82
82
  "engines": {
83
83
  "node": ">=16.0.0",
@@ -69,11 +69,16 @@
69
69
  </template>
70
70
 
71
71
  <script>
72
+ import { useId } from 'vue'
72
73
  import AccordionContent from './accordion-content.vue'
73
74
 
74
75
  export default {
75
76
  name: 'w-accordion-item',
76
77
 
78
+ setup () {
79
+ return { accordionItemUid: useId() }
80
+ },
81
+
77
82
  components: { AccordionContent },
78
83
 
79
84
  props: {
@@ -100,7 +105,7 @@ export default {
100
105
  computed: {
101
106
  accordionItem: {
102
107
  get () {
103
- return this.getAccordionItem(this._.uid)
108
+ return this.getAccordionItem(this.accordionItemUid)
104
109
  },
105
110
  set () {}
106
111
  },
@@ -130,7 +135,7 @@ export default {
130
135
  created () {
131
136
  // Register this item to the w-accordion component.
132
137
  this.registerItem({
133
- _cuid: this._.uid,
138
+ _cuid: this.accordionItemUid,
134
139
  _index: 0,
135
140
  _expanded: this.expanded,
136
141
  _disabled: this.disabled,
@@ -140,7 +145,7 @@ export default {
140
145
  },
141
146
 
142
147
  beforeUnmount () {
143
- this.unregisterItem(this._.uid)
148
+ this.unregisterItem(this.accordionItemUid)
144
149
  }
145
150
  }
146
151
  </script>
@@ -2,13 +2,13 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister && !wCheckboxes ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue: isChecked, disabled: isDisabled, readonly: isReadonly }"
5
+ v-bind="formRegister && { validators, inputValue: isChecked, disabled: isDisabled, readonly: isReadonly, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="$emit('update:modelValue', isChecked = null);$emit('input', null)"
8
8
  :class="classes")
9
9
  input(
10
10
  ref="input"
11
- :id="`w-checkbox--${_.uid}`"
11
+ :id="inputId"
12
12
  type="checkbox"
13
13
  :name="inputName"
14
14
  :checked="isChecked || null"
@@ -24,12 +24,12 @@ component(
24
24
  template(v-if="hasLabel && labelOnLeft")
25
25
  label.w-checkbox__label.w-form-el-shakable.pr2(
26
26
  v-if="$slots.default"
27
- :for="`w-checkbox--${_.uid}`"
27
+ :for="inputId"
28
28
  :class="labelClasses")
29
29
  slot {{ label }}
30
30
  label.w-checkbox__label.w-form-el-shakable.pr2(
31
31
  v-else-if="label"
32
- :for="`w-checkbox--${_.uid}`"
32
+ :for="inputId"
33
33
  :class="labelClasses"
34
34
  v-html="label")
35
35
  .w-checkbox__input(@click="$refs.input.focus();$refs.input.click()" :class="this.color")
@@ -38,12 +38,12 @@ component(
38
38
  template(v-if="hasLabel && !labelOnLeft")
39
39
  label.w-checkbox__label.w-form-el-shakable.pl2(
40
40
  v-if="$slots.default"
41
- :for="`w-checkbox--${_.uid}`"
41
+ :for="inputId"
42
42
  :class="labelClasses")
43
43
  slot {{ label }}
44
44
  label.w-checkbox__label.w-form-el-shakable.pl2(
45
45
  v-else-if="label"
46
- :for="`w-checkbox--${_.uid}`"
46
+ :for="inputId"
47
47
  :class="labelClasses"
48
48
  v-html="label")
49
49
  </template>
@@ -73,8 +73,8 @@ export default {
73
73
  round: { type: Boolean },
74
74
  dark: { type: Boolean },
75
75
  light: { type: Boolean }
76
- // Props from mixin: name, disabled, readonly, required, tabindex, validators.
77
- // Computed from mixin: inputName, isDisabled & isReadonly.
76
+ // Props from mixin: id, name, disabled, readonly, required, tabindex, validators.
77
+ // Computed from mixin: inputId, inputName, isDisabled & isReadonly.
78
78
  },
79
79
 
80
80
  emits: ['input', 'update:modelValue', 'focus', 'blur'],
@@ -2,7 +2,7 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue: checkboxItems.some(item => item._isChecked), disabled: isDisabled }"
5
+ v-bind="formRegister && { validators, inputValue: checkboxItems.some(item => item._isChecked), disabled: isDisabled, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="reset"
8
8
  :column="!inline"
@@ -51,7 +51,7 @@ export default {
51
51
  round: { type: Boolean },
52
52
  color: { type: String, default: 'primary' },
53
53
  labelColor: { type: String, default: 'primary' }
54
- // Props from mixin: name, disabled, readonly, required, validators.
54
+ // Props from mixin: id, name, disabled, readonly, required, validators.
55
55
  // Computed from mixin: inputName, isDisabled & isReadonly.
56
56
  },
57
57
 
@@ -20,6 +20,8 @@ export default {
20
20
  readonly: { type: Boolean },
21
21
  inputValue: { required: true }, // The form element's input value.
22
22
  validators: { type: Array },
23
+ /** When `true`, skip blur validation for this element. When `false`, force blur validation even if `w-form` has `no-blur-validation`. When unset, inherit from the form. */
24
+ noBlurValidation: { type: Boolean },
23
25
  isFocused: { default: false }, // Watched.
24
26
  column: { default: false }, // Flex direction of the embedded component: column or row by default.
25
27
  wrap: { default: false } // Flex-wrap if needed.
@@ -57,6 +59,13 @@ export default {
57
59
  'w-form-el',
58
60
  classes[this.Validation.isValid === null ? 2 : ~~this.Validation.isValid]
59
61
  ]
62
+ },
63
+
64
+ // Per-element `no-blur-validation` overrides the form when set; otherwise inherit `w-form`.
65
+ shouldSkipBlurValidation () {
66
+ if (this.noBlurValidation === true) return true
67
+ if (this.noBlurValidation === false) return false
68
+ return !!this.formProps.noBlurValidation
60
69
  }
61
70
  },
62
71
 
@@ -89,7 +98,7 @@ export default {
89
98
  // When focusing, reset the hasJustReset flag so the input value is watched again.
90
99
  if (val) this.hasJustReset = false
91
100
  // On blur, Update the form element's validity.
92
- else if (!this.formProps.noBlurValidation && this.validators && !this.readonly) {
101
+ else if (!this.shouldSkipBlurValidation && this.validators && !this.readonly) {
93
102
  this.$emit('update:valid', await this.validateElement(this))
94
103
  }
95
104
  }
@@ -49,7 +49,8 @@ export default {
49
49
  formUnregister: this.unregister,
50
50
  validateElement: this.validateElement,
51
51
  // Give access to the form params (like disabled) to all the form components.
52
- // To keep it reactive, we need an object not a list of props (by design in Vue).
52
+ // Use the form instance (not `this.$props`): injected descendants must read reactive
53
+ // props (`no-blur-validation`, `no-keyup-validation`, etc.) through the instance proxy.
53
54
  formProps: this.$props
54
55
  }
55
56
  },
@@ -77,7 +78,7 @@ export default {
77
78
  },
78
79
 
79
80
  unregister (formElement) {
80
- this.formElements = this.formElements.filter(item => item._.uid !== formElement._.uid)
81
+ this.formElements = this.formElements.filter(item => item !== formElement)
81
82
  },
82
83
 
83
84
  /**
@@ -2,7 +2,7 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue, disabled: isDisabled, readonly: isReadonly, isFocused }"
5
+ v-bind="formRegister && { validators, inputValue, disabled: isDisabled, readonly: isReadonly, isFocused, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="$emit('update:modelValue', inputValue = '');$emit('input', '')"
8
8
  :wrap="hasLabel && labelPosition !== 'inside'"
@@ -15,17 +15,17 @@ component(
15
15
  template(v-if="labelPosition === 'left'")
16
16
  label.w-input__label.w-input__label--left.w-form-el-shakable(
17
17
  v-if="$slots.default || label"
18
- :for="`w-input--${_.uid}`"
18
+ :for="inputId"
19
19
  :class="labelClasses")
20
20
  slot {{ label }}
21
21
 
22
22
  //- Input wrapper.
23
23
  .w-input__input-wrap(:class="inputWrapClasses")
24
- slot(name="icon-left" :input-id="`w-input--${_.uid}`")
24
+ slot(name="icon-left" :input-id="inputId")
25
25
  w-icon.w-input__icon.w-input__icon--inner-left(
26
26
  v-if="innerIconLeft"
27
27
  tag="label"
28
- :for="`w-input--${_.uid}`"
28
+ :for="inputId"
29
29
  @click="$emit('click:inner-icon-left', $event)") {{ innerIconLeft }}
30
30
  //- All types of input except file.
31
31
  input.w-input__input(
@@ -35,7 +35,7 @@ component(
35
35
  @input="onInput"
36
36
  @focus="onFocus"
37
37
  @blur="onBlur"
38
- :id="`w-input--${_.uid}`"
38
+ :id="inputId"
39
39
  :type="type"
40
40
  :name="inputName"
41
41
  :placeholder="placeholder || null"
@@ -55,7 +55,7 @@ component(
55
55
  template(v-else)
56
56
  input(
57
57
  ref="input"
58
- :id="`w-input--${_.uid}`"
58
+ :id="inputId"
59
59
  type="file"
60
60
  :name="name || null"
61
61
  @focus="onFocus"
@@ -68,7 +68,7 @@ component(
68
68
  transition-group.w-input__input.w-input__input--file(
69
69
  tag="label"
70
70
  name="fade"
71
- :for="`w-input--${_.uid}`")
71
+ :for="inputId")
72
72
  span.w-input__no-file(v-if="!inputFiles.length && isFocused" key="no-file")
73
73
  slot(name="no-file")
74
74
  template(v-if="$slots['no-file'] === undefined") No file
@@ -83,11 +83,11 @@ component(
83
83
  :class="labelClasses")
84
84
  slot {{ label }}
85
85
 
86
- slot(name="icon-right" :input-id="`w-input--${_.uid}`")
86
+ slot(name="icon-right" :input-id="inputId")
87
87
  w-icon.w-input__icon.w-input__icon--inner-right(
88
88
  v-if="innerIconRight"
89
89
  tag="label"
90
- :for="`w-input--${_.uid}`"
90
+ :for="inputId"
91
91
  @click="$emit('click:inner-icon-right', $event)") {{ innerIconRight }}
92
92
 
93
93
  w-progress.fill-width(
@@ -97,7 +97,7 @@ component(
97
97
  :model-value="showProgress ? (uploadInProgress || uploadComplete) && overallFilesProgress : loadingValue")
98
98
 
99
99
  //- Files preview.
100
- label.d-flex(v-if="type === 'file' && preview && inputFiles.length" :for="`w-input--${_.uid}`")
100
+ label.d-flex(v-if="type === 'file' && preview && inputFiles.length" :for="inputId")
101
101
  template(v-for="(file, i) in inputFiles")
102
102
  i.w-icon.wi-spinner.w-icon--spin.size--sm.w-input__file-preview.primary(
103
103
  v-if="file.progress < 100"
@@ -116,7 +116,7 @@ component(
116
116
  template(v-if="labelPosition === 'right'")
117
117
  label.w-input__label.w-input__label--right.w-form-el-shakable(
118
118
  v-if="$slots.default || label"
119
- :for="`w-input--${_.uid}`"
119
+ :for="inputId"
120
120
  :class="labelClasses")
121
121
  slot {{ label }}
122
122
  </template>
@@ -168,8 +168,8 @@ export default {
168
168
  dark: { type: Boolean },
169
169
  light: { type: Boolean },
170
170
  inputClass: { type: String } // Additional classes for the input element.
171
- // Props from mixin: name, disabled, readonly, required, tabindex, validators.
172
- // Computed from mixin: inputName, isDisabled & isReadonly.
171
+ // Props from mixin: id, name, disabled, readonly, required, tabindex, validators.
172
+ // Computed from mixin: inputId, inputName, isDisabled & isReadonly.
173
173
  },
174
174
 
175
175
  emits: ['input', 'update:modelValue', 'focus', 'blur', 'click:inner-icon-left', 'click:inner-icon-right', 'update:overallProgress'],
@@ -43,9 +43,15 @@ ul.w-list(:class="classes")
43
43
  </template>
44
44
 
45
45
  <script>
46
+ import { useId } from 'vue'
47
+
46
48
  export default {
47
49
  name: 'w-list',
48
50
 
51
+ setup () {
52
+ return { _waveUiUseId: useId() }
53
+ },
54
+
49
55
  props: {
50
56
  items: { type: [Array, Number], required: true }, // All the possible options.
51
57
  modelValue: {}, // v-model on selected item if any.
@@ -89,7 +95,8 @@ export default {
89
95
  },
90
96
 
91
97
  listId () {
92
- return this.addIds ? (typeof this.addIds === 'string' ? this.addIds : `w-list--${this._.uid}`) : null
98
+ if (!this.addIds) return null
99
+ return typeof this.addIds === 'string' ? this.addIds : `w-list--${this._waveUiUseId}`
93
100
  },
94
101
 
95
102
  selectedItems () {
@@ -2,13 +2,13 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister && !wRadios ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue, disabled: isDisabled, readonly: isReadonly }"
5
+ v-bind="formRegister && { validators, inputValue, disabled: isDisabled, readonly: isReadonly, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="$emit('update:modelValue', inputValue = null);$emit('input', null)"
8
8
  :class="classes")
9
9
  input(
10
10
  ref="input"
11
- :id="`w-radio--${_.uid}`"
11
+ :id="inputId"
12
12
  type="radio"
13
13
  :name="inputName"
14
14
  :checked="inputValue || null"
@@ -22,12 +22,12 @@ component(
22
22
  template(v-if="hasLabel && labelOnLeft")
23
23
  label.w-radio__label.w-form-el-shakable.pr2(
24
24
  v-if="$slots.default"
25
- :for="`w-radio--${_.uid}`"
25
+ :for="inputId"
26
26
  :class="labelClasses")
27
27
  slot {{ label }}
28
28
  label.w-radio__label.w-form-el-shakable.pr2(
29
29
  v-else-if="label"
30
- :for="`w-radio--${_.uid}`"
30
+ :for="inputId"
31
31
  :class="labelClasses"
32
32
  v-html="label")
33
33
  .w-radio__input(
@@ -36,12 +36,12 @@ component(
36
36
  template(v-if="hasLabel && !labelOnLeft")
37
37
  label.w-radio__label.w-form-el-shakable.pl2(
38
38
  v-if="$slots.default"
39
- :for="`w-radio--${_.uid}`"
39
+ :for="inputId"
40
40
  :class="labelClasses")
41
41
  slot {{ label }}
42
42
  label.w-radio__label.w-form-el-shakable.pl2(
43
43
  v-else-if="label"
44
- :for="`w-radio--${_.uid}`"
44
+ :for="inputId"
45
45
  :class="labelClasses"
46
46
  v-html="label")
47
47
  </template>
@@ -66,8 +66,8 @@ export default {
66
66
  noRipple: { type: Boolean },
67
67
  dark: { type: Boolean },
68
68
  light: { type: Boolean }
69
- // Props from mixin: name, disabled, readonly, required, tabindex, validators.
70
- // Computed from mixin: inputName, isDisabled & isReadonly.
69
+ // Props from mixin: id, name, disabled, readonly, required, tabindex, validators.
70
+ // Computed from mixin: inputId, inputName, isDisabled & isReadonly.
71
71
  },
72
72
 
73
73
  emits: ['input', 'update:modelValue', 'focus'],
@@ -2,7 +2,7 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue, disabled: isDisabled }"
5
+ v-bind="formRegister && { validators, inputValue, disabled: isDisabled, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="$emit('update:modelValue', inputValue = null);$emit('input', null)"
8
8
  :column="!inline"
@@ -46,7 +46,7 @@ export default {
46
46
  inline: { type: Boolean },
47
47
  color: { type: String, default: 'primary' },
48
48
  labelColor: { type: String, default: 'primary' }
49
- // Props from mixin: name, disabled, readonly, required, validators.
49
+ // Props from mixin: id, name, disabled, readonly, required, validators.
50
50
  // Computed from mixin: inputName, isDisabled & isReadonly.
51
51
  },
52
52
 
@@ -2,11 +2,11 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue: rating, disabled: isDisabled, readonly: isReadonly }"
5
+ v-bind="formRegister && { validators, inputValue: rating, disabled: isDisabled, readonly: isReadonly, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="$emit('update:modelValue', rating = null);$emit('input', null)"
8
8
  :class="classes")
9
- input(:id="inputName" :name="inputName" type="hidden" :value="rating")
9
+ input(:id="inputId" :name="inputName" type="hidden" :value="rating")
10
10
  template(v-for="i in max" :key="i")
11
11
  slot(v-if="$slots.item" name="item" :index="i + 1")
12
12
  button.w-rating__button(
@@ -49,8 +49,8 @@ export default {
49
49
  noRipple: { type: Boolean },
50
50
  dark: { type: Boolean },
51
51
  light: { type: Boolean }
52
- // Props from mixin: name, disabled, readonly, required, validators.
53
- // Computed from mixin: inputName, isDisabled & isReadonly.
52
+ // Props from mixin: id, name, disabled, readonly, required, validators.
53
+ // Computed from mixin: inputId, inputName, isDisabled & isReadonly.
54
54
  },
55
55
 
56
56
  emits: ['input', 'update:modelValue', 'focus', 'blur'],
@@ -2,7 +2,7 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue: selectionString, disabled: isDisabled, readonly: isReadonly, isFocused }"
5
+ v-bind="formRegister && { validators, inputValue: selectionString, disabled: isDisabled, readonly: isReadonly, isFocused, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="onReset"
8
8
  :wrap="hasLabel && labelPosition !== 'inside'"
@@ -31,8 +31,8 @@ component(
31
31
  role="button"
32
32
  aria-haspopup="listbox"
33
33
  :aria-expanded="showMenu ? 'true' : 'false'"
34
- :aria-owns="`w-select-menu--${_.uid}`"
35
- :aria-activedescendant="`w-select-menu--${_.uid}_item-1`"
34
+ :aria-owns="selectListId"
35
+ :aria-activedescendant="`${selectListId}_item-1`"
36
36
  :class="inputWrapClasses")
37
37
  slot(name="icon-left")
38
38
  w-icon.w-select__icon.w-select__icon--inner-left(
@@ -79,7 +79,7 @@ component(
79
79
  :multiple="multiple"
80
80
  arrows-navigation
81
81
  return-object
82
- :add-ids="`w-select-menu--${_.uid}`"
82
+ :add-ids="selectListId"
83
83
  :no-unselect="noUnselect"
84
84
  :selection-color="selectionColor"
85
85
  :item-color-key="itemColorKey"
@@ -146,8 +146,8 @@ export default {
146
146
  dark: { type: Boolean },
147
147
  light: { type: Boolean },
148
148
  fitToContent: { type: Boolean }
149
- // Props from mixin: name, disabled, readonly, required, tabindex, validators.
150
- // Computed from mixin: inputName, isDisabled & isReadonly.
149
+ // Props from mixin: id, name, disabled, readonly, required, tabindex, validators.
150
+ // Computed from mixin: inputId, selectListId, inputName, isDisabled & isReadonly.
151
151
  },
152
152
 
153
153
  emits: ['input', 'update:modelValue', 'focus', 'blur', 'item-click', 'item-select', 'click:inner-icon-left', 'click:inner-icon-right'],
@@ -166,6 +166,11 @@ export default {
166
166
  }),
167
167
 
168
168
  computed: {
169
+ /** Prefix for w-list item ids (`id_item-N`) and ARIA listbox linkage; stable with SSR. */
170
+ selectListId () {
171
+ return this.id ? `${this.id}__listbox` : `w-select-menu--${this._waveUiUseId}`
172
+ },
173
+
169
174
  // Check all the items and add a `value` if missing, containing either: value, label or index
170
175
  // in this order.
171
176
  selectItems () {
@@ -394,7 +399,7 @@ export default {
394
399
  setTimeout(() => {
395
400
  const itemIndex = this.inputValue.length ? this.inputValue[0].index : 0 // Real index starts at 0.
396
401
  // User visible index starts at 1.
397
- this.$refs['w-list'].$el.querySelector(`#w-select-menu--${this._.uid}_item-${itemIndex + 1}`)?.focus()
402
+ document.getElementById(`${this.selectListId}_item-${itemIndex + 1}`)?.focus()
398
403
  }, 100)
399
404
  },
400
405
 
@@ -2,19 +2,19 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue: rangeValueScaled, disabled: isDisabled, readonly: isReadonly }"
5
+ v-bind="formRegister && { validators, inputValue: rangeValueScaled, disabled: isDisabled, readonly: isReadonly, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @reset="rangeValuePercent = 0;updateRangeValueScaled()"
8
8
  :wrap="formRegister || null"
9
9
  :class="wrapperClasses")
10
10
  label.w-slider__label.w-slider__label--left.w-form-el-shakable(
11
11
  v-if="$slots['label-left']"
12
- :for="`button--${_.uid}`"
12
+ :for="thumbId"
13
13
  :class="labelClasses")
14
14
  slot(name="label-left")
15
15
  label.w-slider__label.w-slider__label--left.w-form-el-shakable(
16
16
  v-else-if="labelLeft"
17
- :for="`button--${_.uid}`"
17
+ :for="thumbId"
18
18
  :class="labelClasses"
19
19
  v-html="labelLeft")
20
20
  .w-slider__track-wrap
@@ -34,7 +34,7 @@ component(
34
34
  .w-slider__thumb(:style="thumbStyles")
35
35
  button.w-slider__thumb-button(
36
36
  ref="thumb"
37
- :id="`button--${_.uid}`"
37
+ :id="thumbId"
38
38
  :class="[color]"
39
39
  :name="inputName"
40
40
  :model-value="rangeValueScaled"
@@ -48,7 +48,7 @@ component(
48
48
  @click.prevent)
49
49
  label.w-slider__thumb-label(
50
50
  v-if="thumbLabel"
51
- :for="`button--${_.uid}`"
51
+ :for="thumbId"
52
52
  :class="thumbClasses")
53
53
  div(v-if="thumbLabel === 'droplet'")
54
54
  slot(name="label" :value="rangeValueScaled") {{ ~~rangeValueScaled }}
@@ -67,12 +67,12 @@ component(
67
67
  style="left: 100%") {{ this.maxVal }}
68
68
  label.w-slider__label.w-slider__label--right.w-form-el-shakable(
69
69
  v-if="$slots['label-right']"
70
- :for="`button--${_.uid}`"
70
+ :for="thumbId"
71
71
  :class="labelClasses")
72
72
  slot(name="label-right")
73
73
  label.w-slider__label.w-slider__label--right.w-form-el-shakable(
74
74
  v-else-if="labelRight"
75
- :for="`button--${_.uid}`"
75
+ :for="thumbId"
76
76
  :class="labelClasses"
77
77
  v-html="labelRight")
78
78
  </template>
@@ -101,8 +101,8 @@ export default {
101
101
  labelRight: { type: String },
102
102
  dark: { type: Boolean },
103
103
  light: { type: Boolean }
104
- // Props from mixin: name, disabled, readonly, required, validators.
105
- // Computed from mixin: inputName, isDisabled & isReadonly.
104
+ // Props from mixin: id, name, disabled, readonly, required, validators.
105
+ // Computed from mixin: inputId, thumbId, inputName, isDisabled & isReadonly.
106
106
  },
107
107
 
108
108
  emits: ['input', 'update:modelValue', 'focus'],
@@ -119,6 +119,11 @@ export default {
119
119
  }),
120
120
 
121
121
  computed: {
122
+ /** DOM id for the thumb control and related labels (distinct from `inputId` on `w-slider`). */
123
+ thumbId () {
124
+ return this.id ? `${this.id}__thumb` : `button--${this._waveUiUseId}`
125
+ },
126
+
122
127
  minVal () {
123
128
  return parseFloat(this.min)
124
129
  },
@@ -2,7 +2,7 @@
2
2
  component(
3
3
  ref="formEl"
4
4
  :is="formRegister ? 'w-form-element' : 'div'"
5
- v-bind="formRegister && { validators, inputValue: isOn, disabled: isDisabled, readonly: isReadonly }"
5
+ v-bind="formRegister && { validators, inputValue: isOn, disabled: isDisabled, readonly: isReadonly, noBlurValidation }"
6
6
  v-model:valid="valid"
7
7
  @mouseenter="$emit('mouseenter', $event)"
8
8
  @mouseleave="$emit('mouseleave', $event)"
@@ -11,7 +11,7 @@ component(
11
11
  :style="$attrs.style")
12
12
  input(
13
13
  ref="input"
14
- :id="`w-switch--${_.uid}`"
14
+ :id="inputId"
15
15
  type="checkbox"
16
16
  :name="inputName"
17
17
  :checked="isOn"
@@ -29,7 +29,7 @@ component(
29
29
  template(v-if="hasLabel && labelOnLeft")
30
30
  label.w-switch__label.w-switch__label--left.w-form-el-shakable(
31
31
  v-if="$slots.default || label"
32
- :for="`w-switch--${_.uid}`"
32
+ :for="inputId"
33
33
  :class="labelClasses")
34
34
  slot {{ label }}
35
35
  .w-switch__input(
@@ -47,7 +47,7 @@ component(
47
47
  template(v-if="hasLabel && !labelOnLeft")
48
48
  label.w-switch__label.w-switch__label--right.w-form-el-shakable(
49
49
  v-if="$slots.default || label"
50
- :for="`w-switch--${_.uid}`"
50
+ :for="inputId"
51
51
  :class="labelClasses")
52
52
  slot {{ label }}
53
53
  </template>
@@ -71,8 +71,8 @@ export default {
71
71
  loading: { type: [Boolean, Number], default: false },
72
72
  dark: { type: Boolean },
73
73
  light: { type: Boolean }
74
- // Props from mixin: name, disabled, readonly, required, tabindex, validators.
75
- // Computed from mixin: inputName, isDisabled & isReadonly.
74
+ // Props from mixin: id, name, disabled, readonly, required, tabindex, validators.
75
+ // Computed from mixin: inputId, inputName, isDisabled & isReadonly.
76
76
  },
77
77
 
78
78
  emits: ['input', 'update:modelValue', 'focus', 'blur', 'mouseenter', 'mouseleave'],
@@ -71,14 +71,17 @@
71
71
  </template>
72
72
 
73
73
  <script>
74
+ import { useId } from 'vue'
74
75
  import { objectifyClasses } from '../../utils/index'
75
76
  import TabContent from './tab-content.vue'
76
77
 
77
- let uid = 0
78
-
79
78
  export default {
80
79
  name: 'w-tabs',
81
80
 
81
+ setup () {
82
+ return { _tabsStableId: useId() }
83
+ },
84
+
82
85
  props: {
83
86
  modelValue: { type: [Number, String] },
84
87
  color: { type: String },
@@ -180,7 +183,7 @@ export default {
180
183
  addTab (item) {
181
184
  // If there is no unique ID provided, inject one in each tab.
182
185
  // This will cause a single other update from watching the tabs items and stop there.
183
- if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = +`${this._.uid}${++uid}`
186
+ if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = `${this._tabsStableId}-${this.tabs.length}`
184
187
 
185
188
  this.tabs.push({
186
189
  _uid: item[this.itemIdKey] ?? item._uid,
@@ -197,7 +200,7 @@ export default {
197
200
  this.tabs = items.map((item, _index) => {
198
201
  // If there is no unique ID provided, inject one in each tab.
199
202
  // This will cause a single other update from watching the tabs items and stop there.
200
- if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = +`${this._.uid}${++uid}`
203
+ if (!(item[this.itemIdKey] ?? item._uid ?? false)) item._uid = `${this._tabsStableId}-${_index}`
201
204
 
202
205
  return {
203
206
  ...item,