vueless 1.4.12-beta.4 → 1.4.12-beta.6
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/package.json +2 -2
- package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
- package/ui.container-modal/tests/UModal.test.ts +10 -17
- package/ui.data-table/UTable.vue +64 -9
- package/ui.data-table/config.ts +10 -1
- package/ui.data-table/storybook/stories.ts +30 -0
- package/ui.data-table/tests/UTable.test.ts +84 -6
- package/ui.data-table/types.ts +15 -1
- package/ui.form-input/UInput.vue +1 -0
- package/ui.form-input/tests/UInput.test.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vueless",
|
|
3
|
-
"version": "1.4.12-beta.
|
|
3
|
+
"version": "1.4.12-beta.6",
|
|
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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@vue/eslint-config-typescript": "^14.7.0",
|
|
60
60
|
"@vue/test-utils": "^2.4.8",
|
|
61
61
|
"@vue/tsconfig": "^0.9.1",
|
|
62
|
-
"@vueless/storybook": "^1.7.
|
|
62
|
+
"@vueless/storybook": "^1.7.9",
|
|
63
63
|
"eslint": "^10.2.1",
|
|
64
64
|
"eslint-plugin-storybook": "^10.3.5",
|
|
65
65
|
"eslint-plugin-tailwindcss": "^4.0.0-alpha.7",
|
|
@@ -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
|
package/ui.data-table/UTable.vue
CHANGED
|
@@ -31,6 +31,7 @@ import UEmpty from "../ui.container-empty/UEmpty.vue";
|
|
|
31
31
|
import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
|
|
32
32
|
import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
|
|
33
33
|
import UDivider from "../ui.container-divider/UDivider.vue";
|
|
34
|
+
import USkeleton from "../ui.skeleton/USkeleton.vue";
|
|
34
35
|
import UTableRow from "./UTableRow.vue";
|
|
35
36
|
|
|
36
37
|
import type { ComputedRef, VNode } from "vue";
|
|
@@ -58,6 +59,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
58
59
|
dateDivider: () => [],
|
|
59
60
|
selectedRows: () => [],
|
|
60
61
|
expandedRows: () => [],
|
|
62
|
+
skeletonWidths: () => defaultConfig.defaults.skeletonWidths,
|
|
61
63
|
});
|
|
62
64
|
|
|
63
65
|
const emit = defineEmits([
|
|
@@ -206,7 +208,9 @@ const isCheckedMoreOneTableItems = computed(() => {
|
|
|
206
208
|
return Boolean(localSelectedRows.value.length);
|
|
207
209
|
});
|
|
208
210
|
|
|
209
|
-
const tableRowWidthStyle = computed(() => ({
|
|
211
|
+
const tableRowWidthStyle = computed(() => ({
|
|
212
|
+
width: `${tableWidth.value / PX_IN_REM}rem`,
|
|
213
|
+
}));
|
|
210
214
|
|
|
211
215
|
const flatTableRows = computed(() => getFlatRows(props.rows));
|
|
212
216
|
|
|
@@ -216,6 +220,18 @@ const visibleFlatRows = computed(() => {
|
|
|
216
220
|
return flatTableRows.value.filter((row) => !row.parentRowId || expanded.has(row.parentRowId));
|
|
217
221
|
});
|
|
218
222
|
|
|
223
|
+
const showSkeletonLoading = computed(() => {
|
|
224
|
+
return props.skeletonLoading && !sortedRows.value.length;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const progressLoading = computed(() => {
|
|
228
|
+
return showSkeletonLoading.value ? false : props.loading;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const skeletonRowIndexes = computed(() => {
|
|
232
|
+
return Array.from({ length: props.skeletonRows }, (_, index) => index);
|
|
233
|
+
});
|
|
234
|
+
|
|
219
235
|
const virtualScroll = useVirtualScroll({
|
|
220
236
|
containerRef: tableWrapperRef,
|
|
221
237
|
totalCount: computed(() => (props.virtualScroll ? visibleFlatRows.value.length : 0)),
|
|
@@ -243,6 +259,10 @@ function isRowVisible(row: FlatRow): boolean {
|
|
|
243
259
|
return !row.parentRowId || expandedRowsSet.value.has(row.parentRowId);
|
|
244
260
|
}
|
|
245
261
|
|
|
262
|
+
function getSkeletonWidthClass(columnIndex: number, rowIndex: number): string {
|
|
263
|
+
return props.skeletonWidths[(columnIndex + rowIndex) % props.skeletonWidths.length];
|
|
264
|
+
}
|
|
265
|
+
|
|
246
266
|
const searchMatches = computed<SearchMatch[]>(() => {
|
|
247
267
|
const query = props.search?.toLowerCase();
|
|
248
268
|
|
|
@@ -663,7 +683,9 @@ function getDateDividerData(rowDate: string | Date | undefined) {
|
|
|
663
683
|
function setFooterCellWidth(zero?: null) {
|
|
664
684
|
const ZERO_WIDTH = 0;
|
|
665
685
|
|
|
666
|
-
if (!props.stickyFooter || !footerRowRef.value || !stickyFooterRowRef.value)
|
|
686
|
+
if (!props.stickyFooter || !footerRowRef.value || !stickyFooterRowRef.value) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
667
689
|
|
|
668
690
|
const mainFooterItems = [...footerRowRef.value.children] as HTMLElement[];
|
|
669
691
|
const stickyFooterItems = [...stickyFooterRowRef.value.children] as HTMLElement[];
|
|
@@ -889,7 +911,7 @@ function getDateDividerConfig(row: Row, isSelected: boolean) {
|
|
|
889
911
|
: bodyDateDividerAttrs.value.config;
|
|
890
912
|
|
|
891
913
|
return getMergedConfig({
|
|
892
|
-
defaultConfig
|
|
914
|
+
defaultConfig,
|
|
893
915
|
globalConfig: getDateDividerData(row.rowDate).config,
|
|
894
916
|
}) as UDividerConfig;
|
|
895
917
|
}
|
|
@@ -917,7 +939,9 @@ function getRowSearchMatchColumns(row: FlatRow): Set<string> | undefined {
|
|
|
917
939
|
}
|
|
918
940
|
|
|
919
941
|
function getRowActiveSearchMatchColumn(row: FlatRow): string | undefined {
|
|
920
|
-
if (!activeMatch.value || activeMatch.value.rowId !== row.id)
|
|
942
|
+
if (!activeMatch.value || activeMatch.value.rowId !== row.id) {
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
921
945
|
|
|
922
946
|
return activeMatch.value.columnKey;
|
|
923
947
|
}
|
|
@@ -1036,6 +1060,8 @@ const {
|
|
|
1036
1060
|
bodyCellSearchMatchTextAttrs,
|
|
1037
1061
|
bodyCellSearchMatchActiveAttrs,
|
|
1038
1062
|
bodyCellSearchMatchTextActiveAttrs,
|
|
1063
|
+
skeletonCellAttrs,
|
|
1064
|
+
skeletonCheckboxAttrs,
|
|
1039
1065
|
} = useUI<Config>(defaultConfig, mutatedProps);
|
|
1040
1066
|
|
|
1041
1067
|
/* Plain object — inner refs are already reactive. */
|
|
@@ -1171,7 +1197,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1171
1197
|
</template>
|
|
1172
1198
|
</div>
|
|
1173
1199
|
|
|
1174
|
-
<ULoaderProgress :loading="
|
|
1200
|
+
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1175
1201
|
</div>
|
|
1176
1202
|
|
|
1177
1203
|
<div
|
|
@@ -1203,7 +1229,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1203
1229
|
-->
|
|
1204
1230
|
<slot name="header-actions" :selected-rows="localSelectedRows" />
|
|
1205
1231
|
|
|
1206
|
-
<ULoaderProgress :loading="
|
|
1232
|
+
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1207
1233
|
</div>
|
|
1208
1234
|
|
|
1209
1235
|
<div
|
|
@@ -1235,7 +1261,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1235
1261
|
-->
|
|
1236
1262
|
<slot name="header-actions" :selected-rows="localSelectedRows" />
|
|
1237
1263
|
|
|
1238
|
-
<ULoaderProgress :loading="
|
|
1264
|
+
<ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
|
|
1239
1265
|
</div>
|
|
1240
1266
|
|
|
1241
1267
|
<div ref="table-wrapper" v-bind="tableWrapperAttrs" @scroll="virtualScroll.onScroll">
|
|
@@ -1293,7 +1319,12 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1293
1319
|
@binding {number} index
|
|
1294
1320
|
-->
|
|
1295
1321
|
<slot
|
|
1296
|
-
v-if="
|
|
1322
|
+
v-if="
|
|
1323
|
+
hasSlotContent($slots[`header-${column.key}`], {
|
|
1324
|
+
column,
|
|
1325
|
+
index,
|
|
1326
|
+
})
|
|
1327
|
+
"
|
|
1297
1328
|
:name="`header-${column.key}`"
|
|
1298
1329
|
:column="column"
|
|
1299
1330
|
:index="index"
|
|
@@ -1305,7 +1336,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1305
1336
|
</th>
|
|
1306
1337
|
</tr>
|
|
1307
1338
|
|
|
1308
|
-
<ULoaderProgress :loading="
|
|
1339
|
+
<ULoaderProgress :loading="progressLoading" v-bind="headerLoaderAttrs" />
|
|
1309
1340
|
</thead>
|
|
1310
1341
|
|
|
1311
1342
|
<tbody
|
|
@@ -1356,6 +1387,30 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
|
|
|
1356
1387
|
</tr>
|
|
1357
1388
|
</tbody>
|
|
1358
1389
|
|
|
1390
|
+
<tbody v-else-if="showSkeletonLoading" v-bind="bodyAttrs">
|
|
1391
|
+
<tr
|
|
1392
|
+
v-for="rowIndex in skeletonRowIndexes"
|
|
1393
|
+
:key="`skeleton-row-${rowIndex}`"
|
|
1394
|
+
v-bind="bodyRowAttrs"
|
|
1395
|
+
>
|
|
1396
|
+
<td v-if="selectable" v-bind="bodyCellCheckboxAttrs">
|
|
1397
|
+
<USkeleton v-bind="skeletonCheckboxAttrs" />
|
|
1398
|
+
</td>
|
|
1399
|
+
|
|
1400
|
+
<td
|
|
1401
|
+
v-for="(column, columnIndex) in visibleColumns"
|
|
1402
|
+
:key="`${column.key}-skeleton-${rowIndex}`"
|
|
1403
|
+
v-bind="bodyCellBaseAttrs"
|
|
1404
|
+
:style="getStickyColumnStyle(column)"
|
|
1405
|
+
>
|
|
1406
|
+
<USkeleton
|
|
1407
|
+
v-bind="skeletonCellAttrs"
|
|
1408
|
+
:class="getSkeletonWidthClass(columnIndex, rowIndex)"
|
|
1409
|
+
/>
|
|
1410
|
+
</td>
|
|
1411
|
+
</tr>
|
|
1412
|
+
</tbody>
|
|
1413
|
+
|
|
1359
1414
|
<tbody v-else>
|
|
1360
1415
|
<tr>
|
|
1361
1416
|
<td :colspan="colsCount" v-bind="bodyEmptyStateCellAttrs">
|
package/ui.data-table/config.ts
CHANGED
|
@@ -13,7 +13,11 @@ export default /*tw*/ {
|
|
|
13
13
|
},
|
|
14
14
|
compoundVariants: [
|
|
15
15
|
{ stickedHeader: true, actionsHeader: true, class: "rounded-none" },
|
|
16
|
-
{
|
|
16
|
+
{
|
|
17
|
+
stickedHeader: true,
|
|
18
|
+
actionsHeader: false,
|
|
19
|
+
class: "border-muted bg-default",
|
|
20
|
+
},
|
|
17
21
|
],
|
|
18
22
|
},
|
|
19
23
|
stickyHeaderCell: "{>headerCellBase} flex-none whitespace-nowrap",
|
|
@@ -111,6 +115,8 @@ export default /*tw*/ {
|
|
|
111
115
|
},
|
|
112
116
|
bodyEmptyState: "{UEmpty} my-8",
|
|
113
117
|
bodyEmptyStateCell: "",
|
|
118
|
+
skeletonCell: "{USkeleton} h-4 rounded-small",
|
|
119
|
+
skeletonCheckbox: "{USkeleton} size-4 rounded-small",
|
|
114
120
|
footer: {
|
|
115
121
|
base: "group/footer border-t border-solid border-muted",
|
|
116
122
|
variants: {
|
|
@@ -147,6 +153,9 @@ export default /*tw*/ {
|
|
|
147
153
|
stickyHeader: false,
|
|
148
154
|
stickyFooter: false,
|
|
149
155
|
loading: false,
|
|
156
|
+
skeletonLoading: false,
|
|
157
|
+
skeletonRows: 5,
|
|
158
|
+
skeletonWidths: ["w-11/12", "w-4/5", "w-3/4", "w-2/3", "w-1/2"],
|
|
150
159
|
textEllipsis: false,
|
|
151
160
|
search: "",
|
|
152
161
|
searchMatch: -1,
|
|
@@ -182,6 +182,36 @@ Loading.parameters = {
|
|
|
182
182
|
},
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
export const SkeletonLoading = DefaultTemplate.bind({});
|
|
186
|
+
SkeletonLoading.args = {
|
|
187
|
+
rows: [],
|
|
188
|
+
skeletonLoading: true,
|
|
189
|
+
};
|
|
190
|
+
SkeletonLoading.parameters = {
|
|
191
|
+
docs: {
|
|
192
|
+
description: {
|
|
193
|
+
story:
|
|
194
|
+
"Use `skeletonLoading` while the first request is pending. The table keeps its structure " +
|
|
195
|
+
"and renders cell skeletons instead of the empty state.",
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const SkeletonLoadingConfigured = DefaultTemplate.bind({});
|
|
201
|
+
SkeletonLoadingConfigured.args = {
|
|
202
|
+
rows: [],
|
|
203
|
+
skeletonLoading: true,
|
|
204
|
+
skeletonRows: 3,
|
|
205
|
+
skeletonWidths: ["w-11/12", "w-2/3", "w-1/2"],
|
|
206
|
+
};
|
|
207
|
+
SkeletonLoadingConfigured.parameters = {
|
|
208
|
+
docs: {
|
|
209
|
+
description: {
|
|
210
|
+
story: "Configure skeleton row count and cell widths for a single table with props.",
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
185
215
|
export const EmptyCellLabel = DefaultTemplate.bind({});
|
|
186
216
|
EmptyCellLabel.args = {
|
|
187
217
|
emptyCellLabel: "NO DATA",
|
|
@@ -7,6 +7,7 @@ import UTableRow from "../UTableRow.vue";
|
|
|
7
7
|
import UEmpty from "../../ui.container-empty/UEmpty.vue";
|
|
8
8
|
import ULoaderProgress from "../../ui.loader-progress/ULoaderProgress.vue";
|
|
9
9
|
import UDivider from "../../ui.container-divider/UDivider.vue";
|
|
10
|
+
import USkeleton from "../../ui.skeleton/USkeleton.vue";
|
|
10
11
|
import {
|
|
11
12
|
LoaderProgressSymbol,
|
|
12
13
|
createLoaderProgress,
|
|
@@ -149,6 +150,29 @@ describe("UTable.vue", () => {
|
|
|
149
150
|
expect(emptyComponent.exists()).toBe(true);
|
|
150
151
|
});
|
|
151
152
|
|
|
153
|
+
it("Skeleton loading – renders skeleton body instead of empty state", () => {
|
|
154
|
+
const component = mountUTable(getDefaultProps({ rows: [], skeletonLoading: true }));
|
|
155
|
+
|
|
156
|
+
expect(component.findComponent(UEmpty).exists()).toBe(false);
|
|
157
|
+
expect(component.findAllComponents(USkeleton).length).toBeGreaterThan(0);
|
|
158
|
+
expect(component.find("table").exists()).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("Skeleton loading – applies skeleton rows and widths from props", () => {
|
|
162
|
+
const component = mountUTable(
|
|
163
|
+
getDefaultProps({
|
|
164
|
+
rows: [],
|
|
165
|
+
skeletonLoading: true,
|
|
166
|
+
skeletonRows: 2,
|
|
167
|
+
skeletonWidths: ["w-1/3"],
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
expect(component.findAll("tbody tr")).toHaveLength(2);
|
|
172
|
+
expect(component.findAllComponents(USkeleton)).toHaveLength(defaultColumns.length * 2);
|
|
173
|
+
expect(component.findComponent(USkeleton).classes()).toContain("w-1/3");
|
|
174
|
+
});
|
|
175
|
+
|
|
152
176
|
it("Selectable – renders select all checkbox when selectable is true", () => {
|
|
153
177
|
const component = mountUTable(getDefaultProps({ selectable: true }));
|
|
154
178
|
|
|
@@ -221,6 +245,20 @@ describe("UTable.vue", () => {
|
|
|
221
245
|
expect(loader.props("loading")).toBe(false);
|
|
222
246
|
});
|
|
223
247
|
|
|
248
|
+
it("Skeleton loading – hides progress loader while skeleton is shown", () => {
|
|
249
|
+
const component = mountUTable(
|
|
250
|
+
getDefaultProps({
|
|
251
|
+
rows: [],
|
|
252
|
+
loading: true,
|
|
253
|
+
skeletonLoading: true,
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const loader = component.findComponent(ULoaderProgress);
|
|
258
|
+
|
|
259
|
+
expect(loader.props("loading")).toBe(false);
|
|
260
|
+
});
|
|
261
|
+
|
|
224
262
|
it("Data Test – applies correct data-test attributes", () => {
|
|
225
263
|
const dataTest = "test-table";
|
|
226
264
|
|
|
@@ -644,7 +682,14 @@ describe("UTable.vue", () => {
|
|
|
644
682
|
name: "Parent Row",
|
|
645
683
|
email: "parent@example.com",
|
|
646
684
|
role: "Admin",
|
|
647
|
-
row: [
|
|
685
|
+
row: [
|
|
686
|
+
{
|
|
687
|
+
id: "1-1",
|
|
688
|
+
name: "Child",
|
|
689
|
+
email: "child@example.com",
|
|
690
|
+
role: "User",
|
|
691
|
+
},
|
|
692
|
+
],
|
|
648
693
|
},
|
|
649
694
|
];
|
|
650
695
|
|
|
@@ -710,7 +755,14 @@ describe("UTable.vue", () => {
|
|
|
710
755
|
name: "Parent Row",
|
|
711
756
|
email: "parent@example.com",
|
|
712
757
|
role: "Admin",
|
|
713
|
-
row: [
|
|
758
|
+
row: [
|
|
759
|
+
{
|
|
760
|
+
id: "1-1",
|
|
761
|
+
name: "Child",
|
|
762
|
+
email: "child@example.com",
|
|
763
|
+
role: "User",
|
|
764
|
+
},
|
|
765
|
+
],
|
|
714
766
|
},
|
|
715
767
|
];
|
|
716
768
|
|
|
@@ -792,7 +844,14 @@ describe("UTable.vue", () => {
|
|
|
792
844
|
name: "Parent Row",
|
|
793
845
|
email: "parent@example.com",
|
|
794
846
|
role: "Admin",
|
|
795
|
-
row: [
|
|
847
|
+
row: [
|
|
848
|
+
{
|
|
849
|
+
id: "1-1",
|
|
850
|
+
name: "Child",
|
|
851
|
+
email: "child@example.com",
|
|
852
|
+
role: "User",
|
|
853
|
+
},
|
|
854
|
+
],
|
|
796
855
|
};
|
|
797
856
|
|
|
798
857
|
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
|
|
@@ -813,7 +872,14 @@ describe("UTable.vue", () => {
|
|
|
813
872
|
name: "Parent Row",
|
|
814
873
|
email: "parent@example.com",
|
|
815
874
|
role: "Admin",
|
|
816
|
-
row: [
|
|
875
|
+
row: [
|
|
876
|
+
{
|
|
877
|
+
id: "1-1",
|
|
878
|
+
name: "Child",
|
|
879
|
+
email: "child@example.com",
|
|
880
|
+
role: "User",
|
|
881
|
+
},
|
|
882
|
+
],
|
|
817
883
|
};
|
|
818
884
|
|
|
819
885
|
const component = mountUTable(
|
|
@@ -879,7 +945,12 @@ describe("UTable.vue", () => {
|
|
|
879
945
|
email: "child1@example.com",
|
|
880
946
|
role: "User",
|
|
881
947
|
row: [
|
|
882
|
-
{
|
|
948
|
+
{
|
|
949
|
+
id: "1-1-1",
|
|
950
|
+
name: "Grandchild",
|
|
951
|
+
email: "grandchild@example.com",
|
|
952
|
+
role: "Guest",
|
|
953
|
+
},
|
|
883
954
|
],
|
|
884
955
|
},
|
|
885
956
|
],
|
|
@@ -902,7 +973,14 @@ describe("UTable.vue", () => {
|
|
|
902
973
|
name: "Parent Row",
|
|
903
974
|
email: "parent@example.com",
|
|
904
975
|
role: "Admin",
|
|
905
|
-
row: [
|
|
976
|
+
row: [
|
|
977
|
+
{
|
|
978
|
+
id: "1-1",
|
|
979
|
+
name: "Child",
|
|
980
|
+
email: "child@example.com",
|
|
981
|
+
role: "User",
|
|
982
|
+
},
|
|
983
|
+
],
|
|
906
984
|
};
|
|
907
985
|
|
|
908
986
|
const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
|
package/ui.data-table/types.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import defaultConfig from "./config";
|
|
2
|
-
|
|
3
2
|
import type { Ref } from "vue";
|
|
4
3
|
import type { ComponentConfig, UnknownObject } from "../types";
|
|
5
4
|
import type { Config as UDividerConfig } from "../ui.container-divider/types";
|
|
@@ -117,6 +116,21 @@ export interface Props {
|
|
|
117
116
|
*/
|
|
118
117
|
loading?: boolean;
|
|
119
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Show skeleton body for the first table load instead of the regular loader.
|
|
121
|
+
*/
|
|
122
|
+
skeletonLoading?: boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Number of skeleton rows shown during initial loading.
|
|
126
|
+
*/
|
|
127
|
+
skeletonRows?: number;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Skeleton cell width classes used to vary the loading placeholder layout.
|
|
131
|
+
*/
|
|
132
|
+
skeletonWidths?: string[];
|
|
133
|
+
|
|
120
134
|
/**
|
|
121
135
|
* Enable virtual scrolling for large datasets.
|
|
122
136
|
*/
|
package/ui.form-input/UInput.vue
CHANGED
|
@@ -302,6 +302,22 @@ describe("UInput.vue", () => {
|
|
|
302
302
|
|
|
303
303
|
expect(component.get("input").attributes("data-test")).toBe(dataTestValue);
|
|
304
304
|
});
|
|
305
|
+
|
|
306
|
+
it("Data Test – forwards data-test to the label so the error block is targetable", () => {
|
|
307
|
+
const dataTestValue = "test-input";
|
|
308
|
+
|
|
309
|
+
const component = mount(UInput, {
|
|
310
|
+
props: {
|
|
311
|
+
dataTest: dataTestValue,
|
|
312
|
+
error: "This is an error",
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const errorElement = component.find(`[data-test="${dataTestValue}-label-error"]`);
|
|
317
|
+
|
|
318
|
+
expect(errorElement.exists()).toBe(true);
|
|
319
|
+
expect(errorElement.text()).toBe("This is an error");
|
|
320
|
+
});
|
|
305
321
|
});
|
|
306
322
|
|
|
307
323
|
describe("Slots", () => {
|