vueless 1.2.8-beta.1 → 1.2.8-beta.10

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 (53) hide show
  1. package/constants.d.ts +3 -0
  2. package/constants.js +3 -0
  3. package/icons/storybook/rocket_launch.svg +1 -0
  4. package/package.json +1 -1
  5. package/ui.container-accordion/UAccordion.vue +0 -1
  6. package/ui.container-accordion/config.ts +1 -1
  7. package/ui.container-accordion/storybook/stories.ts +13 -1
  8. package/ui.container-accordion-item/UAccordionItem.vue +17 -4
  9. package/ui.container-accordion-item/config.ts +1 -1
  10. package/ui.container-accordion-item/storybook/stories.ts +26 -1
  11. package/ui.container-accordion-item/tests/UAccordionItem.test.ts +186 -0
  12. package/ui.dropdown-badge/UDropdownBadge.vue +51 -2
  13. package/ui.dropdown-badge/config.ts +5 -1
  14. package/ui.dropdown-badge/storybook/stories.ts +280 -4
  15. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +194 -0
  16. package/ui.dropdown-badge/types.ts +30 -0
  17. package/ui.dropdown-button/UDropdownButton.vue +51 -2
  18. package/ui.dropdown-button/config.ts +5 -1
  19. package/ui.dropdown-button/storybook/stories.ts +288 -3
  20. package/ui.dropdown-button/tests/UDropdownButton.test.ts +190 -0
  21. package/ui.dropdown-button/types.ts +30 -0
  22. package/ui.dropdown-link/UDropdownLink.vue +51 -2
  23. package/ui.dropdown-link/config.ts +5 -1
  24. package/ui.dropdown-link/storybook/stories.ts +281 -4
  25. package/ui.dropdown-link/tests/UDropdownLink.test.ts +194 -0
  26. package/ui.dropdown-link/types.ts +30 -0
  27. package/ui.form-checkbox/tests/UCheckbox.test.ts +2 -2
  28. package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +2 -2
  29. package/ui.form-input/UInput.vue +4 -2
  30. package/ui.form-input/tests/UInput.test.ts +2 -2
  31. package/ui.form-input-counter/UInputCounter.vue +25 -1
  32. package/ui.form-input-counter/config.ts +7 -2
  33. package/ui.form-input-counter/tests/UInputCounter.test.ts +85 -1
  34. package/ui.form-input-counter/types.ts +25 -0
  35. package/ui.form-input-file/tests/UInputFile.test.ts +2 -2
  36. package/ui.form-input-number/UInputNumber.vue +15 -3
  37. package/ui.form-input-number/utilFormat.ts +17 -7
  38. package/ui.form-input-password/UInputPassword.vue +23 -1
  39. package/ui.form-label/ULabel.vue +10 -4
  40. package/ui.form-label/tests/ULabel.test.ts +29 -12
  41. package/ui.form-listbox/UListbox.vue +21 -5
  42. package/ui.form-listbox/storybook/stories.ts +188 -1
  43. package/ui.form-listbox/tests/UListbox.test.ts +36 -0
  44. package/ui.form-listbox/types.ts +5 -0
  45. package/ui.form-radio/tests/URadio.test.ts +2 -2
  46. package/ui.form-radio-group/tests/URadioGroup.test.ts +2 -2
  47. package/ui.form-select/USelect.vue +19 -2
  48. package/ui.form-select/config.ts +1 -0
  49. package/ui.form-select/storybook/stories.ts +31 -4
  50. package/ui.form-select/tests/USelect.test.ts +143 -0
  51. package/ui.form-select/types.ts +10 -0
  52. package/ui.form-textarea/tests/UTextarea.test.ts +2 -2
  53. package/utils/theme.ts +61 -5
