vueless 1.2.8 → 1.2.10-beta.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.
Files changed (78) hide show
  1. package/constants.d.ts +4 -0
  2. package/constants.js +4 -0
  3. package/icons/storybook/rocket_launch.svg +1 -0
  4. package/index.d.ts +3 -1
  5. package/index.ts +3 -1
  6. package/package.json +9 -5
  7. package/plugin-vite.js +6 -1
  8. package/types.ts +14 -2
  9. package/ui.button/config.ts +4 -4
  10. package/ui.button/tests/UButton.test.ts +3 -3
  11. package/ui.button-toggle/config.ts +2 -2
  12. package/ui.container-accordion/UAccordion.vue +0 -1
  13. package/ui.container-accordion/config.ts +1 -1
  14. package/ui.container-accordion/storybook/stories.ts +13 -1
  15. package/ui.container-accordion-item/UAccordionItem.vue +17 -4
  16. package/ui.container-accordion-item/config.ts +1 -1
  17. package/ui.container-accordion-item/storybook/stories.ts +26 -1
  18. package/ui.container-accordion-item/tests/UAccordionItem.test.ts +186 -0
  19. package/ui.container-card/config.ts +1 -1
  20. package/ui.data-table/config.ts +4 -4
  21. package/ui.dropdown-badge/UDropdownBadge.vue +68 -3
  22. package/ui.dropdown-badge/config.ts +5 -1
  23. package/ui.dropdown-badge/storybook/stories.ts +280 -4
  24. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +194 -0
  25. package/ui.dropdown-badge/types.ts +30 -0
  26. package/ui.dropdown-button/UDropdownButton.vue +69 -6
  27. package/ui.dropdown-button/config.ts +5 -1
  28. package/ui.dropdown-button/storybook/stories.ts +288 -3
  29. package/ui.dropdown-button/tests/UDropdownButton.test.ts +190 -0
  30. package/ui.dropdown-button/types.ts +30 -0
  31. package/ui.dropdown-link/UDropdownLink.vue +69 -6
  32. package/ui.dropdown-link/config.ts +5 -1
  33. package/ui.dropdown-link/storybook/stories.ts +281 -4
  34. package/ui.dropdown-link/tests/UDropdownLink.test.ts +194 -0
  35. package/ui.dropdown-link/types.ts +30 -0
  36. package/ui.form-calendar/config.ts +4 -2
  37. package/ui.form-checkbox/config.ts +1 -1
  38. package/ui.form-checkbox/tests/UCheckbox.test.ts +2 -2
  39. package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +2 -2
  40. package/ui.form-date-picker-range/config.ts +1 -1
  41. package/ui.form-input/UInput.vue +4 -2
  42. package/ui.form-input/config.ts +1 -1
  43. package/ui.form-input/tests/UInput.test.ts +2 -2
  44. package/ui.form-input-counter/UInputCounter.vue +25 -1
  45. package/ui.form-input-counter/config.ts +7 -2
  46. package/ui.form-input-counter/tests/UInputCounter.test.ts +85 -1
  47. package/ui.form-input-counter/types.ts +25 -0
  48. package/ui.form-input-file/tests/UInputFile.test.ts +2 -2
  49. package/ui.form-input-number/UInputNumber.vue +15 -3
  50. package/ui.form-input-number/utilFormat.ts +17 -7
  51. package/ui.form-input-password/UInputPassword.vue +23 -1
  52. package/ui.form-label/ULabel.vue +10 -4
  53. package/ui.form-label/tests/ULabel.test.ts +29 -12
  54. package/ui.form-listbox/UListbox.vue +21 -9
  55. package/ui.form-listbox/config.ts +1 -1
  56. package/ui.form-listbox/storybook/stories.ts +188 -1
  57. package/ui.form-listbox/tests/UListbox.test.ts +36 -0
  58. package/ui.form-listbox/types.ts +5 -0
  59. package/ui.form-radio/config.ts +1 -1
  60. package/ui.form-radio/tests/URadio.test.ts +2 -2
  61. package/ui.form-radio-group/tests/URadioGroup.test.ts +2 -2
  62. package/ui.form-select/USelect.vue +20 -2
  63. package/ui.form-select/config.ts +2 -1
  64. package/ui.form-select/storybook/stories.ts +31 -4
  65. package/ui.form-select/tests/USelect.test.ts +143 -0
  66. package/ui.form-select/types.ts +10 -0
  67. package/ui.form-textarea/config.ts +1 -1
  68. package/ui.form-textarea/tests/UTextarea.test.ts +2 -2
  69. package/ui.text-alert/config.ts +1 -1
  70. package/ui.text-badge/config.ts +1 -1
  71. package/utils/helper.ts +4 -0
  72. package/utils/node/dynamicProps.d.ts +5 -2
  73. package/utils/node/dynamicProps.js +126 -53
  74. package/utils/node/helper.d.ts +10 -7
  75. package/utils/node/helper.js +59 -2
  76. package/utils/node/tailwindSafelist.js +9 -2
  77. package/utils/theme.ts +75 -31
  78. package/utils/ui.ts +32 -3
