@vueye-table/headless 3.0.0-alpha.7 → 3.0.0-alpha.9

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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { injectDataGrid, injectDataTable, provideDataGrid, provideDataTable, useDataGrid, useVirtualColumns, useVirtualRows } from "@vueye-table/vue";
2
2
  import { Fragment, computed, defineComponent, h, inject, nextTick, onMounted, onScopeDispose, provide, ref, shallowRef, useId, watch } from "vue";
3
+ import { getRowItemKey, gridCommand } from "@vueye-table/core";
3
4
 
4
5
  //#region src/shared.ts
5
6
  /** The element a component renders. Every headless component accepts `as`. */
@@ -76,14 +77,16 @@ const DataTableSelectRow = defineComponent({
76
77
  setup(props, { slots }) {
77
78
  const table = injectDataTable("<DataTableSelectRow>");
78
79
  return () => {
79
- const selected = table.isSelected(props.row.key);
80
+ const selected = table.tree ? props.row.selection === "all" : table.isSelected(props.row.key);
81
+ const indeterminate = table.tree && props.row.selection === "some";
80
82
  const toggle = () => table.toggleRow(props.row.key);
81
83
  return slots.default?.({
82
84
  selected,
85
+ indeterminate,
83
86
  toggle
84
87
  }) ?? checkbox({
85
88
  checked: selected,
86
- indeterminate: false,
89
+ indeterminate,
87
90
  label: props.label,
88
91
  disabled: table.selectionMode === "none",
89
92
  onChange: toggle
@@ -244,10 +247,6 @@ const DataTableColumnVisibility = defineComponent({
244
247
  };
245
248
  }
246
249
  });
247
- /**
248
- * A polite live region describing what is shown, such as "1–10 of 57 rows". Screen readers
249
- * announce it after a search, filter, or page change.
250
- */
251
250
  const DataTableStatus = defineComponent({
252
251
  name: "DataTableStatus",
253
252
  slots: Object,
@@ -260,17 +259,71 @@ const DataTableStatus = defineComponent({
260
259
  end: table.pageEnd,
261
260
  rowCount: table.rowCount,
262
261
  totalRowCount: table.totalRowCount,
263
- selectedCount: table.selectedCount
262
+ selectedCount: table.selectedCount,
263
+ loadState: table.loadState,
264
+ loadedRowCount: table.loadedRowCount,
265
+ loadError: table.loadError,
266
+ canLoadMore: table.canLoadMore,
267
+ retry: () => table.retry(),
268
+ loadNext: () => table.loadNext(),
269
+ loadingChildrenCount: table.tree ? table.processedRows.filter((row) => row.childStatus === "loading").length : 0,
270
+ childErrorCount: table.tree ? table.processedRows.filter((row) => row.childStatus === "error").length : 0
264
271
  };
265
- const text = table.rowCount === 0 ? "No rows" : `${table.pageStart}–${table.pageEnd} of ${table.rowCount} rows` + (table.selectedCount > 0 ? `, ${table.selectedCount} selected` : "");
272
+ const count = `Loaded ${table.loadedRowCount.toLocaleString()} ${table.loadedRowCount === 1 ? "row" : "rows"}`;
273
+ const text = table.loadingMode ? table.loadState === "error" ? `${count}. Could not load rows. Retry loading.` : table.loadState === "aborted" ? `${count}. Loading stopped.` : table.loadState === "loading" || table.loadState === "streaming" ? `${count}…` : `${count}${table.loadState === "done" ? ". All rows loaded." : "."}` : table.rowCount === 0 ? "No rows" : `${table.pageStart}–${table.pageEnd} of ${table.rowCount} rows` + (table.selectedCount > 0 ? `, ${table.selectedCount} selected` : "");
266
274
  return h(props.as, {
267
275
  role: "status",
268
276
  "aria-live": "polite"
269
- }, slots.default?.(slotProps) ?? text);
277
+ }, slots.default?.(slotProps) ?? text + (slotProps.loadingChildrenCount ? " Loading children…" : "") + (slotProps.childErrorCount ? " Could not load children. Retry children in the affected row." : ""));
270
278
  };
271
279
  }
272
280
  });
273
281
 
282
+ //#endregion
283
+ //#region src/loading.ts
284
+ /** Keep this visual sentinel out of the live region and logical data-row count. */
285
+ function loadSentinel(table, colspan) {
286
+ return table.loadingMode === "cursor" && table.loadedRowCount > 0 && (table.loadState === "loading" || table.loadState === "streaming") ? h("tr", {
287
+ key: "vt-load-sentinel",
288
+ "data-load-sentinel": "",
289
+ "aria-hidden": "true"
290
+ }, [h("td", {
291
+ colspan: Math.max(1, colspan),
292
+ class: "vt-load-sentinel"
293
+ }, "Loading more rows…")]) : void 0;
294
+ }
295
+ /** A keyboard-accessible alternative to automatic virtual-end loading, with error recovery. */
296
+ const DataTableLoadMore = defineComponent({
297
+ name: "DataTableLoadMore",
298
+ props: {
299
+ label: {
300
+ type: String,
301
+ default: "Load more rows"
302
+ },
303
+ retryLabel: {
304
+ type: String,
305
+ default: "Retry loading"
306
+ },
307
+ loadingLabel: {
308
+ type: String,
309
+ default: "Loading rows…"
310
+ }
311
+ },
312
+ setup(props) {
313
+ const table = injectDataTable("<DataTableLoadMore>");
314
+ return () => table.loadState === "error" ? h("button", {
315
+ type: "button",
316
+ onClick: () => table.retry()
317
+ }, props.retryLabel) : table.loadingMode === "cursor" && table.loadState !== "done" ? h("button", {
318
+ type: "button",
319
+ disabled: !table.canLoadMore,
320
+ onClick: () => {
321
+ table.loadNext();
322
+ }
323
+ }, table.loadState === "loading" || table.loadState === "streaming" ? props.loadingLabel : props.label) : null;
324
+ }
325
+ });
326
+
274
327
  //#endregion
275
328
  //#region src/virtual.ts
276
329
  /** Shared opt-in renderer props. Options are applied when the virtual renderer is created. */
@@ -349,6 +402,15 @@ function createComponentVirtual(table, options, tableElement, grid, virtualColum
349
402
  grid,
350
403
  scrollMargin: () => tableElement()?.querySelector("thead")?.getBoundingClientRect().height ?? 0
351
404
  });
