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.
Files changed (62) hide show
  1. package/index.d.ts +31 -5
  2. package/index.ts +31 -5
  3. package/package.json +1 -1
  4. package/ui.button-toggle/UToggle.vue +6 -4
  5. package/ui.button-toggle/storybook/stories.ts +4 -1
  6. package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
  7. package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
  8. package/ui.button-toggle/types.ts +12 -2
  9. package/ui.data-list/UDataList.vue +23 -13
  10. package/ui.data-list/storybook/stories.ts +4 -1
  11. package/ui.data-list/tests/UDataList.test-d.ts +76 -0
  12. package/ui.data-list/tests/UDataListTypes.vue +110 -0
  13. package/ui.data-list/types.ts +26 -2
  14. package/ui.data-table/UTable.vue +21 -8
  15. package/ui.data-table/config.ts +1 -1
  16. package/ui.data-table/storybook/stories.ts +4 -1
  17. package/ui.data-table/tests/UTable.test-d.ts +179 -0
  18. package/ui.data-table/tests/UTable.test.ts +1 -1
  19. package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
  20. package/ui.data-table/tests/UTableTypes.vue +193 -0
  21. package/ui.data-table/types.ts +74 -4
  22. package/ui.data-table/utilTable.ts +1 -1
  23. package/ui.dropdown/UDropdown.vue +14 -6
  24. package/ui.dropdown/storybook/stories.ts +4 -1
  25. package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
  26. package/ui.dropdown/tests/UDropdown.test.ts +8 -1
  27. package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
  28. package/ui.dropdown/types.ts +27 -3
  29. package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
  30. package/ui.dropdown-badge/storybook/stories.ts +6 -1
  31. package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
  32. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
  33. package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
  34. package/ui.dropdown-badge/types.ts +17 -3
  35. package/ui.dropdown-button/UDropdownButton.vue +9 -4
  36. package/ui.dropdown-button/storybook/stories.ts +6 -1
  37. package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
  38. package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
  39. package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
  40. package/ui.dropdown-button/types.ts +17 -3
  41. package/ui.dropdown-link/UDropdownLink.vue +9 -4
  42. package/ui.dropdown-link/storybook/stories.ts +4 -1
  43. package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
  44. package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
  45. package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
  46. package/ui.dropdown-link/types.ts +17 -3
  47. package/ui.form-listbox/UListbox.vue +20 -7
  48. package/ui.form-listbox/storybook/stories.ts +4 -1
  49. package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
  50. package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
  51. package/ui.form-listbox/types.ts +31 -2
  52. package/ui.form-select/USelect.vue +41 -13
  53. package/ui.form-select/storybook/stories.ts +4 -1
  54. package/ui.form-select/tests/USelect.test-d.ts +76 -0
  55. package/ui.form-select/tests/USelect.test.ts +8 -1
  56. package/ui.form-select/tests/USelectTypes.vue +113 -0
  57. package/ui.form-select/types.ts +51 -3
  58. package/ui.navigation-tabs/UTabs.vue +6 -4
  59. package/ui.navigation-tabs/storybook/stories.ts +4 -1
  60. package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
  61. package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
  62. 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 { Option, BaseOption, ListboxOption, SelectedValue } from "./ui.form-listbox/types";
204
- export type { SelectOption } from "./ui.form-select/types";
205
- export type { UTabsOption, BaseTabOption } from "./ui.navigation-tabs/types";
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 { DataListItem, BaseDataListItem } from "./ui.data-list/types";
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 { Option, BaseOption, ListboxOption, SelectedValue } from "./ui.form-listbox/types";
210
- export type { SelectOption } from "./ui.form-select/types";
211
- export type { UTabsOption, BaseTabOption } from "./ui.navigation-tabs/types";
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 { DataListItem, BaseDataListItem } from "./ui.data-list/types";
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.18",
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, Config>(defaultConfig, COMPONENT_NAME),
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 UToggle from "../../ui.button-toggle/UToggle.vue";
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
- export interface Props {
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?: UToggleOption[];
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, Config>(defaultConfig, COMPONENT_NAME),
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
- <!-- @vue-ignore -->
209
- <template #drag="slotProps: { item: DataListItem; iconName: string }">
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 name="drag" :item="slotProps.item" :icon-name="slotProps.iconName" />
219
+ <slot
220
+ name="drag"
221
+ :item="slotProps.item as SlotItem<TItem>"
222
+ :icon-name="slotProps.iconName"
223
+ />
216
224
  </template>
217
225
 
218
- <!-- @vue-ignore -->
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 name="label" :item="slotProps.item" :crossed="slotProps.crossed" />
232
+ <slot
233
+ name="label"
234
+ :item="slotProps.item as SlotItem<TItem>"
235
+ :crossed="slotProps.crossed"
236
+ />
226
237
  </template>
227
238
 
228
- <!-- @vue-ignore -->
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 UDataList from "../../ui.data-list/UDataList.vue";
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>
@@ -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
- export interface Props {
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?: DataListItem[];
51
+ list?: TItem[];
28
52
 
29
53
  /**
30
54
  * Group name.