@@ -12,7 +12,7 @@ export default /*tw*/ {
12
12
  wrapper: {
13
13
  base: `
14
14
  flex gap-3 w-full px-3 relative bg-default transition
15
- border rounded-medium border-default outline-transparent
15
+ border border-solid rounded-medium border-default outline-transparent
16
16
  hover:border-lifted hover:focus-within:border-primary focus-within:border-primary
17
17
  focus-within:outline focus-within:outline-small focus-within:outline-primary focus-within:transition
18
18
  `,
@@ -318,7 +318,7 @@ describe("UInput.vue", () => {
318
318
  });
319
319
 
320
320
  const labelComponent = component.getComponent(ULabel);
321
- const labelElement = labelComponent.find("label");
321
+ const labelElement = labelComponent.find("[vl-child-key='label']");
322
322
 
323
323
  expect(labelElement.text()).toBe(customLabelContent);
324
324
  });
@@ -336,7 +336,7 @@ describe("UInput.vue", () => {
336
336
  });
337
337
 
338
338
  const labelComponent = component.getComponent(ULabel);
339
- const labelElement = labelComponent.find("label");
339
+ const labelElement = labelComponent.find("[vl-child-key='label']");
340
340
 
341
341
  expect(labelElement.text()).toBe(`Modified ${defaultLabel}`);
342
342
  });
@@ -27,6 +27,18 @@ const emit = defineEmits([
27
27
  * @property {number} modelValue
28
28
  */
29
29
  "update:modelValue",
30
+
31
+ /**
32
+ * Triggers when the input gains focus.
33
+ * @property {FocusEvent} event
34
+ */
35
+ "focus",
36
+
37
+ /**
38
+ * Triggers when the input loses focus.
39
+ * @property {FocusEvent} event
40
+ */
41
+ "blur",
30
42
  ]);
31
43
 
32
44
  const inputComponentRef = useTemplateRef<InstanceType<typeof UInput>>("inputComponent");
@@ -120,9 +132,15 @@ function onMouseLeave() {
120
132
  clearIntervals();
121
133
  }
122
134
 
