vueless 1.4.12-beta.9 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -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.data-list/UDataList.vue +48 -17
- 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/UDataList.test.ts +50 -1
- package/ui.data-list/tests/UDataListTypes.vue +110 -0
- package/ui.data-list/types.ts +37 -5
- package/ui.data-table/UTable.vue +68 -10
- 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 +91 -8
- 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/config.ts +1 -1
- 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-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 +25 -3
- 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
|
@@ -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
|
|
|
@@ -13,15 +13,18 @@ import { COMPONENT_NAME } from "./constants";
|
|
|
13
13
|
import defaultConfig from "./config";
|
|
14
14
|
import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
|
|
15
15
|
|
|
16
|
-
import type {
|
|
16
|
+
import type { UnknownObject } from "../types";
|
|
17
|
+
import type { Props, DataListItem, SlotItem, UDataListSlots, Config } from "./types";
|
|
17
18
|
|
|
18
19
|
defineOptions({ inheritAttrs: false });
|
|
19
20
|
|
|
20
|
-
const props = withDefaults(defineProps<Props
|
|
21
|
-
...getDefaults<Props
|
|
21
|
+
const props = withDefaults(defineProps<Props<TItem>>(), {
|
|
22
|
+
...getDefaults<Props<TItem>, Config>(defaultConfig, COMPONENT_NAME),
|
|
22
23
|
list: () => [],
|
|
23
24
|
});
|
|
24
25
|
|
|
26
|
+
defineSlots<UDataListSlots<TItem>>();
|
|
27
|
+
|
|
25
28
|
const emit = defineEmits([
|
|
26
29
|
/**
|
|
27
30
|
* Triggers when item is sorted (after drag).
|
|
@@ -42,6 +45,12 @@ function isCrossed(element: DataListItem) {
|
|
|
42
45
|
return Boolean(element.crossed);
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
function hasNestedList(element: DataListItem) {
|
|
49
|
+
return Boolean(
|
|
50
|
+
element.children?.length || (props.nestedKey && (element as UnknownObject)[props.nestedKey]),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
45
54
|
function onDragEnd() {
|
|
46
55
|
const sortData = prepareSortData(props.list);
|
|
47
56
|
|
|
@@ -57,7 +66,7 @@ function prepareSortData(list: DataListItem[] = [], parentValue: string | number
|
|
|
57
66
|
if (hasItemChildren) {
|
|
58
67
|
const childrenItem = prepareSortData(
|
|
59
68
|
item.children,
|
|
60
|
-
item[props.valueKey] as string | number | null,
|
|
69
|
+
(item as UnknownObject)[props.valueKey] as string | number | null,
|
|
61
70
|
);
|
|
62
71
|
|
|
63
72
|
childrenItem.forEach((item) => sortData.push(item));
|
|
@@ -181,36 +190,58 @@ const {
|
|
|
181
190
|
</div>
|
|
182
191
|
|
|
183
192
|
<UDataList
|
|
184
|
-
v-if="element
|
|
193
|
+
v-if="hasNestedList(element)"
|
|
185
194
|
hide-empty-state-for-nesting
|
|
186
195
|
:list="element.children"
|
|
187
196
|
:group="group"
|
|
197
|
+
:size="size"
|
|
198
|
+
:label-key="labelKey"
|
|
199
|
+
:value-key="valueKey"
|
|
200
|
+
:nested-key="nestedKey"
|
|
201
|
+
:animation-duration="animationDuration"
|
|
202
|
+
:force-fallback="forceFallback"
|
|
203
|
+
:fallback-on-body="fallbackOnBody"
|
|
204
|
+
:fallback-class="fallbackClass"
|
|
188
205
|
v-bind="nestedAttrs"
|
|
206
|
+
:class="!element.children?.length && 'min-h-4'"
|
|
189
207
|
:data-test="getDataTest('table')"
|
|
190
208
|
@drag-sort="onDragEnd"
|
|
191
209
|
>
|
|
192
|
-
<!--
|
|
193
|
-
|
|
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">
|
|
214
|
+
<!--
|
|
215
|
+
@slot Use it to add something instead of the drag icon.
|
|
216
|
+
@binding {object} item
|
|
217
|
+
@binding {string} icon-name
|
|
218
|
+
-->
|
|
219
|
+
<slot
|
|
220
|
+
name="drag"
|
|
221
|
+
:item="slotProps.item as SlotItem<TItem>"
|
|
222
|
+
:icon-name="slotProps.iconName"
|
|
223
|
+
/>
|
|
224
|
+
</template>
|
|
225
|
+
|
|
226
|
+
<template #label="slotProps">
|
|
194
227
|
<!--
|
|
195
228
|
@slot Use it to modify label.
|
|
196
229
|
@binding {object} item
|
|
197
230
|
@binding {boolean} crossed
|
|
198
231
|
-->
|
|
199
|
-
<slot
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
</slot>
|
|
232
|
+
<slot
|
|
233
|
+
name="label"
|
|
234
|
+
:item="slotProps.item as SlotItem<TItem>"
|
|
235
|
+
:crossed="slotProps.crossed"
|
|
236
|
+
/>
|
|
205
237
|
</template>
|
|
206
238
|
|
|
207
|
-
|
|
208
|
-
<template #actions="slotProps: { item: DataListItem }">
|
|
239
|
+
<template #actions="slotProps">
|
|
209
240
|
<!--
|
|
210
241
|
@slot Use it to add custom actions.
|
|
211
242
|
@binding {object} item
|
|
212
243
|
-->
|
|
213
|
-
<slot name="actions" :item="slotProps.item" />
|
|
244
|
+
<slot name="actions" :item="slotProps.item as SlotItem<TItem>" />
|
|
214
245
|
</template>
|
|
215
246
|
</UDataList>
|
|
216
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
|
+
});
|
|
@@ -9,7 +9,7 @@ import draggable from "vuedraggable";
|
|
|
9
9
|
import type { Props, DataListItem } from "../types";
|
|
10
10
|
|
|
11
11
|
describe("UDataList.vue", () => {
|
|
12
|
-
const defaultList
|
|
12
|
+
const defaultList = [
|
|
13
13
|
{ value: 1, label: "Item 1" },
|
|
14
14
|
{ value: 2, label: "Item 2", crossed: true },
|
|
15
15
|
{ value: 3, label: "Item 3", actions: false },
|
|
@@ -253,6 +253,55 @@ describe("UDataList.vue", () => {
|
|
|
253
253
|
expect(nestedComponent.props("list")).toEqual(nestedList[0].children);
|
|
254
254
|
});
|
|
255
255
|
|
|
256
|
+
it("Nesting – renders empty nested drop-zone for childless items when nestedKey is truthy", () => {
|
|
257
|
+
const component = mount(UDataList, {
|
|
258
|
+
props: {
|
|
259
|
+
list: [{ value: 1, label: "Leaf", canNest: true }],
|
|
260
|
+
nestedKey: "canNest",
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const nestedComponent = component.findComponent("[vl-key='nested']");
|
|
265
|
+
|
|
266
|
+
expect(nestedComponent.exists()).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("Nesting – does not render drop-zone for childless items when nestedKey value is falsy", () => {
|
|
270
|
+
const component = mount(UDataList, {
|
|
271
|
+
props: {
|
|
272
|
+
list: [{ value: 1, label: "Leaf", canNest: false }],
|
|
273
|
+
nestedKey: "canNest",
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const nestedComponent = component.findComponent("[vl-key='nested']");
|
|
278
|
+
|
|
279
|
+
expect(nestedComponent.exists()).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("Nesting – emits dragged item with parentValue of the leaf after drop into empty drop-zone", async () => {
|
|
283
|
+
const leaf = { value: 1, label: "Leaf", canNest: true, children: [] as DataListItem[] };
|
|
284
|
+
const list: DataListItem[] = [leaf, { value: 2, label: "Dragged", canNest: true }];
|
|
285
|
+
|
|
286
|
+
const component = mount(UDataList, {
|
|
287
|
+
props: { list, nestedKey: "canNest" },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Simulate vuedraggable moving item 2 into the leaf's (previously empty) nested list.
|
|
291
|
+
list.splice(1, 1);
|
|
292
|
+
leaf.children!.push({ value: 2, label: "Dragged", canNest: true });
|
|
293
|
+
|
|
294
|
+
await component.findComponent(draggable).vm.$emit("end");
|
|
295
|
+
|
|
296
|
+
const sortData = component.emitted("dragSort")?.[0]?.[0] as {
|
|
297
|
+
value: number;
|
|
298
|
+
parentValue: unknown;
|
|
299
|
+
}[];
|
|
300
|
+
const dragged = sortData.find((item) => item.value === 2);
|
|
301
|
+
|
|
302
|
+
expect(dragged?.parentValue).toBe(leaf.value);
|
|
303
|
+
});
|
|
304
|
+
|
|
256
305
|
it("Data Test – applies correct data-test attribute", () => {
|
|
257
306
|
const dataTest = "test-data-list";
|
|
258
307
|
|
|
@@ -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
|
@@ -2,7 +2,7 @@ import defaultConfig from "./config";
|
|
|
2
2
|
|
|
3
3
|
import DraggableContext from "vuedraggable";
|
|
4
4
|
|
|
5
|
-
import type { ComponentConfig,
|
|
5
|
+
import type { ComponentConfig, UnknownObject } from "../types";
|
|
6
6
|
|
|
7
7
|
export type Config = typeof defaultConfig;
|
|
8
8
|
|
|
@@ -11,18 +11,44 @@ export interface DragMoveEvent extends DragEvent {
|
|
|
11
11
|
relatedContext: typeof DraggableContext | null;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface BaseDataListItem {
|
|
15
15
|
crossed?: boolean;
|
|
16
16
|
actions?: boolean;
|
|
17
17
|
children?: DataListItem[];
|
|
18
|
-
[key: string]: UnknownType | DataListItem[];
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
/* Any object shape is a valid item, only reserved item keys are type checked. */
|
|
21
|
+
export type DataListItem = BaseDataListItem & (object | UnknownObject);
|
|
22
|
+
|
|
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> {
|
|
22
48
|
/**
|
|
23
49
|
* Data item options.
|
|
24
50
|
*/
|
|
25
|
-
list?:
|
|
51
|
+
list?: TItem[];
|
|
26
52
|
|
|
27
53
|
/**
|
|
28
54
|
* Group name.
|
|
@@ -70,6 +96,12 @@ export interface Props {
|
|
|
70
96
|
*/
|
|
71
97
|
hideEmptyStateForNesting?: boolean;
|
|
72
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Item key whose truthy value renders an empty nested drop-zone,
|
|
101
|
+
* so a deeper level can be created by drag even without children.
|
|
102
|
+
*/
|
|
103
|
+
nestedKey?: string;
|
|
104
|
+
|
|
73
105
|
/**
|
|
74
106
|
* Component config object.
|
|
75
107
|
*/
|
package/ui.data-table/UTable.vue
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="TRow extends TableRow, TCol extends string = string">
|
|
2
2
|
import {
|
|
3
3
|
shallowRef,
|
|
4
4
|
ref,
|
|
@@ -47,13 +47,16 @@ import type {
|
|
|
47
47
|
FlatRow,
|
|
48
48
|
ColumnObject,
|
|
49
49
|
SearchMatch,
|
|
50
|
+
TableRow,
|
|
50
51
|
UTableRowProps,
|
|
52
|
+
UTableSlots,
|
|
53
|
+
SelectedSlotRow,
|
|
51
54
|
} from "./types";
|
|
52
55
|
|
|
53
56
|
defineOptions({ inheritAttrs: false });
|
|
54
57
|
|
|
55
|
-
const props = withDefaults(defineProps<Props
|
|
56
|
-
...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
|
|
58
|
+
const props = withDefaults(defineProps<Props<TRow, TCol>>(), {
|
|
59
|
+
...getDefaults<Props<TRow, TCol>, Config>(defaultConfig, COMPONENT_NAME),
|
|
57
60
|
columns: () => [],
|
|
58
61
|
rows: () => [],
|
|
59
62
|
dateDivider: () => [],
|
|
@@ -112,6 +115,8 @@ const emit = defineEmits([
|
|
|
112
115
|
"search",
|
|
113
116
|
]);
|
|
114
117
|
|
|
118
|
+
defineSlots<UTableSlots<TRow, TCol>>();
|
|
119
|
+
|
|
115
120
|
const slots = useSlots();
|
|
116
121
|
|
|
117
122
|
const selectAll = ref(false);
|
|
@@ -145,7 +150,7 @@ const sortedRows: ComputedRef<FlatRow[]> = computed(() => {
|
|
|
145
150
|
typeof column === "object" ? column.key : column,
|
|
146
151
|
);
|
|
147
152
|
|
|
148
|
-
const keyOrder = new Map(headerKeys.map((key, i) => [key, i]));
|
|
153
|
+
const keyOrder = new Map<string, number>(headerKeys.map((key, i) => [key, i]));
|
|
149
154
|
|
|
150
155
|
return flatTableRows.value.map((row) => {
|
|
151
156
|
const entries = Object.entries(row);
|
|
@@ -171,6 +176,11 @@ const isFooterSticky = computed(() => {
|
|
|
171
176
|
|
|
172
177
|
const normalizedColumns = computed(() => normalizeColumns(props.columns));
|
|
173
178
|
|
|
179
|
+
/* Slot keys are narrowed to `TCol` for callers; internal dispatch is driven by runtime strings. */
|
|
180
|
+
function headerSlotName(key: string) {
|
|
181
|
+
return `header-${key}` as `header-${TCol}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
174
184
|
const visibleColumns = computed(() => {
|
|
175
185
|
return normalizedColumns.value.filter((column) => column.isShown !== false);
|
|
176
186
|
});
|
|
@@ -208,11 +218,14 @@ const isCheckedMoreOneTableItems = computed(() => {
|
|
|
208
218
|
return Boolean(localSelectedRows.value.length);
|
|
209
219
|
});
|
|
210
220
|
|
|
221
|
+
/* Select-all includes nested children, which need not carry `TRow`'s members. */
|
|
222
|
+
const selectedRowsSlotValue = computed(() => localSelectedRows.value as SelectedSlotRow<TRow>[]);
|
|
223
|
+
|
|
211
224
|
const tableRowWidthStyle = computed(() => ({
|
|
212
225
|
width: `${tableWidth.value / PX_IN_REM}rem`,
|
|
213
226
|
}));
|
|
214
227
|
|
|
215
|
-
const flatTableRows = computed(() => getFlatRows(props.rows));
|
|
228
|
+
const flatTableRows = computed(() => getFlatRows(props.rows as Row[]));
|
|
216
229
|
|
|
217
230
|
const visibleFlatRows = computed(() => {
|
|
218
231
|
const expanded = expandedRowsSet.value;
|
|
@@ -532,7 +545,7 @@ onBeforeUnmount(() => {
|
|
|
532
545
|
|
|
533
546
|
function onChangeSelectedRows() {
|
|
534
547
|
if (!isEqual(props.selectedRows, localSelectedRows.value)) {
|
|
535
|
-
localSelectedRows.value = props.selectedRows;
|
|
548
|
+
localSelectedRows.value = props.selectedRows as Row[];
|
|
536
549
|
}
|
|
537
550
|
}
|
|
538
551
|
|
|
@@ -838,6 +851,50 @@ function onBodyDoubleClick(event: MouseEvent) {
|
|
|
838
851
|
onDoubleClickRow(rowData);
|
|
839
852
|
}
|
|
840
853
|
|
|
854
|
+
// Detect double-tap on touch devices and dispatch a native `dblclick`,
|
|
855
|
+
// which bubbles to the tbody and triggers onBodyDoubleClick.
|
|
856
|
+
const DOUBLE_TAP_DELAY = 300;
|
|
857
|
+
const DOUBLE_TAP_MAX_MOVE = 24;
|
|
858
|
+
|
|
859
|
+
let lastTapTime = 0;
|
|
860
|
+
let lastTapRow: Element | null = null;
|
|
861
|
+
let lastTapX = 0;
|
|
862
|
+
let lastTapY = 0;
|
|
863
|
+
|
|
864
|
+
function onBodyTouchEnd(event: TouchEvent) {
|
|
865
|
+
if (event.changedTouches.length !== 1) return;
|
|
866
|
+
|
|
867
|
+
const [touch] = event.changedTouches;
|
|
868
|
+
const target = touch.target as HTMLElement | null;
|
|
869
|
+
const row = target?.closest("tr[data-row-id]") ?? null;
|
|
870
|
+
|
|
871
|
+
if (!row) {
|
|
872
|
+
lastTapRow = null;
|
|
873
|
+
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const now = event.timeStamp;
|
|
878
|
+
const isSameRow = row === lastTapRow;
|
|
879
|
+
const isQuick = now - lastTapTime < DOUBLE_TAP_DELAY;
|
|
880
|
+
const isNear =
|
|
881
|
+
Math.abs(touch.clientX - lastTapX) < DOUBLE_TAP_MAX_MOVE &&
|
|
882
|
+
Math.abs(touch.clientY - lastTapY) < DOUBLE_TAP_MAX_MOVE;
|
|
883
|
+
|
|
884
|
+
if (isSameRow && isQuick && isNear) {
|
|
885
|
+
lastTapTime = 0;
|
|
886
|
+
lastTapRow = null;
|
|
887
|
+
row.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));
|
|
888
|
+
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
lastTapTime = now;
|
|
893
|
+
lastTapRow = row;
|
|
894
|
+
lastTapX = touch.clientX;
|
|
895
|
+
lastTapY = touch.clientY;
|
|
896
|
+
}
|
|
897
|
+
|
|
841
898
|
function onChangeSelectAll(selectAll: boolean) {
|
|
842
899
|
if (selectAll && canSelectAll.value) {
|
|
843
900
|
localSelectedRows.value = [...flatTableRows.value];
|
|
@@ -1256,7 +1313,7 @@ const BodyRows = () =>
|
|
|
1256
1313
|
@binding {object} column
|
|
1257
1314
|
@binding {number} index
|
|
1258
1315
|
-->
|
|
1259
|
-
<slot :name="
|
|
1316
|
+
<slot :name="headerSlotName(column.key)" :column="column" :index="index" />
|
|
1260
1317
|
</template>
|
|
1261
1318
|
|
|
1262
1319
|
<template v-else>
|
|
@@ -1294,7 +1351,7 @@ const BodyRows = () =>
|
|
|
1294
1351
|
@slot Use it to add action buttons within the actions header, which appear when rows are selected.
|
|
1295
1352
|
@binding {array} selected-rows
|
|
1296
1353
|
-->
|
|
1297
|
-
<slot name="header-actions" :selected-rows="
|
|
1354
|
+
<slot name="header-actions" :selected-rows="selectedRowsSlotValue" />
|
|
1298
1355
|
|
|
1299
1356
|
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1300
1357
|
</div>
|
|
@@ -1326,7 +1383,7 @@ const BodyRows = () =>
|
|
|
1326
1383
|
@slot Use it to add action buttons within the actions header, which appear when rows are selected.
|
|
1327
1384
|
@binding {array} selected-rows
|
|
1328
1385
|
-->
|
|
1329
|
-
<slot name="header-actions" :selected-rows="
|
|
1386
|
+
<slot name="header-actions" :selected-rows="selectedRowsSlotValue" />
|
|
1330
1387
|
|
|
1331
1388
|
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1332
1389
|
</div>
|
|
@@ -1392,7 +1449,7 @@ const BodyRows = () =>
|
|
|
1392
1449
|
index,
|
|
1393
1450
|
})
|
|
1394
1451
|
"
|
|
1395
|
-
:name="
|
|
1452
|
+
:name="headerSlotName(column.key)"
|
|
1396
1453
|
:column="column"
|
|
1397
1454
|
:index="index"
|
|
1398
1455
|
/>
|
|
@@ -1411,6 +1468,7 @@ const BodyRows = () =>
|
|
|
1411
1468
|
v-bind="bodyAttrs"
|
|
1412
1469
|
@click="onBodyClick"
|
|
1413
1470
|
@dblclick="onBodyDoubleClick"
|
|
1471
|
+
@touchend="onBodyTouchEnd"
|
|
1414
1472
|
>
|
|
1415
1473
|
<tr
|
|
1416
1474
|
v-if="hasBeforeFirstRowSlot"
|
package/ui.data-table/config.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { getRandomId } from "../../utils/helper";
|
|
|
12
12
|
|
|
13
13
|
import defaultConfig from "../config";
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import UTableComponent from "../UTable.vue";
|
|
16
16
|
import UButton from "../../ui.button/UButton.vue";
|
|
17
17
|
import ULink from "../../ui.button-link/ULink.vue";
|
|
18
18
|
import UNumber from "../../ui.text-number/UNumber.vue";
|
|
@@ -27,6 +27,9 @@ import tooltip from "../../v.tooltip/vTooltip";
|
|
|
27
27
|
import type { Row, Props, ColumnObject } from "../types";
|
|
28
28
|
import { StickySide } from "../types";
|
|
29
29
|
|
|
30
|
+
/* Generic SFCs are typed as a function, which drops the compiler-generated `__name`. */
|
|
31
|
+
const UTable = UTableComponent as typeof UTableComponent & { __name: string };
|
|
32
|
+
|
|
30
33
|
interface UTableArgs extends Props {
|
|
31
34
|
slotTemplate?: string;
|
|
32
35
|
enum: "size";
|