@@ -8,10 +8,21 @@ import {
8
8
 
9
9
  import UListbox from "../UListbox.vue";
10
10
  import URow from "../../ui.container-row/URow.vue";
11
+ import UCol from "../../ui.container-col/UCol.vue";
12
+ import UAvatar from "../../ui.image-avatar/UAvatar.vue";
13
+ import UIcon from "../../ui.image-icon/UIcon.vue";
14
+ import UBadge from "../../ui.text-badge/UBadge.vue";
15
+ import UText from "../../ui.text-block/UText.vue";
16
+ import ULoader from "../../ui.loader/ULoader.vue";
11
17
 
12
18
  import type { Meta, StoryFn } from "@storybook/vue3-vite";
13
19
  import type { Option, Props } from "../types";
14
20
 
21
+ import johnDoe from "../../ui.form-select/storybook/assets/images/john-doe.png";
22
+ import emilyDavis from "../../ui.form-select/storybook/assets/images/emily-davis.png";
23
+ import alexJohnson from "../../ui.form-select/storybook/assets/images/alex-johnson.png";
24
+ import patMorgan from "../../ui.form-select/storybook/assets/images/pat-morgan.png";
25
+
15
26
  interface DefaultUListboxArgs extends Props {
16
27
  slotTemplate?: string;
17
28
  }
@@ -47,7 +58,7 @@ export default {
47
58
  } as Meta;
48
59
 
49
60
  const DefaultTemplate: StoryFn<DefaultUListboxArgs> = (args: DefaultUListboxArgs) => ({
50
- components: { UListbox },
61
+ components: { UListbox, ULoader, UText, URow },
51
62
  setup: () => ({ args, slots: getSlotNames(UListbox.__name) }),
52
63
  template: `
53
64
  <UListbox
@@ -82,6 +93,9 @@ Default.args = {};
82
93
  export const Searchable = DefaultTemplate.bind({});
83
94
  Searchable.args = { searchable: true };
84
95
 
96
+ export const SearchModelValue = DefaultTemplate.bind({});
97
+ SearchModelValue.args = { search: "New York", searchable: true };
98
+
85
99
  export const Multiple = DefaultTemplate.bind({});
86
100
  Multiple.args = { multiple: true, modelValue: [] };
87
101
 
@@ -205,3 +219,176 @@ OptionSettings.parameters = {
205
219
  },
206
220
  },
207
221
  };
222
+
223
+ export const EmptySlot = DefaultTemplate.bind({});
224
+ EmptySlot.args = {
225
+ options: [],
226
+ slotTemplate: `
227
+ <template #empty>
228
+ <URow align="center">
229
+ <ULoader loading size="sm" />
230
+ <UText label="Loading, this may take a while..." />
231
+ </URow>
232
+ </template>
233
+ `,
234
+ };
235
+
236
+ export const OptionSlots: StoryFn<DefaultUListboxArgs> = (args) => ({
237
+ components: { UListbox, URow, UCol, UAvatar, UIcon, UBadge, UText },
238
+ setup: () => ({ args, johnDoe, emilyDavis, alexJohnson, patMorgan }),
239
+ template: `
240
+ <URow gap="lg">
241
+ <UListbox
242
+ v-model="args.beforeOptionModel"
243
+ class="static"
244
+ :options="[
245
+ {
246
+ label: 'John Doe',
247
+ id: '1',
248
+ role: 'Developer',
249
+ avatar: johnDoe,
250
+ status: 'online',
251
+ statusColor: 'success',
252
+ },
253
+ {
254
+ label: 'Jane Smith',
255
+ id: '2',
256
+ role: 'Designer',
257
+ avatar: emilyDavis,
258
+ status: 'away',
259
+ statusColor: 'warning',
260
+ },
261
+ {
262
+ label: 'Mike Johnson',
263
+ id: '3',
264
+ role: 'Product Manager',
265
+ avatar: alexJohnson,
266
+ status: 'offline',
267
+ statusColor: 'grayscale',
268
+ },
269
+ {
270
+ label: 'Sarah Wilson',
271
+ id: '4',
272
+ role: 'QA Engineer',
273
+ avatar: patMorgan,
274
+ status: 'online',
275
+ statusColor: 'success',
276
+ },
277
+ ]"
278
+ >
279
+ <template #before-option="{ option }">
280
+ <UAvatar :src="option.avatar" size="sm" />
281
+ </template>
282
+ </UListbox>
283
+
284
+ <UListbox
285
+ v-model="args.optionModel"
286
+ class="static"
287
+ :options="[
288
+ {
289
+ label: 'John Doe',
290
+ id: '1',
291
+ role: 'Developer',
292
+ avatar: johnDoe,
293
+ status: 'online',
294
+ statusColor: 'success',
295
+ },
296
+ {
297
+ label: 'Jane Smith',
298
+ id: '2',
299
+ role: 'Designer',
300
+ avatar: emilyDavis,
301
+ status: 'away',
302
+ statusColor: 'warning',
303
+ },
304
+ {
305
+ label: 'Mike Johnson',
306
+ id: '3',
307
+ role: 'Product Manager',
308
+ avatar: alexJohnson,
309
+ status: 'offline',
310
+ statusColor: 'grayscale',
311
+ },
312
+ {
313
+ label: 'Sarah Wilson',
314
+ id: '4',
315
+ role: 'QA Engineer',
316
+ avatar: patMorgan,
317
+ status: 'online',
318
+ statusColor: 'success',
319
+ },
320
+ ]"
321
+ >
322
+ <template #option="{ option }">
323
+ <URow align="center" gap="xs">
324
+ <UCol gap="none">
325
+ <UText size="sm">{{ option.label }}</UText>
326
+ <UText variant="lifted" size="xs">{{ option.role }}</UText>
327
+ </UCol>
328
+ <UBadge
329
+ :label="option.status"
330
+ :color="option.statusColor"
331
+ size="sm"
332
+ variant="subtle"
333
+ />
334
+ </URow>
335
+ </template>
336
+ </UListbox>
337
+
338
+ <UListbox
339
+ v-model="args.afterOptionModel"
340
+ class="static"
341
+ :options="[
342
+ {
343
+ label: 'John Doe',
344
+ id: '1',
345
+ role: 'Developer',
346
+ avatar: johnDoe,
347
+ status: 'online',
348
+ statusColor: 'success',
349
+ },
350
+ {
351
+ label: 'Jane Smith',
352
+ id: '2',
353
+ role: 'Designer',
354
+ avatar: emilyDavis,
355
+ status: 'away',
356
+ statusColor: 'warning',
357
+ },
358
+ {
359
+ label: 'Mike Johnson',
360
+ id: '3',
361
+ role: 'Product Manager',
362
+ avatar: alexJohnson,
363
+ status: 'offline',
364
+ statusColor: 'grayscale',
365
+ },
366
+ {
367
+ label: 'Sarah Wilson',
368
+ id: '4',
369
+ role: 'QA Engineer',
370
+ avatar: patMorgan,
371
+ status: 'online',
372
+ statusColor: 'success',
373
+ },
374
+ ]"
375
+ >
376
+ <template #after-option="{ option }">
377
+ <UBadge
378
+ :label="option.status"
379
+ :color="option.statusColor"
380
+ size="sm"
381
+ variant="subtle"
382
+ />
383
+ </template>
384
+ </UListbox>
385
+ </URow>
386
+ `,
387
+ });
388
+ OptionSlots.parameters = {
389
+ docs: {
390
+ story: {
391
+ height: "300px",
392
+ },
393
+ },
394
+ };
@@ -417,6 +417,41 @@ describe("UListbox.vue", () => {
417
417
  });
418
418
 
419
419
  describe("Functionality", () => {
420
+ it("Search v-model – filters options using external search prop", async () => {
421
+ const targetValue = "Option 2";
422
+
423
+ const component = mount(UListbox, {
424
+ props: {
425
+ searchable: true,
426
+ options: defaultOptions,
427
+ search: targetValue,
428
+ },
429
+ });
430
+
431
+ await flushPromises();
432
+
433
+ const options = component.findAll('[vl-key="option"]');
434
+
435
+ expect(options).toHaveLength(1);
436
+ expect(options[0].text()).toBe(targetValue);
437
+ });
438
+
439
+ it("Search v-model – emits update:search on input change", async () => {
440
+ const component = mount(UListbox, {
441
+ props: {
442
+ searchable: true,
443
+ options: defaultOptions,
444
+ },
445
+ });
446
+
447
+ const searchInput = component.getComponent(UInputSearch);
448
+
449
+ await searchInput.setValue("Option 3");
450
+
451
+ expect(component.emitted("update:search")).toBeTruthy();
452
+ expect(component.emitted("update:search")![0][0]).toBe("Option 3");
453
+ });
454
+
420
455
  it("Search – filters options based on search input", async () => {
421
456
  const targetValue = "Option 1";
422
457
  const filteredOptionsAmount = 1;
@@ -431,6 +466,7 @@ describe("UListbox.vue", () => {
431
466
  const searchInput = component.getComponent(UInputSearch);
432
467
 
433
468
  await searchInput.setValue(targetValue);
469
+ await flushPromises();
434
470
 
435
471
  const options = component.findAll('[vl-key="option"]');
436
472
 
@@ -24,6 +24,11 @@ export interface Props {
24
24
  */
25
25
  modelValue?: string | number | UnknownObject | (string | number | UnknownObject)[];
26
26
 
27
+ /**
28
+ * Search input model value.
29
+ */
30
+ search?: string;
31
+
27
32
  /**
28
33
  * List options.
29
34
  */
@@ -183,7 +183,7 @@ describe("URadio.vue", () => {
183
183
  });
184
184
 
185
185
  const labelComponent = component.getComponent(ULabel);
186
- const labelElement = labelComponent.find("label");
186
+ const labelElement = labelComponent.find("[vl-child-key='label']");
187
187
 
188
188
  expect(labelElement.text()).toBe(customLabelContent);
189
189
  });
@@ -201,7 +201,7 @@ describe("URadio.vue", () => {
201
201
  });
202
202
 
203
203
  const labelComponent = component.getComponent(ULabel);
204
- const labelElement = labelComponent.find("label");
204
+ const labelElement = labelComponent.find("[vl-child-key='label']");
205
205
 
206
206
  expect(labelElement.text()).toBe(`Modified ${defaultLabel}`);
207
207
  });
@@ -211,7 +211,7 @@ describe("URadioGroup.vue", () => {
211
211
  });
212
212
 
213
213
  const labelComponent = component.getComponent(ULabel);
214
- const labelElement = labelComponent.find("label");
214
+ const labelElement = labelComponent.find("[vl-child-key='label']");
215
215
 
216
216
  expect(labelElement.text()).toBe(customLabelContent);
217
217
  });
@@ -230,7 +230,7 @@ describe("URadioGroup.vue", () => {
230
230
  });
231
231
 
232
232
  const labelComponent = component.getComponent(ULabel);
233
- const labelElement = labelComponent.find("label");
233
+ const labelElement = labelComponent.find("[vl-child-key='label']");
234
234
 
235
235
  expect(labelElement.text()).toBe(`Modified ${defaultLabel}`);
236
236
  });
@@ -57,6 +57,12 @@ const emit = defineEmits([
57
57
  */
58
58
  "searchChange",
59
59
 
60
+ /**
61
+ * Triggers when the search v-model updates.
62
+ * @property {string} query
63
+ */
64
+ "update:search",
65
+
60
66
  /**
61
67
  * Triggers when the option from multiple select is removed.
62
68
  * @property {string} option
@@ -129,7 +135,7 @@ const dropdownValue = computed({
129
135
  emit("update:modelValue", value);
130
136
  emit("change", { value, options: props.options });
131
137
 
132
- if (!props.multiple) deactivate();
138
+ if (!props.multiple && props.closeOnSelect) deactivate();
133
139
  },
134
140
  });
135
141
 
@@ -181,6 +187,11 @@ const selectedOptionsLabel = computed(() => {
181
187
  };
182
188
  });
183
189
 
190
+ const dropdownSearch = computed({
191
+ get: () => props.search ?? "",
192
+ set: (value: string) => emit("update:search", value),
193
+ });
194
+
184
195
  const hiddenSelectedOptionsCount = computed(() => {
185
196
  return selectedOptions.value.hidden.length;
186
197
  });
@@ -472,7 +483,6 @@ const {
472
483
  <template>
473
484
  <ULabel
474
485
  ref="labelComponent"
475
- :for="elementId"
476
486
  :size="size"
477
487
  :label="label"
478
488
  :error="error"
@@ -797,6 +807,7 @@ const {
797
807
  v-if="isOpen"
798
808
  ref="listbox"
799
809
  v-model="dropdownValue as string | number"
810
+ v-model:search="dropdownSearch"
800
811
  :searchable="searchable"
801
812
  :options-limit="optionsLimit"
802
813
  :multiple="multiple"
@@ -818,6 +829,7 @@ const {
818
829
  @blur="onListboxBlur"
819
830
  @search-blur="onListboxSearchBlur"
820
831
  @search-change="onSearchChange"
832
+ @update:search="(value) => emit('update:search', value)"
821
833
  >
822
834
  <template #before-option="{ option, index }">
823
835
  <!--
@@ -845,6 +857,11 @@ const {
845
857
  -->
846
858
  <slot name="after-option" :option="option" :index="index" />
847
859
  </template>
860
+
861
+ <template #empty>
862
+ <!-- @slot Use it to add something instead of empty state. -->
863
+ <slot name="empty" />
864
+ </template>
848
865
  </UListbox>
849
866
 
850
867
  <div
@@ -152,6 +152,7 @@ export default /*tw*/ {
152
152
  searchable: false,
153
153
  clearable: true,
154
154
  addOption: false,
155
+ closeOnSelect: true,
155
156
  /* icons */
156
157
  toggleIcon: "keyboard_arrow_down",
157
158
  clearIcon: "close_small",
@@ -15,6 +15,7 @@ import UIcon from "../../ui.image-icon/UIcon.vue";
15
15
  import ULink from "../../ui.button-link/ULink.vue";
16
16
  import UAvatar from "../../ui.image-avatar/UAvatar.vue";
17
17
  import UText from "../../ui.text-block/UText.vue";
18
+ import ULoader from "../../ui.loader/ULoader.vue";
18
19
 
19
20
  import johnDoe from "./assets/images/john-doe.png";
20
21
  import emilyDavis from "./assets/images/emily-davis.png";
@@ -59,7 +60,7 @@ export default {
59
60
  } as Meta;
60
61
 
61
62
  const DefaultTemplate: StoryFn<USelectArgs> = (args: USelectArgs) => ({
62
- components: { USelect, UIcon, ULink, UText },
63
+ components: { USelect, UIcon, ULink, UText, ULoader, URow },
63
64
  setup: () => ({ args, slots: getSlotNames(USelect.__name) }),
64
65
  template: `
