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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.4.12-beta.4",
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.4",
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
- closeOnOverlay.forEach(async (value) => {
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 drawer = component.find('[vl-key="drawer"]');
166
+ const closeRequests = component.emitted("update:modelValue");
173
167
 
174
- expect(drawer.exists()).toBe(!value);
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
- closeOnEsc.forEach(async (value) => {
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 drawer = component.find('[vl-key="drawer"]');
187
+ const closeRequests = component.emitted("update:modelValue");
195
188
 
196
- expect(drawer.exists()).toBe(!value);
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
- closeOnOverlay.forEach(async (value) => {
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 modal = component.find('[vl-key="modal"]');
205
+ const closeRequests = component.emitted("update:modelValue");
212
206
 
213
- expect(modal.exists()).toBe(!value);
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
- closeOnEsc.forEach(async (value) => {
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 modal = component.find('[vl-key="modal"]');
227
+ const closeRequests = component.emitted("update:modelValue");
235
228
 
236
- expect(modal.exists()).toBe(!value);
237
- });
229
+ expect(value ? closeRequests?.at(-1) : closeRequests).toEqual(value ? [false] : undefined);
230
+ }
238
231
  });
239
232
 
240
233
  // Inner prop
@@ -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(() => ({ width: `${tableWidth.value / PX_IN_REM}rem` }));
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) return;
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: 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) return undefined;
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="loading" v-bind="stickyHeaderLoaderAttrs" />
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="loading" v-bind="stickyHeaderLoaderAttrs" />
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="loading" v-bind="stickyHeaderLoaderAttrs" />
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="hasSlotContent($slots[`header-${column.key}`], { column, index })"
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="loading" v-bind="headerLoaderAttrs" />
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">
@@ -13,7 +13,11 @@ export default /*tw*/ {
13
13
  },
14
14
  compoundVariants: [
15
15
  { stickedHeader: true, actionsHeader: true, class: "rounded-none" },
16
- { stickedHeader: true, actionsHeader: false, class: "border-muted bg-default" },
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: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
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: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
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: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
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: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
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
- { id: "1-1-1", name: "Grandchild", email: "grandchild@example.com", role: "Guest" },
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: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
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] }));
@@ -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
  */
@@ -281,6 +281,7 @@ const {
281
281
  :align="labelAlign"
282
282
  centred
283
283
  v-bind="inputLabelAttrs"
284
+ :data-test="getDataTest('label')"
284
285
  >
285
286
  <template #label>
286
287
  <!--
@@ -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", () => {