vueless 1.3.9-beta.14 → 1.3.9-beta.15

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.3.9-beta.14",
3
+ "version": "1.3.9-beta.15",
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",
@@ -57,7 +57,7 @@
57
57
  "@vue/eslint-config-typescript": "^14.6.0",
58
58
  "@vue/test-utils": "^2.4.6",
59
59
  "@vue/tsconfig": "^0.7.0",
60
- "@vueless/storybook": "^1.4.8",
60
+ "@vueless/storybook": "^1.4.9",
61
61
  "eslint": "^9.32.0",
62
62
  "eslint-plugin-storybook": "^10.0.2",
63
63
  "eslint-plugin-vue": "^10.3.0",
@@ -14,24 +14,25 @@ import {
14
14
  } from "vue";
15
15
  import { isEqual } from "lodash-es";
16
16
 
17
- import UEmpty from "../ui.container-empty/UEmpty.vue";
18
- import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
19
- import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
20
- import UTableRow from "./UTableRow.vue";
21
- import UDivider from "../ui.container-divider/UDivider.vue";
22
-
23
17
  import { useUI } from "../composables/useUI";
24
18
  import { useVirtualScroll } from "../composables/useVirtualScroll";
19
+ import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
25
20
  import { getDefaults, cx, getMergedConfig } from "../utils/ui";
26
21
  import { hasSlotContent } from "../utils/helper";
27
- import { useComponentLocaleMessages } from "../composables/useComponentLocaleMassages";
28
-
29
- import defaultConfig from "./config";
30
- import { normalizeColumns, mapRowColumns, getFlatRows, getRowChildrenIds } from "./utilTable";
31
22
 
32
23
  import { PX_IN_REM } from "../constants";
33
24
  import { COMPONENT_NAME } from "./constants";
34
25
 
26
+ import defaultConfig from "./config";
27
+ import { normalizeColumns, getFlatRows, getRowChildrenIds } from "./utilTable";
28
+ import { StickySide } from "./types";
29
+
30
+ import UEmpty from "../ui.container-empty/UEmpty.vue";
31
+ import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
32
+ import ULoaderProgress from "../ui.loader-progress/ULoaderProgress.vue";
33
+ import UDivider from "../ui.container-divider/UDivider.vue";
34
+ import UTableRow from "./UTableRow.vue";
35
+
35
36
  import type { ComputedRef, VNode } from "vue";
36
37
  import type { Config as UDividerConfig } from "../ui.container-divider/types";