65
66
  <USelect
@@ -148,6 +149,19 @@ NotClearable.args = { clearable: false };
148
149
  export const Searchable = DefaultTemplate.bind({});
149
150
  Searchable.args = { searchable: true };
150
151
 
152
+ export const SearchModelValue = DefaultTemplate.bind({});
153
+ SearchModelValue.args = { search: "New York", searchable: true };
154
+ SearchModelValue.parameters = {
155
+ docs: {
156
+ story: {
157
+ height: "350px",
158
+ },
159
+ },
160
+ };
161
+
162
+ export const NoCloseOnSelect = DefaultTemplate.bind({});
163
+ NoCloseOnSelect.args = { modelValue: 3, closeOnSelect: false };
164
+
151
165
  export const Readonly = DefaultTemplate.bind({});
152
166
  Readonly.args = { readonly: true, modelValue: "1", clearable: false };
153
167
 
@@ -212,9 +226,9 @@ GroupValue.parameters = {
212
226
  },
213
227
  };
214
228
 
215
- export const OptionsLimit2 = DefaultTemplate.bind({});
216
- OptionsLimit2.args = { optionsLimit: 2 };
217
- OptionsLimit2.parameters = {
229
+ export const OptionsLimit = DefaultTemplate.bind({});
230
+ OptionsLimit.args = { optionsLimit: 2 };
231
+ OptionsLimit.parameters = {
218
232
  docs: {
219
233
  description: {
220
234
  story: "`optionsLimit` prop controls the number of options displayed in the dropdown.",
@@ -530,6 +544,19 @@ SelectedCounterSlot.args = {
530
544
  `,
531
545
  };
532
546
 
547
+ export const EmptySlot = DefaultTemplate.bind({});
548
+ EmptySlot.args = {
549
+ options: [],
550
+ slotTemplate: `
551
+ <template #empty>
552
+ <URow align="center">
553
+ <ULoader loading size="sm" />
554
+ <UText label="Loading, this may take a while..." />
555
+ </URow>
556
+ </template>
557
+ `,
558
+ };
559
+
533
560
  export const OptionSlots: StoryFn<USelectArgs> = (args) => ({
534
561
  components: { USelect, URow, UCol, UAvatar, UIcon, UBadge, UText },
535
562
  setup: () => ({ args, johnDoe, emilyDavis, alexJohnson, patMorgan }),
@@ -255,6 +255,25 @@ describe("USelect.vue", () => {
255
255
  expect(component.getComponent(UListbox).props("searchable")).toBe(true);
256
256
  });
257
257
 
258
+ it("Search v-model – passes search to UListbox and filters", async () => {
259
+ const component = mount(USelect, {
260
+ props: {
261
+ searchable: true,
262
+ options: defaultOptions,
263
+ search: "Option 2",
264
+ },
265
+ });
266
+
267
+ await component.get("[role='combobox']").trigger("focus");
268
+ await flushPromises();
269
+
270
+ const listbox = component.getComponent(UListbox);
271
+ const options = listbox.findAll("[vl-child-key='option']");
272
+
273
+ expect(options).toHaveLength(1);
274
+ expect(options[0].text()).toBe("Option 2");
275
+ });
276
+
258
277
  it("Clearable – renders clear icon when true", async () => {
259
278
  const component = mount(USelect, {
260
279
  props: {
@@ -356,6 +375,24 @@ describe("USelect.vue", () => {
356
375
  expect(component.getComponent(UListbox).props("valueKey")).toBe(valueKey);
357
376
  });
358
377
 
378
+ it("Keeps dropdown open when closeOnSelect is false", async () => {
379
+ const component = mount(USelect, {
380
+ props: {
381
+ modelValue: "",
382
+ options: defaultOptions,
383
+ closeOnSelect: false,
384
+ },
385
+ });
386
+
387
+ await component.get("[role='combobox']").trigger("focus");
388
+
389
+ const firstOption = component.find("[vl-child-key='option']");
390
+
391
+ await firstOption.trigger("click");
392
+
393
+ expect(component.findComponent(UListbox).exists()).toBe(true);
394
+ });
395
+
359
396
  it("Options Limit – passes optionsLimit prop to UListbox", async () => {
360
397
  const optionsLimit = 5;
361
398
 
@@ -659,6 +696,93 @@ describe("USelect.vue", () => {
659
696
 
660
697
  expect(component.text()).toContain(slotContent);
661
698
  });
699
+
700
+ it("Before Option – renders custom content from before-option slot", async () => {
701
+ const slotContent = "Before Option";
702
+ const slotClass = "before-option-content";
703
+
704
+ const component = mount(USelect, {
705
+ props: {
706
+ options: defaultOptions,
707
+ },
708
+ slots: {
709
+ "before-option": `<span class='${slotClass}'>${slotContent}</span>`,
710
+ },
711
+ });
712
+
713
+ await component.get("[role='combobox']").trigger("focus");
714
+
715
+ const listbox = component.findComponent(UListbox);
716
+ const beforeOptionSlot = listbox.find(`.${slotClass}`);
717
+
718
+ expect(beforeOptionSlot.exists()).toBe(true);
719
+ expect(beforeOptionSlot.text()).toBe(slotContent);
720
+ });
721
+
722
+ it("Option – renders custom content from option slot", async () => {
723
+ const slotClass = "custom-option-content";
724
+
725
+ const component = mount(USelect, {
726
+ props: {
727
+ options: defaultOptions,
728
+ },
729
+ slots: {
730
+ option: `<span class='${slotClass}'>Custom {{ params.option.label }}</span>`,
731
+ },
732
+ });
733
+
734
+ await component.get("[role='combobox']").trigger("focus");
735
+
736
+ const listbox = component.findComponent(UListbox);
737
+ const customOptionSlot = listbox.find(`.${slotClass}`);
738
+
739
+ expect(customOptionSlot.exists()).toBe(true);
740
+ expect(customOptionSlot.text()).toBe("Custom Option 1");
741
+ });
742
+
743
+ it("After Option – renders custom content from after-option slot", async () => {
744
+ const slotContent = "After Option";
745
+ const slotClass = "after-option-content";
746
+
747
+ const component = mount(USelect, {
748
+ props: {
749
+ options: defaultOptions,
750
+ },
751
+ slots: {
752
+ "after-option": `<span class='${slotClass}'>${slotContent}</span>`,
753
+ },
754
+ });
755
+
756
+ await component.get("[role='combobox']").trigger("focus");
757
+
758
+ const listbox = component.findComponent(UListbox);
759
+ const afterOptionSlot = listbox.find(`.${slotClass}`);
760
+
761
+ expect(afterOptionSlot.exists()).toBe(true);
762
+ expect(afterOptionSlot.text()).toBe(slotContent);
763
+ });
764
+
765
+ it("Empty – renders custom content from empty slot", async () => {
766
+ const slotContent = "No options available";
767
+ const slotClass = "custom-empty";
768
+
769
+ const component = mount(USelect, {
770
+ props: {
771
+ options: [],
772
+ },
773
+ slots: {
774
+ empty: `<span class='${slotClass}'>${slotContent}</span>`,
775
+ },
776
+ });
777
+
778
+ await component.get("[role='combobox']").trigger("focus");
779
+
780
+ const listbox = component.findComponent(UListbox);
781
+ const emptySlot = listbox.find(`.${slotClass}`);
782
+
783
+ expect(emptySlot.exists()).toBe(true);
784
+ expect(emptySlot.text()).toBe(slotContent);
785
+ });
662
786
  });
663
787
 
664
788
  describe("Events", () => {
@@ -728,6 +852,25 @@ describe("USelect.vue", () => {
728
852
  vi.useRealTimers();
729
853
  });
730
854
 
855
+ it("Update:search – re-emits from UListbox", async () => {
856
+ const component = mount(USelect, {
857
+ props: {
858
+ searchable: true,
859
+ options: defaultOptions,
860
+ },
861
+ });
862
+
863
+ await component.get("[role='combobox']").trigger("focus");
864
+
865
+ const input = component.get("input");
866
+
867
+ await input.setValue("Option 3");
868
+ await flushPromises();
869
+
870
+ expect(component.emitted("update:search")).toBeTruthy();
871
+ expect(component.emitted("update:search")![0][0]).toBe("Option 3");
872
+ });
873
+
731
874
  it("Clear – emits when clear icon is clicked", async () => {
732
875
  const component = mount(USelect, {
733
876
  props: {
@@ -51,6 +51,16 @@ export interface Props {
51
51
  */
52
52
  debounce?: number | string;
53
53
 
54
+ /**
55
+ * Search input model value for the dropdown list.
56
+ */
57
+ search?: string;
58
+
59
+ /**
60
+ * Close dropdown on option select.
61
+ */
62
+ closeOnSelect?: boolean;
63
+
54
64
  /**
55
65
  * Left icon name.
56
66
  */
@@ -408,7 +408,7 @@ describe("UTextarea.vue", () => {
408
408
  },
409
409
  });
410
410
 
411
- const labelElement = component.getComponent(ULabel).find("label");
411
+ const labelElement = component.getComponent(ULabel).find("[vl-child-key='label']");
412
412
 
413
413
  expect(labelElement.text()).toBe(customLabelContent);
414
414
  });
@@ -425,7 +425,7 @@ describe("UTextarea.vue", () => {
425
425
  },
426
426
  });
427
427
 
428
- const labelElement = component.getComponent(ULabel).find("label");
428
+ const labelElement = component.getComponent(ULabel).find("[vl-child-key='label']");
429
429
 
430
430
  expect(labelElement.text()).toBe(`Modified ${defaultLabel}`);
431
431
  });