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.
- package/constants.d.ts +4 -0
- package/constants.js +4 -0
- package/icons/storybook/rocket_launch.svg +1 -0
- package/index.d.ts +3 -1
- package/index.ts +3 -1
- package/package.json +9 -5
- package/plugin-vite.js +6 -1
- package/types.ts +14 -2
- package/ui.button/config.ts +4 -4
- package/ui.button/tests/UButton.test.ts +3 -3
- package/ui.button-toggle/config.ts +2 -2
- package/ui.container-accordion/UAccordion.vue +0 -1
- package/ui.container-accordion/config.ts +1 -1
- package/ui.container-accordion/storybook/stories.ts +13 -1
- package/ui.container-accordion-item/UAccordionItem.vue +17 -4
- package/ui.container-accordion-item/config.ts +1 -1
- package/ui.container-accordion-item/storybook/stories.ts +26 -1
- package/ui.container-accordion-item/tests/UAccordionItem.test.ts +186 -0
- package/ui.container-card/config.ts +1 -1
- package/ui.data-table/config.ts +4 -4
- package/ui.dropdown-badge/UDropdownBadge.vue +68 -3
- package/ui.dropdown-badge/config.ts +5 -1
- package/ui.dropdown-badge/storybook/stories.ts +280 -4
- package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +194 -0
- package/ui.dropdown-badge/types.ts +30 -0
- package/ui.dropdown-button/UDropdownButton.vue +69 -6
- package/ui.dropdown-button/config.ts +5 -1
- package/ui.dropdown-button/storybook/stories.ts +288 -3
- package/ui.dropdown-button/tests/UDropdownButton.test.ts +190 -0
- package/ui.dropdown-button/types.ts +30 -0
- package/ui.dropdown-link/UDropdownLink.vue +69 -6
- package/ui.dropdown-link/config.ts +5 -1
- package/ui.dropdown-link/storybook/stories.ts +281 -4
- package/ui.dropdown-link/tests/UDropdownLink.test.ts +194 -0
- package/ui.dropdown-link/types.ts +30 -0
- package/ui.form-calendar/config.ts +4 -2
- package/ui.form-checkbox/config.ts +1 -1
- package/ui.form-checkbox/tests/UCheckbox.test.ts +2 -2
- package/ui.form-checkbox-group/tests/UCheckboxGroup.test.ts +2 -2
- package/ui.form-date-picker-range/config.ts +1 -1
- package/ui.form-input/UInput.vue +4 -2
- package/ui.form-input/config.ts +1 -1
- package/ui.form-input/tests/UInput.test.ts +2 -2
- package/ui.form-input-counter/UInputCounter.vue +25 -1
- package/ui.form-input-counter/config.ts +7 -2
- package/ui.form-input-counter/tests/UInputCounter.test.ts +85 -1
- package/ui.form-input-counter/types.ts +25 -0
- package/ui.form-input-file/tests/UInputFile.test.ts +2 -2
- package/ui.form-input-number/UInputNumber.vue +15 -3
- package/ui.form-input-number/utilFormat.ts +17 -7
- package/ui.form-input-password/UInputPassword.vue +23 -1
- package/ui.form-label/ULabel.vue +10 -4
- package/ui.form-label/tests/ULabel.test.ts +29 -12
- package/ui.form-listbox/UListbox.vue +21 -9
- package/ui.form-listbox/config.ts +1 -1
- package/ui.form-listbox/storybook/stories.ts +188 -1
- package/ui.form-listbox/tests/UListbox.test.ts +36 -0
- package/ui.form-listbox/types.ts +5 -0
- package/ui.form-radio/config.ts +1 -1
- package/ui.form-radio/tests/URadio.test.ts +2 -2
- package/ui.form-radio-group/tests/URadioGroup.test.ts +2 -2
- package/ui.form-select/USelect.vue +20 -2
- package/ui.form-select/config.ts +2 -1
- package/ui.form-select/storybook/stories.ts +31 -4
- package/ui.form-select/tests/USelect.test.ts +143 -0
- package/ui.form-select/types.ts +10 -0
- package/ui.form-textarea/config.ts +1 -1
- package/ui.form-textarea/tests/UTextarea.test.ts +2 -2
- package/ui.text-alert/config.ts +1 -1
- package/ui.text-badge/config.ts +1 -1
- package/utils/helper.ts +4 -0
- package/utils/node/dynamicProps.d.ts +5 -2
- package/utils/node/dynamicProps.js +126 -53
- package/utils/node/helper.d.ts +10 -7
- package/utils/node/helper.js +59 -2
- package/utils/node/tailwindSafelist.js +9 -2
- package/utils/theme.ts +75 -31
- package/utils/ui.ts +32 -3
|
@@ -300,6 +300,97 @@ describe("UDropdownButton.vue", () => {
|
|
|
300
300
|
|
|
301
301
|
expect(component.findComponent(UButton).attributes("data-test")).toBe(dataTest);
|
|
302
302
|
});
|
|
303
|
+
|
|
304
|
+
// OptionsLimit prop
|
|
305
|
+
it("passes optionsLimit prop to UListbox component", async () => {
|
|
306
|
+
const optionsLimit = 2;
|
|
307
|
+
|
|
308
|
+
const component = mount(UDropdownButton, {
|
|
309
|
+
props: {
|
|
310
|
+
optionsLimit,
|
|
311
|
+
options: defaultOptions,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await component.findComponent(UButton).trigger("click");
|
|
316
|
+
|
|
317
|
+
expect(component.findComponent(UListbox).props("optionsLimit")).toBe(optionsLimit);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("Search v-model – passes search to UListbox and filters", async () => {
|
|
321
|
+
const component = mount(UDropdownButton, {
|
|
322
|
+
props: {
|
|
323
|
+
searchable: true,
|
|
324
|
+
options: defaultOptions,
|
|
325
|
+
search: "Option 2",
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
await component.findComponent(UButton).trigger("click");
|
|
330
|
+
|
|
331
|
+
const listbox = component.getComponent(UListbox);
|
|
332
|
+
const options = listbox.findAll("[vl-child-key='option']");
|
|
333
|
+
|
|
334
|
+
expect(options).toHaveLength(1);
|
|
335
|
+
expect(options[0].text()).toBe("Option 2");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// VisibleOptions prop
|
|
339
|
+
it("passes visibleOptions prop to UListbox component", async () => {
|
|
340
|
+
const visibleOptions = 5;
|
|
341
|
+
|
|
342
|
+
const component = mount(UDropdownButton, {
|
|
343
|
+
props: {
|
|
344
|
+
visibleOptions,
|
|
345
|
+
options: defaultOptions,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await component.findComponent(UButton).trigger("click");
|
|
350
|
+
|
|
351
|
+
expect(component.findComponent(UListbox).props("visibleOptions")).toBe(visibleOptions);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// GroupLabelKey prop
|
|
355
|
+
it("passes groupLabelKey prop to UListbox component", async () => {
|
|
356
|
+
const groupLabelKey = "category";
|
|
357
|
+
const groupedOptions = [
|
|
358
|
+
{ groupLabel: "Group 1", category: "group1" },
|
|
359
|
+
{ label: "Option 1", id: "option1", category: "group1" },
|
|
360
|
+
{ groupLabel: "Group 2", category: "group2" },
|
|
361
|
+
{ label: "Option 2", id: "option2", category: "group2" },
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const component = mount(UDropdownButton, {
|
|
365
|
+
props: {
|
|
366
|
+
groupLabelKey,
|
|
367
|
+
options: groupedOptions,
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await component.findComponent(UButton).trigger("click");
|
|
372
|
+
|
|
373
|
+
expect(component.findComponent(UListbox).props("groupLabelKey")).toBe(groupLabelKey);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// CloseOnSelect prop
|
|
377
|
+
it("keeps dropdown open when closeOnSelect is false", async () => {
|
|
378
|
+
const component = mount(UDropdownButton, {
|
|
379
|
+
props: {
|
|
380
|
+
options: defaultOptions,
|
|
381
|
+
closeOnSelect: false,
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
await component.findComponent(UButton).trigger("click");
|
|
386
|
+
expect(component.findComponent(UListbox).exists()).toBe(true);
|
|
387
|
+
|
|
388
|
+
const listbox = component.findComponent(UListbox);
|
|
389
|
+
|
|
390
|
+
listbox.vm.$emit("update:modelValue", 2);
|
|
391
|
+
|
|
392
|
+
expect(component.findComponent(UListbox).exists()).toBe(true);
|
|
393
|
+
});
|
|
303
394
|
});
|
|
304
395
|
|
|
305
396
|
// Slots tests
|
|
@@ -361,6 +452,105 @@ describe("UDropdownButton.vue", () => {
|
|
|
361
452
|
expect(component.find(`.${slotClass}`).exists()).toBe(true);
|
|
362
453
|
expect(component.find(`.${slotClass}`).text()).toBe(slotText);
|
|
363
454
|
});
|
|
455
|
+
|
|
456
|
+
// Before-option slot
|
|
457
|
+
it("renders content from before-option slot", async () => {
|
|
458
|
+
const label = "Dropdown Button";
|
|
459
|
+
const slotText = "Before";
|
|
460
|
+
const slotClass = "before-option-content";
|
|
461
|
+
|
|
462
|
+
const component = mount(UDropdownButton, {
|
|
463
|
+
props: {
|
|
464
|
+
label,
|
|
465
|
+
options: defaultOptions,
|
|
466
|
+
},
|
|
467
|
+
slots: {
|
|
468
|
+
"before-option": `<span class='${slotClass}'>${slotText}</span>`,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
await component.findComponent(UButton).trigger("click");
|
|
473
|
+
|
|
474
|
+
const listbox = component.findComponent(UListbox);
|
|
475
|
+
const beforeOptionSlot = listbox.find(`.${slotClass}`);
|
|
476
|
+
|
|
477
|
+
expect(beforeOptionSlot.exists()).toBe(true);
|
|
478
|
+
expect(beforeOptionSlot.text()).toBe(slotText);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Option slot
|
|
482
|
+
it("renders custom content from option slot", async () => {
|
|
483
|
+
const label = "Dropdown Button";
|
|
484
|
+
const slotClass = "custom-option-content";
|
|
485
|
+
|
|
486
|
+
const component = mount(UDropdownButton, {
|
|
487
|
+
props: {
|
|
488
|
+
label,
|
|
489
|
+
options: defaultOptions,
|
|
490
|
+
},
|
|
491
|
+
slots: {
|
|
492
|
+
option: `<span class='${slotClass}'>Custom {{ params.option.label }}</span>`,
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
await component.findComponent(UButton).trigger("click");
|
|
497
|
+
|
|
498
|
+
const listbox = component.findComponent(UListbox);
|
|
499
|
+
const customOptionSlot = listbox.find(`.${slotClass}`);
|
|
500
|
+
|
|
501
|
+
expect(customOptionSlot.exists()).toBe(true);
|
|
502
|
+
expect(customOptionSlot.text()).toBe("Custom Option 1");
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// After-option slot
|
|
506
|
+
it("renders content from after-option slot", async () => {
|
|
507
|
+
const label = "Dropdown Button";
|
|
508
|
+
const slotText = "After";
|
|
509
|
+
const slotClass = "after-option-content";
|
|
510
|
+
|
|
511
|
+
const component = mount(UDropdownButton, {
|
|
512
|
+
props: {
|
|
513
|
+
label,
|
|
514
|
+
options: defaultOptions,
|
|
515
|
+
},
|
|
516
|
+
slots: {
|
|
517
|
+
"after-option": `<span class='${slotClass}'>${slotText}</span>`,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await component.findComponent(UButton).trigger("click");
|
|
522
|
+
|
|
523
|
+
const listbox = component.findComponent(UListbox);
|
|
524
|
+
const afterOptionSlot = listbox.find(`.${slotClass}`);
|
|
525
|
+
|
|
526
|
+
expect(afterOptionSlot.exists()).toBe(true);
|
|
527
|
+
expect(afterOptionSlot.text()).toBe(slotText);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// Empty slot
|
|
531
|
+
it("renders custom content from empty slot", async () => {
|
|
532
|
+
const label = "Dropdown Button";
|
|
533
|
+
const slotContent = "No options available";
|
|
534
|
+
const slotClass = "custom-empty";
|
|
535
|
+
|
|
536
|
+
const component = mount(UDropdownButton, {
|
|
537
|
+
props: {
|
|
538
|
+
label,
|
|
539
|
+
options: [],
|
|
540
|
+
},
|
|
541
|
+
slots: {
|
|
542
|
+
empty: `<span class='${slotClass}'>${slotContent}</span>`,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
await component.findComponent(UButton).trigger("click");
|
|
547
|
+
|
|
548
|
+
const listbox = component.findComponent(UListbox);
|
|
549
|
+
const emptySlot = listbox.find(`.${slotClass}`);
|
|
550
|
+
|
|
551
|
+
expect(emptySlot.exists()).toBe(true);
|
|
552
|
+
expect(emptySlot.text()).toBe(slotContent);
|
|
553
|
+
});
|
|
364
554
|
});
|
|
365
555
|
|
|
366
556
|
// Events tests
|
|
@@ -36,6 +36,26 @@ export interface Props {
|
|
|
36
36
|
*/
|
|
37
37
|
valueKey?: string;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Set a name of the property containing the group label.
|
|
41
|
+
*/
|
|
42
|
+
groupLabelKey?: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set a name of the property containing the group values.
|
|
46
|
+
*/
|
|
47
|
+
groupValueKey?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Number of options displayed in the dropdown.
|
|
51
|
+
*/
|
|
52
|
+
optionsLimit?: number;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Number of options you can see without a scroll.
|
|
56
|
+
*/
|
|
57
|
+
visibleOptions?: number;
|
|
58
|
+
|
|
39
59
|
/**
|
|
40
60
|
* Button variant.
|
|
41
61
|
*/
|
|
@@ -70,6 +90,16 @@ export interface Props {
|
|
|
70
90
|
*/
|
|
71
91
|
searchable?: boolean;
|
|
72
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Search input model value for the dropdown list.
|
|
95
|
+
*/
|
|
96
|
+
search?: string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Close dropdown on option select.
|
|
100
|
+
*/
|
|
101
|
+
closeOnSelect?: boolean;
|
|
102
|
+
|
|
73
103
|
/**
|
|
74
104
|
* Allows multiple selection.
|
|
75
105
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { nextTick, computed,
|
|
2
|
+
import { nextTick, computed, ref, useId, useTemplateRef } from "vue";
|
|
3
3
|
import { isEqual } from "lodash-es";
|
|
4
4
|
|
|
5
5
|
import useUI from "../composables/useUI";
|
|
@@ -55,13 +55,18 @@ const emit = defineEmits([
|
|
|
55
55
|
* @property {string} query
|
|
56
56
|
*/
|
|
57
57
|
"searchChange",
|
|
58
|
-
]);
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Triggers when the search v-model updates.
|
|
61
|
+
* @property {string} query
|
|
62
|
+
*/
|
|
63
|
+
"update:search",
|
|
64
|
+
]);
|
|
61
65
|
|
|
62
66
|
type ULisboxRef = InstanceType<typeof ULisbox>;
|
|
63
67
|
|
|
64
68
|
const isShownOptions = ref(false);
|
|
69
|
+
const isClickingOption = ref(false);
|
|
65
70
|
const listboxRef = useTemplateRef<ULisboxRef>("dropdown-list");
|
|
66
71
|
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper");
|
|
67
72
|
|
|
@@ -78,6 +83,11 @@ const dropdownValue = computed({
|
|
|
78
83
|
set: (value) => emit("update:modelValue", value),
|
|
79
84
|
});
|
|
80
85
|
|
|
86
|
+
const dropdownSearch = computed({
|
|
87
|
+
get: () => props.search ?? "",
|
|
88
|
+
set: (value: string) => emit("update:search", value),
|
|
89
|
+
});
|
|
90
|
+
|
|
81
91
|
const selectedOptions = computed(() => {
|
|
82
92
|
if (props.multiple) {
|
|
83
93
|
return props.options.filter((option) => {
|
|
@@ -150,14 +160,29 @@ function onClickLink() {
|
|
|
150
160
|
|
|
151
161
|
function hideOptions() {
|
|
152
162
|
isShownOptions.value = false;
|
|
163
|
+
dropdownSearch.value = "";
|
|
153
164
|
|
|
154
165
|
emit("close");
|
|
155
166
|
}
|
|
156
167
|
|
|
157
168
|
function onClickOption(option: Option) {
|
|
169
|
+
isClickingOption.value = true;
|
|
170
|
+
|
|
158
171
|
emit("clickOption", option);
|
|
159
172
|
|
|
160
|
-
if (!props.multiple) hideOptions();
|
|
173
|
+
if (!props.multiple && props.closeOnSelect) hideOptions();
|
|
174
|
+
|
|
175
|
+
nextTick(() => {
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
isClickingOption.value = false;
|
|
178
|
+
}, 10);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function handleClickOutside() {
|
|
183
|
+
if (isClickingOption.value) return;
|
|
184
|
+
|
|
185
|
+
hideOptions();
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
defineExpose({
|
|
@@ -190,7 +215,7 @@ const { config, getDataTest, wrapperAttrs, dropdownLinkAttrs, listboxAttrs, togg
|
|
|
190
215
|
<template>
|
|
191
216
|
<div
|
|
192
217
|
ref="wrapper"
|
|
193
|
-
v-click-outside="
|
|
218
|
+
v-click-outside="handleClickOutside"
|
|
194
219
|
tabindex="1"
|
|
195
220
|
v-bind="wrapperAttrs"
|
|
196
221
|
:data-test="getDataTest('wrapper')"
|
|
@@ -249,17 +274,55 @@ const { config, getDataTest, wrapperAttrs, dropdownLinkAttrs, listboxAttrs, togg
|
|
|
249
274
|
v-if="isShownOptions"
|
|
250
275
|
ref="dropdown-list"
|
|
251
276
|
v-model="dropdownValue"
|
|
277
|
+
v-model:search="dropdownSearch"
|
|
252
278
|
:searchable="searchable"
|
|
253
279
|
:multiple="multiple"
|
|
254
280
|
:size="size"
|
|
255
281
|
:color="color"
|
|
256
282
|
:options="options"
|
|
283
|
+
:options-limit="optionsLimit"
|
|
284
|
+
:visible-options="visibleOptions"
|
|
257
285
|
:label-key="labelKey"
|
|
258
286
|
:value-key="valueKey"
|
|
287
|
+
:group-label-key="groupLabelKey"
|
|
288
|
+
:group-value-key="groupValueKey"
|
|
259
289
|
v-bind="listboxAttrs"
|
|
260
290
|
:data-test="getDataTest('list')"
|
|
261
291
|
@click-option="onClickOption"
|
|
262
292
|
@search-change="onSearchChange"
|
|
263
|
-
|
|
293
|
+
@update:search="(value) => emit('update:search', value)"
|
|
294
|
+
>
|
|
295
|
+
<template #before-option="{ option, index }">
|
|
296
|
+
<!--
|
|
297
|
+
@slot Use it to add something before option.
|
|
298
|
+
@binding {object} option
|
|
299
|
+
@binding {number} index
|
|
300
|
+
-->
|
|
301
|
+
<slot name="before-option" :option="option" :index="index" />
|
|
302
|
+
</template>
|
|
303
|
+
|
|
304
|
+
<template #option="{ option, index }">
|
|
305
|
+
<!--
|
|
306
|
+
@slot Use it to customize the option.
|
|
307
|
+
@binding {object} option
|
|
308
|
+
@binding {number} index
|
|
309
|
+
-->
|
|
310
|
+
<slot name="option" :option="option" :index="index" />
|
|
311
|
+
</template>
|
|
312
|
+
|
|
313
|
+
<template #after-option="{ option, index }">
|
|
314
|
+
<!--
|
|
315
|
+
@slot Use it to add something after option.
|
|
316
|
+
@binding {object} option
|
|
317
|
+
@binding {number} index
|
|
318
|
+
-->
|
|
319
|
+
<slot name="after-option" :option="option" :index="index" />
|
|
320
|
+
</template>
|
|
321
|
+
|
|
322
|
+
<template #empty>
|
|
323
|
+
<!-- @slot Use it to add something instead of empty state. -->
|
|
324
|
+
<slot name="empty" />
|
|
325
|
+
</template>
|
|
326
|
+
</ULisbox>
|
|
264
327
|
</div>
|
|
265
328
|
</template>
|
|
@@ -33,14 +33,18 @@ export default /*tw*/ {
|
|
|
33
33
|
size: "md",
|
|
34
34
|
labelKey: "label",
|
|
35
35
|
valueKey: "id",
|
|
36
|
+
groupLabelKey: "label",
|
|
36
37
|
yPosition: "bottom",
|
|
37
38
|
xPosition: "left",
|
|
39
|
+
optionsLimit: 0,
|
|
40
|
+
visibleOptions: 8,
|
|
41
|
+
labelDisplayCount: 2,
|
|
38
42
|
underlined: undefined,
|
|
39
43
|
dashed: false,
|
|
40
44
|
disabled: false,
|
|
41
45
|
searchable: false,
|
|
42
46
|
multiple: false,
|
|
43
|
-
|
|
47
|
+
closeOnSelect: true,
|
|
44
48
|
/* icons */
|
|
45
49
|
toggleIcon: "keyboard_arrow_down",
|
|
46
50
|
},
|