vueless 1.4.12-beta.18 → 1.4.12-beta.19
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/index.d.ts +31 -5
- package/index.ts +31 -5
- package/package.json +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 +12 -2
- package/ui.data-list/UDataList.vue +23 -13
- package/ui.data-list/storybook/stories.ts +4 -1
- package/ui.data-list/tests/UDataList.test-d.ts +76 -0
- package/ui.data-list/tests/UDataListTypes.vue +110 -0
- package/ui.data-list/types.ts +26 -2
- package/ui.data-table/UTable.vue +21 -8
- package/ui.data-table/config.ts +1 -1
- package/ui.data-table/storybook/stories.ts +4 -1
- package/ui.data-table/tests/UTable.test-d.ts +179 -0
- package/ui.data-table/tests/UTable.test.ts +1 -1
- package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
- package/ui.data-table/tests/UTableTypes.vue +193 -0
- package/ui.data-table/types.ts +74 -4
- package/ui.data-table/utilTable.ts +1 -1
- package/ui.dropdown/UDropdown.vue +14 -6
- 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/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-listbox/UListbox.vue +20 -7
- 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 +31 -2
- package/ui.form-select/USelect.vue +41 -13
- 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 +8 -1
- package/ui.form-select/tests/USelectTypes.vue +113 -0
- package/ui.form-select/types.ts +51 -3
- package/ui.navigation-tabs/UTabs.vue +6 -4
- package/ui.navigation-tabs/storybook/stories.ts +4 -1
- package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
- package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
- package/ui.navigation-tabs/types.ts +21 -2
package/index.d.ts
CHANGED
|
@@ -199,14 +199,40 @@ export type {
|
|
|
199
199
|
TableColumn,
|
|
200
200
|
DateDivider,
|
|
201
201
|
BaseDateDivider,
|
|
202
|
+
SlotRow,
|
|
203
|
+
SelectedSlotRow,
|
|
204
|
+
UTableSlots,
|
|
205
|
+
UTableStaticSlots,
|
|
206
|
+
UTableDynamicSlots,
|
|
202
207
|
} from "./ui.data-table/types";
|
|
203
|
-
export type {
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
export type {
|
|
209
|
+
Option,
|
|
210
|
+
BaseOption,
|
|
211
|
+
ListboxOption,
|
|
212
|
+
SelectedValue,
|
|
213
|
+
SlotOption,
|
|
214
|
+
UListboxSlots,
|
|
215
|
+
} from "./ui.form-listbox/types";
|
|
216
|
+
export type {
|
|
217
|
+
SelectOption,
|
|
218
|
+
SelectedSlotOption,
|
|
219
|
+
SelectedOptionsBinding,
|
|
220
|
+
USelectSlots,
|
|
221
|
+
} from "./ui.form-select/types";
|
|
222
|
+
export type { UDropdownSlots } from "./ui.dropdown/types";
|
|
223
|
+
export type { UDropdownBadgeSlots } from "./ui.dropdown-badge/types";
|
|
224
|
+
export type { UDropdownButtonSlots } from "./ui.dropdown-button/types";
|
|
225
|
+
export type { UDropdownLinkSlots } from "./ui.dropdown-link/types";
|
|
226
|
+
export type { UTabsOption, BaseTabOption, UTabsSlots } from "./ui.navigation-tabs/types";
|
|
206
227
|
export type { UBreadcrumb, BaseBreadcrumb } from "./ui.navigation-breadcrumbs/types";
|
|
207
|
-
export type { UToggleOption, BaseToggleOption } from "./ui.button-toggle/types";
|
|
228
|
+
export type { UToggleOption, BaseToggleOption, UToggleSlots } from "./ui.button-toggle/types";
|
|
208
229
|
export type { UAccordionOption, BaseAccordionOption } from "./ui.container-accordion/types";
|
|
209
|
-
export type {
|
|
230
|
+
export type {
|
|
231
|
+
DataListItem,
|
|
232
|
+
BaseDataListItem,
|
|
233
|
+
SlotItem,
|
|
234
|
+
UDataListSlots,
|
|
235
|
+
} from "./ui.data-list/types";
|
|
210
236
|
export type { UCheckboxOption, BaseCheckboxOption } from "./ui.form-checkbox/types";
|
|
211
237
|
export type { ChipItem, BaseChipItem } from "./ui.image-avatar/types";
|
|
212
238
|
export type { AvatarItem, BaseAvatarItem } from "./ui.image-avatar-group/types";
|
package/index.ts
CHANGED
|
@@ -205,14 +205,40 @@ export type {
|
|
|
205
205
|
TableColumn,
|
|
206
206
|
DateDivider,
|
|
207
207
|
BaseDateDivider,
|
|
208
|
+
SlotRow,
|
|
209
|
+
SelectedSlotRow,
|
|
210
|
+
UTableSlots,
|
|
211
|
+
UTableStaticSlots,
|
|
212
|
+
UTableDynamicSlots,
|
|
208
213
|
} from "./ui.data-table/types";
|
|
209
|
-
export type {
|
|
210
|
-
|
|
211
|
-
|
|
214
|
+
export type {
|
|
215
|
+
Option,
|
|
216
|
+
BaseOption,
|
|
217
|
+
ListboxOption,
|
|
218
|
+
SelectedValue,
|
|
219
|
+
SlotOption,
|
|
220
|
+
UListboxSlots,
|
|
221
|
+
} from "./ui.form-listbox/types";
|
|
222
|
+
export type {
|
|
223
|
+
SelectOption,
|
|
224
|
+
SelectedSlotOption,
|
|
225
|
+
SelectedOptionsBinding,
|
|
226
|
+
USelectSlots,
|
|
227
|
+
} from "./ui.form-select/types";
|
|
228
|
+
export type { UDropdownSlots } from "./ui.dropdown/types";
|
|
229
|
+
export type { UDropdownBadgeSlots } from "./ui.dropdown-badge/types";
|
|
230
|
+
export type { UDropdownButtonSlots } from "./ui.dropdown-button/types";
|
|
231
|
+
export type { UDropdownLinkSlots } from "./ui.dropdown-link/types";
|
|
232
|
+
export type { UTabsOption, BaseTabOption, UTabsSlots } from "./ui.navigation-tabs/types";
|
|
212
233
|
export type { UBreadcrumb, BaseBreadcrumb } from "./ui.navigation-breadcrumbs/types";
|
|
213
|
-
export type { UToggleOption, BaseToggleOption } from "./ui.button-toggle/types";
|
|
234
|
+
export type { UToggleOption, BaseToggleOption, UToggleSlots } from "./ui.button-toggle/types";
|
|
214
235
|
export type { UAccordionOption, BaseAccordionOption } from "./ui.container-accordion/types";
|
|
215
|
-
export type {
|
|
236
|
+
export type {
|
|
237
|
+
DataListItem,
|
|
238
|
+
BaseDataListItem,
|
|
239
|
+
SlotItem,
|
|
240
|
+
UDataListSlots,
|
|
241
|
+
} from "./ui.data-list/types";
|
|
216
242
|
export type { UCheckboxOption, BaseCheckboxOption } from "./ui.form-checkbox/types";
|
|
217
243
|
export type { ChipItem, BaseChipItem } from "./ui.image-avatar/types";
|
|
218
244
|
export type { AvatarItem, BaseAvatarItem } from "./ui.image-avatar-group/types";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueless",
|
|
3
|
-
"version": "1.4.12-beta.
|
|
3
|
+
"version": "1.4.12-beta.19",
|
|
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",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="TItem extends UToggleOption">
|
|
2
2
|
import { computed, ref, useId, useTemplateRef } from "vue";
|
|
3
3
|
|
|
4
4
|
import UButton from "../ui.button/UButton.vue";
|
|
@@ -9,16 +9,18 @@ import { getDefaults } from "../utils/ui";
|
|
|
9
9
|
import defaultConfig from "./config";
|
|
10
10
|
import { COMPONENT_NAME } from "./constants";
|
|
11
11
|
|
|
12
|
-
import type { Props, Config, UToggleOption } from "./types";
|
|
12
|
+
import type { Props, Config, UToggleOption, UToggleSlots } from "./types";
|
|
13
13
|
|
|
14
14
|
defineOptions({ inheritAttrs: false });
|
|
15
15
|
|
|
16
|
-
const props = withDefaults(defineProps<Props
|
|
17
|
-
...getDefaults<Props
|
|
16
|
+
const props = withDefaults(defineProps<Props<TItem>>(), {
|
|
17
|
+
...getDefaults<Props<TItem>, Config>(defaultConfig, COMPONENT_NAME),
|
|
18
18
|
options: () => [],
|
|
19
19
|
modelValue: () => [],
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
defineSlots<UToggleSlots<TItem>>();
|
|
23
|
+
|
|
22
24
|
const emit = defineEmits([
|
|
23
25
|
/**
|
|
24
26
|
* Triggers when toggle option is selected.
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
getDocsDescription,
|
|
8
8
|
} from "../../utils/storybook";
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import UToggleComponent from "../../ui.button-toggle/UToggle.vue";
|
|
11
11
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
12
12
|
import URow from "../../ui.container-row/URow.vue";
|
|
13
13
|
import UBadge from "../../ui.text-badge/UBadge.vue";
|
|
@@ -17,6 +17,9 @@ import ULabel from "../../ui.form-label/ULabel.vue";
|
|
|
17
17
|
import type { Meta, StoryFn } from "@storybook/vue3-vite";
|
|
18
18
|
import type { Props } from "../types";
|
|
19
19
|
|
|
20
|
+
/* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
|
|
21
|
+
const UToggle = UToggleComponent as typeof UToggleComponent & { __name: string };
|
|
22
|
+
|
|
20
23
|
interface UToggleArgs extends Props {
|
|
21
24
|
slotTemplate?: string;
|
|
22
25
|
enum: "size";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { Props, UToggleOption, UToggleSlots } from "../types";
|
|
4
|
+
|
|
5
|
+
interface CountedOption {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
count: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type OptionProps = Parameters<NonNullable<UToggleSlots<CountedOption>["option"]>>[0];
|
|
12
|
+
|
|
13
|
+
describe("UToggle types", () => {
|
|
14
|
+
it("infers the caller's option shape in a slot", () => {
|
|
15
|
+
expectTypeOf<OptionProps["option"]>().toEqualTypeOf<CountedOption>();
|
|
16
|
+
expectTypeOf<OptionProps["option"]["count"]>().toEqualTypeOf<number>();
|
|
17
|
+
expectTypeOf<OptionProps["index"]>().toEqualTypeOf<number>();
|
|
18
|
+
expectTypeOf<OptionProps["label"]>().toEqualTypeOf<string>();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("promises the caller's members — options reach slots untransformed", () => {
|
|
22
|
+
/* Unlike UTable, `UToggle` iterates `props.options` directly: no flattening, filtering
|
|
23
|
+
* or synthetic entries, so the members are guaranteed rather than optional. */
|
|
24
|
+
expectTypeOf<OptionProps["option"]["label"]>().toEqualTypeOf<string>();
|
|
25
|
+
expectTypeOf<undefined>().not.toExtend<OptionProps["option"]["count"]>();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("does not expose undeclared option properties to a slot", () => {
|
|
29
|
+
type HasIndexSignature = string extends keyof OptionProps["option"] ? true : false;
|
|
30
|
+
|
|
31
|
+
expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
|
|
32
|
+
expectTypeOf<keyof OptionProps["option"]>().not.toEqualTypeOf<string>();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("types the remaining slots", () => {
|
|
36
|
+
type Left = Parameters<NonNullable<UToggleSlots<CountedOption>["left"]>>[0];
|
|
37
|
+
type Right = Parameters<NonNullable<UToggleSlots<CountedOption>["right"]>>[0];
|
|
38
|
+
|
|
39
|
+
expectTypeOf<Left["option"]>().toEqualTypeOf<CountedOption>();
|
|
40
|
+
expectTypeOf<Right["option"]>().toEqualTypeOf<CountedOption>();
|
|
41
|
+
expectTypeOf<Left["iconName"]>().toEqualTypeOf<string | undefined>();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("resolves Props with no type argument", () => {
|
|
45
|
+
expectTypeOf<Props>().toEqualTypeOf<Props<UToggleOption>>();
|
|
46
|
+
expectTypeOf<Props<CountedOption>["options"]>().toEqualTypeOf<CountedOption[] | undefined>();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
<!-- `UToggle` iterates `options` verbatim, so the caller's shape is guaranteed, not optional. -->
|
|
5
|
+
<UToggle name="fixture" :options="options">
|
|
6
|
+
<template #left="{ option, index, iconName }">
|
|
7
|
+
{{ expectString(option.color) }}
|
|
8
|
+
{{ expectNumber(index) }}
|
|
9
|
+
{{ expectOptionalString(iconName) }}
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<template #option="{ option, label, index }">
|
|
13
|
+
{{ expectString(option.color) }}
|
|
14
|
+
{{ expectString(label) }}
|
|
15
|
+
{{ expectNumber(index) }}
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<template #right="{ option }">
|
|
19
|
+
<!-- @vue-expect-error `nonExistent` is not a key of the inferred option. -->
|
|
20
|
+
{{ option.nonExistent }}
|
|
21
|
+
</template>
|
|
22
|
+
</UToggle>
|
|
23
|
+
|
|
24
|
+
<!-- Options from a non-literal source still expose the declared option shape. -->
|
|
25
|
+
<UToggle name="typed" :options="typedOptions">
|
|
26
|
+
<template #option="{ option }">{{ expectNumber(option.count) }}</template>
|
|
27
|
+
</UToggle>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
import { ref } from "vue";
|
|
32
|
+
|
|
33
|
+
import UToggle from "../UToggle.vue";
|
|
34
|
+
|
|
35
|
+
interface CountedOption {
|
|
36
|
+
value: string;
|
|
37
|
+
label: string;
|
|
38
|
+
count: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const options = [{ value: "1", label: "Success", color: "success" }];
|
|
42
|
+
const typedOptions = ref<CountedOption[]>([]);
|
|
43
|
+
|
|
44
|
+
function expectString(value: string) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function expectNumber(value: number) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function expectOptionalString(value: string | undefined) {
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
@@ -17,7 +17,17 @@ export interface BaseToggleOption {
|
|
|
17
17
|
/* Any object shape is a valid option, only reserved option keys are type checked. */
|
|
18
18
|
export type UToggleOption = BaseToggleOption & (object | UnknownObject);
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Slots receive each option verbatim — `UToggle` iterates `props.options` without
|
|
22
|
+
* filtering, flattening or synthesising entries, so the caller's shape is guaranteed.
|
|
23
|
+
*/
|
|
24
|
+
export interface UToggleSlots<TItem extends UToggleOption = UToggleOption> {
|
|
25
|
+
left?: (props: { option: TItem; index: number; iconName?: string }) => unknown;
|
|
26
|
+
option?: (props: { option: TItem; index: number; label: string; iconName?: string }) => unknown;
|
|
27
|
+
right?: (props: { option: TItem; index: number; iconName?: string }) => unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Props<TItem extends UToggleOption = UToggleOption> {
|
|
21
31
|
/**
|
|
22
32
|
* Selected value.
|
|
23
33
|
*/
|
|
@@ -26,7 +36,7 @@ export interface Props {
|
|
|
26
36
|
/**
|
|
27
37
|
* Toggle item options.
|
|
28
38
|
*/
|
|
29
|
-
options?:
|
|
39
|
+
options?: TItem[];
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
42
|
* Toggle size.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="TItem extends DataListItem">
|
|
2
2
|
import { useTemplateRef } from "vue";
|
|
3
3
|
import draggable from "vuedraggable/src/vuedraggable";
|
|
4
4
|
|
|
@@ -14,15 +14,17 @@ import defaultConfig from "./config";
|
|
|
14
14
|
import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
|
|
15
15
|
|
|
16
16
|
import type { UnknownObject } from "../types";
|
|
17
|
-
import type { Props, DataListItem, Config } from "./types";
|
|
17
|
+
import type { Props, DataListItem, SlotItem, UDataListSlots, Config } from "./types";
|
|
18
18
|
|
|
19
19
|
defineOptions({ inheritAttrs: false });
|
|
20
20
|
|
|
21
|
-
const props = withDefaults(defineProps<Props
|
|
22
|
-
...getDefaults<Props
|
|
21
|
+
const props = withDefaults(defineProps<Props<TItem>>(), {
|
|
22
|
+
...getDefaults<Props<TItem>, Config>(defaultConfig, COMPONENT_NAME),
|
|
23
23
|
list: () => [],
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
defineSlots<UDataListSlots<TItem>>();
|
|
27
|
+
|
|
26
28
|
const emit = defineEmits([
|
|
27
29
|
/**
|
|
28
30
|
* Triggers when item is sorted (after drag).
|
|
@@ -205,33 +207,41 @@ const {
|
|
|
205
207
|
:data-test="getDataTest('table')"
|
|
206
208
|
@drag-sort="onDragEnd"
|
|
207
209
|
>
|
|
208
|
-
<!--
|
|
209
|
-
|
|
210
|
+
<!-- The nested list is built from `element.children` (`DataListItem[]`, not `TItem[]`),
|
|
211
|
+
so it yields `SlotItem<DataListItem>`. `SlotItem` is all-optional, so forwarding it
|
|
212
|
+
as `SlotItem<TItem>` promises nothing a deep child may lack. -->
|
|
213
|
+
<template #drag="slotProps">
|
|
210
214
|
<!--
|
|
211
215
|
@slot Use it to add something instead of the drag icon.
|
|
212
216
|
@binding {object} item
|
|
213
217
|
@binding {string} icon-name
|
|
214
218
|
-->
|
|
215
|
-
<slot
|
|
219
|
+
<slot
|
|
220
|
+
name="drag"
|
|
221
|
+
:item="slotProps.item as SlotItem<TItem>"
|
|
222
|
+
:icon-name="slotProps.iconName"
|
|
223
|
+
/>
|
|
216
224
|
</template>
|
|
217
225
|
|
|
218
|
-
|
|
219
|
-
<template #label="slotProps: { item: DataListItem; crossed: boolean }">
|
|
226
|
+
<template #label="slotProps">
|
|
220
227
|
<!--
|
|
221
228
|
@slot Use it to modify label.
|
|
222
229
|
@binding {object} item
|
|
223
230
|
@binding {boolean} crossed
|
|
224
231
|
-->
|
|
225
|
-
<slot
|
|
232
|
+
<slot
|
|
233
|
+
name="label"
|
|
234
|
+
:item="slotProps.item as SlotItem<TItem>"
|
|
235
|
+
:crossed="slotProps.crossed"
|
|
236
|
+
/>
|
|
226
237
|
</template>
|
|
227
238
|
|
|
228
|
-
|
|
229
|
-
<template #actions="slotProps: { item: DataListItem }">
|
|
239
|
+
<template #actions="slotProps">
|
|
230
240
|
<!--
|
|
231
241
|
@slot Use it to add custom actions.
|
|
232
242
|
@binding {object} item
|
|
233
243
|
-->
|
|
234
|
-
<slot name="actions" :item="slotProps.item" />
|
|
244
|
+
<slot name="actions" :item="slotProps.item as SlotItem<TItem>" />
|
|
235
245
|
</template>
|
|
236
246
|
</UDataList>
|
|
237
247
|
</div>
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getDocsDescription,
|
|
7
7
|
} from "../../utils/storybook";
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import UDataListComponent from "../../ui.data-list/UDataList.vue";
|
|
10
10
|
import UIcon from "../../ui.image-icon/UIcon.vue";
|
|
11
11
|
import UButton from "../../ui.button/UButton.vue";
|
|
12
12
|
import URow from "../../ui.container-row/URow.vue";
|
|
@@ -19,6 +19,9 @@ import tooltip from "../../v.tooltip/vTooltip";
|
|
|
19
19
|
import type { Meta, StoryFn } from "@storybook/vue3-vite";
|
|
20
20
|
import type { Props, DataListItem } from "../types";
|
|
21
21
|
|
|
22
|
+
/* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
|
|
23
|
+
const UDataList = UDataListComponent as typeof UDataListComponent & { __name: string };
|
|
24
|
+
|
|
22
25
|
interface UDataListArgs extends Props {
|
|
23
26
|
slotTemplate?: string;
|
|
24
27
|
enum: "size";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, it, expectTypeOf } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { Props, DataListItem, SlotItem, UDataListSlots } from "../types";
|
|
4
|
+
|
|
5
|
+
interface UserItem {
|
|
6
|
+
value: number;
|
|
7
|
+
label: string;
|
|
8
|
+
owner: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type LabelSlot = NonNullable<UDataListSlots<UserItem>["label"]>;
|
|
12
|
+
type LabelProps = Parameters<LabelSlot>[0];
|
|
13
|
+
|
|
14
|
+
describe("UDataList types", () => {
|
|
15
|
+
it("infers the caller's item shape in the label slot", () => {
|
|
16
|
+
expectTypeOf<LabelProps["item"]["label"]>().toEqualTypeOf<string | undefined>();
|
|
17
|
+
expectTypeOf<LabelProps["item"]["owner"]>().toEqualTypeOf<string | undefined>();
|
|
18
|
+
expectTypeOf<LabelProps["crossed"]>().toEqualTypeOf<boolean>();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("does not expose undeclared item properties to a slot", () => {
|
|
22
|
+
/* An index signature would make any key legal, so `item.nonExistent` must not resolve. */
|
|
23
|
+
type HasIndexSignature = string extends keyof LabelProps["item"] ? true : false;
|
|
24
|
+
|
|
25
|
+
expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
|
|
26
|
+
expectTypeOf<keyof LabelProps["item"]>().not.toEqualTypeOf<string>();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("keeps the reserved item keys available", () => {
|
|
30
|
+
expectTypeOf<SlotItem<UserItem>["crossed"]>().toEqualTypeOf<boolean | undefined>();
|
|
31
|
+
expectTypeOf<SlotItem<UserItem>["actions"]>().toEqualTypeOf<boolean | undefined>();
|
|
32
|
+
expectTypeOf<SlotItem<UserItem>["children"]>().toEqualTypeOf<DataListItem[] | undefined>();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("types the drag and actions slots", () => {
|
|
36
|
+
type DragProps = Parameters<NonNullable<UDataListSlots<UserItem>["drag"]>>[0];
|
|
37
|
+
type ActionsProps = Parameters<NonNullable<UDataListSlots<UserItem>["actions"]>>[0];
|
|
38
|
+
|
|
39
|
+
expectTypeOf<DragProps["iconName"]>().toEqualTypeOf<string>();
|
|
40
|
+
expectTypeOf<DragProps["item"]["label"]>().toEqualTypeOf<string | undefined>();
|
|
41
|
+
expectTypeOf<ActionsProps["item"]["owner"]>().toEqualTypeOf<string | undefined>();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("types the empty slot", () => {
|
|
45
|
+
type EmptyProps = Parameters<NonNullable<UDataListSlots<UserItem>["empty"]>>[0];
|
|
46
|
+
|
|
47
|
+
expectTypeOf<EmptyProps["emptyTitle"]>().toEqualTypeOf<string>();
|
|
48
|
+
expectTypeOf<EmptyProps["emptyDescription"]>().toEqualTypeOf<string>();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not promise the caller's members at depth >= 1", () => {
|
|
52
|
+
/* The component renders itself with `:list="element.children"`, and `children` is
|
|
53
|
+
* `DataListItem[]`, not `TItem[]` — so a nested child need not carry the parent's members.
|
|
54
|
+
* A runtime probe over 3 levels confirmed the grandchild arrives without `owner`. */
|
|
55
|
+
expectTypeOf<SlotItem<UserItem>["owner"]>().toEqualTypeOf<string | undefined>();
|
|
56
|
+
|
|
57
|
+
/* The grandchild shape the probe actually observed must be assignable to every item slot. */
|
|
58
|
+
type Grandchild = { value: number; label: string; extraOnly: boolean };
|
|
59
|
+
|
|
60
|
+
type DragProps = Parameters<NonNullable<UDataListSlots<UserItem>["drag"]>>[0];
|
|
61
|
+
type ActionsProps = Parameters<NonNullable<UDataListSlots<UserItem>["actions"]>>[0];
|
|
62
|
+
|
|
63
|
+
expectTypeOf<Grandchild>().toExtend<LabelProps["item"]>();
|
|
64
|
+
expectTypeOf<Grandchild>().toExtend<DragProps["item"]>();
|
|
65
|
+
expectTypeOf<Grandchild>().toExtend<ActionsProps["item"]>();
|
|
66
|
+
|
|
67
|
+
/* `nestedKey` can open a nested list from a source `children` does not constrain, so even
|
|
68
|
+
* an item carrying none of the caller's members must be admitted. */
|
|
69
|
+
expectTypeOf<{ value: number }>().toExtend<LabelProps["item"]>();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("resolves Props with no type argument", () => {
|
|
73
|
+
expectTypeOf<Props>().toEqualTypeOf<Props<DataListItem>>();
|
|
74
|
+
expectTypeOf<Props<UserItem>["list"]>().toEqualTypeOf<UserItem[] | undefined>();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
|
|
2
|
+
Proves slot props infer from the `list` prop at a real call site. -->
|
|
3
|
+
<template>
|
|
4
|
+
<UDataList :list="list">
|
|
5
|
+
<!-- A nested child may lack the parent's members, so they are optional;
|
|
6
|
+
the reserved keys (`crossed`, `actions`, `children`) are optional already. -->
|
|
7
|
+
<template #label="{ item, crossed }">
|
|
8
|
+
{{ expectOptionalString(item.label) }}
|
|
9
|
+
{{ expectOptionalString(item.owner) }}
|
|
10
|
+
{{ expectOptionalNumber(item.value) }}
|
|
11
|
+
{{ expectBoolean(crossed) }}
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<template #drag="{ item, iconName }">
|
|
15
|
+
{{ expectOptionalString(item.label) }}
|
|
16
|
+
{{ expectString(iconName) }}
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<template #actions="{ item }">
|
|
20
|
+
{{ expectOptionalString(item.owner) }}
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<template #empty="{ emptyTitle, emptyDescription }">
|
|
24
|
+
{{ expectString(emptyTitle) }}
|
|
25
|
+
{{ expectString(emptyDescription) }}
|
|
26
|
+
</template>
|
|
27
|
+
</UDataList>
|
|
28
|
+
|
|
29
|
+
<!-- A list from a non-literal source still exposes the declared item shape. -->
|
|
30
|
+
<UDataList :list="typedList">
|
|
31
|
+
<template #label="{ item }">{{ expectOptionalString(item.label) }}</template>
|
|
32
|
+
</UDataList>
|
|
33
|
+
|
|
34
|
+
<UDataList :list="list">
|
|
35
|
+
<template #label="{ item }">
|
|
36
|
+
<!-- @vue-expect-error `nonExistent` is not a key of the inferred item. -->
|
|
37
|
+
{{ item.nonExistent }}
|
|
38
|
+
</template>
|
|
39
|
+
</UDataList>
|
|
40
|
+
|
|
41
|
+
<!-- Regression guard: an item slot must never promise the caller's members as present.
|
|
42
|
+
The component renders ITSELF with `:list="element.children"`, and `children` is
|
|
43
|
+
`DataListItem[]`, not `TItem[]` — a runtime probe over 3 levels observed the
|
|
44
|
+
grandchild arriving with `{ value, label, extraOnly }` and NO `owner`. -->
|
|
45
|
+
<UDataList :list="deepList">
|
|
46
|
+
<template #label="{ item }">
|
|
47
|
+
<!-- @vue-expect-error a grandchild at depth 2 need not carry `owner`. -->
|
|
48
|
+
{{ expectString(item.owner) }}
|
|
49
|
+
</template>
|
|
50
|
+
<template #drag="{ item }">
|
|
51
|
+
<!-- @vue-expect-error a nested child need not carry `label`. -->
|
|
52
|
+
{{ expectString(item.label) }}
|
|
53
|
+
</template>
|
|
54
|
+
<template #actions="{ item }">
|
|
55
|
+
<!-- @vue-expect-error a nested child need not carry `value`. -->
|
|
56
|
+
{{ expectNumber(item.value) }}
|
|
57
|
+
</template>
|
|
58
|
+
</UDataList>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<script setup lang="ts">
|
|
62
|
+
import { ref } from "vue";
|
|
63
|
+
|
|
64
|
+
import UDataList from "../UDataList.vue";
|
|
65
|
+
|
|
66
|
+
interface UserItem {
|
|
67
|
+
value: number;
|
|
68
|
+
label: string;
|
|
69
|
+
owner: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const list = [{ value: 1, label: "a", owner: "alice" }];
|
|
73
|
+
const typedList = ref<UserItem[]>([]);
|
|
74
|
+
|
|
75
|
+
/* A parent whose grandchild lacks the parent's members — exactly what the probe observed. */
|
|
76
|
+
const deepList = [
|
|
77
|
+
{
|
|
78
|
+
value: 1,
|
|
79
|
+
label: "Parent",
|
|
80
|
+
owner: "alice",
|
|
81
|
+
children: [
|
|
82
|
+
{
|
|
83
|
+
value: 11,
|
|
84
|
+
label: "Child",
|
|
85
|
+
children: [{ value: 111, label: "Grandchild", extraOnly: true }],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
function expectString(value: string) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function expectNumber(value: number) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function expectBoolean(value: boolean) {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function expectOptionalString(value: string | undefined) {
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function expectOptionalNumber(value: number | undefined) {
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
</script>
|
package/ui.data-list/types.ts
CHANGED
|
@@ -20,11 +20,35 @@ export interface BaseDataListItem {
|
|
|
20
20
|
/* Any object shape is a valid item, only reserved item keys are type checked. */
|
|
21
21
|
export type DataListItem = BaseDataListItem & (object | UnknownObject);
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/* Drops `DataListItem`'s index signature so slot items only expose statically known keys. */
|
|
24
|
+
type KnownKeys<T> = {
|
|
25
|
+
[K in keyof T as string extends K ? never : number extends K ? never : K]: T[K];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Item shape exposed to slots: the caller's item plus the reserved item keys.
|
|
30
|
+
*
|
|
31
|
+
* `TItem`'s own keys are optional. The component renders itself for nested children
|
|
32
|
+
* (`:list="element.children"`), and `BaseDataListItem.children` is `DataListItem[]`, not `TItem[]`
|
|
33
|
+
* — so a child need not carry the parent's members. `nestedKey` can open a nested list from a
|
|
34
|
+
* different source entirely. `BaseDataListItem`'s keys are optional already, so nothing is
|
|
35
|
+
* promised that can be absent.
|
|
36
|
+
*/
|
|
37
|
+
export type SlotItem<TItem extends DataListItem = DataListItem> = Partial<KnownKeys<TItem>> &
|
|
38
|
+
BaseDataListItem;
|
|
39
|
+
|
|
40
|
+
export interface UDataListSlots<TItem extends DataListItem = DataListItem> {
|
|
41
|
+
empty?: (props: { emptyTitle: string; emptyDescription: string }) => unknown;
|
|
42
|
+
drag?: (props: { item: SlotItem<TItem>; iconName: string }) => unknown;
|
|
43
|
+
label?: (props: { item: SlotItem<TItem>; crossed: boolean }) => unknown;
|
|
44
|
+
actions?: (props: { item: SlotItem<TItem> }) => unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Props<TItem extends DataListItem = DataListItem> {
|
|
24
48
|
/**
|
|
25
49
|
* Data item options.
|
|
26
50
|
*/
|
|
27
|
-
list?:
|
|
51
|
+
list?: TItem[];
|
|
28
52
|
|
|
29
53
|
/**
|
|
30
54
|
* Group name.
|