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.
Files changed (109) hide show
  1. package/README.md +1 -1
  2. package/composables/useUI.ts +19 -1
  3. package/index.d.ts +128 -0
  4. package/index.ts +128 -0
  5. package/package.json +10 -9
  6. package/ui.button/config.ts +1 -1
  7. package/ui.button-link/config.ts +1 -1
  8. package/ui.button-toggle/UToggle.vue +6 -4
  9. package/ui.button-toggle/storybook/stories.ts +4 -1
  10. package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
  11. package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
  12. package/ui.button-toggle/types.ts +18 -6
  13. package/ui.container-accordion/types.ts +5 -2
  14. package/ui.container-collapsible/config.ts +1 -1
  15. package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
  16. package/ui.container-modal/tests/UModal.test.ts +10 -17
  17. package/ui.data-list/UDataList.vue +48 -17
  18. package/ui.data-list/config.ts +2 -2
  19. package/ui.data-list/storybook/stories.ts +5 -2
  20. package/ui.data-list/tests/UDataList.test-d.ts +76 -0
  21. package/ui.data-list/tests/UDataList.test.ts +52 -3
  22. package/ui.data-list/tests/UDataListTypes.vue +110 -0
  23. package/ui.data-list/types.ts +37 -5
  24. package/ui.data-table/UTable.vue +202 -24
  25. package/ui.data-table/UTableRow.vue +4 -3
  26. package/ui.data-table/config.ts +11 -2
  27. package/ui.data-table/storybook/stories.ts +34 -1
  28. package/ui.data-table/tests/UTable.test-d.ts +179 -0
  29. package/ui.data-table/tests/UTable.test.ts +85 -7
  30. package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
  31. package/ui.data-table/tests/UTableTypes.vue +193 -0
  32. package/ui.data-table/types.ts +106 -9
  33. package/ui.data-table/utilTable.ts +1 -1
  34. package/ui.dropdown/UDropdown.vue +19 -8
  35. package/ui.dropdown/storybook/stories.ts +4 -1
  36. package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
  37. package/ui.dropdown/tests/UDropdown.test.ts +8 -1
  38. package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
  39. package/ui.dropdown/types.ts +27 -3
  40. package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
  41. package/ui.dropdown-badge/storybook/stories.ts +6 -1
  42. package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
  43. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
  44. package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
  45. package/ui.dropdown-badge/types.ts +17 -3
  46. package/ui.dropdown-button/UDropdownButton.vue +9 -4
  47. package/ui.dropdown-button/storybook/stories.ts +6 -1
  48. package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
  49. package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
  50. package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
  51. package/ui.dropdown-button/types.ts +17 -3
  52. package/ui.dropdown-link/UDropdownLink.vue +9 -4
  53. package/ui.dropdown-link/config.ts +1 -1
  54. package/ui.dropdown-link/storybook/stories.ts +4 -1
  55. package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
  56. package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
  57. package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
  58. package/ui.dropdown-link/types.ts +17 -3
  59. package/ui.form-calendar/config.ts +1 -1
  60. package/ui.form-checkbox/config.ts +1 -1
  61. package/ui.form-checkbox/tests/UCheckbox.test.ts +4 -4
  62. package/ui.form-checkbox/types.ts +4 -2
  63. package/ui.form-checkbox-group/UCheckboxGroup.vue +8 -2
  64. package/ui.form-date-picker/UDatePicker.vue +11 -1
  65. package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
  66. package/ui.form-input/UInput.vue +1 -0
  67. package/ui.form-input/config.ts +1 -1
  68. package/ui.form-input/tests/UInput.test.ts +16 -0
  69. package/ui.form-input-file/config.ts +1 -1
  70. package/ui.form-listbox/UListbox.vue +29 -12
  71. package/ui.form-listbox/config.ts +1 -1
  72. package/ui.form-listbox/storybook/stories.ts +4 -1
  73. package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
  74. package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
  75. package/ui.form-listbox/types.ts +34 -2
  76. package/ui.form-radio/config.ts +1 -1
  77. package/ui.form-radio-group/URadioGroup.vue +15 -3
  78. package/ui.form-radio-group/config.ts +8 -3
  79. package/ui.form-radio-group/types.ts +2 -3
  80. package/ui.form-select/USelect.vue +49 -17
  81. package/ui.form-select/config.ts +4 -4
  82. package/ui.form-select/storybook/stories.ts +4 -1
  83. package/ui.form-select/tests/USelect.test-d.ts +76 -0
  84. package/ui.form-select/tests/USelect.test.ts +65 -1
  85. package/ui.form-select/tests/USelectTypes.vue +113 -0
  86. package/ui.form-select/types.ts +55 -4
  87. package/ui.form-switch/USwitch.vue +13 -12
  88. package/ui.form-switch/config.ts +2 -1
  89. package/ui.form-switch/tests/USwitch.test.ts +151 -0
  90. package/ui.form-textarea/config.ts +1 -1
  91. package/ui.image-avatar/types.ts +5 -2
  92. package/ui.image-avatar-group/types.ts +5 -2
  93. package/ui.image-icon/UIcon.vue +3 -2
  94. package/ui.image-icon/types.ts +2 -2
  95. package/ui.navigation-breadcrumbs/types.ts +5 -2
  96. package/ui.navigation-tab/UTab.vue +14 -9
  97. package/ui.navigation-tab/tests/UTab.test.ts +14 -0
  98. package/ui.navigation-tabs/UTabs.vue +158 -6
  99. package/ui.navigation-tabs/config.ts +2 -1
  100. package/ui.navigation-tabs/constants.ts +2 -0
  101. package/ui.navigation-tabs/storybook/stories.ts +12 -1
  102. package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
  103. package/ui.navigation-tabs/tests/UTabs.test.ts +213 -1
  104. package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
  105. package/ui.navigation-tabs/types.ts +26 -4
  106. package/ui.skeleton-text/constants.ts +1 -1
  107. package/ui.text-badge/config.ts +1 -1
  108. package/utils/node/loaderIcon.js +1 -1
  109. package/v.tooltip/vTooltip.ts +1 -1
