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
@@ -55,11 +55,18 @@ const emit = defineEmits([
55
55
  * @property {string} query
56
56
  */
57
57
  "searchChange",
58
+
59
+ /**
60
+ * Triggers when the search v-model updates.
61
+ * @property {string} query
62
+ */
63
+ "update:search",
58
64
  ]);
59
65
 
60
66
  type UListboxRef = InstanceType<typeof UListbox>;
61
67
 
62
68
  const isShownOptions = ref(false);
69
+ const isClickingOption = ref(false);
63
70
  const listboxRef = useTemplateRef<UListboxRef>("dropdown-list");
64
71
  const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
65
72
 
@@ -76,6 +83,11 @@ const dropdownValue = computed({
76
83
  set: (value) => emit("update:modelValue", value),
77
84
  });
78
85
 
86
+ const dropdownSearch = computed({
87
+ get: () => props.search ?? "",
88
+ set: (value: string) => emit("update:search", value),
89
+ });
90
+
79
91
  const selectedOptions = computed(() => {
80
92
  if (props.multiple) {
81
93
  return props.options.filter((option) => {
@@ -146,14 +158,29 @@ function onClickBadge() {
146
158
 
147
159
  function hideOptions() {
148
160
  isShownOptions.value = false;
161
+ dropdownSearch.value = "";
149
162
 
150
163
  emit("close");
151
164
  }
152
165
 
153
166
  function onClickOption(option: Option) {
167
+ isClickingOption.value = true;
168
+
154
169
  emit("clickOption", option);
155
170
 
156
- if (!props.multiple) hideOptions();
171
+ if (!props.multiple && props.closeOnSelect) hideOptions();
172
+
173
+ nextTick(() => {
174
+ setTimeout(() => {
175
+ isClickingOption.value = false;
176
+ }, 10);
177
+ });
178
+ }
179
+
180
+ function handleClickOutside() {
181
+ if (isClickingOption.value) return;
182
+
183
+ hideOptions();
157
184
  }
158
185
 
159
186
  defineExpose({
@@ -186,7 +213,7 @@ const { getDataTest, config, wrapperAttrs, dropdownBadgeAttrs, listboxAttrs, tog
186
213
  <template>
187
214
  <div
188
215
  ref="wrapper"
189
- v-click-outside="hideOptions"
216
+ v-click-outside="handleClickOutside"
190
217
  v-bind="wrapperAttrs"
191
218
  :data-test="getDataTest('wrapper')"
192
219
  >
@@ -243,17 +270,55 @@ const { getDataTest, config, wrapperAttrs, dropdownBadgeAttrs, listboxAttrs, tog
243
270
  v-if="isShownOptions"
244
271
  ref="dropdown-list"
245
272
  v-model="dropdownValue"
273
+ v-model:search="dropdownSearch"
246
274
  :searchable="searchable"
247
275
  :multiple="multiple"
248
276
  :size="size"
249
277
  :color="color"
250
278
  :options="options"
279
+ :options-limit="optionsLimit"
280
+ :visible-options="visibleOptions"
251
281
  :label-key="labelKey"
252
282
  :value-key="valueKey"
283
+ :group-label-key="groupLabelKey"
284
+ :group-value-key="groupValueKey"
253
285
  v-bind="listboxAttrs"
254
286
  :data-test="getDataTest('list')"
255
287
  @click-option="onClickOption"
256
288
  @search-change="onSearchChange"
257
- />
289
+ @update:search="(value) => emit('update:search', value)"
290
+ >
291
+ <template #before-option="{ option, index }">
292
+ <!--
293
+ @slot Use it to add something before option.
294
+ @binding {object} option
295
+ @binding {number} index
296
+ -->
297
+ <slot name="before-option" :option="option" :index="index" />
298
+ </template>
299
+
300
+ <template #option="{ option, index }">
301
+ <!--
302
+ @slot Use it to customize the option.
303
+ @binding {object} option
304
+ @binding {number} index
305
+ -->
306
+ <slot name="option" :option="option" :index="index" />
307
+ </template>
308
+
309
+ <template #after-option="{ option, index }">
310
+ <!--
311
+ @slot Use it to add something after option.
312
+ @binding {object} option
313
+ @binding {number} index
314
+ -->
315
+ <slot name="after-option" :option="option" :index="index" />
316
+ </template>
317
+
318
+ <template #empty>
319
+ <!-- @slot Use it to add something instead of empty state. -->
320
+ <slot name="empty" />
321
+ </template>
322
+ </UListbox>
258
323
  </div>
259
324
  </template>
@@ -38,12 +38,16 @@ export default /*tw*/ {
38
38
  variant: "solid",
39
39
  labelKey: "label",
40
40
  valueKey: "id",
41
+ groupLabelKey: "label",
41
42
  yPosition: "bottom",
42
43
  xPosition: "left",
44
+ optionsLimit: 0,
45
+ visibleOptions: 8,
46
+ labelDisplayCount: 2,
43
47
  round: false,
44
48
  searchable: false,
45
49
  multiple: false,
46
- labelDisplayCount: 2,
50
+ closeOnSelect: true,
47
51
  /* icons */
48
52
  toggleIcon: "keyboard_arrow_down",
49
53
  },
@@ -13,10 +13,17 @@ import UIcon from "../../ui.image-icon/UIcon.vue";
13
13
  import ULink from "../../ui.button-link/ULink.vue";
14
14
  import UAvatar from "../../ui.image-avatar/UAvatar.vue";
15
15
  import UText from "../../ui.text-block/UText.vue";
16
+ import UBadge from "../../ui.text-badge/UBadge.vue";
17
+ import ULoader from "../../ui.loader/ULoader.vue";
16
18
 
17
19
  import type { Meta, StoryFn } from "@storybook/vue3-vite";
18
20
  import type { Props } from "../types";
19
21
 
22
+ import johnDoe from "../../ui.form-select/storybook/assets/images/john-doe.png";
23
+ import emilyDavis from "../../ui.form-select/storybook/assets/images/emily-davis.png";
24
+ import alexJohnson from "../../ui.form-select/storybook/assets/images/alex-johnson.png";
25
+ import patMorgan from "../../ui.form-select/storybook/assets/images/pat-morgan.png";
26
+
20
27
  interface DefaultUDropdownBadgeArgs extends Props {
21
28
  slotTemplate?: string;
22
29
  }
@@ -53,7 +60,7 @@ export default {
53
60
  } as Meta;
54
61
 
55
62
  const DefaultTemplate: StoryFn<DefaultUDropdownBadgeArgs> = (args: DefaultUDropdownBadgeArgs) => ({
56
- components: { UDropdownBadge, UIcon, ULink, UAvatar, URow, UCol, UText },
63
+ components: { UDropdownBadge, UIcon, ULink, UAvatar, URow, UCol, UText, ULoader },
57
64
  setup: () => ({ args, slots: getSlotNames(UDropdownBadge.__name) }),
58
65
  template: `
59
66
  <UDropdownBadge v-bind="args">
@@ -110,6 +117,35 @@ const MultiEnumTemplate: StoryFn<EnumUDropdownBadgeArgs> = (
110
117
  `,
111
118
  });
112
119
 
120
+ const GroupValuesTemplate: StoryFn<DefaultUDropdownBadgeArgs> = (
121
+ args: DefaultUDropdownBadgeArgs,
122
+ ) => ({
123
+ components: { UDropdownBadge },
124
+ setup() {
125
+ return {
126
+ args,
127
+ };
128
+ },
129
+ template: `
130
+ <UDropdownBadge
131
+ v-bind="args"
132
+ v-model="args.modelValue"
133
+ label="Single"
134
+ :config="{ listbox: 'min-w-[200px]' }"
135
+ class="max-w-96 mr-20"
136
+ />
137
+
138
+ <UDropdownBadge
139
+ v-bind="args"
140
+ v-model="args.modelValueMultiple"
141
+ label="Multiple"
142
+ multiple
143
+ :config="{ listbox: 'min-w-[200px]' }"
144
+ class="mt-5 max-w-96"
145
+ />
146
+ `,
147
+ });
148
+
113
149
  export const Default = DefaultTemplate.bind({});
114
150
  Default.args = {};
115
151
  Default.parameters = {
@@ -120,6 +156,9 @@ Default.parameters = {
120
156
  },
121
157
  };
122
158
 
159
+ export const Disabled = DefaultTemplate.bind({});
160
+ Disabled.args = { disabled: true };
161
+
123
162
  export const Searchable = DefaultTemplate.bind({});
124
163
  Searchable.args = { searchable: true };
125
164
  Searchable.parameters = {
@@ -130,6 +169,19 @@ Searchable.parameters = {
130
169
  },
131
170
  };
132
171
 
172
+ export const SearchModelValue = DefaultTemplate.bind({});
173
+ SearchModelValue.args = { searchable: true, search: "Delivered" };
174
+ SearchModelValue.parameters = {
175
+ docs: {
176
+ story: {
177
+ height: "250px",
178
+ },
179
+ },
180
+ };
181
+
182
+ export const NoCloseOnSelect = SelectableTemplate.bind({});
183
+ NoCloseOnSelect.args = { modelValue: "delivered", closeOnSelect: false };
184
+
133
185
  export const OptionSelection = SelectableTemplate.bind({});
134
186
  OptionSelection.args = { modelValue: "pending" };
135
187
 
@@ -158,6 +210,60 @@ ListboxYPosition.parameters = {
158
210
  storyClasses: "h-[350px] flex items-center px-6 pt-8 pb-12",
159
211
  };
160
212
 
213
+ export const GroupValue = GroupValuesTemplate.bind({});
214
+ GroupValue.args = {
215
+ modelValue: "",
216
+ groupValueKey: "libs",
217
+ groupLabelKey: "language",
218
+ labelKey: "name",
219
+ valueKey: "name",
220
+ options: [
221
+ {
222
+ language: "Javascript",
223
+ libs: [{ name: "Vue.js" }, { name: "Adonis" }],
224
+ },
225
+ {
226
+ language: "Ruby",
227
+ libs: [
228
+ { name: "Frameworks", isSubGroup: true, level: 2 },
229
+ { name: "Rails", level: 3 },
230
+ { name: "Sinatra", level: 3 },
231
+ ],
232
+ },
233
+ {
234
+ language: "Other",
235
+ libs: [{ name: "Laravel" }, { name: "Phoenix" }],
236
+ },
237
+ ],
238
+ };
239
+ GroupValue.parameters = {
240
+ docs: {
241
+ story: {
242
+ height: "400px",
243
+ },
244
+ },
245
+ };
246
+
247
+ export const OptionsLimit = DefaultTemplate.bind({});
248
+ OptionsLimit.args = { optionsLimit: 2 };
249
+ OptionsLimit.parameters = {
250
+ docs: {
251
+ description: {
252
+ story: "`optionsLimit` prop controls the number of options displayed in the dropdown.",
253
+ },
254
+ },
255
+ };
256
+
257
+ export const VisibleOptions = DefaultTemplate.bind({});
258
+ VisibleOptions.args = { visibleOptions: 2 };
259
+ VisibleOptions.parameters = {
260
+ docs: {
261
+ description: {
262
+ story: "`visibleOptions` prop controls the number of options you can see without a scroll.",
263
+ },
264
+ },
265
+ };
266
+
161
267
  export const Color = MultiEnumTemplate.bind({});
162
268
  Color.args = {
163
269
  outerEnum: "variant",
@@ -166,9 +272,6 @@ Color.args = {
166
272
  options: [],
167
273
  };
168
274
 
169
- export const Disabled = DefaultTemplate.bind({});
170
- Disabled.args = { disabled: true };
171
-
172
275
  export const WithoutToggleIcon = Default.bind({});
173
276
  WithoutToggleIcon.args = { toggleIcon: false };
174
277
 
@@ -217,3 +320,176 @@ ToggleSlot.args = {
217
320
  </template>
218
321
  `,
219
322
  };
323
+
324
+ export const EmptySlot = DefaultTemplate.bind({});
325
+ EmptySlot.args = {
326
+ options: [],
327
+ slotTemplate: `
328
+ <template #empty>
329
+ <URow align="center">
330
+ <ULoader loading size="sm" />
331
+ <UText label="Loading, this may take a while..." />
332
+ </URow>
333
+ </template>
334
+ `,
335
+ };
336
+
337
+ export const OptionSlots: StoryFn<DefaultUDropdownBadgeArgs> = (args) => ({
338
+ components: { UDropdownBadge, URow, UCol, UAvatar, UIcon, UBadge, UText },
339
+ setup: () => ({ args, johnDoe, emilyDavis, alexJohnson, patMorgan }),
340
+ template: `
341
+ <URow>
342
+ <UDropdownBadge
343
+ v-model="args.beforeOptionModel"
344
+ label="Before option slot"
345
+ :options="[
346
+ {
347
+ label: 'John Doe',
348
+ id: '1',
349
+ role: 'Developer',
350
+ avatar: johnDoe,
351
+ status: 'online',
352
+ statusColor: 'success',
353
+ },
354
+ {
355
+ label: 'Jane Smith',
356
+ id: '2',
357
+ role: 'Designer',
358
+ avatar: emilyDavis,
359
+ status: 'away',
360
+ statusColor: 'warning',
361
+ },
362
+ {
363
+ label: 'Mike Johnson',
364
+ id: '3',
365
+ role: 'Product Manager',
366
+ avatar: alexJohnson,
367
+ status: 'offline',
368
+ statusColor: 'grayscale',
369
+ },
370
+ {
371
+ label: 'Sarah Wilson',
372
+ id: '4',
373
+ role: 'QA Engineer',
374
+ avatar: patMorgan,
375
+ status: 'online',
376
+ statusColor: 'success',
377
+ },
378
+ ]"
379
+ >
380
+ <template #before-option="{ option }">
381
+ <UAvatar :src="option.avatar" size="sm" />
382
+ </template>
383
+ </UDropdownBadge>
384
+
385
+ <UDropdownBadge
386
+ v-model="args.optionModel"
387
+ label="Option slot"
388
+ :options="[
389
+ {
390
+ label: 'John Doe',
391
+ id: '1',
392
+ role: 'Developer',
393
+ avatar: johnDoe,
394
+ status: 'online',
395
+ statusColor: 'success',
396
+ },
397
+ {
398
+ label: 'Jane Smith',
399
+ id: '2',
400
+ role: 'Designer',
401
+ avatar: emilyDavis,
402
+ status: 'away',
403
+ statusColor: 'warning',
404
+ },
405
+ {
406
+ label: 'Mike Johnson',
407
+ id: '3',
408
+ role: 'Product Manager',
409
+ avatar: alexJohnson,
410
+ status: 'offline',
411
+ statusColor: 'grayscale',
412
+ },
413
+ {
414
+ label: 'Sarah Wilson',
415
+ id: '4',
416
+ role: 'QA Engineer',
417
+ avatar: patMorgan,
418
+ status: 'online',
419
+ statusColor: 'success',
420
+ },
421
+ ]"
422
+ >
423
+ <template #option="{ option }">
424
+ <URow align="center" gap="xs">
425
+ <UCol gap="none">
426
+ <UText size="sm">{{ option.label }}</UText>
427
+ <UText variant="lifted" size="xs">{{ option.role }}</UText>
428
+ </UCol>
429
+ <UBadge
430
+ :label="option.status"
431
+ :color="option.statusColor"
432
+ size="sm"
433
+ variant="subtle"
434
+ />
435
+ </URow>
436
+ </template>
437
+ </UDropdownBadge>
438
+
439
+ <UDropdownBadge
440
+ v-model="args.afterOptionModel"
441
+ label="After option slot"
442
+ :options="[
443
+ {
444
+ label: 'John Doe',
445
+ id: '1',
446
+ role: 'Developer',
447
+ avatar: johnDoe,
448
+ status: 'online',
449
+ statusColor: 'success',
450
+ },
451
+ {
452
+ label: 'Jane Smith',
453
+ id: '2',
454
+ role: 'Designer',
455
+ avatar: emilyDavis,
456
+ status: 'away',
457
+ statusColor: 'warning',
458
+ },
459
+ {
460
+ label: 'Mike Johnson',
461
+ id: '3',
462
+ role: 'Product Manager',
463
+ avatar: alexJohnson,
464
+ status: 'offline',
465
+ statusColor: 'grayscale',
466
+ },
467
+ {
468
+ label: 'Sarah Wilson',
469
+ id: '4',
470
+ role: 'QA Engineer',
471
+ avatar: patMorgan,
472
+ status: 'online',
473
+ statusColor: 'success',
474
+ },
475
+ ]"
476
+ >
477
+ <template #after-option="{ option }">
478
+ <UBadge
479
+ :label="option.status"
480
+ :color="option.statusColor"
481
+ size="sm"
482
+ variant="subtle"
483
+ />
484
+ </template>
485
+ </UDropdownBadge>
486
+ </URow>
487
+ `,
488
+ });
489
+ OptionSlots.parameters = {
490
+ docs: {
491
+ story: {
492
+ height: "300px",
493
+ },
494
+ },
495
+ };
@@ -256,6 +256,101 @@ describe("UDropdownBadge.vue", () => {
256
256
 
257
257
  expect(component.findComponent(UBadge).attributes("data-test")).toBe(dataTest);
258
258
  });
259
+
260
+ // OptionsLimit prop
261
+ it("passes optionsLimit prop to UListbox component", async () => {
262
+ const optionsLimit = 2;
263
+
264
+ const component = mount(UDropdownBadge, {
265
+ props: {
266
+ optionsLimit,
267
+ options: defaultOptions,
268
+ },
269
+ });
270
+
271
+ await component.findComponent(UBadge).trigger("click");
272
+
273
+ expect(component.findComponent(UListbox).props("optionsLimit")).toBe(optionsLimit);
274
+ });
275
+
276
+ // VisibleOptions prop
277
+ it("passes visibleOptions prop to UListbox component", async () => {
278
+ const visibleOptions = 5;
279
+
280
+ const component = mount(UDropdownBadge, {
281
+ props: {
282
+ visibleOptions,
283
+ options: defaultOptions,
284
+ },
285
+ });
286
+
287
+ await component.findComponent(UBadge).trigger("click");
288
+
289
+ expect(component.findComponent(UListbox).props("visibleOptions")).toBe(visibleOptions);
290
+ });
291
+
292
+ // GroupLabelKey prop
293
+ it("passes groupLabelKey prop to UListbox component", async () => {
294
+ const groupLabelKey = "category";
295
+ const groupedOptions = [
296
+ { groupLabel: "Group 1", category: "group1" },
297
+ { label: "Option 1", id: "option1", category: "group1" },
298
+ { groupLabel: "Group 2", category: "group2" },
299
+ { label: "Option 2", id: "option2", category: "group2" },
300
+ ];
301
+
302
+ const component = mount(UDropdownBadge, {
303
+ props: {
304
+ groupLabelKey,
305
+ options: groupedOptions,
306
+ },
307
+ });
308
+
309
+ await component.findComponent(UBadge).trigger("click");
310
+
311
+ expect(component.findComponent(UListbox).props("groupLabelKey")).toBe(groupLabelKey);
312
+ });
313
+
314
+ it("Search v-model – passes search to UListbox and filters", async () => {
315
+ const component = mount(UDropdownBadge, {
316
+ props: {
317
+ searchable: true,
318
+ options: defaultOptions,
319
+ search: "Option 1",
320
+ },
321
+ });
322
+
323
+ await component.findComponent(UBadge).trigger("click");
324
+
325
+ const listbox = component.getComponent(UListbox);
326
+ const options = listbox.findAll("[vl-child-key='option']");
327
+
328
+ expect(options).toHaveLength(1);
329
+ expect(options[0].text()).toBe("Option 1");
330
+ });
331
+
332
+ // CloseOnSelect prop
333
+ it("keeps dropdown open when closeOnSelect is false", async () => {
334
+ const component = mount(UDropdownBadge, {
335
+ props: {
336
+ options: defaultOptions,
337
+ closeOnSelect: false,
338
+ },
339
+ });
340
+
341
+ // Open the dropdown
342
+ await component.findComponent(UBadge).trigger("click");
343
+ expect(component.findComponent(UListbox).exists()).toBe(true);
344
+
345
+ // Find the listbox component
346
+ const listbox = component.findComponent(UListbox);
347
+
348
+ // Simulate selecting an option by emitting update:modelValue from the listbox
349
+ listbox.vm.$emit("update:modelValue", 2);
350
+
351
+ // Dropdown should remain open
352
+ expect(component.findComponent(UListbox).exists()).toBe(true);
353
+ });
259
354
  });
260
355
 
261
356
  // Slots tests
@@ -317,6 +412,105 @@ describe("UDropdownBadge.vue", () => {
317
412
  expect(component.find(`.${slotClass}`).exists()).toBe(true);
318
413
  expect(component.find(`.${slotClass}`).text()).toBe(slotText);
319
414
  });
415
+
416
+ // Before-option slot
417
+ it("renders content from before-option slot", async () => {
418
+ const label = "Dropdown Badge";
419
+ const slotText = "Before";
420
+ const slotClass = "before-option-content";
421
+
422
+ const component = mount(UDropdownBadge, {
423
+ props: {
424
+ label,
425
+ options: defaultOptions,
426
+ },
427
+ slots: {
428
+ "before-option": `<span class='${slotClass}'>${slotText}</span>`,
429
+ },
430
+ });
431
+
432
+ await component.findComponent(UBadge).trigger("click");
433
+
434
+ const listbox = component.findComponent(UListbox);
435
+ const beforeOptionSlot = listbox.find(`.${slotClass}`);
436
+
437
+ expect(beforeOptionSlot.exists()).toBe(true);
438
+ expect(beforeOptionSlot.text()).toBe(slotText);
439
+ });
440
+
441
+ // Option slot
442
+ it("renders custom content from option slot", async () => {
443
+ const label = "Dropdown Badge";
444
+ const slotClass = "custom-option-content";
445
+
446
+ const component = mount(UDropdownBadge, {
447
+ props: {
448
+ label,
449
+ options: defaultOptions,
450
+ },
451
+ slots: {
452
+ option: `<span class='${slotClass}'>Custom {{ params.option.label }}</span>`,
453
+ },
454
+ });
455
+
456
+ await component.findComponent(UBadge).trigger("click");
457
+
458
+ const listbox = component.findComponent(UListbox);
459
+ const customOptionSlot = listbox.find(`.${slotClass}`);
460
+
461
+ expect(customOptionSlot.exists()).toBe(true);
462
+ expect(customOptionSlot.text()).toBe("Custom Option 1");
463
+ });
464
+
465
+ // After-option slot
466
+ it("renders content from after-option slot", async () => {
467
+ const label = "Dropdown Badge";
468
+ const slotText = "After";
469
+ const slotClass = "after-option-content";
470
+
471
+ const component = mount(UDropdownBadge, {
472
+ props: {
473
+ label,
474
+ options: defaultOptions,
475
+ },
476
+ slots: {
477
+ "after-option": `<span class='${slotClass}'>${slotText}</span>`,
478
+ },
479
+ });
480
+
481
+ await component.findComponent(UBadge).trigger("click");
482
+
483
+ const listbox = component.findComponent(UListbox);
484
+ const afterOptionSlot = listbox.find(`.${slotClass}`);
485
+
486
+ expect(afterOptionSlot.exists()).toBe(true);
487
+ expect(afterOptionSlot.text()).toBe(slotText);
488
+ });
489
+
490
+ // Empty slot
491
+ it("renders custom content from empty slot", async () => {
492
+ const label = "Dropdown Badge";
493
+ const slotContent = "No options available";
494
+ const slotClass = "custom-empty";
495
+
496
+ const component = mount(UDropdownBadge, {
497
+ props: {
498
+ label,
499
+ options: [],
500
+ },
501
+ slots: {
502
+ empty: `<span class='${slotClass}'>${slotContent}</span>`,
503
+ },
504
+ });
505
+
506
+ await component.findComponent(UBadge).trigger("click");
507
+
508
+ const listbox = component.findComponent(UListbox);
509
+ const emptySlot = listbox.find(`.${slotClass}`);
510
+
511
+ expect(emptySlot.exists()).toBe(true);
512
+ expect(emptySlot.text()).toBe(slotContent);
513
+ });
320
514
  });
321
515
 
322
516
  // Events tests