123
- function onBlur() {
135
+ function onFocus(event: FocusEvent) {
136
+ emit("focus", event);
137
+ }
138
+
139
+ function onBlur(event: FocusEvent) {
124
140
  if (Number(count.value) > props.max) count.value = props.max;
125
141
  if (Number(count.value) < props.min) count.value = props.min;
142
+
143
+ emit("blur", event);
126
144
  }
127
145
 
128
146
  function onInput() {
@@ -187,7 +205,13 @@ const {
187
205
  :size="size"
188
206
  :disabled="disabled"
189
207
  :readonly="readonly"
208
+ :min-fraction-digits="minFractionDigits"
209
+ :max-fraction-digits="maxFractionDigits"
210
+ :decimal-separator="decimalSeparator"
211
+ :thousands-separator="thousandsSeparator"
212
+ :prefix="prefix"
190
213
  v-bind="counterInputAttrs"
214
+ @focus="onFocus"
191
215
  @blur="onBlur"
192
216
  @input="onInput"
193
217
  />
@@ -10,7 +10,7 @@ export default /*tw*/ {
10
10
  },
11
11
  },
12
12
  counterInput: {
13
- base: "{UInputNumber} w-fit",
13
+ base: "{UInputNumber} w-inherit",
14
14
  numberInput: {
15
15
  base: "{UInput}",
16
16
  input: "text-center",
@@ -33,9 +33,14 @@ export default /*tw*/ {
33
33
  subtractIcon: "{UIcon}",
34
34
  defaults: {
35
35
  size: "md",
36
+ decimalSeparator: ",",
37
+ thousandsSeparator: " ",
38
+ prefix: "",
36
39
  step: 1,
37
- min: 1,
40
+ min: 0,
38
41
  max: 999,
42
+ minFractionDigits: 0,
43
+ maxFractionDigits: 2,
39
44
  readonly: false,
40
45
  disabled: false,
41
46
  /* icons */
@@ -1,4 +1,4 @@
1
- import { mount } from "@vue/test-utils";
1
+ import { flushPromises, mount } from "@vue/test-utils";
2
2
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
3
 
4
4
  import UInputCounter from "../UInputCounter.vue";
@@ -169,6 +169,90 @@ describe("UInputCounter.vue", () => {
169
169
  expect(addButton.props("disabled")).toBe(true);
170
170
  });
171
171
 
172
+ it("Min Fraction Digit – set fraction digit when it was not provided", async () => {
173
+ const initialValue = 12345678;
174
+ const initialValueWithFactions = "12 345 678,00";
175
+
176
+ const component = mount(UInputCounter, {
177
+ props: {
178
+ modelValue: initialValue,
179
+ minFractionDigits: 2,
180
+ },
181
+ });
182
+
183
+ await flushPromises();
184
+
185
+ expect(component.get("input").element.value).toBe(initialValueWithFactions);
186
+ });
187
+
188
+ it("Max Fraction Digit – truncate fraction digit to max value", async () => {
189
+ const initialValue = 12345678.011;
190
+ const initialValueWithFactions = "12 345 678,01";
191
+
192
+ const component = mount(UInputCounter, {
193
+ props: {
194
+ modelValue: initialValue,
195
+ maxFractionDigits: 2,
196
+ },
197
+ });
198
+
199
+ await flushPromises();
200
+
201
+ expect(component.get("input").element.value).toBe(initialValueWithFactions);
202
+ });
203
+
204
+ it("Decimal Separator – set correct decimal separator char", async () => {
205
+ const initialValue = 12345678.01;
206
+ const expectedDecimalSubString = "/01";
207
+
208
+ const component = mount(UInputCounter, {
209
+ props: {
210
+ modelValue: initialValue,
211
+ decimalSeparator: "/",
212
+ },
213
+ });
214
+
215
+ await flushPromises();
216
+
217
+ expect(component.get("input").element.value).toContain(expectedDecimalSubString);
218
+ });
219
+
220
+ it("Thousands Separator – set correct decimal separator char", async () => {
221
+ const initialValue = 12_345_678.01;
222
+ const expectedThousandsSeparator = "/";
223
+ const expectedThousandsSeparatorAmount = 2;
224
+
225
+ const component = mount(UInputCounter, {
226
+ props: {
227
+ modelValue: initialValue,
228
+ thousandsSeparator: expectedThousandsSeparator,
229
+ },
230
+ });
231
+
232
+ await flushPromises();
233
+
234
+ const inputValue = component.get("input").element.value;
235
+ const separatorCount = inputValue.split(expectedThousandsSeparator).length - 1;
236
+
237
+ expect(separatorCount).toBe(expectedThousandsSeparatorAmount);
238
+ });
239
+
240
+ it("Prefix – displays prefix at the beginning of the value", async () => {
241
+ const initialValue = 12345678;
242
+ const prefix = "pizza";
243
+
244
+ const component = mount(UInputCounter, {
245
+ props: {
246
+ modelValue: initialValue,
247
+ prefix: prefix,
248
+ },
249
+ });
250
+
251
+ await flushPromises();
252
+
253
+ expect(component.get("input").element.value.startsWith(prefix)).toBe(true);
254
+ });
255
+
172
256
  it("Readonly – renders text instead of input when readonly set to true", async () => {
173
257
  const component = mount(UInputCounter, {
174
258
  props: {
@@ -25,6 +25,31 @@ export interface Props {
25
25
  */
26
26
  max?: number;
27
27
 
28
+ /**
29
+ * Minimal number of signs after the decimal separator (fractional part of a number).
30
+ */
31
+ minFractionDigits?: number;
32
+
33
+ /**
34
+ * Maximal number of signs after the decimal separator (fractional part of a number).
35
+ */
36
+ maxFractionDigits?: number;
37
+
38
+ /**
39
+ * A symbol used to separate the integer part from the fractional part of a number.
40
+ */
41
+ decimalSeparator?: string;
42
+
43
+ /**
44
+ * A symbol used to separate the thousand parts of a number.
45
+ */
46
+ thousandsSeparator?: string;
47
+
48
+ /**
49
+ * Prefix to display before input value.
50
+ */
51
+ prefix?: string;
52
+
28
53
  /**
29
54
  * Input size.
30
55
  */
@@ -269,7 +269,7 @@ describe("UInputFile.vue", () => {
269
269
  });
270
270
 
271
271
  const labelComponent = component.getComponent(ULabel);
272
- const labelElement = labelComponent.find("label");
272
+ const labelElement = labelComponent.find("[vl-child-key='label']");
273
273
 
274
274
  expect(labelElement.text()).toBe(customLabelContent);
275
275
  });
@@ -287,7 +287,7 @@ describe("UInputFile.vue", () => {
287
287
  });
288
288
 
289
289
  const labelComponent = component.getComponent(ULabel);
290
- const labelElement = labelComponent.find("label");
290
+ const labelElement = labelComponent.find("[vl-child-key='label']");
291
291
 
292
292
  expect(labelElement.text()).toBe(`Custom ${defaultLabel}`);
293
293
  });
@@ -11,7 +11,7 @@ import useFormatNumber from "./useFormatNumber";
11
11
  import { COMPONENT_NAME, RAW_DECIMAL_MARK } from "./constants";
12
12
 
13
13
  import type { Props, Config } from "./types";
14
- import { getRawValue } from "./utilFormat";
14
+ import { getRawValue, getFixedNumber } from "./utilFormat";
15
15
 
16
16
  defineOptions({ inheritAttrs: false });
17
17
 
@@ -41,8 +41,15 @@ const emit = defineEmits([
41
41
  */
42
42
  "keyup",
43
43
 
44
+ /**
45
+ * Triggers when the input gains focus.
46
+ * @property {FocusEvent} event
47
+ */
48
+ "focus",
49
+
44
50
  /**
45
51
  * Triggers when the input loses focus.
52
+ * @property {FocusEvent} event
46
53
  */
47
54
  "blur",
48
55
  ]);
@@ -97,19 +104,23 @@ watch(
97
104
  );
98
105
 
99
106
  onMounted(() => {
100
- if (localValue.value) {
107
+ if (localValue.value || localValue.value === 0) {
101
108
  setValue(stringLocalValue.value);
102
109
  }
103
110
  });
104
111
 
105
112
  function onKeyup(event: KeyboardEvent) {
106
- const numberValue = !Number.isNaN(parseFloat(rawValue.value)) ? parseFloat(rawValue.value) : "";
113
+ const numberValue = getFixedNumber(parseFloat(rawValue.value), props.maxFractionDigits || 10);
107
114
 
108
115
  localValue.value = props.valueType === "number" ? numberValue : rawValue.value || "";
109
116
 
110
117
  emit("keyup", event);
111
118
  }
112
119
 
120
+ function onFocus(event: FocusEvent) {
121
+ emit("focus", event);
122
+ }
123
+
113
124
  function onBlur(event: FocusEvent) {
114
125
  emit("blur", event);
115
126
  }
@@ -164,6 +175,7 @@ const { getDataTest, numberInputAttrs } = useUI<Config>(defaultConfig);
164
175
  v-bind="numberInputAttrs"
165
176
  :data-test="getDataTest()"
166
177
  @keyup="onKeyup"
178
+ @focus="onFocus"
167
179
  @blur="onBlur"
168
180
  @input="onInput"
169
181
  >
@@ -2,6 +2,13 @@ import { RAW_DECIMAL_MARK } from "./constants";
2
2
 
3
3
  import type { FormatOptions } from "./types";
4
4
 
5
+ export function getFixedNumber(value: number, maxDecimals: number = 10): number | "" {
6
+ if (!isFinite(value) || isNaN(value)) return "";
7
+ if (maxDecimals < 0) maxDecimals = 10;
8
+
9
+ return parseFloat(value.toFixed(maxDecimals));
10
+ }
11
+
5
12
  export function getRawValue(
6
13
  value: string,
7
14
  options: Pick<FormatOptions, "prefix" | "decimalSeparator" | "thousandsSeparator">,
@@ -26,6 +33,11 @@ export function getFormattedValue(value: string, options: FormatOptions): string
26
33
  const isValidMinFractionDigits = minFractionDigits <= maxFractionDigits;
27
34
  const actualMinFractionDigit = isValidMinFractionDigits ? minFractionDigits : maxFractionDigits;
28
35
 
36
+ const numericValue = parseFloat(value);
37
+ const fixedValue = getFixedNumber(numericValue, Math.max(maxFractionDigits, 10));
38
+
39
+ if (fixedValue === "") return prefix;
40
+
29
41
  const intlNumberOptions: Intl.NumberFormatOptions = {
30
42
  minimumFractionDigits: actualMinFractionDigit,
31
43
  maximumFractionDigits: maxFractionDigits,
@@ -38,14 +50,12 @@ export function getFormattedValue(value: string, options: FormatOptions): string
38
50
 
39
51
  const intlNumber = new Intl.NumberFormat("en-US", intlNumberOptions);
40
52
 
41
- const formattedValue = intlNumber
42
- .formatToParts(value as Intl.StringNumericLiteral)
43
- .map((part) => {
44
- if (part.type === "group") part.value = thousandsSeparator;
45
- if (part.type === "decimal") part.value = decimalSeparator;
53
+ const formattedValue = intlNumber.formatToParts(fixedValue).map((part) => {
54
+ if (part.type === "group") part.value = thousandsSeparator;
55
+ if (part.type === "decimal") part.value = decimalSeparator;
46
56
 
47
- return part;
48
- });
57
+ return part;
58
+ });
49
59
 
50
60
  formattedValue.unshift({ value: prefix, type: "minusSign" });
51
61
 
@@ -21,11 +21,23 @@ const props = withDefaults(defineProps<Props>(), {
21
21
 
22
22
  const emit = defineEmits([
23
23
  /**
24
- * Triggers when the input value is changes.
24
+ * Triggers when the input value is changed.
25
25
  * @property {string} modelValue
26
26
  * @property {number} modelValue
27
27
  */
28
28
  "update:modelValue",
29
+
30
+ /**
31
+ * Triggers when the input gains focus.
32
+ * @property {FocusEvent} event
33
+ */
34
+ "focus",
35
+
36
+ /**
37
+ * Triggers when the input loses focus.
38
+ * @property {FocusEvent} event
39
+ */
40
+ "blur",
29
41
  ]);
30
42
 
31
43
  const elementId = props.id || useId();
@@ -51,6 +63,14 @@ function onClickShowPassword() {
51
63
  isShownPassword.value = !isShownPassword.value;
52
64
  }
53
65
 
66
+ function onFocus(event: FocusEvent) {
67
+ emit("focus", event);
68
+ }
69
+
70
+ function onBlur(event: FocusEvent) {
71
+ emit("blur", event);
72
+ }
73
+
54
74
  /**
55
75
  * Get element / nested component attributes for each config token ✨
56
76
  * Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
@@ -80,6 +100,8 @@ const { getDataTest, config, passwordInputAttrs, passwordIconAttrs, passwordIcon
80
100
  :disabled="disabled"
81
101
  v-bind="passwordInputAttrs"
82
102
  :data-test="getDataTest()"
103
+ @focus="onFocus"
104
+ @blur="onBlur"
83
105
  >
84
106
  <template #left>
85
107
  <!--
@@ -33,6 +33,10 @@ const isHorizontalPlacement = computed(() => {
33
33
  return props.align === PLACEMENT.left || props.align === PLACEMENT.right;
34
34
  });
35
35
 
36
+ const tag = computed(() => {
37
+ return props.for ? "label" : "div";
38
+ });
39
+
36
40
  const isTopWithDescPlacement = computed(() => {
37
41
  return props.align === PLACEMENT.topWithDesc;
38
42
  });
@@ -95,7 +99,8 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
95
99
 
96
100
  <!-- `v-bind` isn't assigned, because the div is system -->
97
101
  <div v-if="label || hasSlotContent(slots['label'], { label }) || error || description">
98
- <label
102
+ <component
103
+ :is="tag"
99
104
  v-if="label || hasSlotContent(slots['label'], { label })"
100
105
  ref="label"
101
106
  :for="props.for"
@@ -110,7 +115,7 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
110
115
  <slot name="label" :label="label">
111
116
  {{ label }}
112
117
  </slot>
113
- </label>
118
+ </component>
114
119
 
115
120
  <div
116
121
  v-if="isShownError"
@@ -132,7 +137,8 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
132
137
  </div>
133
138
 
134
139
  <div v-else ref="wrapper" v-bind="wrapperAttrs">
135
- <label
140
+ <component
141
+ :is="tag"
136
142
  v-if="label || hasSlotContent(slots['label'], { label })"
137
143
  v-bind="labelAttrs"
138
144
  ref="label"
@@ -147,7 +153,7 @@ const { getDataTest, wrapperAttrs, contentAttrs, labelAttrs, descriptionAttrs, e
147
153
  <slot name="label" :label="label">
148
154
  {{ label }}
149
155
  </slot>
150
- </label>
156
+ </component>
151
157
 
152
158
  <div v-bind="contentAttrs" :data-test="getDataTest('content')">
153
159
  <!-- @slot Use it to add label content. -->
@@ -16,7 +16,7 @@ describe("ULabel.vue", () => {
16
16
  },
17
17
  });
18
18
 
19
- expect(component.get("label").text()).toContain(label);
19
+ expect(component.find("[vl-key='label']").text()).toContain(label);
20
20
  });
21
21
 
22
22
  it("For – applied to label element", () => {
@@ -29,7 +29,7 @@ describe("ULabel.vue", () => {
29
29
  },
30
30
  });
31
31
 
32
- expect(component.get("label").attributes("for")).toBe(forId);
32
+ expect(component.find("[vl-key='label']").attributes("for")).toBe(forId);
33
33
  });
34
34
 
35
35
  it("For – applies interactive classes when true", () => {
@@ -42,7 +42,7 @@ describe("ULabel.vue", () => {
42
42
  },
43
43
  });
44
44
 
45
- expect(component.get("label").attributes("class")).toContain(interactiveClasses);
45
+ expect(component.find("[vl-key='label']").attributes("class")).toContain(interactiveClasses);
46
46
  });
47
47
 
48
48
  it("Description – description is rendered with provided text", () => {
@@ -133,7 +133,7 @@ describe("ULabel.vue", () => {
133
133
  },
134
134
  });
135
135
 
136
- expect(component.get("label").attributes("class")).toContain(expectedClass);
136
+ expect(component.find("[vl-key='label']").attributes("class")).toContain(expectedClass);
137
137
  });
138
138
  });
139
139
 
@@ -148,7 +148,7 @@ describe("ULabel.vue", () => {
148
148
  });
149
149
 
150
150
  expect(component.attributes("class")).toContain(disabledClasses);
151
- expect(component.get("label").attributes("class")).toContain(disabledClasses);
151
+ expect(component.find("[vl-key='label']").attributes("class")).toContain(disabledClasses);
152
152
  });
153
153
 
154
154
  it("Disabled – does not apply disabled class when false", () => {
@@ -162,7 +162,7 @@ describe("ULabel.vue", () => {
162
162
  });
163
163
 
164
164
  expect(component.attributes("class")).not.toContain(disabledClasses);
165
- expect(component.get("label").attributes("class")).not.toContain(disabledClasses);
165
+ expect(component.find("[vl-key='label']").attributes("class")).not.toContain(disabledClasses);
166
166
  });
167
167
 
168
168
  it("Disabled – interactive class is not applied when disabled true", () => {
@@ -176,7 +176,9 @@ describe("ULabel.vue", () => {
176
176
  },
177
177
  });
178
178
 
179
- expect(component.get("label").attributes("class")).not.toContain(interactiveClasses);
179
+ expect(component.find("[vl-key='label']").attributes("class")).not.toContain(
180
+ interactiveClasses,
181
+ );
180
182
  });
181
183
 
182
184
  it("Disabled – topInside label doesn't have focus styles disabled", () => {
@@ -190,7 +192,7 @@ describe("ULabel.vue", () => {
190
192
  },
191
193
  });
192
194
 
193
- expect(component.get("label").attributes("class")).not.toContain(focusClasses);
195
+ expect(component.find("[vl-key='label']").attributes("class")).not.toContain(focusClasses);
194
196
  });
195
197
 
196
198
  it("Disabled – error is not rendered when disabled is true", () => {
@@ -294,7 +296,7 @@ describe("ULabel.vue", () => {
294
296
  },
295
297
  });
296
298
 
297
- const labelElement = component.get("label");
299
+ const labelElement = component.find("[vl-key='label']");
298
300
 
299
301
  expect(labelElement.text()).toBe(slotContent);
300
302
  expect(labelElement.text()).not.toContain(defaultLabel);
@@ -313,7 +315,7 @@ describe("ULabel.vue", () => {
313
315
  },
314
316
  });
315
317
 
316
- expect(component.get("label").text()).toBe(slotContent);
318
+ expect(component.find("[vl-key='label']").text()).toBe(slotContent);
317
319
  });
318
320
 
319
321
  it("Bottom – renders content from bottom slot", () => {
@@ -337,7 +339,7 @@ describe("ULabel.vue", () => {
337
339
  },
338
340
  });
339
341
 
340
- const labelElement = component.get("label");
342
+ const labelElement = component.find("[vl-key='label']");
341
343
 
342
344
  await labelElement.trigger("click");
343
345
 
@@ -346,7 +348,7 @@ describe("ULabel.vue", () => {
346
348
  });
347
349
 
348
350
  describe("Exposed properties", () => {
349
- it("Exposes label element ref", () => {
351
+ it("Exposes label element ref as a div", () => {
350
352
  const defaultLabel = "Label";
351
353
 
352
354
  const component = mount(ULabel, {
@@ -355,6 +357,21 @@ describe("ULabel.vue", () => {
355
357
  },
356
358
  });
357
359
 
360
+ expect(component.vm.labelElement).toBeDefined();
361
+ expect(component.vm.labelElement!.tagName).toBe("DIV");
362
+ expect(component.vm.labelElement!.textContent).toBe(defaultLabel);
363
+ });
364
+
365
+ it("Exposes label element ref as a label", () => {
366
+ const defaultLabel = "Label";
367
+
368
+ const component = mount(ULabel, {
369
+ props: {
370
+ label: defaultLabel,
371
+ for: "test-id",
372
+ },
373
+ });
374
+
358
375
  expect(component.vm.labelElement).toBeDefined();
359
376
  expect(component.vm.labelElement!.tagName).toBe("LABEL");
360
377
  expect(component.vm.labelElement!.textContent).toBe(defaultLabel);
@@ -49,6 +49,7 @@ const emit = defineEmits([
49
49
 
50
50
  /**
51
51
  * Triggers when the search input value changes.
52
+ * @property {string} value
52
53
  */
53
54
  "searchChange",
54
55
 
@@ -56,6 +57,12 @@ const emit = defineEmits([
56
57
  * Triggers when the search input loses focus.
57
58
  */
58
59
  "searchBlur",
60
+
61
+ /**
62
+ * Triggers when the search v-model updates.
63
+ * @property {string} query
64
+ */
65
+ "update:search",
59
66
  ]);
60
67
 
61
68
  const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
@@ -66,7 +73,7 @@ const addOptionRef = useTemplateRef<HTMLLIElement>("add-option");
66
73
 
67
74
  const wrapperMaxHeight = ref("");
68
75
 
69
- const search = ref("");
76
+ const localSearch = ref(props.search ?? "");
70
77
 
71
78
  const { pointer, pointerDirty, pointerSet, pointerBackward, pointerForward, pointerReset } =
72
79
  usePointer(props.options, optionsRef, wrapperRef);
@@ -79,6 +86,15 @@ const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>
79
86
  props?.config?.i18n,
80
87
  );
81
88
 
89
+ const searchModel = computed({
90
+ get: () => localSearch.value,
91
+ set: (value: string) => {
92
+ emit("update:search", value);
93
+
94
+ localSearch.value = value ?? "";
95
+ },
96
+ });
97
+
82
98
  const selectedValue = computed({
83
99
  get: () => {
84
100
  if (props.multiple && !Array.isArray(props.modelValue)) {
@@ -87,11 +103,7 @@ const selectedValue = computed({
87
103
 
88
104
  return props.modelValue;
89
105
  },
90
- set: (value) => {
91
- if (search.value) search.value = "";
92
-
93
- emit("update:modelValue", value);
94
- },
106
+ set: (value) => emit("update:modelValue", value),
95
107
  });
96
108
 
97
109
  const addOptionKeyCombination = computed(() => {
@@ -99,7 +111,7 @@ const addOptionKeyCombination = computed(() => {
99
111
  });
100
112
 
101
113
  const filteredOptions = computed(() => {
102
- const normalizedSearch = search.value.toLowerCase().trim();
114
+ const normalizedSearch = searchModel.value.toLowerCase().trim();
103
115
 
104
116
  let options = [...props.options];
105
117
 
@@ -117,7 +129,7 @@ const filteredOptions = computed(() => {
117
129
  });
118
130
 
119
131
  watch(
120
- () => [props.options, props.size, props.visibleOptions, props.searchable],
132
+ () => [props.options, props.size, props.visibleOptions, props.searchable, searchModel.value],
121
133
  () => {
122
134
  nextTick(() => {
123
135
  const options = [
@@ -418,7 +430,7 @@ const {
418
430
  v-if="searchable"
419
431
  :id="elementId"
420
432
  ref="listbox-input"
421
- v-model="search"
433
+ v-model="searchModel"
422
434
  :placeholder="localeMessages.search"
423
435
  :size="size"
424
436
  :debounce="debounce"
@@ -2,7 +2,7 @@ export default /*tw*/ {
2
2
  wrapper: {
3
3
  base: `
4
4
  my-2 p-1 flex flex-col gap-1 w-auto absolute z-50 shadow-sm
5
- rounded-medium border border-default bg-default
5
+ rounded-medium border border-solid border-default bg-default
6
6
  overflow-auto [-webkit-overflow-scrolling:touch]
7
7
  focus:outline-hidden
8
8
  `,