vueless 1.4.12-beta.2 → 1.4.12-beta.21
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/README.md +1 -1
- package/composables/useUI.ts +19 -1
- package/index.d.ts +128 -0
- package/index.ts +128 -0
- package/package.json +10 -9
- package/ui.button/config.ts +1 -1
- package/ui.button-link/config.ts +1 -1
- package/ui.button-toggle/UToggle.vue +6 -4
- package/ui.button-toggle/storybook/stories.ts +4 -1
- package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
- package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
- package/ui.button-toggle/types.ts +18 -6
- package/ui.container-accordion/types.ts +5 -2
- package/ui.container-collapsible/config.ts +1 -1
- package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
- package/ui.container-modal/tests/UModal.test.ts +10 -17
- package/ui.data-list/UDataList.vue +48 -17
- package/ui.data-list/config.ts +2 -2
- package/ui.data-list/storybook/stories.ts +5 -2
- package/ui.data-list/tests/UDataList.test-d.ts +76 -0
- package/ui.data-list/tests/UDataList.test.ts +52 -3
- package/ui.data-list/tests/UDataListTypes.vue +110 -0
- package/ui.data-list/types.ts +37 -5
- package/ui.data-table/UTable.vue +202 -24
- package/ui.data-table/UTableRow.vue +4 -3
- package/ui.data-table/config.ts +11 -2
- package/ui.data-table/storybook/stories.ts +34 -1
- package/ui.data-table/tests/UTable.test-d.ts +179 -0
- package/ui.data-table/tests/UTable.test.ts +85 -7
- package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
- package/ui.data-table/tests/UTableTypes.vue +193 -0
- package/ui.data-table/types.ts +106 -9
- package/ui.data-table/utilTable.ts +1 -1
- package/ui.dropdown/UDropdown.vue +19 -8
- package/ui.dropdown/storybook/stories.ts +4 -1
- package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
- package/ui.dropdown/tests/UDropdown.test.ts +8 -1
- package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
- package/ui.dropdown/types.ts +27 -3
- package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
- package/ui.dropdown-badge/storybook/stories.ts +6 -1
- package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
- package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
- package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
- package/ui.dropdown-badge/types.ts +17 -3
- package/ui.dropdown-button/UDropdownButton.vue +9 -4
- package/ui.dropdown-button/storybook/stories.ts +6 -1
- package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
- package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
- package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
- package/ui.dropdown-button/types.ts +17 -3
- package/ui.dropdown-link/UDropdownLink.vue +9 -4
- package/ui.dropdown-link/config.ts +1 -1
- package/ui.dropdown-link/storybook/stories.ts +4 -1
- package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
- package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
- package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
- package/ui.dropdown-link/types.ts +17 -3
- package/ui.form-calendar/config.ts +1 -1
- package/ui.form-checkbox/config.ts +1 -1
- package/ui.form-checkbox/tests/UCheckbox.test.ts +4 -4
- package/ui.form-checkbox/types.ts +4 -2
- package/ui.form-checkbox-group/UCheckboxGroup.vue +8 -2
- package/ui.form-date-picker/UDatePicker.vue +11 -1
- package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
- package/ui.form-input/UInput.vue +1 -0
- package/ui.form-input/config.ts +1 -1
- package/ui.form-input/tests/UInput.test.ts +16 -0
- package/ui.form-input-file/config.ts +1 -1
- package/ui.form-listbox/UListbox.vue +29 -12
- package/ui.form-listbox/config.ts +1 -1
- package/ui.form-listbox/storybook/stories.ts +4 -1
- package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
- package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
- package/ui.form-listbox/types.ts +34 -2
- package/ui.form-radio/config.ts +1 -1
- package/ui.form-radio-group/URadioGroup.vue +15 -3
- package/ui.form-radio-group/config.ts +8 -3
- package/ui.form-radio-group/types.ts +2 -3
- package/ui.form-select/USelect.vue +49 -17
- package/ui.form-select/config.ts +4 -4
- package/ui.form-select/storybook/stories.ts +4 -1
- package/ui.form-select/tests/USelect.test-d.ts +76 -0
- package/ui.form-select/tests/USelect.test.ts +65 -1
- package/ui.form-select/tests/USelectTypes.vue +113 -0
- package/ui.form-select/types.ts +55 -4
- package/ui.form-switch/USwitch.vue +13 -12
- package/ui.form-switch/config.ts +2 -1
- package/ui.form-switch/tests/USwitch.test.ts +151 -0
- package/ui.form-textarea/config.ts +1 -1
- package/ui.image-avatar/types.ts +5 -2
- package/ui.image-avatar-group/types.ts +5 -2
- package/ui.image-icon/UIcon.vue +3 -2
- package/ui.image-icon/types.ts +2 -2
- package/ui.navigation-breadcrumbs/types.ts +5 -2
- package/ui.navigation-tab/UTab.vue +14 -9
- package/ui.navigation-tab/tests/UTab.test.ts +14 -0
- package/ui.navigation-tabs/UTabs.vue +158 -6
- package/ui.navigation-tabs/config.ts +2 -1
- package/ui.navigation-tabs/constants.ts +2 -0
- package/ui.navigation-tabs/storybook/stories.ts +12 -1
- package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
- package/ui.navigation-tabs/tests/UTabs.test.ts +213 -1
- package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
- package/ui.navigation-tabs/types.ts +26 -4
- package/ui.skeleton-text/constants.ts +1 -1
- package/ui.text-badge/config.ts +1 -1
- package/utils/node/loaderIcon.js +1 -1
- package/v.tooltip/vTooltip.ts +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
|
|
2
|
+
Proves slot props infer from the `options` prop at a real call site. -->
|
|
3
|
+
<template>
|
|
4
|
+
<UDropdownBadge :options="options">
|
|
5
|
+
<!-- Forwarded through `UDropdown` to `UListbox`, so they inherit its group flattening — a
|
|
6
|
+
flattened member need not carry the parent's members, hence the optional types. -->
|
|
7
|
+
<template #before-option="{ option, index }">
|
|
8
|
+
{{ expectOptionalString(option.label) }}
|
|
9
|
+
{{ expectOptionalNumber(option.qty) }}
|
|
10
|
+
{{ expectNumber(index) }}
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<template #option="{ option, index }">
|
|
14
|
+
{{ expectOptionalString(option.label) }}
|
|
15
|
+
{{ expectNumber(index) }}
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<template #after-option="{ option, index }">
|
|
19
|
+
{{ expectOptionalString(option.label) }}
|
|
20
|
+
{{ expectNumber(index) }}
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #empty>empty</template>
|
|
24
|
+
</UDropdownBadge>
|
|
25
|
+
|
|
26
|
+
<!-- Options from a non-literal source still expose the declared option shape. -->
|
|
27
|
+
<UDropdownBadge :options="typedOptions">
|
|
28
|
+
<template #option="{ option }">{{ expectOptionalString(option.label) }}</template>
|
|
29
|
+
</UDropdownBadge>
|
|
30
|
+
|
|
31
|
+
<UDropdownBadge :options="options">
|
|
32
|
+
<template #option="{ option }">
|
|
33
|
+
<!-- @vue-expect-error `nonExistent` is not a key of the inferred option. -->
|
|
34
|
+
{{ option.nonExistent }}
|
|
35
|
+
</template>
|
|
36
|
+
</UDropdownBadge>
|
|
37
|
+
|
|
38
|
+
<!-- `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. -->
|
|
39
|
+
<UDropdownBadge :options="options">
|
|
40
|
+
<template #default="{ label }">
|
|
41
|
+
<!-- @vue-expect-error the label is `undefined` when it cannot be resolved. -->
|
|
42
|
+
{{ expectString(label) }}
|
|
43
|
+
</template>
|
|
44
|
+
</UDropdownBadge>
|
|
45
|
+
|
|
46
|
+
<!-- Regression guard: an option slot must never promise the caller's members as present. -->
|
|
47
|
+
<UDropdownBadge
|
|
48
|
+
:options="groupedOptions"
|
|
49
|
+
group-label-key="groupName"
|
|
50
|
+
group-value-key="children"
|
|
51
|
+
label-key="label"
|
|
52
|
+
>
|
|
53
|
+
<template #option="{ option }">
|
|
54
|
+
<!-- @vue-expect-error a flattened group member need not carry `label`. -->
|
|
55
|
+
{{ expectString(option.label) }}
|
|
56
|
+
</template>
|
|
57
|
+
<template #before-option="{ option }">
|
|
58
|
+
<!-- @vue-expect-error a flattened group member need not carry `qty`. -->
|
|
59
|
+
{{ expectNumber(option.qty) }}
|
|
60
|
+
</template>
|
|
61
|
+
<template #after-option="{ option }">
|
|
62
|
+
<!-- @vue-expect-error a flattened group member need not carry `groupName`. -->
|
|
63
|
+
{{ expectString(option.groupName) }}
|
|
64
|
+
</template>
|
|
65
|
+
</UDropdownBadge>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref } from "vue";
|
|
70
|
+
|
|
71
|
+
import UDropdownBadge from "../UDropdownBadge.vue";
|
|
72
|
+
|
|
73
|
+
interface UserOption {
|
|
74
|
+
label: string;
|
|
75
|
+
value: string;
|
|
76
|
+
qty: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const options = [{ label: "a", value: "a", qty: 1 }];
|
|
80
|
+
const typedOptions = ref<UserOption[]>([]);
|
|
81
|
+
|
|
82
|
+
/* A group whose members lack the parent's members — exactly what `filterGroups` flattens. */
|
|
83
|
+
const groupedOptions = [
|
|
84
|
+
{
|
|
85
|
+
groupName: "Group A",
|
|
86
|
+
label: "Group A",
|
|
87
|
+
qty: 1,
|
|
88
|
+
children: [{ value: "c1", title: "Child" }],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function expectString(value: string) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expectNumber(value: number) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectOptionalString(value: string | undefined) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function expectOptionalNumber(value: number | undefined) {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import defaultConfig from "./config";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ListboxOption, SlotOption } from "../ui.form-listbox/types";
|
|
4
4
|
import type { ComponentConfig, UnknownObject } from "../types";
|
|
5
5
|
|
|
6
6
|
export type Config = typeof defaultConfig;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/* Option slots are forwarded through `UDropdown` to `UListbox`, inheriting its group flattening —
|
|
9
|
+
a flattened member need not carry the parent's members, so `SlotOption` is `Partial`. */
|
|
10
|
+
export interface UDropdownBadgeSlots<TOption extends ListboxOption = ListboxOption> {
|
|
11
|
+
/* `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups — `undefined`
|
|
12
|
+
when the label cannot be resolved. */
|
|
13
|
+
default?: (props: { label: string | undefined; opened: boolean }) => unknown;
|
|
14
|
+
left?: (props: { opened: boolean }) => unknown;
|
|
15
|
+
toggle?: (props: { opened: boolean }) => unknown;
|
|
16
|
+
"before-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
|
|
17
|
+
option?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
|
|
18
|
+
"after-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
|
|
19
|
+
empty?: () => unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Props<TOption extends ListboxOption = ListboxOption> {
|
|
9
23
|
/**
|
|
10
24
|
* Selected badge.
|
|
11
25
|
*/
|
|
@@ -24,7 +38,7 @@ export interface Props {
|
|
|
24
38
|
/**
|
|
25
39
|
* Options list.
|
|
26
40
|
*/
|
|
27
|
-
options?:
|
|
41
|
+
options?: TOption[];
|
|
28
42
|
|
|
29
43
|
/**
|
|
30
44
|
* Label key in the item object of options.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="TOption extends ListboxOption">
|
|
2
2
|
import { computed, useTemplateRef } from "vue";
|
|
3
3
|
|
|
4
4
|
import { useUI } from "../composables/useUI";
|
|
@@ -11,11 +11,13 @@ import UDropdown from "../ui.dropdown/UDropdown.vue";
|
|
|
11
11
|
import defaultConfig from "./config";
|
|
12
12
|
import { COMPONENT_NAME } from "./constants";
|
|
13
13
|
|
|
14
|
-
import type { Props, Config } from "./types";
|
|
14
|
+
import type { Props, Config, UDropdownButtonSlots } from "./types";
|
|
15
|
+
import type { ListboxOption } from "../ui.form-listbox/types";
|
|
16
|
+
import type { ComponentExposed } from "../types";
|
|
15
17
|
|
|
16
18
|
defineOptions({ inheritAttrs: false });
|
|
17
19
|
|
|
18
|
-
const props = withDefaults(defineProps<Props
|
|
20
|
+
const props = withDefaults(defineProps<Props<TOption>>(), {
|
|
19
21
|
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
20
22
|
options: () => [],
|
|
21
23
|
modelValue: "",
|
|
@@ -59,7 +61,10 @@ const emit = defineEmits([
|
|
|
59
61
|
"update:search",
|
|
60
62
|
]);
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
defineSlots<UDropdownButtonSlots<TOption>>();
|
|
65
|
+
|
|
66
|
+
/* `UDropdown` is generic, so its type is a function — `InstanceType` does not apply. */
|
|
67
|
+
type UDropdownRef = ComponentExposed<typeof UDropdown>;
|
|
63
68
|
|
|
64
69
|
const dropdownRef = useTemplateRef<UDropdownRef>("dropdown");
|
|
65
70
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getDocsDescription,
|
|
7
7
|
} from "../../utils/storybook";
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import UDropdownButtonComponent from "../../ui.dropdown-button/UDropdownButton.vue";
|
|
10
10
|
import URow from "../../ui.container-row/URow.vue";
|
|
11
11
|
import UCol from "../../ui.container-col/UCol.vue";
|
|
12
12
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
@@ -24,6 +24,11 @@ import emilyDavis from "../../ui.form-select/storybook/assets/images/emily-davis
|
|
|
24
24
|
import alexJohnson from "../../ui.form-select/storybook/assets/images/alex-johnson.png";
|
|
25
25
|
import patMorgan from "../../ui.form-select/storybook/assets/images/pat-morgan.png";
|
|
26
26
|
|
|
27
|
+
/* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
|
|
28
|
+
const UDropdownButton = UDropdownButtonComponent as typeof UDropdownButtonComponent & {
|
|
29
|
+
__name: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
27
32
|
interface DefaultUDropdownButtonArgs extends Props {
|
|
28
33
|
slotTemplate?: string;
|
|
29
34
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { Props, UDropdownButtonSlots } from "../types";
|
|
4
|
+
import type { ListboxOption, SlotOption } from "../../ui.form-listbox/types";
|
|
5
|
+
|
|
6
|
+
interface UserOption {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
qty: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type OptionProps = Parameters<NonNullable<UDropdownButtonSlots<UserOption>["option"]>>[0];
|
|
13
|
+
|
|
14
|
+
describe("UDropdownButton types", () => {
|
|
15
|
+
it("infers the caller's option shape in a forwarded option slot", () => {
|
|
16
|
+
expectTypeOf<OptionProps["option"]["label"]>().toEqualTypeOf<string | undefined>();
|
|
17
|
+
expectTypeOf<OptionProps["option"]["qty"]>().toEqualTypeOf<number | undefined>();
|
|
18
|
+
expectTypeOf<OptionProps["index"]>().toEqualTypeOf<number>();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not expose undeclared option properties to a slot", () => {
|
|
22
|
+
type HasIndexSignature = string extends keyof OptionProps["option"] ? true : false;
|
|
23
|
+
|
|
24
|
+
expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reuses UListbox's SlotOption rather than a second dialect", () => {
|
|
28
|
+
expectTypeOf<OptionProps["option"]>().toEqualTypeOf<SlotOption<UserOption>>();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not promise the caller's members on any option slot", () => {
|
|
32
|
+
/* Forwarded through `UDropdown` to `UListbox`, whose `filterGroups` concats nested group
|
|
33
|
+
* members into the flat list — a member need not carry the parent's members. */
|
|
34
|
+
type BeforeProps = Parameters<
|
|
35
|
+
NonNullable<UDropdownButtonSlots<UserOption>["before-option"]>
|
|
36
|
+
>[0];
|
|
37
|
+
type AfterProps = Parameters<NonNullable<UDropdownButtonSlots<UserOption>["after-option"]>>[0];
|
|
38
|
+
|
|
39
|
+
type GroupMember = { value: string; title: string };
|
|
40
|
+
|
|
41
|
+
expectTypeOf<GroupMember>().toExtend<BeforeProps["option"]>();
|
|
42
|
+
expectTypeOf<GroupMember>().toExtend<OptionProps["option"]>();
|
|
43
|
+
expectTypeOf<GroupMember>().toExtend<AfterProps["option"]>();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("types the default slot label as possibly undefined", () => {
|
|
47
|
+
/* It is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. */
|
|
48
|
+
type DefaultProps = Parameters<NonNullable<UDropdownButtonSlots<UserOption>["default"]>>[0];
|
|
49
|
+
|
|
50
|
+
expectTypeOf<DefaultProps["label"]>().toEqualTypeOf<string | undefined>();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("resolves Props with no type argument", () => {
|
|
54
|
+
expectTypeOf<Props>().toEqualTypeOf<Props<ListboxOption>>();
|
|
55
|
+
expectTypeOf<Props<UserOption>["options"]>().toEqualTypeOf<UserOption[] | undefined>();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -4,10 +4,17 @@ import { describe, it, expect } from "vitest";
|
|
|
4
4
|
import UDropdownButton from "../UDropdownButton.vue";
|
|
5
5
|
import UButton from "../../ui.button/UButton.vue";
|
|
6
6
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
7
|
-
import
|
|
7
|
+
import UListboxComponent from "../../ui.form-listbox/UListbox.vue";
|
|
8
8
|
|
|
9
9
|
import type { Props } from "../types";
|
|
10
10
|
|
|
11
|
+
import type { ComponentProps } from "../../types";
|
|
12
|
+
|
|
13
|
+
/* Generic SFCs are typed as a function, which `findComponent` does not accept. */
|
|
14
|
+
const UListbox = UListboxComponent as unknown as new () => {
|
|
15
|
+
$props: ComponentProps<typeof UListboxComponent>;
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
describe("UDropdownButton.vue", () => {
|
|
12
19
|
const defaultOptions = [
|
|
13
20
|
{ value: 1, label: "Option 1" },
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
|
|
2
|
+
Proves slot props infer from the `options` prop at a real call site. -->
|
|
3
|
+
<template>
|
|
4
|
+
<UDropdownButton :options="options">
|
|
5
|
+
<!-- Forwarded through `UDropdown` to `UListbox`, so they inherit its group flattening — a
|
|
6
|
+
flattened member need not carry the parent's members, hence the optional types. -->
|
|
7
|
+
<template #before-option="{ option, index }">
|
|
8
|
+
{{ expectOptionalString(option.label) }}
|
|
9
|
+
{{ expectOptionalNumber(option.qty) }}
|
|
10
|
+
{{ expectNumber(index) }}
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<template #option="{ option, index }">
|
|
14
|
+
{{ expectOptionalString(option.label) }}
|
|
15
|
+
{{ expectNumber(index) }}
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<template #after-option="{ option, index }">
|
|
19
|
+
{{ expectOptionalString(option.label) }}
|
|
20
|
+
{{ expectNumber(index) }}
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #empty>empty</template>
|
|
24
|
+
</UDropdownButton>
|
|
25
|
+
|
|
26
|
+
<!-- Options from a non-literal source still expose the declared option shape. -->
|
|
27
|
+
<UDropdownButton :options="typedOptions">
|
|
28
|
+
<template #option="{ option }">{{ expectOptionalString(option.label) }}</template>
|
|
29
|
+
</UDropdownButton>
|
|
30
|
+
|
|
31
|
+
<UDropdownButton :options="options">
|
|
32
|
+
<template #option="{ option }">
|
|
33
|
+
<!-- @vue-expect-error `nonExistent` is not a key of the inferred option. -->
|
|
34
|
+
{{ option.nonExistent }}
|
|
35
|
+
</template>
|
|
36
|
+
</UDropdownButton>
|
|
37
|
+
|
|
38
|
+
<!-- `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. -->
|
|
39
|
+
<UDropdownButton :options="options">
|
|
40
|
+
<template #default="{ label }">
|
|
41
|
+
<!-- @vue-expect-error the label is `undefined` when it cannot be resolved. -->
|
|
42
|
+
{{ expectString(label) }}
|
|
43
|
+
</template>
|
|
44
|
+
</UDropdownButton>
|
|
45
|
+
|
|
46
|
+
<!-- Regression guard: an option slot must never promise the caller's members as present. -->
|
|
47
|
+
<UDropdownButton
|
|
48
|
+
:options="groupedOptions"
|
|
49
|
+
group-label-key="groupName"
|
|
50
|
+
group-value-key="children"
|
|
51
|
+
label-key="label"
|
|
52
|
+
>
|
|
53
|
+
<template #option="{ option }">
|
|
54
|
+
<!-- @vue-expect-error a flattened group member need not carry `label`. -->
|
|
55
|
+
{{ expectString(option.label) }}
|
|
56
|
+
</template>
|
|
57
|
+
<template #before-option="{ option }">
|
|
58
|
+
<!-- @vue-expect-error a flattened group member need not carry `qty`. -->
|
|
59
|
+
{{ expectNumber(option.qty) }}
|
|
60
|
+
</template>
|
|
61
|
+
<template #after-option="{ option }">
|
|
62
|
+
<!-- @vue-expect-error a flattened group member need not carry `groupName`. -->
|
|
63
|
+
{{ expectString(option.groupName) }}
|
|
64
|
+
</template>
|
|
65
|
+
</UDropdownButton>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref } from "vue";
|
|
70
|
+
|
|
71
|
+
import UDropdownButton from "../UDropdownButton.vue";
|
|
72
|
+
|
|
73
|
+
interface UserOption {
|
|
74
|
+
label: string;
|
|
75
|
+
value: string;
|
|
76
|
+
qty: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const options = [{ label: "a", value: "a", qty: 1 }];
|
|
80
|
+
const typedOptions = ref<UserOption[]>([]);
|
|
81
|
+
|
|
82
|
+
/* A group whose members lack the parent's members — exactly what `filterGroups` flattens. */
|
|
83
|
+
const groupedOptions = [
|
|
84
|
+
{
|
|
85
|
+
groupName: "Group A",
|
|
86
|
+
label: "Group A",
|
|
87
|
+
qty: 1,
|
|
88
|
+
children: [{ value: "c1", title: "Child" }],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function expectString(value: string) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expectNumber(value: number) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectOptionalString(value: string | undefined) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function expectOptionalNumber(value: number | undefined) {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
</script>
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import defaultConfig from "./config";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type { ListboxOption, SlotOption } from "../ui.form-listbox/types";
|
|
4
4
|
import type { ComponentConfig, UnknownObject } from "../types";
|
|
5
5
|
|
|
6
6
|
export type Config = typeof defaultConfig;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/* Option slots are forwarded through `UDropdown` to `UListbox`, inheriting its group flattening —
|
|
9
|
+
a flattened member need not carry the parent's members, so `SlotOption` is `Partial`. */
|
|
10
|
+
export interface UDropdownButtonSlots<TOption extends ListboxOption = ListboxOption> {
|
|
11
|
+
/* `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups — `undefined`
|
|
12
|
+
when the label cannot be resolved. */
|
|
13
|
+
default?: (props: { label: string | undefined; opened: boolean }) => unknown;
|
|
14
|
+
left?: (props: { opened: boolean }) => unknown;
|
|
15
|
+
toggle?: (props: { opened: boolean }) => unknown;
|
|
16
|
+
"before-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
|
|
17
|
+
option?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
|
|
18
|
+
"after-option"?: (props: { option: SlotOption<TOption>; index: number }) => unknown;
|
|
19
|
+
empty?: () => unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Props<TOption extends ListboxOption = ListboxOption> {
|
|
9
23
|
/**
|
|
10
24
|
* Selected button.
|
|
11
25
|
*/
|
|
@@ -24,7 +38,7 @@ export interface Props {
|
|
|
24
38
|
/**
|
|
25
39
|
* Options list.
|
|
26
40
|
*/
|
|
27
|
-
options?:
|
|
41
|
+
options?: TOption[];
|
|
28
42
|
|
|
29
43
|
/**
|
|
30
44
|
* Label key in the item object of options.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="TOption extends ListboxOption">
|
|
2
2
|
import { computed, useTemplateRef } from "vue";
|
|
3
3
|
|
|
4
4
|
import { useUI } from "../composables/useUI";
|
|
@@ -11,11 +11,13 @@ import UDropdown from "../ui.dropdown/UDropdown.vue";
|
|
|
11
11
|
import { COMPONENT_NAME } from "./constants";
|
|
12
12
|
import defaultConfig from "./config";
|
|
13
13
|
|
|
14
|
-
import type { Props, Config } from "./types";
|
|
14
|
+
import type { Props, Config, UDropdownLinkSlots } from "./types";
|
|
15
|
+
import type { ListboxOption } from "../ui.form-listbox/types";
|
|
16
|
+
import type { ComponentExposed } from "../types";
|
|
15
17
|
|
|
16
18
|
defineOptions({ inheritAttrs: false });
|
|
17
19
|
|
|
18
|
-
const props = withDefaults(defineProps<Props
|
|
20
|
+
const props = withDefaults(defineProps<Props<TOption>>(), {
|
|
19
21
|
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
20
22
|
options: () => [],
|
|
21
23
|
modelValue: "",
|
|
@@ -59,7 +61,10 @@ const emit = defineEmits([
|
|
|
59
61
|
"update:search",
|
|
60
62
|
]);
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
defineSlots<UDropdownLinkSlots<TOption>>();
|
|
65
|
+
|
|
66
|
+
/* `UDropdown` is generic, so its type is a function — `InstanceType` does not apply. */
|
|
67
|
+
type UDropdownRef = ComponentExposed<typeof UDropdown>;
|
|
63
68
|
|
|
64
69
|
const dropdownRef = useTemplateRef<UDropdownRef>("dropdown");
|
|
65
70
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getDocsDescription,
|
|
7
7
|
} from "../../utils/storybook";
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import UDropdownLinkComponent from "../../ui.dropdown-link/UDropdownLink.vue";
|
|
10
10
|
import URow from "../../ui.container-row/URow.vue";
|
|
11
11
|
import UCol from "../../ui.container-col/UCol.vue";
|
|
12
12
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
@@ -24,6 +24,9 @@ import emilyDavis from "../../ui.form-select/storybook/assets/images/emily-davis
|
|
|
24
24
|
import alexJohnson from "../../ui.form-select/storybook/assets/images/alex-johnson.png";
|
|
25
25
|
import patMorgan from "../../ui.form-select/storybook/assets/images/pat-morgan.png";
|
|
26
26
|
|
|
27
|
+
/* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
|
|
28
|
+
const UDropdownLink = UDropdownLinkComponent as typeof UDropdownLinkComponent & { __name: string };
|
|
29
|
+
|
|
27
30
|
interface DefaultUDropdownLinkArgs extends Props {
|
|
28
31
|
slotTemplate?: string;
|
|
29
32
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { Props, UDropdownLinkSlots } from "../types";
|
|
4
|
+
import type { ListboxOption, SlotOption } from "../../ui.form-listbox/types";
|
|
5
|
+
|
|
6
|
+
interface UserOption {
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
qty: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type OptionProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["option"]>>[0];
|
|
13
|
+
|
|
14
|
+
describe("UDropdownLink types", () => {
|
|
15
|
+
it("infers the caller's option shape in a forwarded option slot", () => {
|
|
16
|
+
expectTypeOf<OptionProps["option"]["label"]>().toEqualTypeOf<string | undefined>();
|
|
17
|
+
expectTypeOf<OptionProps["option"]["qty"]>().toEqualTypeOf<number | undefined>();
|
|
18
|
+
expectTypeOf<OptionProps["index"]>().toEqualTypeOf<number>();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not expose undeclared option properties to a slot", () => {
|
|
22
|
+
type HasIndexSignature = string extends keyof OptionProps["option"] ? true : false;
|
|
23
|
+
|
|
24
|
+
expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("reuses UListbox's SlotOption rather than a second dialect", () => {
|
|
28
|
+
expectTypeOf<OptionProps["option"]>().toEqualTypeOf<SlotOption<UserOption>>();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("does not promise the caller's members on any option slot", () => {
|
|
32
|
+
/* Forwarded through `UDropdown` to `UListbox`, whose `filterGroups` concats nested group
|
|
33
|
+
* members into the flat list — a member need not carry the parent's members. */
|
|
34
|
+
type BeforeProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["before-option"]>>[0];
|
|
35
|
+
type AfterProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["after-option"]>>[0];
|
|
36
|
+
|
|
37
|
+
type GroupMember = { value: string; title: string };
|
|
38
|
+
|
|
39
|
+
expectTypeOf<GroupMember>().toExtend<BeforeProps["option"]>();
|
|
40
|
+
expectTypeOf<GroupMember>().toExtend<OptionProps["option"]>();
|
|
41
|
+
expectTypeOf<GroupMember>().toExtend<AfterProps["option"]>();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("types the default slot label as possibly undefined", () => {
|
|
45
|
+
/* It is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. */
|
|
46
|
+
type DefaultProps = Parameters<NonNullable<UDropdownLinkSlots<UserOption>["default"]>>[0];
|
|
47
|
+
|
|
48
|
+
expectTypeOf<DefaultProps["label"]>().toEqualTypeOf<string | undefined>();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("resolves Props with no type argument", () => {
|
|
52
|
+
expectTypeOf<Props>().toEqualTypeOf<Props<ListboxOption>>();
|
|
53
|
+
expectTypeOf<Props<UserOption>["options"]>().toEqualTypeOf<UserOption[] | undefined>();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -4,10 +4,17 @@ import { describe, it, expect } from "vitest";
|
|
|
4
4
|
import UDropdownLink from "../UDropdownLink.vue";
|
|
5
5
|
import ULink from "../../ui.button-link/ULink.vue";
|
|
6
6
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
7
|
-
import
|
|
7
|
+
import UListboxComponent from "../../ui.form-listbox/UListbox.vue";
|
|
8
8
|
|
|
9
9
|
import type { Props } from "../types";
|
|
10
10
|
|
|
11
|
+
import type { ComponentProps } from "../../types";
|
|
12
|
+
|
|
13
|
+
/* Generic SFCs are typed as a function, which `findComponent` does not accept. */
|
|
14
|
+
const UListbox = UListboxComponent as unknown as new () => {
|
|
15
|
+
$props: ComponentProps<typeof UListboxComponent>;
|
|
16
|
+
};
|
|
17
|
+
|
|
11
18
|
describe("UDropdownLink.vue", () => {
|
|
12
19
|
const defaultOptions = [
|
|
13
20
|
{ value: 1, label: "Option 1" },
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
|
|
2
|
+
Proves slot props infer from the `options` prop at a real call site. -->
|
|
3
|
+
<template>
|
|
4
|
+
<UDropdownLink :options="options">
|
|
5
|
+
<!-- Forwarded through `UDropdown` to `UListbox`, so they inherit its group flattening — a
|
|
6
|
+
flattened member need not carry the parent's members, hence the optional types. -->
|
|
7
|
+
<template #before-option="{ option, index }">
|
|
8
|
+
{{ expectOptionalString(option.label) }}
|
|
9
|
+
{{ expectOptionalNumber(option.qty) }}
|
|
10
|
+
{{ expectNumber(index) }}
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<template #option="{ option, index }">
|
|
14
|
+
{{ expectOptionalString(option.label) }}
|
|
15
|
+
{{ expectNumber(index) }}
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<template #after-option="{ option, index }">
|
|
19
|
+
{{ expectOptionalString(option.label) }}
|
|
20
|
+
{{ expectNumber(index) }}
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #empty>empty</template>
|
|
24
|
+
</UDropdownLink>
|
|
25
|
+
|
|
26
|
+
<!-- Options from a non-literal source still expose the declared option shape. -->
|
|
27
|
+
<UDropdownLink :options="typedOptions">
|
|
28
|
+
<template #option="{ option }">{{ expectOptionalString(option.label) }}</template>
|
|
29
|
+
</UDropdownLink>
|
|
30
|
+
|
|
31
|
+
<UDropdownLink :options="options">
|
|
32
|
+
<template #option="{ option }">
|
|
33
|
+
<!-- @vue-expect-error `nonExistent` is not a key of the inferred option. -->
|
|
34
|
+
{{ option.nonExistent }}
|
|
35
|
+
</template>
|
|
36
|
+
</UDropdownLink>
|
|
37
|
+
|
|
38
|
+
<!-- `label` is `UDropdown`'s `displayLabel`, a join of `option[labelKey]` lookups. -->
|
|
39
|
+
<UDropdownLink :options="options">
|
|
40
|
+
<template #default="{ label }">
|
|
41
|
+
<!-- @vue-expect-error the label is `undefined` when it cannot be resolved. -->
|
|
42
|
+
{{ expectString(label) }}
|
|
43
|
+
</template>
|
|
44
|
+
</UDropdownLink>
|
|
45
|
+
|
|
46
|
+
<!-- Regression guard: an option slot must never promise the caller's members as present. -->
|
|
47
|
+
<UDropdownLink
|
|
48
|
+
:options="groupedOptions"
|
|
49
|
+
group-label-key="groupName"
|
|
50
|
+
group-value-key="children"
|
|
51
|
+
label-key="label"
|
|
52
|
+
>
|
|
53
|
+
<template #option="{ option }">
|
|
54
|
+
<!-- @vue-expect-error a flattened group member need not carry `label`. -->
|
|
55
|
+
{{ expectString(option.label) }}
|
|
56
|
+
</template>
|
|
57
|
+
<template #before-option="{ option }">
|
|
58
|
+
<!-- @vue-expect-error a flattened group member need not carry `qty`. -->
|
|
59
|
+
{{ expectNumber(option.qty) }}
|
|
60
|
+
</template>
|
|
61
|
+
<template #after-option="{ option }">
|
|
62
|
+
<!-- @vue-expect-error a flattened group member need not carry `groupName`. -->
|
|
63
|
+
{{ expectString(option.groupName) }}
|
|
64
|
+
</template>
|
|
65
|
+
</UDropdownLink>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref } from "vue";
|
|
70
|
+
|
|
71
|
+
import UDropdownLink from "../UDropdownLink.vue";
|
|
72
|
+
|
|
73
|
+
interface UserOption {
|
|
74
|
+
label: string;
|
|
75
|
+
value: string;
|
|
76
|
+
qty: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const options = [{ label: "a", value: "a", qty: 1 }];
|
|
80
|
+
const typedOptions = ref<UserOption[]>([]);
|
|
81
|
+
|
|
82
|
+
/* A group whose members lack the parent's members — exactly what `filterGroups` flattens. */
|
|
83
|
+
const groupedOptions = [
|
|
84
|
+
{
|
|
85
|
+
groupName: "Group A",
|
|
86
|
+
label: "Group A",
|
|
87
|
+
qty: 1,
|
|
88
|
+
children: [{ value: "c1", title: "Child" }],
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
function expectString(value: string) {
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expectNumber(value: number) {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function expectOptionalString(value: string | undefined) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function expectOptionalNumber(value: number | undefined) {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
</script>
|