vueless 1.2.8-beta.1 → 1.2.8-beta.11

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 (63) 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 +2 -0
  5. package/index.ts +2 -0
  6. package/package.json +1 -1
  7. package/plugin-vite.js +6 -1
  8. package/types.ts +14 -2
  9. package/ui.container-accordion/UAccordion.vue +0 -1
  10. package/ui.container-accordion/config.ts +1 -1
  11. package/ui.container-accordion/storybook/stories.ts +13 -1
  12. package/ui.container-accordion-item/UAccordionItem.vue +17 -4
  13. package/ui.container-accordion-item/config.ts +1 -1
  14. package/ui.container-accordion-item/storybook/stories.ts +26 -1
  15. package/ui.container-accordion-item/tests/UAccordionItem.test.ts +186 -0
  16. package/ui.dropdown-badge/UDropdownBadge.vue +51 -2
  17. package/ui.dropdown-badge/config.ts +5 -1
  18. package/ui.dropdown-badge/storybook/stories.ts +280 -4
  19. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +194 -0
  20. package/ui.dropdown-badge/types.ts +30 -0
  21. package/ui.dropdown-button/UDropdownButton.vue +51 -2
  22. package/ui.dropdown-button/config.ts +5 -1
  23. package/ui.dropdown-button/storybook/stories.ts +288 -3
  24. package/ui.dropdown-button/tests/UDropdownButton.test.ts +190 -0
  25. package/ui.dropdown-button/types.ts +30 -0
  26. package/ui.dropdown-link/UDropdownLink.vue +51 -2
  27. package/ui.dropdown-link/config.ts +5 -1
  28. package/ui.dropdown-link/storybook/stories.ts +281 -4
  29. package/ui.dropdown-link/tests/UDropdownLink.test.ts +194 -0
  30. package/ui.dropdown-link/types.ts +30 -0
  31. package/ui.form-checkbox/tests/UCheckbox.test.ts +2 -2
  32. package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +2 -2
  33. package/ui.form-input/UInput.vue +4 -2
  34. package/ui.form-input/tests/UInput.test.ts +2 -2
  35. package/ui.form-input-counter/UInputCounter.vue +25 -1
  36. package/ui.form-input-counter/config.ts +7 -2
  37. package/ui.form-input-counter/tests/UInputCounter.test.ts +85 -1
  38. package/ui.form-input-counter/types.ts +25 -0
  39. package/ui.form-input-file/tests/UInputFile.test.ts +2 -2
  40. package/ui.form-input-number/UInputNumber.vue +15 -3
  41. package/ui.form-input-number/utilFormat.ts +17 -7
  42. package/ui.form-input-password/UInputPassword.vue +23 -1
  43. package/ui.form-label/ULabel.vue +10 -4
  44. package/ui.form-label/tests/ULabel.test.ts +29 -12
  45. package/ui.form-listbox/UListbox.vue +21 -5
  46. package/ui.form-listbox/storybook/stories.ts +188 -1
  47. package/ui.form-listbox/tests/UListbox.test.ts +36 -0
  48. package/ui.form-listbox/types.ts +5 -0
  49. package/ui.form-radio/tests/URadio.test.ts +2 -2
  50. package/ui.form-radio-group/tests/URadioGroup.test.ts +2 -2
  51. package/ui.form-select/USelect.vue +19 -2
  52. package/ui.form-select/config.ts +1 -0
  53. package/ui.form-select/storybook/stories.ts +31 -4
  54. package/ui.form-select/tests/USelect.test.ts +143 -0
  55. package/ui.form-select/types.ts +10 -0
  56. package/ui.form-textarea/tests/UTextarea.test.ts +2 -2
  57. package/utils/node/dynamicProps.d.ts +5 -2
  58. package/utils/node/dynamicProps.js +126 -53
  59. package/utils/node/helper.d.ts +10 -7
  60. package/utils/node/helper.js +59 -2
  61. package/utils/node/tailwindSafelist.js +2 -2
  62. package/utils/theme.ts +61 -5
  63. package/utils/ui.ts +32 -3