@@ -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,
@@ -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";
@@ -46,18 +47,22 @@ import type {
46
47
  FlatRow,
47
48
  ColumnObject,
48
49
  SearchMatch,
50
+ TableRow,
49
51
  UTableRowProps,
52
+ UTableSlots,
53
+ SelectedSlotRow,
50
54
  } from "./types";
51
55
 
52
56
  defineOptions({ inheritAttrs: false });
53
57
 
54
- const props = withDefaults(defineProps<Props>(), {
55
- ...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
58
+ const props = withDefaults(defineProps<Props<TRow, TCol>>(), {
59
+ ...getDefaults<Props<TRow, TCol>, Config>(defaultConfig, COMPONENT_NAME),
56
60
  columns: () => [],
57
61
  rows: () => [],
58
62
  dateDivider: () => [],
59
63
  selectedRows: () => [],
60
64
  expandedRows: () => [],
65
+ skeletonWidths: () => defaultConfig.defaults.skeletonWidths,
61
66
  });
62
67
 
63
68
  const emit = defineEmits([
@@ -110,6 +115,8 @@ const emit = defineEmits([
110
115
  "search",
111
116
  ]);
112
117
 
118
+ defineSlots<UTableSlots<TRow, TCol>>();
119
+
113
120
  const slots = useSlots();
114
121
 
115
122
  const selectAll = ref(false);
@@ -143,7 +150,7 @@ const sortedRows: ComputedRef<FlatRow[]> = computed(() => {
143
150
  typeof column === "object" ? column.key : column,
144
151
  );
145
152
 
146
- const keyOrder = new Map(headerKeys.map((key, i) => [key, i]));
153
+ const keyOrder = new Map<string, number>(headerKeys.map((key, i) => [key, i]));
147
154
 
148
155
  return flatTableRows.value.map((row) => {
149
156
  const entries = Object.entries(row);
@@ -169,6 +176,11 @@ const isFooterSticky = computed(() => {
169
176
 
170
177
  const normalizedColumns = computed(() => normalizeColumns(props.columns));
171
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
+
172
184
  const visibleColumns = computed(() => {
173
185
  return normalizedColumns.value.filter((column) => column.isShown !== false);
174
186
  });
@@ -206,9 +218,14 @@ const isCheckedMoreOneTableItems = computed(() => {
206
218
  return Boolean(localSelectedRows.value.length);
207
219
  });
208
220
 
209
- const tableRowWidthStyle = computed(() => ({ width: `${tableWidth.value / PX_IN_REM}rem` }));
221
+ /* Select-all includes nested children, which need not carry `TRow`'s members. */
222
+ const selectedRowsSlotValue = computed(() => localSelectedRows.value as SelectedSlotRow<TRow>[]);
223
+
224
+ const tableRowWidthStyle = computed(() => ({
225
+ width: `${tableWidth.value / PX_IN_REM}rem`,
226
+ }));
210
227
 
211
- const flatTableRows = computed(() => getFlatRows(props.rows));
228
+ const flatTableRows = computed(() => getFlatRows(props.rows as Row[]));
212
229
 
213
230
  const visibleFlatRows = computed(() => {
214
231
  const expanded = expandedRowsSet.value;
@@ -216,6 +233,18 @@ const visibleFlatRows = computed(() => {
216
233
  return flatTableRows.value.filter((row) => !row.parentRowId || expanded.has(row.parentRowId));
217
234
  });
218
235
 
236
+ const showSkeletonLoading = computed(() => {
237
+ return props.skeletonLoading && !sortedRows.value.length;
238
+ });
239
+
240
+ const progressLoading = computed(() => {
241
+ return showSkeletonLoading.value ? false : props.loading;
242
+ });
243
+
244
+ const skeletonRowIndexes = computed(() => {
245
+ return Array.from({ length: props.skeletonRows }, (_, index) => index);
246
+ });
247
+
219
248
  const virtualScroll = useVirtualScroll({
220
249
  containerRef: tableWrapperRef,
221
250
  totalCount: computed(() => (props.virtualScroll ? visibleFlatRows.value.length : 0)),
@@ -243,6 +272,10 @@ function isRowVisible(row: FlatRow): boolean {
243
272
  return !row.parentRowId || expandedRowsSet.value.has(row.parentRowId);
244
273
  }
245
274
 
275
+ function getSkeletonWidthClass(columnIndex: number, rowIndex: number): string {
276
+ return props.skeletonWidths[(columnIndex + rowIndex) % props.skeletonWidths.length];
277
+ }
278
+
246
279
  const searchMatches = computed<SearchMatch[]>(() => {
247
280
  const query = props.search?.toLowerCase();
248
281
 
@@ -512,7 +545,7 @@ onBeforeUnmount(() => {
512
545
 
513
546
  function onChangeSelectedRows() {
514
547
  if (!isEqual(props.selectedRows, localSelectedRows.value)) {
515
- localSelectedRows.value = props.selectedRows;
548
+ localSelectedRows.value = props.selectedRows as Row[];
516
549
  }
517
550
  }
518
551
 
@@ -663,7 +696,9 @@ function getDateDividerData(rowDate: string | Date | undefined) {
663
696
  function setFooterCellWidth(zero?: null) {
664
697
  const ZERO_WIDTH = 0;
665
698
 
666
- if (!props.stickyFooter || !footerRowRef.value || !stickyFooterRowRef.value) return;
699
+ if (!props.stickyFooter || !footerRowRef.value || !stickyFooterRowRef.value) {
700
+ return;
701
+ }
667
702
 
668
703
  const mainFooterItems = [...footerRowRef.value.children] as HTMLElement[];
669
704
  const stickyFooterItems = [...stickyFooterRowRef.value.children] as HTMLElement[];
@@ -816,6 +851,50 @@ function onBodyDoubleClick(event: MouseEvent) {
816
851
  onDoubleClickRow(rowData);
817
852
  }
818
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
+
819
898
  function onChangeSelectAll(selectAll: boolean) {
820
899
  if (selectAll && canSelectAll.value) {
821
900
  localSelectedRows.value = [...flatTableRows.value];
@@ -889,7 +968,7 @@ function getDateDividerConfig(row: Row, isSelected: boolean) {
889
968
  : bodyDateDividerAttrs.value.config;
890
969
 
891
970
  return getMergedConfig({
892
- defaultConfig: defaultConfig,
971
+ defaultConfig,
893
972
  globalConfig: getDateDividerData(row.rowDate).config,
894
973
  }) as UDividerConfig;
895
974
  }
@@ -917,7 +996,9 @@ function getRowSearchMatchColumns(row: FlatRow): Set<string> | undefined {
917
996
  }
918
997
 
919
998
  function getRowActiveSearchMatchColumn(row: FlatRow): string | undefined {
920
- if (!activeMatch.value || activeMatch.value.rowId !== row.id) return undefined;
999
+ if (!activeMatch.value || activeMatch.value.rowId !== row.id) {
1000
+ return;
1001
+ }
921
1002
 
922
1003
  return activeMatch.value.columnKey;
923
1004
  }
@@ -1036,9 +1117,11 @@ const {
1036
1117
  bodyCellSearchMatchTextAttrs,
1037
1118
  bodyCellSearchMatchActiveAttrs,
1038
1119
  bodyCellSearchMatchTextActiveAttrs,
1120
+ skeletonCellAttrs,
1121
+ skeletonCheckboxAttrs,
1039
1122
  } = useUI<Config>(defaultConfig, mutatedProps);
1040
1123
 
1041
- /* Plain object — inner refs are already reactive. */
1124
+ /* Plain object — inner refs are already reactive and identity-stable (see useUI). */
1042
1125
  const tableRowAttrs = {
1043
1126
  bodyCellContentAttrs,
1044
1127
  bodyCellCheckboxAttrs,
@@ -1085,8 +1168,40 @@ function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
1085
1168
  ]);
1086
1169
  }
1087
1170
 
1171
+ /**
1172
+ * Per-row VNode memo cache. Toggling one checkbox invalidates the `selectedRowIds`
1173
+ * computed, which re-runs the body render function. Without memoization every row's
1174
+ * VNode would be rebuilt and diffed on each toggle (200+ rows → visible lag). We cache
1175
+ * each row's VNode keyed by row id and only rebuild it when an input that actually
1176
+ * affects that row changes — so an unrelated row keeps the same VNode reference and
1177
+ * Vue skips it entirely during patch.
1178
+ */
1179
+ const rowVNodeCache = new Map<RowId, { signature: string; row: FlatRow; vnode: VNode }>();
1180
+
1181
+ function getRowSignature(row: FlatRow, rowIndex: number): string {
1182
+ return [
1183
+ rowIndex,
1184
+ Number(isRowSelected(row)),
1185
+ Number(expandedRowsSet.value.has(row.id)),
1186
+ Number(isRowVisible(row)),
1187
+ props.selectable ? 1 : 0,
1188
+ props.search || "",
1189
+ getRowActiveSearchMatchColumn(row) || "",
1190
+ [...(getRowSearchMatchColumns(row) || [])].join(","),
1191
+ ].join("|");
1192
+ }
1193
+
1088
1194
  function renderTableRow(row: FlatRow, rowIndex: number): VNode {
1089
- return h(
1195
+ const signature = getRowSignature(row, rowIndex);
1196
+ const cached = rowVNodeCache.get(row.id);
1197
+
1198
+ // `row` identity guards against stale data: `flatTableRows` yields fresh row
1199
+ // objects whenever `props.rows` changes, so a new reference means new cell data.
1200
+ if (cached && cached.row === row && cached.signature === signature) {
1201
+ return cached.vnode;
1202
+ }
1203
+
1204
+ const vnode = h(
1090
1205
  UTableRow,
1091
1206
  {
1092
1207
  key: row.id,
@@ -1112,13 +1227,48 @@ function renderTableRow(row: FlatRow, rowIndex: number): VNode {
1112
1227
  } as unknown as UTableRowProps,
1113
1228
  slots,
1114
1229
  );
1230
+
1231
+ rowVNodeCache.set(row.id, { signature, row, vnode });
1232
+
1233
+ return vnode;
1115
1234
  }
1116
1235
 
1236
+ /* Column layout / config changes affect every row — invalidate the whole cache. */
1237
+ watch(
1238
+ [
1239
+ normalizedColumns,
1240
+ config,
1241
+ columnPositions,
1242
+ () => props.textEllipsis,
1243
+ () => props.emptyCellLabel,
1244
+ ],
1245
+ () => rowVNodeCache.clear(),
1246
+ );
1247
+
1248
+ /* Drop cache entries for rows that no longer exist (filters, pagination reset). */
1249
+ watch(flatTableRows, (rows) => {
1250
+ const liveIds = new Set(rows.map((row) => row.id));
1251
+
1252
+ for (const id of rowVNodeCache.keys()) {
1253
+ if (!liveIds.has(id)) rowVNodeCache.delete(id);
1254
+ }
1255
+ });
1256
+
1117
1257
  function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1118
1258
  return [renderDateDividerRow(row, rowIndex), renderTableRow(row, rowIndex)].filter(
1119
1259
  Boolean,
1120
1260
  ) as VNode[];
1121
1261
  }
1262
+
1263
+ /**
1264
+ * Stable functional component for the body rows. Defined once so its type
1265
+ * identity never changes across parent re-renders — a previous inline `:is`
1266
+ * arrow created a new type on every render, forcing Vue to unmount and rebuild
1267
+ * the entire tbody (e.g. on a sticky-header toggle). Reading `renderedRows`
1268
+ * through the closure keeps it reactive while row keys drive reconciliation.
1269
+ */
1270
+ const BodyRows = () =>
1271
+ renderedRows.value.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat();
1122
1272
  </script>
1123
1273
 
1124
1274
  <template>
@@ -1163,7 +1313,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1163
1313
  @binding {object} column
1164
1314
  @binding {number} index
1165
1315
  -->
1166
- <slot :name="`header-${column.key}`" :column="column" :index="index" />
1316
+ <slot :name="headerSlotName(column.key)" :column="column" :index="index" />
1167
1317
  </template>
1168
1318
 
1169
1319
  <template v-else>
@@ -1171,7 +1321,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1171
1321
  </template>
1172
1322
  </div>
1173
1323
 
1174
- <ULoaderProgress :loading="loading" v-bind="stickyHeaderLoaderAttrs" />
1324
+ <ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
1175
1325
  </div>
1176
1326
 
1177
1327
  <div
@@ -1201,9 +1351,9 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1201
1351
  @slot Use it to add action buttons within the actions header, which appear when rows are selected.
1202
1352
  @binding {array} selected-rows
1203
1353
  -->
1204
- <slot name="header-actions" :selected-rows="localSelectedRows" />
1354
+ <slot name="header-actions" :selected-rows="selectedRowsSlotValue" />
1205
1355
 
1206
- <ULoaderProgress :loading="loading" v-bind="stickyHeaderLoaderAttrs" />
1356
+ <ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
1207
1357
  </div>
1208
1358
 
1209
1359
  <div
@@ -1233,9 +1383,9 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1233
1383
  @slot Use it to add action buttons within the actions header, which appear when rows are selected.
1234
1384
  @binding {array} selected-rows
1235
1385
  -->
1236
- <slot name="header-actions" :selected-rows="localSelectedRows" />
1386
+ <slot name="header-actions" :selected-rows="selectedRowsSlotValue" />
1237
1387
 
1238
- <ULoaderProgress :loading="loading" v-bind="stickyHeaderLoaderAttrs" />
1388
+ <ULoaderProgress :loading="progressLoading" v-bind="stickyHeaderLoaderAttrs" />
1239
1389
  </div>
1240
1390
 
1241
1391
  <div ref="table-wrapper" v-bind="tableWrapperAttrs" @scroll="virtualScroll.onScroll">
@@ -1293,8 +1443,13 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1293
1443
  @binding {number} index
1294
1444
  -->
1295
1445
  <slot
1296
- v-if="hasSlotContent($slots[`header-${column.key}`], { column, index })"
1297
- :name="`header-${column.key}`"
1446
+ v-if="
1447
+ hasSlotContent($slots[`header-${column.key}`], {
1448
+ column,
1449
+ index,
1450
+ })
1451
+ "
1452
+ :name="headerSlotName(column.key)"
1298
1453
  :column="column"
1299
1454
  :index="index"
1300
1455
  />
@@ -1305,7 +1460,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1305
1460
  </th>
1306
1461
  </tr>
1307
1462
 
1308
- <ULoaderProgress :loading="loading" v-bind="headerLoaderAttrs" />
1463
+ <ULoaderProgress :loading="progressLoading" v-bind="headerLoaderAttrs" />
1309
1464
  </thead>
1310
1465
 
1311
1466
  <tbody
@@ -1313,6 +1468,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1313
1468
  v-bind="bodyAttrs"
1314
1469
  @click="onBodyClick"
1315
1470
  @dblclick="onBodyDoubleClick"
1471
+ @touchend="onBodyTouchEnd"
1316
1472
  >
1317
1473
  <tr
1318
1474
  v-if="hasBeforeFirstRowSlot"
@@ -1331,9 +1487,7 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1331
1487
  />
1332
1488
  </tr>
1333
1489
 
1334
- <component
1335
- :is="() => renderedRows.map((row, rowIndex) => renderRowTemplate(row, rowIndex)).flat()"
1336
- />
1490
+ <component :is="BodyRows" />
1337
1491
 
1338
1492
  <tr v-if="props.virtualScroll && virtualScroll.bottomSpacerHeight.value > 0">
1339
1493
  <td
@@ -1356,6 +1510,30 @@ function renderRowTemplate(row: FlatRow, rowIndex: number): VNode[] {
1356
1510
  </tr>
1357
1511
  </tbody>
1358
1512
 
1513
+ <tbody v-else-if="showSkeletonLoading" v-bind="bodyAttrs">
1514
+ <tr
1515
+ v-for="rowIndex in skeletonRowIndexes"
1516
+ :key="`skeleton-row-${rowIndex}`"
1517
+ v-bind="bodyRowAttrs"
1518
+ >
1519
+ <td v-if="selectable" v-bind="bodyCellCheckboxAttrs">
1520
+ <USkeleton v-bind="skeletonCheckboxAttrs" />
1521
+ </td>
1522
+
1523
+ <td
1524
+ v-for="(column, columnIndex) in visibleColumns"
1525
+ :key="`${column.key}-skeleton-${rowIndex}`"
1526
+ v-bind="bodyCellBaseAttrs"
1527
+ :style="getStickyColumnStyle(column)"
1528
+ >
1529
+ <USkeleton
1530
+ v-bind="skeletonCellAttrs"
1531
+ :class="getSkeletonWidthClass(columnIndex, rowIndex)"
1532
+ />
1533
+ </td>
1534
+ </tr>
1535
+ </tbody>
1536
+
1359
1537
  <tbody v-else>
1360
1538
  <tr>
1361
1539
  <td :colspan="colsCount" v-bind="bodyEmptyStateCellAttrs">
@@ -400,6 +400,7 @@ function renderNestedFirstCell(value: Cell, key: string, cellIndex: number): VNo
400
400
 
401
401
  function renderTableCell(value: Cell, key: string, index: number): VNode {
402
402
  const keyStr = String(key);
403
+ const column = props.columns.find((col) => col.key === key) ?? props.columns[index];
403
404
 
404
405
  const nestedCellNode = isNestedFirstCell(index)
405
406
  ? renderNestedFirstCell(value, key, index)
@@ -414,12 +415,12 @@ function renderTableCell(value: Cell, key: string, index: number): VNode {
414
415
  ...props.attrs.bodyCellBaseAttrs.value,
415
416
  class: cx([
416
417
  props.attrs.bodyCellBaseAttrs.value.class,
417
- props.columns[index].tdClass,
418
+ column.tdClass,
418
419
  getCellClasses(props.row, keyStr),
419
- getStickyColumnClass(props.columns[index]),
420
+ getStickyColumnClass(column),
420
421
  getSearchMatchCellClass(keyStr),
421
422
  ]),
422
- style: getStickyColumnStyle(props.columns[index]),
423
+ style: getStickyColumnStyle(column),
423
424
  "data-cell-key": key,
424
425
  "data-test": getDataTest(`${key}-cell`),
425
426
  },
@@ -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",
@@ -41,7 +45,7 @@ export default /*tw*/ {
41
45
  base: "min-w-full border-none text-medium w-auto table-auto border-separate border-spacing-0",
42
46
  variants: {
43
47
  virtualScroll: {
44
- true: "table-layout-fixed w-full",
48
+ true: "table-fixed w-full",
45
49
  },
46
50
  },
47
51
  },
@@ -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,
@@ -12,7 +12,7 @@ import { getRandomId } from "../../utils/helper";
12
12
 
13
13
  import defaultConfig from "../config";
14
14
 
15
- import UTable from "../UTable.vue";
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";
@@ -182,6 +185,36 @@ Loading.parameters = {
182
185
  },
183
186
  };
184
187
 
188
+ export const SkeletonLoading = DefaultTemplate.bind({});
189
+ SkeletonLoading.args = {
190
+ rows: [],
191
+ skeletonLoading: true,
192
+ };
193
+ SkeletonLoading.parameters = {
194
+ docs: {
195
+ description: {
196
+ story:
197
+ "Use `skeletonLoading` while the first request is pending. The table keeps its structure " +
198
+ "and renders cell skeletons instead of the empty state.",
199
+ },
200
+ },
201
+ };
202
+
203
+ export const SkeletonLoadingConfigured = DefaultTemplate.bind({});
204
+ SkeletonLoadingConfigured.args = {
205
+ rows: [],
206
+ skeletonLoading: true,
207
+ skeletonRows: 3,
208
+ skeletonWidths: ["w-11/12", "w-2/3", "w-1/2"],
209
+ };
210
+ SkeletonLoadingConfigured.parameters = {
211
+ docs: {
212
+ description: {
213
+ story: "Configure skeleton row count and cell widths for a single table with props.",
214
+ },
215
+ },
216
+ };
217
+
185
218
  export const EmptyCellLabel = DefaultTemplate.bind({});
186
219
  EmptyCellLabel.args = {
187
220
  emptyCellLabel: "NO DATA",