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
|
@@ -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
|
|
package/ui.form-listbox/types.ts
CHANGED
package/ui.form-radio/config.ts
CHANGED
|
@@ -3,7 +3,7 @@ export default /*tw*/ {
|
|
|
3
3
|
radio: {
|
|
4
4
|
base: `
|
|
5
5
|
bg-default cursor-pointer transition
|
|
6
|
-
border border-default rounded-full outline-transparent
|
|
6
|
+
border border-solid border-default rounded-full outline-transparent
|
|
7
7
|
appearance-none p-0 print:color-adjust-exact inline-block align-middle bg-origin-border select-none shrink-0
|
|
8
8
|
hover:border-lifted hover:checked:border-{color}
|
|
9
9
|
active:border-{color} active:bg-{color}/15
|
|
@@ -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
|
});
|
|
@@ -255,6 +266,7 @@ function deactivate() {
|
|
|
255
266
|
wrapperRef.value?.blur();
|
|
256
267
|
|
|
257
268
|
isOpen.value = false;
|
|
269
|
+
dropdownSearch.value = "";
|
|
258
270
|
|
|
259
271
|
nextTick(() => emit("close", localValue.value, elementId));
|
|
260
272
|
}
|
|
@@ -472,7 +484,6 @@ const {
|
|
|
472
484
|
<template>
|
|
473
485
|
<ULabel
|
|
474
486
|
ref="labelComponent"
|
|
475
|
-
:for="elementId"
|
|
476
487
|
:size="size"
|
|
477
488
|
:label="label"
|
|
478
489
|
:error="error"
|
|
@@ -797,6 +808,7 @@ const {
|
|
|
797
808
|
v-if="isOpen"
|
|
798
809
|
ref="listbox"
|
|
799
810
|
v-model="dropdownValue as string | number"
|
|
811
|
+
v-model:search="dropdownSearch"
|
|
800
812
|
:searchable="searchable"
|
|
801
813
|
:options-limit="optionsLimit"
|
|
802
814
|
:multiple="multiple"
|
|
@@ -818,6 +830,7 @@ const {
|
|
|
818
830
|
@blur="onListboxBlur"
|
|
819
831
|
@search-blur="onListboxSearchBlur"
|
|
820
832
|
@search-change="onSearchChange"
|
|
833
|
+
@update:search="(value) => emit('update:search', value)"
|
|
821
834
|
>
|
|
822
835
|
<template #before-option="{ option, index }">
|
|
823
836
|
<!--
|
|
@@ -845,6 +858,11 @@ const {
|
|
|
845
858
|
-->
|
|
846
859
|
<slot name="after-option" :option="option" :index="index" />
|
|
847
860
|
</template>
|
|
861
|
+
|
|
862
|
+
<template #empty>
|
|
863
|
+
<!-- @slot Use it to add something instead of empty state. -->
|
|
864
|
+
<slot name="empty" />
|
|
865
|
+
</template>
|
|
848
866
|
</UListbox>
|
|
849
867
|
|
|
850
868
|
<div
|
package/ui.form-select/config.ts
CHANGED
|
@@ -12,7 +12,7 @@ export default /*tw*/ {
|
|
|
12
12
|
wrapper: {
|
|
13
13
|
base: `
|
|
14
14
|
flex flex-row-reverse justify-between w-full min-h-full box-border relative
|
|
15
|
-
rounded-medium border border-default bg-default outline-transparent
|
|
15
|
+
rounded-medium border border-solid border-default bg-default outline-transparent
|
|
16
16
|
hover:border-lifted hover:transition 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
|
`,
|
|
@@ -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
|
|
216
|
-
|
|
217
|
-
|
|
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: {
|
package/ui.form-select/types.ts
CHANGED
|
@@ -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
|
*/
|
|
@@ -15,7 +15,7 @@ export default /*tw*/ {
|
|
|
15
15
|
wrapper: {
|
|
16
16
|
base: `
|
|
17
17
|
flex px-3 py-2 gap-3 w-full bg-default transition
|
|
18
|
-
rounded-medium border border-default outline-transparent
|
|
18
|
+
rounded-medium border border-solid border-default outline-transparent
|
|
19
19
|
hover:border-lifted hover:focus-within:border-primary focus-within:border-primary
|
|
20
20
|
focus-within:outline focus-within:outline-small focus-within:outline-primary focus-within:transition
|
|
21
21
|
`,
|