@mendylanda/ui 0.3.0-alpha.0 → 0.3.0-alpha.10
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/README.md +20 -3
- package/dist/class-names.js +1 -1
- package/dist/filters/filter-bar.d.ts +1 -1
- package/dist/filters/filter-bar.js +53 -12
- package/dist/filters/filter-menu-focus.d.ts +1 -1
- package/dist/filters/filter-menu-panel.d.ts +1 -1
- package/dist/filters/use-menu-placement.d.ts +1 -1
- package/dist/primitives/popover.d.ts +5 -0
- package/dist/primitives/popover.js +10 -0
- package/dist/styles.css +153 -27
- package/dist/styles.tailwind3.css +153 -27
- package/dist/table/clipboard.d.ts +1 -1
- package/dist/table/clipboard.js +35 -22
- package/dist/table/column-widths.d.ts +9 -0
- package/dist/table/column-widths.js +23 -0
- package/dist/table/columns.d.ts +6 -0
- package/dist/table/columns.js +4 -0
- package/dist/table/data-table.d.ts +3 -0
- package/dist/table/data-table.js +6 -3
- package/dist/table/index.d.ts +1 -0
- package/dist/table/index.js +1 -0
- package/dist/table/table-column-settings.d.ts +6 -0
- package/dist/table/table-column-settings.js +55 -0
- package/dist/table/table-controls.d.ts +1 -3
- package/dist/table/table-controls.js +4 -41
- package/dist/table/table-feedback.d.ts +5 -1
- package/dist/table/table-feedback.js +5 -4
- package/dist/table/table-filters.d.ts +8 -0
- package/dist/table/table-filters.js +9 -0
- package/dist/table/table-frame.d.ts +10 -0
- package/dist/table/table-frame.js +10 -0
- package/dist/table/table-layout.d.ts +44 -0
- package/dist/table/table-layout.js +68 -0
- package/dist/table/table-loading.d.ts +12 -0
- package/dist/table/table-loading.js +6 -0
- package/dist/table/table-parts.d.ts +20 -2
- package/dist/table/table-parts.js +91 -37
- package/dist/table/table-view.d.ts +19 -2
- package/dist/table/table-view.js +60 -53
- package/dist/table/use-column-window.d.ts +42 -0
- package/dist/table/use-column-window.js +65 -0
- package/dist/table/use-data-table.d.ts +2 -2
- package/dist/table/use-data-table.js +15 -2
- package/dist/table/use-fill-layout.d.ts +2 -0
- package/dist/table/use-fill-layout.js +67 -0
- package/dist/table/use-inline-row-selection.d.ts +10 -0
- package/dist/table/use-inline-row-selection.js +33 -0
- package/dist/table/use-stable-array.d.ts +3 -0
- package/dist/table/use-stable-array.js +16 -0
- package/dist/table/use-table-interaction.d.ts +1 -1
- package/dist/table/use-table-interaction.js +32 -3
- package/dist/table/use-table-results.d.ts +8 -0
- package/dist/table/use-table-results.js +63 -0
- package/dist/utils.js +13 -2
- package/package.json +9 -8
package/dist/table/table-view.js
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
|
|
3
|
+
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from "react";
|
|
4
4
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
5
5
|
import { TableHeaderCell, TableBodyRow } from "./table-parts.js";
|
|
6
6
|
import { Button } from "../primitives/button.js";
|
|
7
7
|
import { cn } from "../utils.js";
|
|
8
|
+
import { useColumnWindow } from "./use-column-window.js";
|
|
8
9
|
import { useViewportWidth } from "./use-viewport-width.js";
|
|
10
|
+
import { tableLayout } from "./table-layout.js";
|
|
11
|
+
import { TableLoadingRows } from "./table-loading.js";
|
|
9
12
|
import { TableInitialState } from "./table-feedback.js";
|
|
10
13
|
import { useTableInteraction } from "./use-table-interaction.js";
|
|
11
|
-
|
|
14
|
+
import { useTableResults } from "./use-table-results.js";
|
|
15
|
+
import { useInlineRowSelection } from "./use-inline-row-selection.js";
|
|
16
|
+
import { TableFrame } from "./table-frame.js";
|
|
17
|
+
export function TableView({ table, label = "Data table", height = "auto", layout: layoutMode = "content", footer, rowHeight: rowHeightOption = 44, rowClassName, renderRowDetail, className, contentClassName, emptyState, loadingState, scrollRef, status = "ready", refreshing, error, retry, loadMore, queryKey, filters, renderHeader, onRowClick, onRowActivate, isRowHighlighted, rowSelectionControls = true, onCopyError, }) {
|
|
18
|
+
const fill = layoutMode === "fill";
|
|
19
|
+
const { resultKey, hasFilters, clearFilters } = useTableResults(table, queryKey, filters);
|
|
20
|
+
const autoRowHeight = rowHeightOption === "auto" || Boolean(renderRowDetail);
|
|
21
|
+
const rowHeight = rowHeightOption === "auto" ? 44 : rowHeightOption;
|
|
12
22
|
const container = useRef(null);
|
|
13
23
|
const setContainer = useCallback((node) => {
|
|
14
24
|
container.current = node;
|
|
@@ -18,32 +28,33 @@ export function TableView({ table, label = "Data table", height = "min(65dvh, 64
|
|
|
18
28
|
scrollRef.current = node;
|
|
19
29
|
}, [scrollRef]);
|
|
20
30
|
const rows = table.getRowModel().rows;
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
...table.getEndVisibleLeafColumns(),
|
|
25
|
-
];
|
|
31
|
+
const getScrollElement = useCallback(() => container.current, []);
|
|
32
|
+
const estimateSize = useCallback(() => rowHeight, [rowHeight]);
|
|
33
|
+
const getItemKey = useCallback((index) => rows[index]?.id ?? index, [rows]);
|
|
26
34
|
const virtual = useVirtualizer({
|
|
27
35
|
count: rows.length,
|
|
28
|
-
getScrollElement
|
|
29
|
-
estimateSize
|
|
30
|
-
getItemKey
|
|
36
|
+
getScrollElement,
|
|
37
|
+
estimateSize,
|
|
38
|
+
getItemKey,
|
|
31
39
|
overscan: 8,
|
|
32
40
|
});
|
|
33
41
|
const viewportWidth = useViewportWidth(container);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
42
|
+
const startColumns = table.getStartVisibleLeafColumns();
|
|
43
|
+
const centerColumns = table.getCenterVisibleLeafColumns();
|
|
44
|
+
const endColumns = table.getEndVisibleLeafColumns();
|
|
45
|
+
const sizing = table.state.columnSizing;
|
|
46
|
+
const layout = useMemo(() => tableLayout(table, viewportWidth, rowHeight), [table, startColumns, centerColumns, endColumns, sizing, viewportWidth, rowHeight]);
|
|
47
|
+
const { totalWidth, pinningActive } = layout;
|
|
48
|
+
const { inlineSelection, selectLoaded } = useInlineRowSelection(table, layout.columns, rowSelectionControls);
|
|
49
|
+
const focused = table.getFocusedCell();
|
|
50
|
+
const { columns, columnGaps, cellStyle } = useColumnWindow(layout, container, viewportWidth, focused?.column.id, !autoRowHeight);
|
|
51
|
+
const columnIndexes = useMemo(() => new Map(layout.columns.map((column, index) => [column.id, index])), [layout.columns]);
|
|
41
52
|
const { announcement, copied, onKeyDown } = useTableInteraction({
|
|
42
53
|
table,
|
|
43
54
|
pinningActive,
|
|
44
55
|
container,
|
|
45
|
-
queryKey,
|
|
46
|
-
onRowActivate,
|
|
56
|
+
queryKey: resultKey,
|
|
57
|
+
onRowActivate: onRowActivate ?? onRowClick,
|
|
47
58
|
onCopyError,
|
|
48
59
|
scrollToIndex: (index) => virtual.scrollToIndex(index, { align: "auto" }),
|
|
49
60
|
});
|
|
@@ -54,57 +65,53 @@ export function TableView({ table, label = "Data table", height = "min(65dvh, 64
|
|
|
54
65
|
useLayoutEffect(() => {
|
|
55
66
|
load.current = loadMore;
|
|
56
67
|
}, [loadMore]);
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
requested.current = null;
|
|
70
|
+
}, [resultKey]);
|
|
57
71
|
useEffect(() => {
|
|
58
72
|
const data = load.current;
|
|
59
|
-
const requestKey = `${queryKey}:${rows.length}`;
|
|
60
73
|
if (!data?.available ||
|
|
61
74
|
data.loading ||
|
|
62
75
|
data.error ||
|
|
63
76
|
last < rows.length - 10 ||
|
|
64
|
-
requested.current ===
|
|
77
|
+
(requested.current?.key === resultKey && requested.current.count === rows.length))
|
|
65
78
|
return;
|
|
66
|
-
|
|
79
|
+
const element = container.current;
|
|
80
|
+
// Only measure near the end when another page can actually be requested.
|
|
81
|
+
// Reading clientHeight on every scroll otherwise forces layout needlessly.
|
|
82
|
+
const nearEnd = element &&
|
|
83
|
+
element.scrollTop + element.clientHeight + rowHeight * 10 >=
|
|
84
|
+
virtual.getTotalSize() + rowHeight;
|
|
85
|
+
if (!nearEnd)
|
|
86
|
+
return;
|
|
87
|
+
requested.current = { key: resultKey, count: rows.length };
|
|
67
88
|
Promise.resolve()
|
|
68
89
|
.then(() => data.load())
|
|
69
90
|
.catch(() => {
|
|
70
91
|
requested.current = null;
|
|
71
92
|
});
|
|
72
|
-
}, [
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
offset += column.getSize();
|
|
83
|
-
}
|
|
84
|
-
function cellStyle(column) {
|
|
85
|
-
const pin = pinningActive ? positions.get(column.id) : undefined;
|
|
86
|
-
return {
|
|
87
|
-
width: column.getSize(),
|
|
88
|
-
minWidth: column.getSize(),
|
|
89
|
-
height: rowHeight,
|
|
90
|
-
...(column.id === table.getEndVisibleLeafColumns()[0]?.id
|
|
91
|
-
? { marginInlineStart: "auto" }
|
|
92
|
-
: {}),
|
|
93
|
-
...(pin ? { position: "sticky", [pin.side]: pin.offset, zIndex: 2 } : {}),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const focused = table.getFocusedCell();
|
|
93
|
+
}, [
|
|
94
|
+
last,
|
|
95
|
+
rows.length,
|
|
96
|
+
rowHeight,
|
|
97
|
+
resultKey,
|
|
98
|
+
loadMore?.available,
|
|
99
|
+
loadMore?.loading,
|
|
100
|
+
loadMore?.error,
|
|
101
|
+
virtual,
|
|
102
|
+
]);
|
|
97
103
|
const headersById = new Map(table.getFlatHeaders().map((header) => [header.column.id, header]));
|
|
98
|
-
return (_jsxs(
|
|
104
|
+
return (_jsxs(TableFrame, { layout: layoutMode, stretch: height === "100%", footer: footer, children: [_jsx("div", { "aria-live": "polite", className: "mui-32fb090591d9", children: announcement }), _jsxs("div", { ref: setContainer, role: "grid", tabIndex: -1, "aria-label": label, "aria-rowcount": renderRowDetail ? -1 : rows.length + 1, "aria-colcount": layout.columns.length, "aria-busy": status === "loading" || refreshing, style: {
|
|
105
|
+
height: fill || height === "auto" ? undefined : height,
|
|
106
|
+
maxHeight: !fill && height === "auto" ? "65dvh" : undefined,
|
|
107
|
+
}, className: cn("mui-d2d9e1f13413 mui-014aadadffad mui-1a26a0d28420 mui-3fa8c572949b mui-4f1a55de40bc mui-5b272f3c5076 mui-c74ab393b96d mui-b30fc56058b6", fill && "mui-410da8dfa8ac mui-7bd5bab6d7f4", className), onKeyDown: onKeyDown, children: [_jsx("div", { role: "row", "aria-rowindex": 1, className: "mui-964a9431ff49 mui-98599e4ee250 mui-4e8f0a87a0dc mui-222f930b8752 mui-2bf6510f1330 mui-bbe39cfb5cc6 mui-5b272f3c5076", style: { width: totalWidth }, children: columns.map((column) => (_jsxs(Fragment, { children: [columnGaps.has(column.id) && (_jsx("div", { "aria-hidden": "true", className: "mui-27ead27a81df", style: { width: columnGaps.get(column.id) } })), _jsx(TableHeaderCell, { table: table, header: headersById.get(column.id), index: columnIndexes.get(column.id), style: cellStyle(column), renderHeader: renderHeader, contentClassName: contentClassName, selectLoaded: selectLoaded }, column.id)] }, column.id))) }), _jsx(TableInitialState, { status: status, hasRows: rows.length > 0, loadingState: loadingState ?? (_jsx(TableLoadingRows, { columns: columns, columnGaps: columnGaps, width: totalWidth, rowHeight: rowHeight, cellStyle: cellStyle, count: !fill && height === "auto"
|
|
108
|
+
? 8
|
|
109
|
+
: Math.max(1, Math.ceil(((virtual.scrollRect?.height ?? 400) - rowHeight) / rowHeight)) })), emptyState: emptyState, hasFilters: hasFilters, pageIndex: table.state.pagination.pageIndex, clearFilters: clearFilters, firstPage: () => table.setPageIndex(0), error: error, retry: retry }), _jsx("div", { role: "rowgroup", className: "mui-d2d9e1f13413 mui-2bf6510f1330", style: { height: virtual.getTotalSize(), width: totalWidth }, children: items.map((item) => (_jsx(TableBodyRow, { row: rows[item.index], rowIndex: item.index, start: item.start, rowHeight: rowHeight, autoRowHeight: autoRowHeight, measureElement: autoRowHeight ? virtual.measureElement : undefined, rowClassName: rowClassName, renderRowDetail: renderRowDetail, detailWidth: viewportWidth ?? totalWidth, cellStyle: cellStyle, columns: columns, columnGaps: columnGaps, columnIndexes: columnIndexes, contentVersion: table.options, contentState: table.state, focusedId: focused?.id, copied: copied, contentClassName: contentClassName, onRowClick: onRowClick, onRowActivate: onRowActivate, isRowHighlighted: isRowHighlighted, inlineSelection: inlineSelection }, item.key))) }), rows.length > 0 && status === "error" && (_jsxs("div", { role: "alert", className: "mui-964a9431ff49 mui-30150dd033ab mui-5b272f3c5076 mui-094f5333853b", children: [error ?? "Could not refresh rows.", retry && (_jsx(Button, { onClick: retry, variant: "outline", size: "sm", children: "Retry" }))] })), loadMore?.available && (_jsxs("div", { className: "mui-964a9431ff49 mui-635702706586 mui-222f930b8752 mui-71556df3b421 mui-a503dd374cca mui-074569488cca mui-96b92a971c95 mui-c74ab393b96d mui-35f35c41d134", children: [loadMore.loading ? (_jsxs("div", { role: "status", className: "mui-222f930b8752 mui-71556df3b421 mui-074569488cca", children: [_jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", className: "mui-2bd43fb57913 mui-27ead27a81df mui-2c14b55d77e6 mui-237747164b60", children: _jsx("path", { d: "M12 3v3m6.366-.366-2.12 2.12M21 12h-3m.366 6.366-2.12-2.12M12 21v-3m-6.366.366 2.12-2.12M3 12h3m-.366-6.366 2.12 2.12" }) }), _jsx("span", { children: "Loading more\u2026" })] })) : (_jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
|
|
99
110
|
requested.current = null;
|
|
100
111
|
void Promise.resolve()
|
|
101
112
|
.then(() => loadMore.load())
|
|
102
113
|
.catch(() => {
|
|
103
114
|
requested.current = null;
|
|
104
115
|
});
|
|
105
|
-
}, children: loadMore.loading
|
|
106
|
-
? "Loading more…"
|
|
107
|
-
: loadMore.error
|
|
108
|
-
? "Retry loading more"
|
|
109
|
-
: "Load more" }), loadMore.error && _jsx("span", { role: "alert", children: loadMore.error })] }))] })] }));
|
|
116
|
+
}, children: loadMore.error ? "Retry loading more" : "Load more" })), loadMore.error && _jsx("span", { role: "alert", children: loadMore.error })] }))] })] }));
|
|
110
117
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
import type { Column } from "@tanstack/react-table";
|
|
3
|
+
import type { DataTableFeatures } from "./features.js";
|
|
4
|
+
import type { tableLayout } from "./table-layout.js";
|
|
5
|
+
/** Keep pinned and focused columns mounted, with two columns of overscan. */
|
|
6
|
+
export declare function useColumnWindow<T extends object>(layout: ReturnType<typeof tableLayout<T>>, container: RefObject<HTMLDivElement | null>, viewportWidth: number | null, focusedColumn?: string, enabled?: boolean): {
|
|
7
|
+
columns: Column<{
|
|
8
|
+
cellSelectionFeature: import("@tanstack/react-table").TableFeature;
|
|
9
|
+
columnResizingFeature: import("@tanstack/react-table").TableFeature;
|
|
10
|
+
columnOrderingFeature: import("@tanstack/react-table").TableFeature;
|
|
11
|
+
columnPinningFeature: import("@tanstack/react-table").TableFeature;
|
|
12
|
+
columnSizingFeature: import("@tanstack/react-table").TableFeature;
|
|
13
|
+
columnVisibilityFeature: import("@tanstack/react-table").TableFeature;
|
|
14
|
+
columnFilteringFeature: import("@tanstack/react-table").TableFeature;
|
|
15
|
+
columnFacetingFeature: import("@tanstack/react-table").TableFeature;
|
|
16
|
+
globalFilteringFeature: import("@tanstack/react-table").TableFeature;
|
|
17
|
+
rowPaginationFeature: import("@tanstack/react-table").TableFeature;
|
|
18
|
+
rowSelectionFeature: import("@tanstack/react-table").TableFeature;
|
|
19
|
+
rowSortingFeature: import("@tanstack/react-table").TableFeature;
|
|
20
|
+
filteredRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
21
|
+
sortedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
22
|
+
paginatedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
23
|
+
facetedRowModel: (table: import("@tanstack/react-table").Table<any, any>, columnId: string) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
24
|
+
facetedUniqueValues: (table: import("@tanstack/react-table").Table<import("@tanstack/react-table").TableFeatures, any>, columnId: string) => () => Map<any, number>;
|
|
25
|
+
filterFns: {
|
|
26
|
+
includesString: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
27
|
+
inNumberRange: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
28
|
+
inDateRange: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
29
|
+
equals: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
30
|
+
arrIncludes: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
31
|
+
weakEquals: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
32
|
+
};
|
|
33
|
+
sortFns: {
|
|
34
|
+
alphanumeric: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
35
|
+
basic: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
36
|
+
datetime: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
37
|
+
text: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
38
|
+
};
|
|
39
|
+
}, T>[];
|
|
40
|
+
columnGaps: Map<string, number>;
|
|
41
|
+
cellStyle: (column: Column<DataTableFeatures, T>) => import("react").CSSProperties;
|
|
42
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect, useMemo, useState } from "react";
|
|
3
|
+
/** Keep pinned and focused columns mounted, with two columns of overscan. */
|
|
4
|
+
export function useColumnWindow(layout, container, viewportWidth, focusedColumn, enabled = true) {
|
|
5
|
+
const [scrollLeft, setScrollLeft] = useState(0);
|
|
6
|
+
const [activeColumn, setActiveColumn] = useState();
|
|
7
|
+
useLayoutEffect(() => {
|
|
8
|
+
const element = container.current;
|
|
9
|
+
if (!element)
|
|
10
|
+
return;
|
|
11
|
+
const rememberFocus = (event) => {
|
|
12
|
+
setActiveColumn(event.target.closest("[data-column-id]")?.dataset.columnId);
|
|
13
|
+
};
|
|
14
|
+
element.addEventListener("focusin", rememberFocus);
|
|
15
|
+
return () => element.removeEventListener("focusin", rememberFocus);
|
|
16
|
+
}, [container]);
|
|
17
|
+
useLayoutEffect(() => {
|
|
18
|
+
const element = container.current;
|
|
19
|
+
if (!element)
|
|
20
|
+
return;
|
|
21
|
+
const measure = () => setScrollLeft(element.scrollLeft);
|
|
22
|
+
measure();
|
|
23
|
+
element.addEventListener("scroll", measure, { passive: true });
|
|
24
|
+
return () => element.removeEventListener("scroll", measure);
|
|
25
|
+
}, [container]);
|
|
26
|
+
const right = scrollLeft + (viewportWidth ?? 0);
|
|
27
|
+
let start = 0;
|
|
28
|
+
let end = layout.columns.length - 1;
|
|
29
|
+
while (start < end && layout.offsets[start + 1] < scrollLeft)
|
|
30
|
+
start++;
|
|
31
|
+
end = start;
|
|
32
|
+
while (end < layout.columns.length - 1 && layout.offsets[end] < right)
|
|
33
|
+
end++;
|
|
34
|
+
start = Math.max(0, start - 2);
|
|
35
|
+
end = Math.min(layout.columns.length - 1, end + 2);
|
|
36
|
+
return useMemo(() => {
|
|
37
|
+
const columns = [];
|
|
38
|
+
const columnGaps = new Map();
|
|
39
|
+
const styles = new Map();
|
|
40
|
+
let previousEnd = 0;
|
|
41
|
+
layout.columns.forEach((column, index) => {
|
|
42
|
+
if (!enabled ||
|
|
43
|
+
(index >= start && index <= end) ||
|
|
44
|
+
(layout.pinningActive && column.getIsPinned()) ||
|
|
45
|
+
column.id === focusedColumn ||
|
|
46
|
+
column.id === activeColumn) {
|
|
47
|
+
columns.push(column);
|
|
48
|
+
const gap = layout.offsets[index] - previousEnd;
|
|
49
|
+
const pinnedEnd = layout.pinningActive && column.getIsPinned() === "end";
|
|
50
|
+
if (gap > 0 && pinnedEnd)
|
|
51
|
+
columnGaps.set(column.id, gap);
|
|
52
|
+
styles.set(column.id, {
|
|
53
|
+
...layout.cellStyle(column),
|
|
54
|
+
...(gap > 0 && !pinnedEnd ? { marginInlineStart: gap } : {}),
|
|
55
|
+
});
|
|
56
|
+
previousEnd = layout.offsets[index + 1];
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
columns,
|
|
61
|
+
columnGaps,
|
|
62
|
+
cellStyle: (column) => styles.get(column.id),
|
|
63
|
+
};
|
|
64
|
+
}, [layout, start, end, focusedColumn, activeColumn, enabled]);
|
|
65
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { DataTableFeatures } from "./features.js";
|
|
|
4
4
|
import type { PreferenceStorage, TablePreferences } from "./state.js";
|
|
5
5
|
export type DataTableInstance<T extends object> = ReactTable<DataTableFeatures, T>;
|
|
6
6
|
export interface UseDataTableOptions<T extends object> extends Omit<TableOptions<DataTableFeatures, T>, "features" | "data" | "columns" | "manualFiltering" | "manualSorting" | "manualPagination"> {
|
|
7
|
-
rows: T[];
|
|
7
|
+
rows: readonly T[];
|
|
8
8
|
columns: TableColumn<T>[];
|
|
9
9
|
getRowId: (row: T, index: number) => string;
|
|
10
10
|
processing?: {
|
|
@@ -21,4 +21,4 @@ export interface UseDataTableOptions<T extends object> extends Omit<TableOptions
|
|
|
21
21
|
}
|
|
22
22
|
export declare function captureTablePreferences<T extends object>(table: DataTableInstance<T>): TablePreferences;
|
|
23
23
|
export declare function applyTablePreferences<T extends object>(table: DataTableInstance<T>, input: unknown): void;
|
|
24
|
-
export declare function useDataTable<T extends object>({ rows, columns, processing, preferences, ...options }: UseDataTableOptions<T>): DataTableInstance<T>;
|
|
24
|
+
export declare function useDataTable<T extends object>({ rows: inputRows, columns: inputColumns, processing, preferences, ...options }: UseDataTableOptions<T>): DataTableInstance<T>;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useTable } from "@tanstack/react-table";
|
|
4
4
|
import { tableFeaturesDefault } from "./features.js";
|
|
5
|
+
import { useStableArray } from "./use-stable-array.js";
|
|
5
6
|
import { reconcilePreferences } from "./state.js";
|
|
6
7
|
const browserStorage = {
|
|
7
8
|
read: (key) => {
|
|
@@ -26,7 +27,9 @@ export function applyTablePreferences(table, input) {
|
|
|
26
27
|
table.setColumnSizing(value.columnSizing);
|
|
27
28
|
table.setColumnPinning(value.columnPinning);
|
|
28
29
|
}
|
|
29
|
-
export function useDataTable({ rows, columns, processing, preferences, ...options }) {
|
|
30
|
+
export function useDataTable({ rows: inputRows, columns: inputColumns, processing, preferences, ...options }) {
|
|
31
|
+
const rows = useStableArray(inputRows);
|
|
32
|
+
const columns = useStableArray(inputColumns);
|
|
30
33
|
const defaults = useMemo(() => reconcilePreferences(null, columns, options.initialState), [columns, options.initialState]);
|
|
31
34
|
const table = useTable({
|
|
32
35
|
features: tableFeaturesDefault,
|
|
@@ -36,6 +39,9 @@ export function useDataTable({ rows, columns, processing, preferences, ...option
|
|
|
36
39
|
defaultColumn: { size: 180, minSize: 48, maxSize: 1200, ...options.defaultColumn },
|
|
37
40
|
columnResizeMode: options.columnResizeMode ?? "onChange",
|
|
38
41
|
enableCellSelection: options.enableCellSelection ?? true,
|
|
42
|
+
enableRowSelection: options.enableRowSelection ??
|
|
43
|
+
(Boolean(options.onRowSelectionChange) ||
|
|
44
|
+
columns.some((column) => column.id === "_selection")),
|
|
39
45
|
enableColumnPinning: options.enableColumnPinning ?? true,
|
|
40
46
|
autoResetCellSelection: options.autoResetCellSelection ?? false,
|
|
41
47
|
manualFiltering: processing?.filtering === "external",
|
|
@@ -82,7 +88,14 @@ export function useDataTable({ rows, columns, processing, preferences, ...option
|
|
|
82
88
|
active = false;
|
|
83
89
|
};
|
|
84
90
|
}, [key, storage]);
|
|
85
|
-
const
|
|
91
|
+
const { columnOrder, columnPinning, columnSizing, columnVisibility } = table.state;
|
|
92
|
+
const serialized = useMemo(() => JSON.stringify({
|
|
93
|
+
version: 1,
|
|
94
|
+
columnOrder,
|
|
95
|
+
columnPinning,
|
|
96
|
+
columnSizing,
|
|
97
|
+
columnVisibility,
|
|
98
|
+
}), [columnOrder, columnPinning, columnSizing, columnVisibility]);
|
|
86
99
|
useEffect(() => {
|
|
87
100
|
if (!key || restoredKey !== key)
|
|
88
101
|
return;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
/** Size the containing frame, not the grid, so controls share its available height. */
|
|
4
|
+
export function useFillLayout(enabled) {
|
|
5
|
+
const dispose = useRef(undefined);
|
|
6
|
+
return useCallback((node) => {
|
|
7
|
+
dispose.current?.();
|
|
8
|
+
dispose.current = undefined;
|
|
9
|
+
if (!node || !enabled)
|
|
10
|
+
return;
|
|
11
|
+
let frame = 0;
|
|
12
|
+
let previous = -1;
|
|
13
|
+
const measure = () => {
|
|
14
|
+
frame = 0;
|
|
15
|
+
let inset = 0;
|
|
16
|
+
let ancestor = node.parentElement;
|
|
17
|
+
while (ancestor) {
|
|
18
|
+
const style = getComputedStyle(ancestor);
|
|
19
|
+
inset += parseFloat(style.paddingBottom) || 0;
|
|
20
|
+
inset += parseFloat(style.borderBottomWidth) || 0;
|
|
21
|
+
ancestor = ancestor.parentElement;
|
|
22
|
+
}
|
|
23
|
+
const height = Math.max(0, window.innerHeight - node.getBoundingClientRect().top - inset);
|
|
24
|
+
if (Math.abs(height - previous) < 0.5)
|
|
25
|
+
return;
|
|
26
|
+
previous = height;
|
|
27
|
+
node.style.height = `${height}px`;
|
|
28
|
+
};
|
|
29
|
+
const schedule = () => {
|
|
30
|
+
if (!frame)
|
|
31
|
+
frame = requestAnimationFrame(measure);
|
|
32
|
+
};
|
|
33
|
+
const observer = new ResizeObserver(schedule);
|
|
34
|
+
const observe = () => {
|
|
35
|
+
observer.disconnect();
|
|
36
|
+
let current = node;
|
|
37
|
+
while (current?.parentElement) {
|
|
38
|
+
observer.observe(current.parentElement);
|
|
39
|
+
let sibling = current.previousElementSibling;
|
|
40
|
+
while (sibling) {
|
|
41
|
+
observer.observe(sibling);
|
|
42
|
+
sibling = sibling.previousElementSibling;
|
|
43
|
+
}
|
|
44
|
+
current = current.parentElement;
|
|
45
|
+
}
|
|
46
|
+
schedule();
|
|
47
|
+
};
|
|
48
|
+
// Newly inserted preceding siblings can move a table inside a fixed-height parent.
|
|
49
|
+
const mutations = new MutationObserver(observe);
|
|
50
|
+
let parent = node.parentElement;
|
|
51
|
+
while (parent) {
|
|
52
|
+
mutations.observe(parent, { childList: true });
|
|
53
|
+
parent = parent.parentElement;
|
|
54
|
+
}
|
|
55
|
+
measure();
|
|
56
|
+
observe();
|
|
57
|
+
window.addEventListener("resize", schedule);
|
|
58
|
+
dispose.current = () => {
|
|
59
|
+
observer.disconnect();
|
|
60
|
+
mutations.disconnect();
|
|
61
|
+
window.removeEventListener("resize", schedule);
|
|
62
|
+
cancelAnimationFrame(frame);
|
|
63
|
+
if (node.style.height === `${previous}px`)
|
|
64
|
+
node.style.removeProperty("height");
|
|
65
|
+
};
|
|
66
|
+
}, [enabled]);
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Column } from "@tanstack/react-table";
|
|
2
|
+
import type { DataTableFeatures } from "./features.js";
|
|
3
|
+
import type { DataTableInstance } from "./use-data-table.js";
|
|
4
|
+
export declare function useInlineRowSelection<T extends object>(table: DataTableInstance<T>, columns: Column<DataTableFeatures, T>[], controls: boolean): {
|
|
5
|
+
inlineSelection: boolean;
|
|
6
|
+
selectLoaded: {
|
|
7
|
+
checked: boolean | "indeterminate";
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
} | undefined;
|
|
10
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
export function useInlineRowSelection(table, columns, controls) {
|
|
3
|
+
const rows = table.getRowModel().rows;
|
|
4
|
+
const inlineSelection = Boolean(controls &&
|
|
5
|
+
table.options.enableRowSelection &&
|
|
6
|
+
!columns.some((column) => column.id === "_selection"));
|
|
7
|
+
const rowSelection = table.state.rowSelection;
|
|
8
|
+
const enableRowSelection = table.options.enableRowSelection;
|
|
9
|
+
const enableMultiRowSelection = table.options.enableMultiRowSelection;
|
|
10
|
+
const selectLoadedAvailability = useMemo(() => {
|
|
11
|
+
if (!inlineSelection || enableMultiRowSelection === false)
|
|
12
|
+
return undefined;
|
|
13
|
+
let hasSelectableRow = false;
|
|
14
|
+
for (const row of rows) {
|
|
15
|
+
if (!row.getCanSelect())
|
|
16
|
+
continue;
|
|
17
|
+
hasSelectableRow = true;
|
|
18
|
+
if (!row.getCanMultiSelect())
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
return { disabled: !hasSelectableRow };
|
|
22
|
+
}, [rows, inlineSelection, enableRowSelection, enableMultiRowSelection]);
|
|
23
|
+
const selectLoaded = useMemo(() => {
|
|
24
|
+
if (!selectLoadedAvailability)
|
|
25
|
+
return undefined;
|
|
26
|
+
return {
|
|
27
|
+
checked: table.getIsAllPageRowsSelected() ||
|
|
28
|
+
(table.getIsSomePageRowsSelected() ? "indeterminate" : false),
|
|
29
|
+
disabled: selectLoadedAvailability.disabled,
|
|
30
|
+
};
|
|
31
|
+
}, [table, rowSelection, selectLoadedAvailability]);
|
|
32
|
+
return { inlineSelection, selectLoaded };
|
|
33
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
3
|
+
export function useStableArray(input) {
|
|
4
|
+
const committed = useRef(input);
|
|
5
|
+
const value = useMemo(() => {
|
|
6
|
+
const previous = committed.current;
|
|
7
|
+
return input.length === previous.length &&
|
|
8
|
+
input.every((item, index) => item === previous[index])
|
|
9
|
+
? previous
|
|
10
|
+
: input;
|
|
11
|
+
}, [input]);
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
committed.current = value;
|
|
14
|
+
}, [value]);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
@@ -4,7 +4,7 @@ export declare function useTableInteraction<T extends object>({ table, pinningAc
|
|
|
4
4
|
table: DataTableInstance<T>;
|
|
5
5
|
pinningActive: boolean;
|
|
6
6
|
container: RefObject<HTMLDivElement | null>;
|
|
7
|
-
queryKey?:
|
|
7
|
+
queryKey?: unknown;
|
|
8
8
|
onRowActivate?: (row: T) => void;
|
|
9
9
|
onCopyError?: (error: unknown) => void;
|
|
10
10
|
scrollToIndex: (index: number) => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
3
3
|
import { interactiveSelector, selectedCellsText } from "./clipboard.js";
|
|
4
4
|
export function useTableInteraction({ table, pinningActive, container, queryKey, onRowActivate, onCopyError, scrollToIndex, }) {
|
|
5
5
|
const rows = table.getRowModel().rows;
|
|
@@ -13,6 +13,32 @@ export function useTableInteraction({ table, pinningActive, container, queryKey,
|
|
|
13
13
|
const [copied, setCopied] = useState(false);
|
|
14
14
|
const copyTimer = useRef(undefined);
|
|
15
15
|
const mounted = useRef(true);
|
|
16
|
+
const ownedFocus = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const element = container.current;
|
|
19
|
+
if (!element)
|
|
20
|
+
return;
|
|
21
|
+
const remember = (event) => {
|
|
22
|
+
const target = event.target;
|
|
23
|
+
ownedFocus.current =
|
|
24
|
+
target.closest('[role="grid"]') === element && target.getAttribute("role") === "gridcell"
|
|
25
|
+
? target
|
|
26
|
+
: null;
|
|
27
|
+
};
|
|
28
|
+
document.addEventListener("focusin", remember);
|
|
29
|
+
return () => document.removeEventListener("focusin", remember);
|
|
30
|
+
}, [container]);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
// A virtual row can unmount the focused cell without a blur event. Keep
|
|
33
|
+
// keyboard commands on the grid rather than silently returning them to body.
|
|
34
|
+
if (ownedFocus.current &&
|
|
35
|
+
!ownedFocus.current.isConnected &&
|
|
36
|
+
document.activeElement === document.body &&
|
|
37
|
+
table.getSelectedCellCount()) {
|
|
38
|
+
ownedFocus.current = null;
|
|
39
|
+
container.current?.focus({ preventScroll: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
16
42
|
useEffect(() => {
|
|
17
43
|
mounted.current = true;
|
|
18
44
|
return () => {
|
|
@@ -28,7 +54,8 @@ export function useTableInteraction({ table, pinningActive, container, queryKey,
|
|
|
28
54
|
}, [queryKey, resetCellSelection, container]);
|
|
29
55
|
useEffect(() => {
|
|
30
56
|
const dismiss = (event) => {
|
|
31
|
-
if (container.current &&
|
|
57
|
+
if (container.current &&
|
|
58
|
+
event.target.closest('[role="grid"]') !== container.current)
|
|
32
59
|
resetCellSelection(true);
|
|
33
60
|
};
|
|
34
61
|
document.addEventListener("pointerdown", dismiss);
|
|
@@ -78,7 +105,9 @@ export function useTableInteraction({ table, pinningActive, container, queryKey,
|
|
|
78
105
|
});
|
|
79
106
|
}
|
|
80
107
|
const onKeyDown = (event) => {
|
|
81
|
-
if (event.
|
|
108
|
+
if (event.defaultPrevented ||
|
|
109
|
+
event.target.closest('[role="grid"]') !== event.currentTarget ||
|
|
110
|
+
event.target.closest(interactiveSelector))
|
|
82
111
|
return;
|
|
83
112
|
if ((event.ctrlKey || event.metaKey) &&
|
|
84
113
|
event.key.toLowerCase() === "c" &&
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FilterController } from "../filters/use-filters.js";
|
|
2
|
+
import type { DataTableInstance } from "./use-data-table.js";
|
|
3
|
+
/** Only query state contributes to this key. Row appends and UI state never do. */
|
|
4
|
+
export declare function useTableResults<T extends object>(table: DataTableInstance<T>, queryKey?: string, filters?: FilterController): {
|
|
5
|
+
resultKey: string | any[];
|
|
6
|
+
hasFilters: boolean;
|
|
7
|
+
clearFilters: (() => void) | undefined;
|
|
8
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
function hasValue(value) {
|
|
4
|
+
if (value == null || value === "")
|
|
5
|
+
return false;
|
|
6
|
+
if (Array.isArray(value))
|
|
7
|
+
return value.some((item) => item != null && item !== "");
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
/** Only query state contributes to this key. Row appends and UI state never do. */
|
|
11
|
+
export function useTableResults(table, queryKey, filters) {
|
|
12
|
+
const { sorting, columnFilters, globalFilter, pagination } = table.state;
|
|
13
|
+
const entries = filters?.entries;
|
|
14
|
+
const search = filters?.search;
|
|
15
|
+
const resultKey = useMemo(() => {
|
|
16
|
+
const state = [
|
|
17
|
+
queryKey,
|
|
18
|
+
sorting,
|
|
19
|
+
columnFilters,
|
|
20
|
+
globalFilter,
|
|
21
|
+
pagination.pageIndex,
|
|
22
|
+
pagination.pageSize,
|
|
23
|
+
search,
|
|
24
|
+
];
|
|
25
|
+
try {
|
|
26
|
+
return JSON.stringify([...state, entries?.map(({ id, field, value }) => [id, field.codec.serialize(value)])], (_key, value) => (typeof value === "bigint" ? { $bigint: String(value) } : value));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Opaque application values may not be serializable. In that case their
|
|
30
|
+
// immutable references drive changes, without crashing table rendering.
|
|
31
|
+
return [...state, entries];
|
|
32
|
+
}
|
|
33
|
+
}, [
|
|
34
|
+
queryKey,
|
|
35
|
+
sorting,
|
|
36
|
+
columnFilters,
|
|
37
|
+
globalFilter,
|
|
38
|
+
pagination.pageIndex,
|
|
39
|
+
pagination.pageSize,
|
|
40
|
+
search,
|
|
41
|
+
entries,
|
|
42
|
+
]);
|
|
43
|
+
const hasFilters = Boolean(columnFilters.some(({ value }) => hasValue(value)) ||
|
|
44
|
+
hasValue(globalFilter) ||
|
|
45
|
+
search?.trim() ||
|
|
46
|
+
filters?.active.length);
|
|
47
|
+
const clearFilters = () => {
|
|
48
|
+
if (filters)
|
|
49
|
+
filters.clear();
|
|
50
|
+
else {
|
|
51
|
+
if (columnFilters.length)
|
|
52
|
+
table.setColumnFilters([]);
|
|
53
|
+
if (hasValue(globalFilter))
|
|
54
|
+
table.setGlobalFilter(undefined);
|
|
55
|
+
if (pagination.pageIndex)
|
|
56
|
+
table.setPageIndex(0);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const canClear = !filters ||
|
|
60
|
+
Boolean(search?.trim()) ||
|
|
61
|
+
filters.entries.some(({ field, value }) => !field.hidden && !field.disabled && field.removable !== false && field.isActive(value));
|
|
62
|
+
return { resultKey, hasFilters, clearFilters: canClear ? clearFilters : undefined };
|
|
63
|
+
}
|