405
+ onMounted(() => {
406
+ watch(() => [
407
+ rows.endIndex,
408
+ table.renderItems.length,
409
+ table.canLoadMore
410
+ ], () => {
411
+ if (table.loadingMode === "cursor" && table.canLoadMore && rows.endIndex >= table.renderItems.length - 1 - table.endThreshold) table.loadNext();
412
+ }, { immediate: true });
413
+ });
352
414
  const columns = grid && virtualColumns ? useVirtualColumns(grid, {
353
415
  scrollElement,
354
416
  estimateColumnWidth: (index) => table.columns[index]?.width ?? options.columnWidth ?? 120,
@@ -499,6 +561,269 @@ function virtualRow(binding, row) {
499
561
  return binding.rowItems.find((item) => item.renderItem.kind === "row" && item.renderItem.row.key === row.key);
500
562
  }
501
563
 
564
+ //#endregion
565
+ //#region src/hierarchy.ts
566
+ const hierarchyProps = {
567
+ treeColumn: {
568
+ type: String,
569
+ default: void 0
570
+ },
571
+ keepAliveDetail: {
572
+ type: Boolean,
573
+ default: false
574
+ }
575
+ };
576
+ const hierarchyKey = Symbol("vueye-table-hierarchy");
577
+ function provideHierarchy(table, props) {
578
+ const focus = shallowRef();
579
+ const focusKey = computed(() => table.rows.some((row) => row.key === focus.value) ? focus.value : table.rows[0]?.key);
580
+ const context = {
581
+ id: `vt-tree-${useId()}`,
582
+ table,
583
+ get treeColumn() {
584
+ return props.treeColumn;
585
+ },
586
+ get keepAliveDetail() {
587
+ return props.keepAliveDetail;
588
+ },
589
+ focus,
590
+ get focusKey() {
591
+ return focusKey.value;
592
+ }
593
+ };
594
+ provide(hierarchyKey, context);
595
+ return context;
596
+ }
597
+ function injectHierarchy() {
598
+ return inject(hierarchyKey, void 0);
599
+ }
600
+ function resolveTreeColumn(table, column) {
601
+ return table.columns.find((candidate) => candidate.id === column)?.id ?? table.columns[0]?.id;
602
+ }
603
+ function itemId(context, row, detail = false) {
604
+ return context ? `${context.id}-${encodeURIComponent(getRowItemKey(row.key, detail ? "detail" : "row"))}` : void 0;
605
+ }
606
+ const siblingCache = /* @__PURE__ */ new WeakMap();
607
+ function hierarchyRowAttrs(table, row, context) {
608
+ if (!table.tree) return {};
609
+ let positions = siblingCache.get(table.processedRows);
610
+ if (!positions) {
611
+ const groups = /* @__PURE__ */ new Map();
612
+ for (const candidate of table.processedRows) {
613
+ const group = groups.get(candidate.parentKey) ?? [];
614
+ group.push(candidate);
615
+ groups.set(candidate.parentKey, group);
616
+ }
617
+ const indices = new Map(table.processedRows.map((candidate, index) => [candidate.key, index]));
618
+ const result = /* @__PURE__ */ new Map();
619
+ for (const group of groups.values()) group.forEach((candidate, index) => result.set(candidate.key, {
620
+ position: index + 1,
621
+ size: group.length,
622
+ index: indices.get(candidate.key)
623
+ }));
624
+ positions = result;
625
+ siblingCache.set(table.processedRows, result);
626
+ }
627
+ const position = positions.get(row.key);
628
+ return {
629
+ id: itemId(context, row),
630
+ "data-tree-key": getRowItemKey(row.key),
631
+ "aria-level": row.depth + 1,
632
+ "aria-expanded": row.canExpand ? String(row.isExpanded) : void 0,
633
+ "aria-setsize": position?.size,
634
+ "aria-posinset": position?.position,
635
+ "aria-rowindex": position ? position.index + 2 : void 0,
636
+ "aria-busy": row.childStatus === "loading" ? "true" : void 0
637
+ };
638
+ }
639
+ /** Row-focused table navigation; cell-focused grids use the same pure core commands. */
640
+ function treeKeydown(event, context, virtual) {
641
+ const table = context.table;
642
+ const target = event.target;
643
+ if (!table.tree || !target?.closest) return;
644
+ if (target.closest("input, textarea, select, [contenteditable=true]") || target.closest("button") && !target.closest("[data-expand-toggle]")) return;
645
+ const key = target.closest("tr")?.getAttribute("data-tree-key");
646
+ const index = table.rows.findIndex((row) => getRowItemKey(row.key) === key);
647
+ const row = table.rows[index];
648
+ if (!row) return;
649
+ const command = gridCommand(event, {
650
+ canExpand: row.canExpand,
651
+ expanded: row.isExpanded
652
+ });
653
+ let next = row;
654
+ if (command?.type === "tree") {
655
+ if (command.action === "expand" || command.action === "collapse") table.toggleExpanded(row.key, command.action === "expand");
656
+ else if (command.action === "siblings") for (const sibling of table.processedRows.filter((candidate) => candidate.parentKey === row.parentKey && candidate.canExpand)) table.toggleExpanded(sibling.key, true);
657
+ else if (command.action === "child") next = table.rows.find((candidate) => candidate.parentKey === row.key) ?? row;
658
+ else next = table.rows.find((candidate) => candidate.key === row.parentKey) ?? row;
659
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") next = table.rows[index + (event.key === "ArrowDown" ? 1 : -1)] ?? row;
660
+ else if (event.key === "Home" || event.key === "End") next = table.rows[event.key === "Home" ? 0 : table.rows.length - 1] ?? row;
661
+ else return;
662
+ event.preventDefault();
663
+ context.focus.value = next.key;
664
+ virtual?.rows.scrollToKey(next.key);
665
+ const id = itemId(context, next);
666
+ nextTick(() => target.closest("table")?.ownerDocument.getElementById(id ?? "")?.focus());
667
+ }
668
+ const expandToggleProps = {
669
+ row: rowProp,
670
+ controls: {
671
+ type: String,
672
+ default: void 0
673
+ },
674
+ expandLabel: {
675
+ type: String,
676
+ default: "Expand row"
677
+ },
678
+ collapseLabel: {
679
+ type: String,
680
+ default: "Collapse row"
681
+ }
682
+ };
683
+ const treeCellProps = {
684
+ row: rowProp,
685
+ as: asProp("div")
686
+ };
687
+ const DataTableExpandToggle = defineComponent({
688
+ name: "DataTableExpandToggle",
689
+ props: expandToggleProps,
690
+ slots: Object,
691
+ setup(props, { slots }) {
692
+ const table = injectDataTable("<DataTableExpandToggle>");
693
+ const context = injectHierarchy();
694
+ const grid = injectDataGrid();
695
+ return () => props.row.canExpand ? h("button", {
696
+ type: "button",
697
+ "data-expand-toggle": "",
698
+ tabindex: table.tree ? -1 : void 0,
699
+ "aria-expanded": String(props.row.isExpanded),
700
+ "aria-controls": props.controls ?? (!table.tree ? itemId(context, props.row, true) : props.row.isExpanded ? table.rows.filter((row) => row.parentKey === props.row.key).map((row) => itemId(context, row)).filter(Boolean).join(" ") || void 0 : void 0),
701
+ "aria-label": props.row.isExpanded ? props.collapseLabel : props.expandLabel,
702
+ onMousedown: (event) => event.stopPropagation(),
703
+ onDblclick: (event) => event.stopPropagation(),
704
+ onClick: (event) => {
705
+ event.stopPropagation();
706
+ if (grid?.table === table) grid.focusCell({
707
+ row: table.rows.indexOf(props.row),
708
+ column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
709
+ });
710
+ table.toggleExpanded(props.row.key);
711
+ if (grid?.table === table) nextTick(() => event.target.closest("table")?.focus());
712
+ }
713
+ }, slots.default?.({
714
+ row: props.row,
715
+ expanded: props.row.isExpanded
716
+ }) ?? (props.row.isExpanded ? props.collapseLabel : props.expandLabel)) : null;
717
+ }
718
+ });
719
+ const DataTableTreeCell = defineComponent({
720
+ name: "DataTableTreeCell",
721
+ props: treeCellProps,
722
+ slots: Object,
723
+ setup(props, { slots }) {
724
+ const table = injectDataTable("<DataTableTreeCell>");
725
+ const context = injectHierarchy();
726
+ const grid = injectDataGrid();
727
+ return () => h(props.as, {
728
+ class: "vt-tree-cell",
729
+ "data-tree-cell": "",
730
+ style: { paddingInlineStart: `calc(var(--vt-tree-indent, 1.25rem) * ${props.row.depth})` }
731
+ }, [
732
+ h("span", { class: "vt-tree-toggle" }, slots.toggle?.({ row: props.row }) ?? h(DataTableExpandToggle, { row: props.row })),
733
+ h("span", { class: "vt-tree-label" }, [slots.default?.()]),
734
+ props.row.childStatus === "loading" ? h("span", { class: "vt-tree-loading" }, "Loading children…") : null,
735
+ props.row.childStatus === "error" ? h("span", { class: "vt-tree-error" }, ["Could not load children. ", h("button", {
736
+ type: "button",
737
+ class: "vt-tree-retry",
738
+ onMousedown: (event) => event.stopPropagation(),
739
+ onClick: (event) => {
740
+ event.stopPropagation();
741
+ const element = event.currentTarget.closest("table");
742
+ if (grid?.table === table) grid.focusCell({
743
+ row: table.rows.indexOf(props.row),
744
+ column: Math.max(0, table.columns.findIndex((column) => column.id === resolveTreeColumn(table, context?.treeColumn)))
745
+ });
746
+ table.toggleExpanded(props.row.key, true);
747
+ nextTick(() => {
748
+ if (grid?.table === table) element?.focus();
749
+ else element?.ownerDocument.getElementById(itemId(context, props.row) ?? "")?.focus();
750
+ });
751
+ }
752
+ }, "Retry children")]) : null
753
+ ]);
754
+ }
755
+ });
756
+ /** One full-width detail; retained collapsed content remains hidden and unmeasured. */
757
+ const DataTableDetailRow = defineComponent({
758
+ name: "DataTableDetailRow",
759
+ props: {
760
+ row: rowProp,
761
+ colspan: {
762
+ type: Number,
763
+ default: void 0
764
+ },
765
+ hidden: {
766
+ type: Boolean,
767
+ default: false
768
+ },
769
+ keepAlive: {
770
+ type: Boolean,
771
+ default: void 0
772
+ }
773
+ },
774
+ slots: Object,
775
+ setup(props, { slots }) {
776
+ const table = injectDataTable("<DataTableDetailRow>");
777
+ const context = injectHierarchy();
778
+ const virtual = injectVirtual();
779
+ let opened = false;
780
+ return () => {
781
+ if (props.row.isExpanded && !props.hidden) opened = true;
782
+ if (!opened || (!props.row.isExpanded || props.hidden) && !(props.keepAlive ?? context?.keepAliveDetail)) return null;
783
+ const hidden = props.hidden || !props.row.isExpanded;
784
+ return h("tr", {
785
+ id: itemId(context, props.row, true),
786
+ "data-detail": "",
787
+ hidden,
788
+ ...virtual && !hidden ? { ref: (target) => virtual.rows.measureElement(target, getRowItemKey(props.row.key, "detail")) } : {}
789
+ }, [h("td", {
790
+ colspan: Math.max(1, props.colspan ?? table.columns.length),
791
+ class: "vt-detail-cell"
792
+ }, [slots.default?.({ row: props.row })])]);
793
+ };
794
+ }
795
+ });
796
+ function createDetails(table, slot, colspan) {
797
+ const context = injectHierarchy();
798
+ const visited = /* @__PURE__ */ new Set();
799
+ const render = (row, rowIndex, hidden = false) => {
800
+ if (!hidden && context?.keepAliveDetail) visited.add(row.key);
801
+ return h(DataTableDetailRow, {
802
+ key: getRowItemKey(row.key, "detail"),
803
+ row,
804
+ colspan: colspan(),
805
+ hidden
806
+ }, { default: () => slot()?.({
807
+ row,
808
+ rowIndex
809
+ }) });
810
+ };
811
+ return {
812
+ render,
813
+ retained(visible) {
814
+ if (!context?.keepAliveDetail) return [];
815
+ return [...visited].flatMap((key) => {
816
+ const index = table.rows.findIndex((row) => row.key === key);
817
+ if (index < 0) {
818
+ visited.delete(key);
819
+ return [];
820
+ }
821
+ return visible.has(key) ? [] : [render(table.rows[index], index, true)];
822
+ });
823
+ }
824
+ };
825
+ }
826
+
502
827
  //#endregion
503
828
  //#region src/table.ts
504
829
  /**
@@ -514,15 +839,20 @@ const DataTableRoot = defineComponent({
514
839
  required: true
515
840
  },
516
841
  as: asProp("table"),
517
- ...virtualProps
842
+ ...virtualProps,
843
+ ...hierarchyProps
518
844
  },
519
845
  setup(props, { slots }) {
520
846
  provideDataTable(props.table);
847
+ const hierarchy = provideHierarchy(props.table, props);
521
848
  const element = shallowRef();
522
849
  const render = (virtual) => h(props.as, {
523
- "aria-rowcount": props.table.rowCount + 1,
850
+ role: props.table.tree ? "treegrid" : void 0,
851
+ "aria-rowcount": (props.table.tree ? props.table.processedRows.length : props.table.rowCount) + 1,
524
852
  "aria-colcount": props.table.columns.length,
853
+ "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
525
854
  "data-empty": flag(props.table.rowCount === 0),
855
+ onKeydown: (event) => treeKeydown(event, hierarchy, virtual),
526
856
  ...virtual ? {
527
857
  ref: element,
528
858
  "data-virtual": ""
@@ -671,19 +1001,28 @@ const DataTableBody = defineComponent({
671
1001
  setup(props, { slots }) {
672
1002
  const table = injectDataTable("<DataTableBody>");
673
1003
  const virtual = injectVirtual();
1004
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
674
1005
  return () => {
675
1006
  if (props.virtual && !virtual) return h(VirtualDataTableBody, props, slots);
676
- if (virtual) return renderTableBody(table, virtual, props.as, props.colspan, slots);
1007
+ if (virtual) return renderTableBody(table, virtual, props.as, props.colspan, slots, details);
677
1008
  let content = slots.default?.({ rows: table.rows });
678
- if (!content) content = table.rows.length === 0 && slots.empty ? h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, table.columns.length) }, slots.empty())]) : table.rows.map((row) => h(DataTableRow, {
679
- key: row.key,
680
- row
681
- }));
682
- return h(props.as, content);
1009
+ if (!content) content = table.rows.length === 0 && slots.empty ? h("tr", { "data-empty": "" }, [h("td", { colspan: Math.max(1, table.columns.length) }, slots.empty())]) : table.renderItems.map((item) => item.kind === "row" ? slots.row ? h(Fragment, { key: item.key }, [slots.row(item)]) : h(DataTableRow, {
1010
+ key: item.key,
1011
+ row: item.row,
1012
+ rowIndex: item.rowIndex
1013
+ }) : details.render(item.row, item.rowIndex));
1014
+ const visibleDetails = new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key));
1015
+ const retained = slots.default ? [] : details.retained(visibleDetails);
1016
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length);
1017
+ return h(props.as, [
1018
+ ...Array.isArray(content) ? content : content ? [content] : [],
1019
+ ...retained,
1020
+ ...sentinel ? [sentinel] : []
1021
+ ]);
683
1022
  };
684
1023
  }
685
1024
  });
686
- function renderTableBody(table, virtual, as, colspan, slots, element) {
1025
+ function renderTableBody(table, virtual, as, colspan, slots, details, element) {
687
1026
  const width = colspan ?? table.columns.length;
688
1027
  const attrs = element ? { ref: element } : {};
689
1028
  const content = slots.default?.({ rows: virtual.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) });
@@ -691,17 +1030,18 @@ function renderTableBody(table, virtual, as, colspan, slots, element) {
691
1030
  if (content) return h(as, attrs, [
692
1031
  rowSpacer(virtual.rows.paddingStart, width, "gap-start"),
693
1032
  ...content,
694
- rowSpacer(virtual.rows.paddingEnd, width, "gap-end")
1033
+ rowSpacer(virtual.rows.paddingEnd, width, "gap-end"),
1034
+ ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
1035
+ ]);
1036
+ return h(as, attrs, [
1037
+ ...renderRows(virtual, width, (item) => item.renderItem.kind === "row" ? slots.row ? h(Fragment, { key: item.key }, [slots.row(item.renderItem)]) : h(DataTableRow, {
1038
+ key: item.key,
1039
+ row: item.renderItem.row,
1040
+ rowIndex: item.renderItem.rowIndex
1041
+ }) : details.render(item.renderItem.row, item.renderItem.rowIndex)),
1042
+ ...details.retained(new Set(virtual.rowItems.filter((item) => item.renderItem.kind === "detail").map((item) => item.renderItem.row.key))),
1043
+ ...loadSentinel(table, width) ? [loadSentinel(table, width)] : []
695
1044
  ]);
696
- return h(as, attrs, renderRows(virtual, width, (item) => item.renderItem.kind === "row" ? h(DataTableRow, {
697
- key: item.key,
698
- row: item.renderItem.row,
699
- rowIndex: item.renderItem.rowIndex
700
- }) : h("tr", {
701
- key: item.key,
702
- "data-detail": "",
703
- ref: (target) => virtual.rows.measureElement(target, item.key)
704
- }, [h("td", { colspan: Math.max(1, width) }, slots.detail?.(item.renderItem))])));
705
1045
  }
706
1046
  const VirtualDataTableBody = defineComponent({
707
1047
  name: "DataTableVirtualBody",
@@ -718,7 +1058,8 @@ const VirtualDataTableBody = defineComponent({
718
1058
  const table = injectDataTable("<DataTableBody>");
719
1059
  const element = shallowRef();
720
1060
  const virtual = createComponentVirtual(table, virtualOptions(props), () => element.value?.closest("table"));
721
- return () => renderTableBody(table, virtual, props.as, props.colspan, slots, element);
1061
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length);
1062
+ return () => renderTableBody(table, virtual, props.as, props.colspan, slots, details, element);
722
1063
  }
723
1064
  });
724
1065
  /** A body row; `aria-selected` and `data-selected` follow the selection. */
@@ -736,6 +1077,7 @@ const DataTableRow = defineComponent({
736
1077
  setup(props, { slots }) {
737
1078
  const table = injectDataTable("<DataTableRow>");
738
1079
  const virtual = injectVirtual();
1080
+ const hierarchy = injectHierarchy();
739
1081
  return () => {
740
1082
  const { row } = props;
741
1083
  const selected = table.isSelected(row.key);
@@ -746,6 +1088,11 @@ const DataTableRow = defineComponent({
746
1088
  "aria-selected": table.selectionMode === "none" ? void 0 : String(selected),
747
1089
  "data-selected": flag(selected),
748
1090
  "data-key": String(row.key),
1091
+ ...hierarchyRowAttrs(table, row, hierarchy),
1092
+ tabindex: table.tree ? (hierarchy?.focusKey ?? table.rows[0]?.key) === row.key ? 0 : -1 : void 0,
1093
+ onFocus: () => {
1094
+ if (hierarchy) hierarchy.focus.value = row.key;
1095
+ },
749
1096
  ...virtual && item ? {
750
1097
  ref: (element) => virtual.rows.measureElement(element, item.key),
751
1098
  "data-virtual-row": ""
@@ -769,22 +1116,29 @@ const DataTableCell = defineComponent({
769
1116
  props: {
770
1117
  row: rowProp,
771
1118
  column: columnProp,
772
- as: asProp("td")
1119
+ as: asProp("td"),
1120
+ tree: {
1121
+ type: Boolean,
1122
+ default: true
1123
+ }
773
1124
  },
774
1125
  setup(props, { slots }) {
1126
+ const table = injectDataTable("<DataTableCell>");
1127
+ const hierarchy = injectHierarchy();
775
1128
  return () => {
776
1129
  const { row, column } = props;
777
1130
  const value = row.getValue(column.id);
778
1131
  const display = row.getDisplay(column.id);
779
- return h(props.as, {
780
- "data-column": column.id,
781
- "data-align": column.align
782
- }, slots.default?.({
1132
+ const content = () => slots.default?.({
783
1133
  row,
784
1134
  column,
785
1135
  value,
786
1136
  display
787
- }) ?? display);
1137
+ }) ?? display;
1138
+ return h(props.as, {
1139
+ "data-column": column.id,
1140
+ "data-align": column.align
1141
+ }, props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: content }) : content());
788
1142
  };
789
1143
  }
790
1144
  });
@@ -833,6 +1187,7 @@ const DataGridRoot = defineComponent({
833
1187
  default: void 0
834
1188
  },
835
1189
  ...virtualProps,
1190
+ ...hierarchyProps,
836
1191
  /** Width reserved before data columns, for a row-number/header column. */
837
1192
  gutter: {
838
1193
  type: Number,
@@ -841,8 +1196,9 @@ const DataGridRoot = defineComponent({
841
1196
  },
842
1197
  setup(props, { slots, expose }) {
843
1198
  provideDataTable(props.table);
844
- const grid = useDataGrid(props.table);
1199
+ const grid = useDataGrid(props.table, { treeColumn: () => props.treeColumn });
845
1200
  provideDataGrid(grid);
1201
+ provideHierarchy(props.table, props);
846
1202
  const element = ref();
847
1203
  const id = `vt-grid-${useId()}`;
848
1204
  const focus = () => {
@@ -904,12 +1260,13 @@ const DataGridRoot = defineComponent({
904
1260
  return h(props.as, {
905
1261
  ref: element,
906
1262
  id,
907
- role: "grid",
1263
+ role: props.table.tree ? "treegrid" : "grid",
908
1264
  tabindex: 0,
909
1265
  "aria-label": props.label,
910
1266
  "aria-multiselectable": "true",
911
- "aria-rowcount": (binding ? props.table.rowCount : props.table.rows.length) + 1,
1267
+ "aria-rowcount": (props.table.tree ? props.table.processedRows.length : binding ? props.table.rowCount : props.table.rows.length) + 1,
912
1268
  "aria-colcount": props.table.columns.length,
1269
+ "aria-busy": props.table.loadState === "loading" || props.table.loadState === "streaming" ? "true" : void 0,
913
1270
  "aria-activedescendant": focusPosition && !grid.editor ? cellId(id, focusPosition.row, focusPosition.column) : void 0,
914
1271
  onKeydown,
915
1272
  onCopy: (event) => onCopy(event),
@@ -954,60 +1311,79 @@ const DataGridBody = defineComponent({
954
1311
  colspan: {
955
1312
  type: Number,
956
1313
  default: void 0
1314
+ },
1315
+ treeCell: {
1316
+ type: Boolean,
1317
+ default: true
957
1318
  }
958
1319
  },
959
1320
  setup(props, { slots }) {
960
1321
  const table = injectDataTable("<DataGridBody>");
961
1322
  const { grid } = useGrid("<DataGridBody>");
962
1323
  const virtual = injectVirtual();
1324
+ const hierarchy = injectHierarchy();
1325
+ const details = createDetails(table, () => slots.detail, () => props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
963
1326
  const element = ref();
964
1327
  const render = (binding) => {
965
- if (binding && slots.default) return h(props.as, { ref: element }, slots.default({ rows: binding.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) }));
966
- if (binding) return h(props.as, { ref: element }, renderRows(binding, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0), (item) => {
967
- if (item.renderItem.kind !== "row") return h("tr", {
968
- key: item.key,
969
- "aria-hidden": "true",
970
- "data-detail": "",
971
- style: { height: `${item.size}px` }
972
- }, [h("td", {
973
- colspan: Math.max(1, table.columns.length),
974
- style: { padding: "0" }
975
- })]);
976
- const { row, rowIndex } = item.renderItem;
977
- const columns = renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
978
- key: column.id,
979
- row,
980
- column,
981
- rowIndex,
982
- columnIndex
983
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0), "td");
1328
+ if (binding && slots.default) {
1329
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1330
+ const content = slots.default({ rows: binding.rowItems.filter((item) => item.renderItem.kind === "row").map((item) => item.renderItem.row) });
1331
+ return h(props.as, { ref: element }, sentinel ? [...content, sentinel] : content);
1332
+ }
1333
+ if (binding) return h(props.as, { ref: element }, [
1334
+ ...renderRows(binding, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0), (item) => {
1335
+ if (item.renderItem.kind !== "row") return details.render(item.renderItem.row, item.renderItem.rowIndex);
1336
+ const { row, rowIndex } = item.renderItem;
1337
+ const columns = renderColumns(table.columns, binding, (column, columnIndex) => h(DataGridCell, {
1338
+ key: column.id,
1339
+ row,
1340
+ column,
1341
+ rowIndex,
1342
+ columnIndex,
1343
+ tree: props.treeCell
1344
+ }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0), "td");
1345
+ return h("tr", {
1346
+ key: item.key,
1347
+ role: "row",
1348
+ "aria-rowindex": table.pageStart + rowIndex + 1,
1349
+ "data-key": String(row.key),
1350
+ ...hierarchyRowAttrs(table, row, hierarchy),
1351
+ "data-virtual-row": "",
1352
+ ref: (target) => binding.rows.measureElement(target, item.key)
1353
+ }, [slots.rowHeader?.({
1354
+ row,
1355
+ index: rowIndex
1356
+ }), ...columns]);
1357
+ }),
1358
+ ...details.retained(new Set(binding.rowItems.filter((item) => item.renderItem.kind === "detail").map((item) => item.renderItem.row.key))),
1359
+ ...loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0)) ? [loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0))] : []
1360
+ ]);
1361
+ const content = slots.default?.({ rows: table.rows }) ?? table.renderItems.map((item) => item.kind === "detail" ? details.render(item.row, item.rowIndex) : (() => {
1362
+ const { row, rowIndex } = item;
984
1363
  return h("tr", {
985
1364
  key: item.key,
986
1365
  role: "row",
987
- "aria-rowindex": table.pageStart + rowIndex + 1,
1366
+ "aria-rowindex": rowIndex + 2,
988
1367
  "data-key": String(row.key),
989
- "data-virtual-row": "",
990
- ref: (target) => binding.rows.measureElement(target, item.key)
1368
+ ...hierarchyRowAttrs(table, row, hierarchy)
991
1369
  }, [slots.rowHeader?.({
992
1370
  row,
993
1371
  index: rowIndex
994
- }), ...columns]);
995
- }));
996
- return h(props.as, slots.default?.({ rows: table.rows }) ?? table.rows.map((row, rowIndex) => h("tr", {
997
- key: row.key,
998
- role: "row",
999
- "aria-rowindex": rowIndex + 2,
1000
- "data-key": String(row.key)
1001
- }, [slots.rowHeader?.({
1002
- row,
1003
- index: rowIndex
1004
- }), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
1005
- key: column.id,
1006
- row,
1007
- column,
1008
- rowIndex,
1009
- columnIndex
1010
- }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))])));
1372
+ }), ...table.columns.map((column, columnIndex) => h(DataGridCell, {
1373
+ key: column.id,
1374
+ row,
1375
+ column,
1376
+ rowIndex,
1377
+ columnIndex,
1378
+ tree: props.treeCell
1379
+ }, slots.cell ? { default: (cellProps) => slots.cell?.(cellProps) } : void 0))]);
1380
+ })());
1381
+ const sentinel = loadSentinel(table, props.colspan ?? table.columns.length + (slots.rowHeader ? 1 : 0));
1382
+ return h(props.as, [
1383
+ ...content,
1384
+ ...slots.default ? [] : details.retained(new Set(table.renderItems.filter((item) => item.kind === "detail").map((item) => item.row.key))),
1385
+ sentinel
1386
+ ]);
1011
1387
  };
1012
1388
  return () => props.virtual && !virtual ? h(VirtualContent, {
1013
1389
  table,
@@ -1075,6 +1451,10 @@ const DataGridCell = defineComponent({
1075
1451
  row: rowProp,
1076
1452
  column: columnProp,
1077
1453
  as: asProp("td"),
1454
+ tree: {
1455
+ type: Boolean,
1456
+ default: true
1457
+ },
1078
1458
  /** The row's position on the page. Found by searching the rows when left out. */
1079
1459
  rowIndex: {
1080
1460
  type: Number,
@@ -1090,6 +1470,7 @@ const DataGridCell = defineComponent({
1090
1470
  const table = injectDataTable("<DataGridCell>");
1091
1471
  const { grid, context } = useGrid("<DataGridCell>");
1092
1472
  const virtual = injectVirtual();
1473
+ const hierarchy = injectHierarchy();
1093
1474
  const element = ref();
1094
1475
  const position = () => ({
1095
1476
  row: props.rowIndex ?? table.rows.indexOf(props.row),
@@ -1115,13 +1496,16 @@ const DataGridCell = defineComponent({
1115
1496
  commit: (then) => grid.commitEdit(then),
1116
1497
  cancel: () => grid.cancelEdit()
1117
1498
  }) ?? h(DataGridEditor, { label: `Edit ${column.header}` });
1118
- else content = slots.default?.({
1119
- row,
1120
- column,
1121
- value,
1122
- display,
1123
- editable
1124
- }) ?? display;
1499
+ else {
1500
+ const children = () => slots.default?.({
1501
+ row,
1502
+ column,
1503
+ value,
1504
+ display,
1505
+ editable
1506
+ }) ?? display;
1507
+ content = props.tree && table.tree && column.id === resolveTreeColumn(table, hierarchy?.treeColumn) ? h(DataTableTreeCell, { row }, { default: children }) : children();
1508
+ }
1125
1509
  return h(props.as, {
1126
1510
  ref: element,
1127
1511
  id: cellId(context.id, at.row, at.column),
@@ -1154,5 +1538,5 @@ const DataGridCell = defineComponent({
1154
1538
  });
1155
1539
 
1156
1540
  //#endregion
1157
- export { DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableEmpty, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableViewport, DataTableVirtualColumns, cellId, columnStyle, injectVirtual as injectVirtualRenderer, virtualProps };
1541
+ export { DataGridBody, DataGridCell, DataGridRoot, DataTableBody, DataTableCaption, DataTableCell, DataTableColumnVisibility, DataTableDetailRow, DataTableEmpty, DataTableExpandToggle, DataTableHeader, DataTableHeaderCell, DataTableHeaderRow, DataTableLoadMore, DataTablePageSize, DataTablePagination, DataTableRoot, DataTableRow, DataTableSearch, DataTableSelectAll, DataTableSelectRow, DataTableSortButton, DataTableStatus, DataTableTreeCell, DataTableViewport, DataTableVirtualColumns, cellId, columnStyle, expandToggleProps, hierarchyProps, injectVirtual as injectVirtualRenderer, resolveTreeColumn, treeCellProps, virtualProps };
1158
1542
  //# sourceMappingURL=index.js.map