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
|
@@ -9,11 +9,6 @@ import type { Props } from "../types";
|
|
|
9
9
|
describe("UDrawer", () => {
|
|
10
10
|
const modelValue = true;
|
|
11
11
|
|
|
12
|
-
// Wait for an async component to load
|
|
13
|
-
function sleep(ms: number = 0) {
|
|
14
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
12
|
// Props tests
|
|
18
13
|
describe("Props", () => {
|
|
19
14
|
it("ModelValue – renders when true", () => {
|
|
@@ -150,10 +145,10 @@ describe("UDrawer", () => {
|
|
|
150
145
|
expect(innerWrapper.attributes("class")).toContain(expectedClass);
|
|
151
146
|
});
|
|
152
147
|
|
|
153
|
-
it("CloseOnOverlay – closes drawer when overlay is clicked", () => {
|
|
148
|
+
it("CloseOnOverlay – closes drawer when overlay is clicked", async () => {
|
|
154
149
|
const closeOnOverlay = [true, false];
|
|
155
150
|
|
|
156
|
-
|
|
151
|
+
for (const value of closeOnOverlay) {
|
|
157
152
|
const component = mount(UDrawer, {
|
|
158
153
|
props: {
|
|
159
154
|
modelValue,
|
|
@@ -167,18 +162,17 @@ describe("UDrawer", () => {
|
|
|
167
162
|
|
|
168
163
|
await innerWrapper.trigger("mousedown");
|
|
169
164
|
await innerWrapper.trigger("click");
|
|
170
|
-
await sleep(1000);
|
|
171
165
|
|
|
172
|
-
const
|
|
166
|
+
const closeRequests = component.emitted("update:modelValue");
|
|
173
167
|
|
|
174
|
-
expect(
|
|
175
|
-
}
|
|
168
|
+
expect(value ? closeRequests?.at(-1) : closeRequests).toEqual(value ? [false] : undefined);
|
|
169
|
+
}
|
|
176
170
|
});
|
|
177
171
|
|
|
178
|
-
it("CloseOnEsc – closes drawer when escape key is pressed", () => {
|
|
172
|
+
it("CloseOnEsc – closes drawer when escape key is pressed", async () => {
|
|
179
173
|
const closeOnEsc = [true, false];
|
|
180
174
|
|
|
181
|
-
|
|
175
|
+
for (const value of closeOnEsc) {
|
|
182
176
|
const component = mount(UDrawer, {
|
|
183
177
|
props: {
|
|
184
178
|
modelValue,
|
|
@@ -189,12 +183,11 @@ describe("UDrawer", () => {
|
|
|
189
183
|
const wrapper = component.find("[vl-key='wrapper']");
|
|
190
184
|
|
|
191
185
|
await wrapper.trigger("keydown", { key: "Escape" });
|
|
192
|
-
await sleep(1000);
|
|
193
186
|
|
|
194
|
-
const
|
|
187
|
+
const closeRequests = component.emitted("update:modelValue");
|
|
195
188
|
|
|
196
|
-
expect(
|
|
197
|
-
}
|
|
189
|
+
expect(value ? closeRequests?.at(-1) : closeRequests).toEqual(value ? [false] : undefined);
|
|
190
|
+
}
|
|
198
191
|
});
|
|
199
192
|
|
|
200
193
|
it("DataTest – applies the correct data-test attribute", () => {
|
|
@@ -34,11 +34,6 @@ const mountWithRouter = (component: unknown, options: UnknownObject) => {
|
|
|
34
34
|
describe("UModal", () => {
|
|
35
35
|
const modelValue = true;
|
|
36
36
|
|
|
37
|
-
// Wait for an async component to load
|
|
38
|
-
function sleep(ms: number = 0) {
|
|
39
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
37
|
// Props tests
|
|
43
38
|
describe("Props", () => {
|
|
44
39
|
// ModelValue prop
|
|
@@ -189,10 +184,10 @@ describe("UModal", () => {
|
|
|
189
184
|
});
|
|
190
185
|
|
|
191
186
|
// CloseOnOverlay prop
|
|
192
|
-
it("renders with closeOnOverlay prop", () => {
|
|
187
|
+
it("renders with closeOnOverlay prop", async () => {
|
|
193
188
|
const closeOnOverlay = [true, false];
|
|
194
189
|
|
|
195
|
-
|
|
190
|
+
for (const value of closeOnOverlay) {
|
|
196
191
|
const component = mount(UModal, {
|
|
197
192
|
props: {
|
|
198
193
|
modelValue,
|
|
@@ -206,19 +201,18 @@ describe("UModal", () => {
|
|
|
206
201
|
|
|
207
202
|
await innerWrapper.trigger("mousedown");
|
|
208
203
|
await innerWrapper.trigger("click");
|
|
209
|
-
await sleep(1000);
|
|
210
204
|
|
|
211
|
-
const
|
|
205
|
+
const closeRequests = component.emitted("update:modelValue");
|
|
212
206
|
|
|
213
|
-
expect(
|
|
214
|
-
}
|
|
207
|
+
expect(value ? closeRequests?.at(-1) : closeRequests).toEqual(value ? [false] : undefined);
|
|
208
|
+
}
|
|
215
209
|
});
|
|
216
210
|
|
|
217
211
|
// CloseOnEsc prop
|
|
218
|
-
it("renders with closeOnEsc prop", () => {
|
|
212
|
+
it("renders with closeOnEsc prop", async () => {
|
|
219
213
|
const closeOnEsc = [true, false];
|
|
220
214
|
|
|
221
|
-
|
|
215
|
+
for (const value of closeOnEsc) {
|
|
222
216
|
const component = mount(UModal, {
|
|
223
217
|
props: {
|
|
224
218
|
modelValue,
|
|
@@ -229,12 +223,11 @@ describe("UModal", () => {
|
|
|
229
223
|
const wrapper = component.find("[vl-key='wrapper']");
|
|
230
224
|
|
|
231
225
|
await wrapper.trigger("keydown", { key: "Escape" });
|
|
232
|
-
await sleep(1000);
|
|
233
226
|
|
|
234
|
-
const
|
|
227
|
+
const closeRequests = component.emitted("update:modelValue");
|
|
235
228
|
|
|
236
|
-
expect(
|
|
237
|
-
}
|
|
229
|
+
expect(value ? closeRequests?.at(-1) : closeRequests).toEqual(value ? [false] : undefined);
|
|
230
|
+
}
|
|
238
231
|
});
|
|
239
232
|
|
|
240
233
|
// Inner prop
|
|
@@ -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>
|
package/ui.data-list/config.ts
CHANGED
|
@@ -2,7 +2,7 @@ export default /*tw*/ {
|
|
|
2
2
|
wrapper: "",
|
|
3
3
|
draggable: "",
|
|
4
4
|
draggableDrag: "bg-lifted",
|
|
5
|
-
draggableGhost: "
|
|
5
|
+
draggableGhost: "data-list-ghost",
|
|
6
6
|
itemWrapper: "group/item-wrapper",
|
|
7
7
|
item: {
|
|
8
8
|
base: `
|
|
@@ -57,7 +57,7 @@ export default /*tw*/ {
|
|
|
57
57
|
animationDuration: 200,
|
|
58
58
|
forceFallback: false,
|
|
59
59
|
fallbackOnBody: false,
|
|
60
|
-
fallbackClass: "
|
|
60
|
+
fallbackClass: "shadow-sm",
|
|
61
61
|
/* icons */
|
|
62
62
|
dragIcon: "drag_indicator",
|
|
63
63
|
},
|
|
@@ -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";
|
|
@@ -122,7 +125,7 @@ export const ForceFallback = DefaultTemplate.bind({});
|
|
|
122
125
|
ForceFallback.args = {
|
|
123
126
|
forceFallback: true,
|
|
124
127
|
fallbackOnBody: true,
|
|
125
|
-
fallbackClass: "
|
|
128
|
+
fallbackClass: "opacity-100!",
|
|
126
129
|
};
|
|
127
130
|
ForceFallback.parameters = {
|
|
128
131
|
docs: {
|
|
@@ -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 },
|
|
@@ -227,7 +227,7 @@ describe("UDataList.vue", () => {
|
|
|
227
227
|
expect(draggableComponent.vm.$attrs["fallback-class"]).toBe(fallbackClass);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
it("Fallback Class – defaults to
|
|
230
|
+
it("Fallback Class – defaults to shadow-sm", () => {
|
|
231
231
|
const component = mount(UDataList, {
|
|
232
232
|
props: {
|
|
233
233
|
list: defaultList,
|
|
@@ -236,7 +236,7 @@ describe("UDataList.vue", () => {
|
|
|
236
236
|
|
|
237
237
|
const draggableComponent = component.findComponent(draggable);
|
|
238
238
|
|
|
239
|
-
expect(draggableComponent.vm.$attrs["fallback-class"]).toBe("
|
|
239
|
+
expect(draggableComponent.vm.$attrs["fallback-class"]).toBe("shadow-sm");
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
it("Nesting – renders nested items when children array is present", async () => {
|
|
@@ -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
|
*/
|