37
38
  import type {
@@ -47,7 +48,6 @@ import type {
47
48
  SearchMatch,
48
49
  UTableRowProps,
49
50
  } from "./types";
50
- import { StickySide } from "./types";
51
51
 
52
52
  defineOptions({ inheritAttrs: false });
53
53
 
@@ -136,6 +136,8 @@ const { localeMessages } = useComponentLocaleMessages<typeof defaultConfig.i18n>
136
136
  const localSelectedRows = shallowRef<Row[]>([]);
137
137
  const localExpandedRows = shallowRef<RowId[]>([]);
138
138
 
139
+ const expandedRowsSet = computed(() => new Set(localExpandedRows.value));
140
+
139
141
  const sortedRows: ComputedRef<FlatRow[]> = computed(() => {
140
142
  const headerKeys = props.columns.map((column) =>
141
143
  typeof column === "object" ? column.key : column,
@@ -209,9 +211,9 @@ const tableRowWidthStyle = computed(() => ({ width: `${tableWidth.value / PX_IN_
209
211
  const flatTableRows = computed(() => getFlatRows(props.rows));
210
212
 
211
213
  const visibleFlatRows = computed(() => {
212
- return flatTableRows.value.filter(
213
- (row) => !row.parentRowId || localExpandedRows.value.includes(row.parentRowId),
214
- );
214
+ const expanded = expandedRowsSet.value;
215
+
216
+ return flatTableRows.value.filter((row) => !row.parentRowId || expanded.has(row.parentRowId));
215
217
  });
216
218
 
217
219
  const virtualScroll = useVirtualScroll({
@@ -222,11 +224,25 @@ const virtualScroll = useVirtualScroll({
222
224
  });
223
225
 
224
226
  const renderedRows = computed(() => {
225
- return props.virtualScroll
226
- ? visibleFlatRows.value.slice(virtualScroll.startIndex.value, virtualScroll.endIndex.value)
227
- : visibleFlatRows.value;
227
+ if (props.virtualScroll) {
228
+ // For virtual scroll, we still need to slice based on visible rows
229
+ // but we need to map back to the actual rows from flatTableRows
230
+ const visibleSlice = visibleFlatRows.value.slice(
231
+ virtualScroll.startIndex.value,
232
+ virtualScroll.endIndex.value,
233
+ );
234
+ const visibleIds = new Set(visibleSlice.map((row) => row.id));
235
+
236
+ return flatTableRows.value.filter((row) => visibleIds.has(row.id));
237
+ }
238
+
239
+ return flatTableRows.value;
228
240
  });
229
241
 
242
+ function isRowVisible(row: FlatRow): boolean {
243
+ return !row.parentRowId || expandedRowsSet.value.has(row.parentRowId);
244
+ }
245
+
230
246
  const searchMatches = computed<SearchMatch[]>(() => {
231
247
  const query = props.search?.toLowerCase();
232
248
 
@@ -445,11 +461,23 @@ watchEffect(() => {
445
461
 
446
462
  let resizeObserver: ResizeObserver | null = null;
447
463
  let scrollRafId: number | null = null;
464
+ let resizeDebounceId: ReturnType<typeof setTimeout> | null = null;
465
+
466
+ function scheduleWindowResize(tableRectWidth: number, tableRectHeight: number) {
467
+ if (resizeDebounceId) clearTimeout(resizeDebounceId);
468
+
469
+ resizeDebounceId = setTimeout(() => {
470
+ resizeDebounceId = null;
471
+ onWindowResize();
472
+
473
+ tableHeight.value = tableRectHeight;
474
+ tableWidth.value = tableRectWidth;
475
+ }, 300);
476
+ }
448
477
 
449
478
  onMounted(async () => {
450
479
  document.addEventListener("keyup", onKeyupEsc);
451
480
  document.addEventListener("scroll", onScroll, { passive: true });
452
- window.addEventListener("resize", onWindowResize);
453
481
 
454
482
  await nextTick();
455
483
  updateHeaderOffsetTop();
@@ -461,9 +489,7 @@ onMounted(async () => {
461
489
 
462
490
  if (!entry) return;
463
491
 
464
- tableHeight.value = entry.contentRect.height;
465
- tableWidth.value = entry.contentRect.width;
466
- calculateStickyColumnPositions();
492
+ scheduleWindowResize(entry.contentRect.width, entry.contentRect.height);
467
493
  });
468
494
 
469
495
  resizeObserver.observe(tableWrapperRef.value);
@@ -473,12 +499,15 @@ onMounted(async () => {
473
499
  onBeforeUnmount(() => {
474
500
  document.removeEventListener("keyup", onKeyupEsc);
475
501
  document.removeEventListener("scroll", onScroll);
476
- window.removeEventListener("resize", onWindowResize);
477
502
  resizeObserver?.disconnect();
478
503
 
479
504
  if (scrollRafId !== null) {
480
505
  cancelAnimationFrame(scrollRafId);
481
506
  }
507
+
508
+ if (resizeDebounceId !== null) {
509
+ clearTimeout(resizeDebounceId);
510
+ }
482
511
  });
483
512
 
484
513
  function onChangeSelectedRows() {
@@ -494,8 +523,6 @@ function onChangeExpandedRows() {
494
523
  }
495
524
 
496
525
  function onWindowResize() {
497
- tableWidth.value = tableWrapperRef.value?.offsetWidth || 0;
498
-
499
526
  updateHeaderOffsetTop();
500
527
  setHeaderCellWidth();
501
528
  setFooterCellWidth();
@@ -721,7 +748,7 @@ function onBodyClick(event: MouseEvent) {
721
748
 
722
749
  if (!rowId) return;
723
750
 
724
- const rowData = visibleFlatRows.value.find((r) => String(r.id) === rowId);
751
+ const rowData = flatTableRows.value.find((r) => String(r.id) === rowId);
725
752
 
726
753
  if (!rowData) return;
727
754
 
@@ -776,7 +803,7 @@ function onBodyDoubleClick(event: MouseEvent) {
776
803
 
777
804
  if (!rowId) return;
778
805
 
779
- const rowData = visibleFlatRows.value.find((r) => String(r.id) === rowId);
806
+ const rowData = flatTableRows.value.find((r) => String(r.id) === rowId);
780
807
 
781
808
  if (!rowData) return;
782
809
 
@@ -820,9 +847,8 @@ function clearSelectedItems() {
820
847
 
821
848
  function onToggleExpand(row: Row) {
822
849
  const expanded = localExpandedRows.value;
823
- const targetIndex = expanded.indexOf(row.id);
824
850
 
825
- if (~targetIndex) {
851
+ if (expandedRowsSet.value.has(row.id)) {
826
852
  const idsToRemove = new Set([row.id, ...getRowChildrenIds(row)]);
827
853
 
828
854
  localExpandedRows.value = expanded.filter((id) => !idsToRemove.has(id));
@@ -1047,7 +1073,7 @@ function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
1047
1073
  config: getDateDividerConfig(row, isSelected),
1048
1074
  });
1049
1075
 
1050
- return h("tr", propsDateDivider, [
1076
+ return h("tr", { ...propsDateDivider, key: `date-divider-${row.id}` }, [
1051
1077
  h(
1052
1078
  "td",
1053
1079
  {
@@ -1059,81 +1085,13 @@ function renderDateDividerRow(row: FlatRow, rowIndex: number): VNode | null {
1059
1085
  ]);
1060
1086
  }
1061
1087
 
1062
- function renderTableRowSlots(row: FlatRow, rowIndex: number) {
1063
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
1064
- const cellSlots: Record<string, Function> = {};
1065
-
1066
- // Create cell slots
1067
- Object.entries(mapRowColumns(row, normalizedColumns.value)).forEach(([key], cellIndex) => {
1068
- cellSlots[`cell-${key}`] = ({
1069
- value: cellValue,
1070
- row: cellRow,
1071
- }: {
1072
- value: Cell;
1073
- row: FlatRow;
1074
- }) => {
1075
- const hasCellSlot = hasSlotContent(slots[`cell-${key}`], {
1076
- value: cellValue,
1077
- row: cellRow,
1078
- index: rowIndex,
1079
- cellIndex,
1080
- });
1081
-
1082
- if (hasCellSlot) {
1083
- return slots[`cell-${key}`]?.({
1084
- value: cellValue,
1085
- row: cellRow,
1086
- index: rowIndex,
1087
- cellIndex,
1088
- });
1089
- }
1090
-
1091
- return null;
1092
- };
1093
- });
1094
-
1095
- // Add expand slot
1096
- cellSlots.expand = ({ row: expandedRow, expanded }: { row: FlatRow; expanded: boolean }) => {
1097
- const hasExpandSlot = hasSlotContent(slots.expand, {
1098
- index: rowIndex,
1099
- row: expandedRow,
1100
- expanded,
1101
- });
1102
-
1103
- if (hasExpandSlot) {
1104
- return slots.expand?.({ index: rowIndex, row: expandedRow, expanded });
1105
- }
1106
-
1107
- return null;
1108
- };
1109
-
1110
- // Add nested-row slot
1111
- cellSlots["nested-row"] = () => {
1112
- const hasNestedRowSlot = hasSlotContent(slots["nested-row"], {
1113
- index: rowIndex,
1114
- row,
1115
- nestedLevel: Number(row.nestedLevel || 0),
1116
- });
1117
-
1118
- if (hasNestedRowSlot && row) {
1119
- return slots["nested-row"]?.({
1120
- index: rowIndex,
1121
- row,
1122
- nestedLevel: Number(row.nestedLevel || 0),
1123
- });
1124
- }
1125
-
1126
- return null;
1127
- };
1128
-
1129
- return cellSlots;
1130
- }
1131
-
1132
1088
  function renderTableRow(row: FlatRow, rowIndex: number): VNode {
1133
1089
  return h(
1134
1090
  UTableRow,
1135
1091
  {
1092
+ key: row.id,
1136
1093
  selectable: props.selectable,
1094
+ rowIndex,
1137
1095
  row,
1138
1096
  columns: normalizedColumns.value,
1139
1097
  config: config.value,
@@ -1143,15 +1101,16 @@ function renderTableRow(row: FlatRow, rowIndex: number): VNode {
1143
1101
  emptyCellLabel: props.emptyCellLabel,
1144
1102
  "data-test": getDataTest("row"),
1145
1103
  "data-row-id": row.id,
1146
- isExpanded: localExpandedRows.value.includes(row.id),
1104
+ isExpanded: expandedRowsSet.value.has(row.id),
1147
1105
  isChecked: isRowSelected(row),
1106
+ isVisible: isRowVisible(row),
1148
1107
  columnPositions: columnPositions.value,
1149
1108
  search: props.search,
1150
1109
  searchMatchColumns: getRowSearchMatchColumns(row),
1151
1110
  activeSearchMatchColumn: getRowActiveSearchMatchColumn(row),
1152
1111
  textEllipsis: props.textEllipsis,
1153
1112
  } as unknown as UTableRowProps,
1154
- renderTableRowSlots(row, rowIndex),
1113
+ slots,
1155
1114
  );
1156
1115
  }
1157
1116
 
@@ -1,21 +1,32 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, useTemplateRef, useSlots, useAttrs, h } from "vue";
3
- import { hasSlotContent, isEmptyValue } from "../utils/helper";
4
- import { cx } from "../utils/ui";
5
-
6
- import { PX_IN_REM } from "../constants";
7
- import { mapRowColumns } from "./utilTable";
2
+ import {
3
+ Comment,
4
+ Fragment,
5
+ Text,
6
+ computed,
7
+ h,
8
+ onMounted,
9
+ useAttrs,
10
+ useSlots,
11
+ useTemplateRef,
12
+ } from "vue";
8
13
 
9
14
  import { useUI } from "../composables/useUI";
10
15
  import { useMutationObserver } from "../composables/useMutationObserver";
11
16
 
12
- import UIcon from "../ui.image-icon/UIcon.vue";
13
- import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
17
+ import { isEmptyValue } from "../utils/helper";
18
+ import { cx } from "../utils/ui";
14
19
 
15
- import defaultConfig from "./config";
20
+ import { PX_IN_REM } from "../constants";
16
21
 
22
+ import defaultConfig from "./config";
23
+ import { mapRowColumns } from "./utilTable";
17
24
  import { StickySide } from "./types";
18
- import type { VNode } from "vue";
25
+
26
+ import UCheckbox from "../ui.form-checkbox/UCheckbox.vue";
27
+ import UIcon from "../ui.image-icon/UIcon.vue";
28
+
29
+ import type { Slot, VNode } from "vue";
19
30
  import type { Cell, CellObject, Row, UTableRowProps, Config, ColumnObject } from "./types";
20
31
 
21
32
  const NESTED_ROW_SHIFT_REM = 1.5;
@@ -29,7 +40,6 @@ const slots = useSlots();
29
40
  const attrs = useAttrs();
30
41
 
31
42
  const cellRef = useTemplateRef<HTMLDivElement[]>("cell");
32
- const toggleWrapperRef = useTemplateRef<HTMLDivElement[]>("toggle-wrapper");
33
43
 
34
44
  if (props.textEllipsis) {
35
45
  useMutationObserver(cellRef, setCellTitle, {
@@ -67,17 +77,6 @@ function getToggleIconName() {
67
77
  : props.config?.defaults?.expandIcon;
68
78
  }
69
79
 
70
- function getIconWidth() {
71
- const icon = document.querySelector(`[data-row-toggle-icon='${props.row.id}']`);
72
- const currentWrapperWidth = toggleWrapperRef.value?.at(0)?.getBoundingClientRect()?.width || 0;
73
-
74
- if (icon) {
75
- return `${icon.getBoundingClientRect().width / PX_IN_REM}rem`;
76
- }
77
-
78
- return `${currentWrapperWidth / PX_IN_REM || 1}rem`;
79
- }
80
-
81
80
  function getCellClasses(row: Row, key: string) {
82
81
  const isCellData = typeof row[key] === "object" && row[key] !== null && "class" in row[key];
83
82
  const cell = row[key] as CellObject;
@@ -234,6 +233,54 @@ function isNestedFirstCell(index: number): boolean {
234
233
  return (Boolean(props.row.row) || Boolean(props.nestedLevel)) && index === 0;
235
234
  }
236
235
 
236
+ function getRowIndex(): number {
237
+ return props.rowIndex ?? 0;
238
+ }
239
+
240
+ function isSlotContentEmpty(content: unknown): boolean {
241
+ const toArray = (arg: unknown) => {
242
+ return Array.isArray(arg) ? arg : [arg];
243
+ };
244
+
245
+ if (content === null || content === undefined) return true;
246
+
247
+ if (typeof content === "boolean" || typeof content === "number") {
248
+ return false;
249
+ }
250
+
251
+ if (typeof content === "string") {
252
+ return content.length === 0;
253
+ }
254
+
255
+ return toArray(content).every((node) => {
256
+ if (node === null || node === undefined) return true;
257
+
258
+ if (typeof node === "boolean" || typeof node === "number") {
259
+ return false;
260
+ }
261
+
262
+ if (typeof node === "string") {
263
+ return node.length === 0;
264
+ }
265
+
266
+ const vnode = node as VNode;
267
+
268
+ return (
269
+ vnode.type === Comment ||
270
+ (vnode.type === Text && !vnode.children?.length) ||
271
+ (vnode.type === Fragment && !vnode.children?.length)
272
+ );
273
+ });
274
+ }
275
+
276
+ function resolveSlotContent(slot: Slot | undefined, slotParams: Record<string, unknown>) {
277
+ if (!slot) return null;
278
+
279
+ const content = slot(slotParams);
280
+
281
+ return isSlotContentEmpty(content) ? null : content;
282
+ }
283
+
237
284
  function shouldRenderCellWrapper(row: Row, key: string): boolean {
238
285
  return Boolean(
239
286
  props.textEllipsis ||
@@ -242,13 +289,19 @@ function shouldRenderCellWrapper(row: Row, key: string): boolean {
242
289
  );
243
290
  }
244
291
 
245
- function renderCellContent(value: Cell, key: string, index: number): VNode | VNode[] | string {
292
+ function renderCellContent(value: Cell, key: string, cellIndex: number): VNode | VNode[] | string {
246
293
  const keyStr = String(key);
247
- const hasCellSlot = hasSlotContent(slots[`cell-${key}`], { value, row: props.row, index });
294
+
295
+ const slotContent = resolveSlotContent(slots[`cell-${key}`], {
296
+ value,
297
+ row: props.row,
298
+ index: getRowIndex(),
299
+ cellIndex,
300
+ });
248
301
 
249
302
  // Check if slot exists
250
- if (hasCellSlot) {
251
- return slots[`cell-${key}`]?.({ value, row: props.row, index }) || "";
303
+ if (slotContent) {
304
+ return slotContent as VNode | VNode[] | string;
252
305
  }
253
306
 
254
307
  // Render cell wrapper with highlighted HTML
@@ -268,9 +321,11 @@ function renderCellContent(value: Cell, key: string, index: number): VNode | VNo
268
321
  return formatCellValue(value);
269
322
  }
270
323
 
271
- function renderNestedFirstCell(value: Cell, key: string, index: number): VNode {
324
+ function renderNestedFirstCell(value: Cell, key: string, cellIndex: number): VNode {
272
325
  const keyStr = String(key);
273
- const hasExpandSlot = hasSlotContent(slots?.expand, {
326
+
327
+ const expandSlotContent = resolveSlotContent(slots.expand, {
328
+ index: getRowIndex(),
274
329
  row: props.row,
275
330
  expanded: props.isExpanded,
276
331
  });
@@ -286,9 +341,7 @@ function renderNestedFirstCell(value: Cell, key: string, index: number): VNode {
286
341
  const toggleWrapperNode = h(
287
342
  "div",
288
343
  {
289
- ref: toggleWrapperRef,
290
344
  ...props.attrs.bodyCellNestedIconWrapperAttrs.value,
291
- style: { width: getIconWidth() },
292
345
  },
293
346
  [toggleIconNode],
294
347
  );
@@ -300,12 +353,15 @@ function renderNestedFirstCell(value: Cell, key: string, index: number): VNode {
300
353
  "data-expand-icon": props.row.id,
301
354
  onDblclick: (e: Event) => e.stopPropagation(),
302
355
  },
303
- hasExpandSlot
304
- ? slots?.expand?.({ row: props.row, expanded: props.isExpanded })
305
- : [toggleWrapperNode],
356
+ expandSlotContent || [toggleWrapperNode],
306
357
  );
307
358
 
308
- const hasCellSlot = hasSlotContent(slots[`cell-${key}`], { value, row: props.row, index });
359
+ const cellSlotContent = resolveSlotContent(slots[`cell-${key}`], {
360
+ value,
361
+ row: props.row,
362
+ index: getRowIndex(),
363
+ cellIndex,
364
+ });
309
365
 
310
366
  return h(
311
367
  "div",
@@ -317,10 +373,8 @@ function renderNestedFirstCell(value: Cell, key: string, index: number): VNode {
317
373
  props.row.row ? toggleIconWrapperNode : null,
318
374
  // Cell content
319
375
  ...(() => {
320
- if (hasCellSlot) {
321
- const slotContent = slots[`cell-${key}`]?.({ value, row: props.row, index });
322
-
323
- return Array.isArray(slotContent) ? slotContent : [slotContent];
376
+ if (cellSlotContent) {
377
+ return Array.isArray(cellSlotContent) ? cellSlotContent : [cellSlotContent];
324
378
  }
325
379
 
326
380
  if (shouldRenderCellWrapper(props.row, keyStr)) {
@@ -351,6 +405,8 @@ function renderTableCell(value: Cell, key: string, index: number): VNode {
351
405
  ? renderNestedFirstCell(value, key, index)
352
406
  : renderCellContent(value, key, index);
353
407
 
408
+ const cellChildren = Array.isArray(nestedCellNode) ? nestedCellNode : [nestedCellNode];
409
+
354
410
  return h(
355
411
  "td",
356
412
  {
@@ -367,7 +423,7 @@ function renderTableCell(value: Cell, key: string, index: number): VNode {
367
423
  "data-cell-key": key,
368
424
  "data-test": getDataTest(`${key}-cell`),
369
425
  },
370
- [nestedCellNode],
426
+ cellChildren,
371
427
  );
372
428
  }
373
429
 
@@ -404,16 +460,7 @@ function renderCheckboxCell(): VNode | null {
404
460
  );
405
461
  }
406
462
 
407
- function renderMainRow(): VNode | null {
408
- const hasNestedRowSlot = hasSlotContent(slots["nested-row"], {
409
- row: props.row,
410
- nestedLevel: props.nestedLevel,
411
- });
412
-
413
- if (hasNestedRowSlot && props.row.parentRowId) {
414
- return null;
415
- }
416
-
463
+ function renderMainRow(): VNode {
417
464
  const cells = Object.entries(mapRowColumns(props.row, props.columns)).map(
418
465
  ([key, value], index) => {
419
466
  return renderTableCell(value, key, index);
@@ -426,38 +473,48 @@ function renderMainRow(): VNode | null {
426
473
  ...attrs,
427
474
  ...getRowAttrs(),
428
475
  class: cx([getRowAttrs().class, getRowClasses(props.row)]),
476
+ style: { display: props.isVisible ? "" : "none" },
429
477
  },
430
478
  [renderCheckboxCell(), ...cells].filter(Boolean),
431
479
  );
432
480
  }
433
481
 
434
- function renderNestedRow(): VNode | null {
435
- const hasNestedRowSlot = hasSlotContent(slots["nested-row"], {
436
- row: props.row,
437
- nestedLevel: props.nestedLevel,
438
- });
439
-
440
- if (!hasNestedRowSlot || !props.row.parentRowId) {
441
- return null;
442
- }
443
-
444
- const nestedRowSlotContent = slots["nested-row"]?.({
445
- row: props.row,
446
- nestedLevel: props.nestedLevel,
447
- });
448
-
482
+ function renderNestedRow(nestedRowSlotContent: VNode[]): VNode {
449
483
  const tdNode = h(
450
484
  "td",
451
485
  { colspan: props.columns.length + Number(props.selectable) },
452
486
  nestedRowSlotContent,
453
487
  );
454
488
 
455
- return h("tr", { class: props.row.class }, [tdNode]);
489
+ return h(
490
+ "tr",
491
+ {
492
+ class: props.row.class,
493
+ style: { display: props.isVisible ? "" : "none" },
494
+ },
495
+ [tdNode],
496
+ );
497
+ }
498
+
499
+ function renderRows(): VNode[] {
500
+ if (props.row.parentRowId) {
501
+ const nestedRowSlotContent = resolveSlotContent(slots["nested-row"], {
502
+ index: getRowIndex(),
503
+ row: props.row,
504
+ nestedLevel: props.nestedLevel,
505
+ });
506
+
507
+ if (nestedRowSlotContent) {
508
+ return [renderNestedRow(nestedRowSlotContent)];
509
+ }
510
+ }
511
+
512
+ return [renderMainRow()];
456
513
  }
457
514
 
458
515
  const { getDataTest } = useUI<Config>(defaultConfig);
459
516
  </script>
460
517
 
461
518
  <template>
462
- <component :is="() => [renderMainRow(), renderNestedRow()].filter(Boolean)" />
519
+ <component :is="renderRows" />
463
520
  </template>
@@ -68,6 +68,7 @@ describe("UTableRow.vue", () => {
68
68
  config: defaultConfig,
69
69
  isChecked: false,
70
70
  isExpanded: false,
71
+ isVisible: true,
71
72
  columnPositions,
72
73
  ...overrides,
73
74
  };
@@ -459,5 +460,26 @@ describe("UTableRow.vue", () => {
459
460
 
460
461
  expect(iconWrapper.exists()).toBe(true);
461
462
  });
463
+
464
+ it("isVisible – shows row when isVisible is true", () => {
465
+ const component = mount(UTableRow, {
466
+ props: getDefaultProps({ isVisible: true }),
467
+ });
468
+
469
+ const row = component.find("tr");
470
+ const style = row.attributes("style") || "";
471
+
472
+ expect(style).not.toContain("display: none");
473
+ });
474
+
475
+ it("isVisible – hides row when isVisible is false", () => {
476
+ const component = mount(UTableRow, {
477
+ props: getDefaultProps({ isVisible: false }),
478
+ });
479
+
480
+ const row = component.find("tr");
481
+
482
+ expect(row.attributes("style")).toContain("display: none");
483
+ });
462
484
  });
463
485
  });
@@ -185,6 +185,11 @@ export interface UTableRowAttrs {
185
185
  export interface UTableRowProps {
186
186
  row: FlatRow;
187
187
  columns: ColumnObject[];
188
+ /**
189
+ * Row index in the parent table (used for slot params).
190
+ * Optional to keep UTableRow mountable standalone in tests/internal usage.
191
+ */
192
+ rowIndex?: number;
188
193
  emptyCellLabel?: string;
189
194
  selectable: boolean;
190
195
  nestedLevel: number;
@@ -194,6 +199,7 @@ export interface UTableRowProps {
194
199
  config: Config;
195
200
  isChecked: boolean;
196
201
  isExpanded: boolean;
202
+ isVisible: boolean;
197
203
  columnPositions: Map<string, number>;
198
204
  search?: string;
199
205
  searchMatchColumns?: Set<string>;