package/constants.d.ts CHANGED
@@ -9,6 +9,8 @@ export const OUTLINE: "outline";
9
9
  export const ROUNDING: "rounding";
10
10
  export const DISABLED_OPACITY: "disabled-opacity";
11
11
  export const LETTER_SPACING: "letter-spacing";
12
+ export const LIGHT_THEME: "light-theme";
13
+ export const DARK_THEME: "dark-theme";
12
14
  export const COLOR_MODE_KEY: "vl-color-mode";
13
15
  export const AUTO_MODE_KEY: "vl-auto-mode";
14
16
  export const DARK_MODE_CLASS: "vl-dark";
@@ -28,6 +30,7 @@ export const DEFAULT_DISABLED_OPACITY: 50;
28
30
  export const DEFAULT_LETTER_SPACING: 0;
29
31
  export const PRIMARY_COLORS: string[];
30
32
  export const STATE_COLORS: string[];
33
+ export const LAYOUT_COLORS: string[];
31
34
  export const NEUTRAL_COLORS: string[];
32
35
  export const COLOR_SHADES: number[];
33
36
  export const DEFAULT_LIGHT_THEME: {
@@ -128,6 +131,7 @@ export namespace SYSTEM_CONFIG_KEY {
128
131
  let unstyled: string;
129
132
  let transition: string;
130
133
  let colors: string;
134
+ let props: string;
131
135
  }
132
136
  export const ICON_NON_PROPS_DEFAULTS: string[];
133
137
  export namespace DIRECTIVES {
package/constants.js CHANGED
@@ -17,6 +17,8 @@ export const OUTLINE = "outline";
17
17
  export const ROUNDING = "rounding";
18
18
  export const DISABLED_OPACITY = "disabled-opacity";
19
19
  export const LETTER_SPACING = "letter-spacing";
20
+ export const LIGHT_THEME = "light-theme";
21
+ export const DARK_THEME = "dark-theme";
20
22
 
21
23
  /* Vueless color mode keys */
22
24
  export const COLOR_MODE_KEY = "vl-color-mode";
@@ -70,6 +72,7 @@ export const STATE_COLORS = [
70
72
  NEUTRAL_COLOR,
71
73
  GRAYSCALE_COLOR,
72
74
  ];
75
+ export const LAYOUT_COLORS = ["text", "border", "bg"];
73
76
  export const NEUTRAL_COLORS = ["slate", "gray", "zinc", "neutral", "stone"];
74
77
  export const COLOR_SHADES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
75
78
 
@@ -222,6 +225,7 @@ export const SYSTEM_CONFIG_KEY = {
222
225
  unstyled: "unstyled",
223
226
  transition: "transition",
224
227
  colors: "colors",
228
+ props: "props",
225
229
  ...CVA_CONFIG_KEY,
226
230
  };
227
231
 
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 -960 960 960"><path d="m188.91-556.74 101.7 43.33q17.28-33.85 36.71-67.18 19.42-33.32 40.98-63.89l-76.37-15.28-103.02 103.02Zm156.39 84.11 131.33 131.56q58.44-27.43 107.48-59.59 49.04-32.17 79.8-62.93 80.05-80.28 117.81-163.84Q819.48-711 823.2-819.2q-108.2 3.72-191.77 41.48-83.56 37.76-163.6 117.81-30.76 30.76-62.93 79.8-32.16 49.04-59.6 107.48Zm231.16-99.59q-19.76-19.76-19.76-48.66 0-28.9 19.76-48.66 19.76-19.53 48.54-19.53 28.78 0 48.54 19.53 19.53 19.76 19.53 48.66 0 28.9-19.53 48.66Q653.78-552.7 625-552.7q-28.78 0-48.54-19.52ZM560.5-184.91l103.26-103.02-15.28-76.37q-30.57 21.56-63.89 41.1-33.33 19.55-67.18 36.59l43.09 101.7Zm331.26-702.61q8.52 143.17-35.08 257.09-43.59 113.91-142.11 212.67-1.48 1.24-2.96 2.84-1.48 1.59-2.72 2.83l22 110.48q3.48 17.15-1.74 33.19-5.22 16.03-17.65 28.46L537.87-65.33l-88.83-206.84-172.87-172.87-206.84-88.83L244.2-707.5q12.43-12.43 28.34-17.65 15.92-5.22 33.07-1.74l110.48 22q1.24-1.24 2.71-2.1 1.48-.86 2.96-2.1 98.76-99 212.79-142.98 114.04-43.97 257.21-35.45ZM141.59-324.04q36.91-36.92 89.56-37.3 52.65-.38 89.33 36.3 36.43 36.43 36.05 89.2-.38 52.77-37.05 89.45-27.68 27.67-85.64 45.63-57.97 17.96-168.64 32 14.04-110.67 31.5-169.26 17.45-58.59 44.89-86.02Zm47.98 48.74q-13.77 14.76-23.93 44.84-10.16 30.09-18.16 78.66 48.56-8 78.65-18.41 30.09-10.4 44.61-24.16 18.28-16.04 18.54-40.35.26-24.3-16.78-42.58-18.28-17.05-42.59-16.55-24.3.5-40.34 18.55Z"/></svg>
package/index.d.ts CHANGED
@@ -140,6 +140,8 @@ export type {
140
140
  NestedComponent,
141
141
  ComponentConfig,
142
142
  ComponentDefaults,
143
+ ComponentCustomProp,
144
+ ComponentCustomProps,
143
145
  CreateVuelessOptions,
144
146
  /* Color and theme types */
145
147
  StateColors,
package/index.ts CHANGED
@@ -146,6 +146,8 @@ export type {
146
146
  NestedComponent,
147
147
  ComponentConfig,
148
148
  ComponentDefaults,
149
+ ComponentCustomProp,
150
+ ComponentCustomProps,
149
151
  CreateVuelessOptions,
150
152
  /* Color and theme types */
151
153
  StateColors,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.2.8-beta.1",
3
+ "version": "1.2.8-beta.11",
4
4
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
5
5
  "author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
6
6
  "homepage": "https://vueless.com",
package/plugin-vite.js CHANGED
@@ -129,9 +129,14 @@ export const Vueless = function (options = {}) {
129
129
  await cacheMergedConfigs({ vuelessSrcDir, basePath });
130
130
  }
131
131
 
132
- await buildWebTypes({ vuelessSrcDir, basePath });
132
+ /* set custom prop types */
133
133
  await setCustomPropTypes({ vuelessSrcDir, basePath });
134
134
 
135
+ /* build web-types.json with delay for right custom props behavior */
136
+ setTimeout(async () => {
137
+ await buildWebTypes({ vuelessSrcDir, basePath });
138
+ }, 2000);
139
+
135
140
  /* collect used in project colors for tailwind safelist */
136
141
  await createTailwindSafelist({ env, srcDir: vuelessSrcDir, targetFiles, basePath, debug });
137
142
 
package/types.ts CHANGED
@@ -198,7 +198,7 @@ export type MergedThemeConfig = Omit<ThemeConfig, "text | outline | rounding"> &
198
198
 
199
199
  export type UnknownObject = Record<string, unknown>;
200
200
  export type UnknownArray = unknown[];
201
- export type UnknownType = string | number | boolean | UnknownObject | undefined | null;
201
+ export type UnknownType = string | number | boolean | UnknownObject | undefined | null | unknown;
202
202
 
203
203
  export type ComponentNames = keyof Components & string; // keys union
204
204
 
@@ -325,7 +325,19 @@ export interface NestedComponent {
325
325
 
326
326
  export type ComponentDefaults = {
327
327
  color?: string;
328
- [key: string]: unknown | UnknownObject;
328
+ [key: string]: UnknownType;
329
+ };
330
+
331
+ export type ComponentCustomProps = {
332
+ [key: string]: ComponentCustomProp;
333
+ };
334
+
335
+ export type ComponentCustomProp = {
336
+ required?: boolean;
337
+ ignore?: boolean;
338
+ values?: string[];
339
+ default?: UnknownType;
340
+ description?: string;
329
341
  };
330
342
 
331
343
  export interface CVA {
@@ -109,7 +109,6 @@ const { getDataTest, accordionItemAttrs, accordionAttrs } = useUI<Config>(defaul
109
109
  <UAccordionItem
110
110
  v-for="(option, index) in options"
111
111
  :key="index"
112
- :model-value="selectedItem"
113
112
  :value="option.value"
114
113
  :title="option.title"
115
114
  :description="option.description"
@@ -1,6 +1,6 @@
1
1
  export default /*tw*/ {
2
2
  accordion: "flex flex-col w-full divide-y divide-muted",
3
- accordionItem: "{UAccordionItem} py-5 first:pt-0 last:pb-0",
3
+ accordionItem: "{UAccordionItem}",
4
4
  defaults: {
5
5
  size: "md",
6
6
  disabled: false,
@@ -8,6 +8,7 @@ import {
8
8
  } from "../../utils/storybook";
9
9
 
10
10
  import UAccordion from "../../ui.container-accordion/UAccordion.vue";
11
+ import UAccordionItem from "../../ui.container-accordion-item/UAccordionItem.vue";
11
12
  import UButton from "../../ui.button/UButton.vue";
12
13
  import ULink from "../../ui.button-link/ULink.vue";
13
14
  import UCol from "../../ui.container-col/UCol.vue";
@@ -68,7 +69,7 @@ export default {
68
69
  } as Meta;
69
70
 
70
71
  const DefaultTemplate: StoryFn<UAccordionArgs> = (args: UAccordionArgs) => ({
71
- components: { UAccordion, ULink, UButton, UCol, URow, UIcon },
72
+ components: { UAccordion, UAccordionItem, ULink, UButton, UCol, URow, UIcon },
72
73
  setup: () => ({ args, slots: getSlotNames(UAccordion.__name) }),
73
74
  template: `
74
75
  <UAccordion v-bind="args" v-model="args.modelValue">
@@ -110,3 +111,14 @@ Sizes.args = {
110
111
  },
111
112
  ],
112
113
  };
114
+
115
+ export const DefaultSlot: StoryFn<UAccordionArgs> = (args: UAccordionArgs, { argTypes }) => ({
116
+ components: { UAccordion, UAccordionItem },
117
+ setup: () => ({ args, argTypes, getArgs }),
118
+ template: `
119
+ <UAccordion v-model="args.modelValue">
120
+ <UAccordionItem title="Custom Accordion Item 1" description="Custom Accordion Item 1" value="1" />
121
+ <UAccordionItem title="Custom Accordion Item 2" description="Custom Accordion Item 2" value="2" />
122
+ </UAccordion>
123
+ `,
124
+ });
@@ -122,7 +122,13 @@ const {
122
122
  <div ref="wrapper" v-bind="wrapperAttrs" :data-test="getDataTest()" @click="onClickItem">
123
123
  <div v-bind="bodyAttrs">
124
124
  <div v-bind="titleAttrs" ref="title">
125
- {{ title }}
125
+ <!--
126
+ @slot Use it to add custom title content.
127
+ @binding {string} title
128
+ -->
129
+ <slot name="title" :title="title">
130
+ {{ title }}
131
+ </slot>
126
132
  <!--
127
133
  @slot Use it to add something instead of the toggle icon.
128
134
  @binding {string} icon-name
@@ -140,11 +146,18 @@ const {
140
146
  </div>
141
147
 
142
148
  <div
143
- v-if="description"
149
+ v-if="description || hasSlotContent(slots['description'])"
144
150
  :id="`description-${elementId}`"
145
151
  v-bind="descriptionAttrs"
146
- v-text="description"
147
- />
152
+ >
153
+ <!--
154
+ @slot Use it to add custom description content.
155
+ @binding {string} description
156
+ -->
157
+ <slot name="description" :description="description">
158
+ {{ description }}
159
+ </slot>
160
+ </div>
148
161
 
149
162
  <div v-if="isOpened && hasSlotContent(slots['default'])" v-bind="contentAttrs">
150
163
  <!-- @slot Use it to add accordion content. -->
@@ -1,6 +1,6 @@
1
1
  export default /*tw*/ {
2
2
  wrapper: {
3
- base: "group cursor-pointer",
3
+ base: "group cursor-pointer py-5 first:pt-0 last:pb-0",
4
4
  variants: {
5
5
  disabled: {
6
6
  true: "cursor-not-allowed text-default/(--vl-disabled-opacity)",
@@ -58,11 +58,12 @@ const EnumTemplate: StoryFn<UAccordionItemArgs> = (args: UAccordionItemArgs, { a
58
58
  components: { UAccordionItem, UCol },
59
59
  setup: () => ({ args, argTypes, getArgs }),
60
60
  template: `
61
- <UCol gap="xl">
61
+ <UCol gap="lg">
62
62
  <UAccordionItem
63
63
  v-for="option in argTypes?.[args.enum]?.options"
64
64
  v-bind="getArgs(args, option)"
65
65
  :key="option"
66
+ class="py-0"
66
67
  />
67
68
  </UCol>
68
69
  `,
@@ -112,3 +113,27 @@ ToggleSlot.args = {
112
113
  </template>
113
114
  `,
114
115
  };
116
+
117
+ export const TitleSlot = DefaultTemplate.bind({});
118
+ TitleSlot.args = {
119
+ slotTemplate: `
120
+ <template #title="{ title }">
121
+ <URow gap="xs" align="center">
122
+ <UIcon name="rocket_launch" size="xs" color="primary" />
123
+ <span>{{ title }}</span>
124
+ </URow>
125
+ </template>
126
+ `,
127
+ };
128
+
129
+ export const DescriptionSlot = DefaultTemplate.bind({});
130
+ DescriptionSlot.args = {
131
+ slotTemplate: `
132
+ <template #description="{ description }">
133
+ <URow gap="xs" align="start">
134
+ <UIcon name="info" size="xs" color="accented" />
135
+ <span>{{ description }}</span>
136
+ </URow>
137
+ </template>
138
+ `,
139
+ };
@@ -176,6 +176,104 @@ describe("UAccordionItem", () => {
176
176
  expect(toggleElement.attributes("data-opened")).toBe("true");
177
177
  });
178
178
 
179
+ // Title slot
180
+ it("renders custom content in title slot", () => {
181
+ const title = "Original Title";
182
+ const slotClass = "custom-title";
183
+ const slotContent = "Custom Title Content";
184
+
185
+ const component = mount(UAccordionItem, {
186
+ props: { title },
187
+ slots: {
188
+ title: `<div class="${slotClass}">${slotContent}</div>`,
189
+ },
190
+ });
191
+
192
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
193
+ expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
194
+
195
+ expect(component.text()).not.toContain(title);
196
+ });
197
+
198
+ // Title slot bindings
199
+ it("provides title binding to title slot", () => {
200
+ const title = "Test Title";
201
+ const slotClass = "custom-title";
202
+
203
+ const component = mount(UAccordionItem, {
204
+ props: { title },
205
+ slots: {
206
+ title: `
207
+ <template #default="{ title }">
208
+ <div class="${slotClass}" :data-title="title"></div>
209
+ </template>
210
+ `,
211
+ },
212
+ });
213
+
214
+ const titleElement = component.find(`.${slotClass}`);
215
+
216
+ expect(titleElement.exists()).toBe(true);
217
+ expect(titleElement.attributes("data-title")).toBe(title);
218
+ });
219
+
220
+ // Description slot
221
+ it("renders custom content in description slot", () => {
222
+ const description = "Original Description";
223
+ const slotClass = "custom-description";
224
+ const slotContent = "Custom Description Content";
225
+
226
+ const component = mount(UAccordionItem, {
227
+ props: { description },
228
+ slots: {
229
+ description: `<div class="${slotClass}">${slotContent}</div>`,
230
+ },
231
+ });
232
+
233
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
234
+ expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
235
+
236
+ expect(component.text()).not.toContain(description);
237
+ });
238
+
239
+ // Description slot bindings
240
+ it("provides description binding to description slot", () => {
241
+ const description = "Test Description";
242
+ const slotClass = "custom-description";
243
+
244
+ const component = mount(UAccordionItem, {
245
+ props: { description },
246
+ slots: {
247
+ description: `
248
+ <template #default="{ description }">
249
+ <div class="${slotClass}" :data-description="description"></div>
250
+ </template>
251
+ `,
252
+ },
253
+ });
254
+
255
+ const descriptionElement = component.find(`.${slotClass}`);
256
+
257
+ expect(descriptionElement.exists()).toBe(true);
258
+ expect(descriptionElement.attributes("data-description")).toBe(description);
259
+ });
260
+
261
+ // Description slot with empty prop
262
+ it("renders description slot even when description prop is empty", () => {
263
+ const slotClass = "custom-description";
264
+ const slotContent = "Custom Description Content";
265
+
266
+ const component = mount(UAccordionItem, {
267
+ props: {},
268
+ slots: {
269
+ description: `<div class="${slotClass}">${slotContent}</div>`,
270
+ },
271
+ });
272
+
273
+ expect(component.find(`.${slotClass}`).exists()).toBe(true);
274
+ expect(component.find(`.${slotClass}`).text()).toBe(slotContent);
275
+ });
276
+
179
277
  // Default slot
180
278
  it("renders default slot content when accordion is opened", async () => {
181
279
  const slotContent = "Custom accordion content";
@@ -221,6 +319,94 @@ describe("UAccordionItem", () => {
221
319
 
222
320
  expect(component.find("[vl-key='content']").exists()).toBe(false);
223
321
  });
322
+
323
+ // Slot interactions with accordion behavior
324
+ it("title slot content is clickable and toggles accordion", async () => {
325
+ const title = "Test Title";
326
+ const slotClass = "custom-title";
327
+ const slotContent = "Custom Title Content";
328
+
329
+ const component = mount(UAccordionItem, {
330
+ props: { title },
331
+ slots: {
332
+ title: `<div class="${slotClass}">${slotContent}</div>`,
333
+ },
334
+ });
335
+
336
+ const titleElement = component.find(`.${slotClass}`);
337
+
338
+ expect(titleElement.exists()).toBe(true);
339
+
340
+ await titleElement.trigger("click");
341
+
342
+ const emitted = component.emitted("click");
343
+
344
+ expect(emitted).toBeTruthy();
345
+ expect(emitted?.[0]).toEqual([expect.any(String), true]);
346
+ });
347
+
348
+ it("description slot content is not clickable and does not toggle accordion", async () => {
349
+ const description = "Test Description";
350
+ const slotClass = "custom-description";
351
+ const slotContent = "Custom Description Content";
352
+
353
+ const component = mount(UAccordionItem, {
354
+ props: { description },
355
+ slots: {
356
+ description: `<div class="${slotClass}">${slotContent}</div>`,
357
+ },
358
+ });
359
+
360
+ const descriptionElement = component.find(`.${slotClass}`);
361
+
362
+ expect(descriptionElement.exists()).toBe(true);
363
+
364
+ await descriptionElement.trigger("click");
365
+
366
+ const emitted = component.emitted("click");
367
+
368
+ expect(emitted).toBeFalsy();
369
+ });
370
+
371
+ it("title slot preserves accordion toggle functionality", async () => {
372
+ const title = "Test Title";
373
+ const description = "Test Description";
374
+ const slotClass = "custom-title";
375
+ const slotContent = "Custom Title Content";
376
+
377
+ const component = mount(UAccordionItem, {
378
+ props: { title, description },
379
+ slots: {
380
+ title: `<div class="${slotClass}">${slotContent}</div>`,
381
+ },
382
+ });
383
+
384
+ expect(component.find("[id^='description-']").classes()).not.toContain("opacity-100");
385
+
386
+ await component.find(`.${slotClass}`).trigger("click");
387
+ expect(component.find("[id^='description-']").classes()).toContain("opacity-100");
388
+
389
+ await component.find(`.${slotClass}`).trigger("click");
390
+ expect(component.find("[id^='description-']").classes()).not.toContain("opacity-100");
391
+ });
392
+
393
+ it("description slot content is always visible when slot is provided", () => {
394
+ const slotClass = "custom-description";
395
+ const slotContent = "Custom Description Content";
396
+
397
+ const component = mount(UAccordionItem, {
398
+ props: {},
399
+ slots: {
400
+ description: `<div class="${slotClass}">${slotContent}</div>`,
401
+ },
402
+ });
403
+
404
+ // Description slot content should be visible even without description prop
405
+ const descriptionElement = component.find(`.${slotClass}`);
406
+
407
+ expect(descriptionElement.exists()).toBe(true);
408
+ expect(descriptionElement.text()).toBe(slotContent);
409
+ });
224
410
  });
225
411
 
226
412
  // Events
@@ -55,6 +55,12 @@ 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>;
@@ -76,6 +82,11 @@ const dropdownValue = computed({
76
82
  set: (value) => emit("update:modelValue", value),
77
83
  });
78
84
 
85
+ const dropdownSearch = computed({
86
+ get: () => props.search ?? "",
87
+ set: (value: string) => emit("update:search", value),
88
+ });
89
+
79
90
  const selectedOptions = computed(() => {
80
91
  if (props.multiple) {
81
92
  return props.options.filter((option) => {
@@ -153,7 +164,7 @@ function hideOptions() {
153
164
  function onClickOption(option: Option) {
154
165
  emit("clickOption", option);
155
166
 
156
- if (!props.multiple) hideOptions();
167
+ if (!props.multiple && props.closeOnSelect) hideOptions();
157
168
  }
158
169
 
159
170
  defineExpose({
@@ -243,17 +254,55 @@ const { getDataTest, config, wrapperAttrs, dropdownBadgeAttrs, listboxAttrs, tog
243
254
  v-if="isShownOptions"
244
255
  ref="dropdown-list"
245
256
  v-model="dropdownValue"
257
+ v-model:search="dropdownSearch"
246
258
  :searchable="searchable"
247
259
  :multiple="multiple"
248
260
  :size="size"
249
261
  :color="color"
250
262
  :options="options"
263
+ :options-limit="optionsLimit"
264
+ :visible-options="visibleOptions"
251
265
  :label-key="labelKey"
252
266
  :value-key="valueKey"
267
+ :group-label-key="groupLabelKey"
268
+ :group-value-key="groupValueKey"
253
269
  v-bind="listboxAttrs"
254
270
  :data-test="getDataTest('list')"
255
271
  @click-option="onClickOption"
256
272
  @search-change="onSearchChange"
257
- />
273
+ @update:search="(value) => emit('update:search', value)"
274
+ >
275
+ <template #before-option="{ option, index }">
276
+ <!--
277
+ @slot Use it to add something before option.
278
+ @binding {object} option
279
+ @binding {number} index
280
+ -->
281
+ <slot name="before-option" :option="option" :index="index" />
282
+ </template>
283
+
284
+ <template #option="{ option, index }">
285
+ <!--
286
+ @slot Use it to customize the option.
287
+ @binding {object} option
288
+ @binding {number} index
289
+ -->
290
+ <slot name="option" :option="option" :index="index" />
291
+ </template>
292
+
293
+ <template #after-option="{ option, index }">
294
+ <!--
295
+ @slot Use it to add something after option.
296
+ @binding {object} option
297
+ @binding {number} index
298
+ -->
299
+ <slot name="after-option" :option="option" :index="index" />
300
+ </template>
301
+
302
+ <template #empty>
303
+ <!-- @slot Use it to add something instead of empty state. -->
304
+ <slot name="empty" />
305
+ </template>
306
+ </UListbox>
258
307
  </div>
259
308
  </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
  },