@prairielearn/ui 1.3.0 → 1.4.0
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/CHANGELOG.md +22 -0
- package/README.md +4 -2
- package/dist/components/CategoricalColumnFilter.d.ts +7 -12
- package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
- package/dist/components/CategoricalColumnFilter.js +15 -11
- package/dist/components/CategoricalColumnFilter.js.map +1 -1
- package/dist/components/ColumnManager.d.ts +6 -3
- package/dist/components/ColumnManager.d.ts.map +1 -1
- package/dist/components/ColumnManager.js +98 -18
- package/dist/components/ColumnManager.js.map +1 -1
- package/dist/components/MultiSelectColumnFilter.d.ts +8 -12
- package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -1
- package/dist/components/MultiSelectColumnFilter.js +21 -13
- package/dist/components/MultiSelectColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.d.ts +13 -13
- package/dist/components/NumericInputColumnFilter.d.ts.map +1 -1
- package/dist/components/NumericInputColumnFilter.js +44 -15
- package/dist/components/NumericInputColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.test.d.ts +2 -0
- package/dist/components/NumericInputColumnFilter.test.d.ts.map +1 -0
- package/dist/components/NumericInputColumnFilter.test.js +90 -0
- package/dist/components/NumericInputColumnFilter.test.js.map +1 -0
- package/dist/components/OverlayTrigger.d.ts +78 -0
- package/dist/components/OverlayTrigger.d.ts.map +1 -0
- package/dist/components/OverlayTrigger.js +89 -0
- package/dist/components/OverlayTrigger.js.map +1 -0
- package/dist/components/TanstackTable.d.ts +15 -4
- package/dist/components/TanstackTable.d.ts.map +1 -1
- package/dist/components/TanstackTable.js +148 -197
- package/dist/components/TanstackTable.js.map +1 -1
- package/dist/components/TanstackTableDownloadButton.d.ts +4 -2
- package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
- package/dist/components/TanstackTableDownloadButton.js +4 -3
- package/dist/components/TanstackTableDownloadButton.js.map +1 -1
- package/dist/components/TanstackTableHeaderCell.d.ts +13 -0
- package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -0
- package/dist/components/TanstackTableHeaderCell.js +98 -0
- package/dist/components/TanstackTableHeaderCell.js.map +1 -0
- package/dist/components/{TanstackTable.css → styles.css} +11 -6
- package/dist/components/useAutoSizeColumns.d.ts +17 -0
- package/dist/components/useAutoSizeColumns.d.ts.map +1 -0
- package/dist/components/useAutoSizeColumns.js +99 -0
- package/dist/components/useAutoSizeColumns.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/react-table.d.ts +13 -0
- package/dist/react-table.d.ts.map +1 -0
- package/dist/react-table.js +3 -0
- package/dist/react-table.js.map +1 -0
- package/package.json +2 -2
- package/src/components/CategoricalColumnFilter.tsx +28 -28
- package/src/components/ColumnManager.tsx +222 -46
- package/src/components/MultiSelectColumnFilter.tsx +45 -32
- package/src/components/NumericInputColumnFilter.test.ts +67 -19
- package/src/components/NumericInputColumnFilter.tsx +102 -42
- package/src/components/OverlayTrigger.tsx +168 -0
- package/src/components/TanstackTable.tsx +315 -363
- package/src/components/TanstackTableDownloadButton.tsx +8 -5
- package/src/components/TanstackTableHeaderCell.tsx +207 -0
- package/src/components/{TanstackTable.css → styles.css} +11 -6
- package/src/components/useAutoSizeColumns.tsx +168 -0
- package/src/index.ts +5 -0
- package/src/react-table.ts +17 -0
- package/tsconfig.json +1 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TanstackTable.d.ts","sourceRoot":"","sources":["../../src/components/TanstackTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"TanstackTable.d.ts","sourceRoot":"","sources":["../../src/components/TanstackTable.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAQ,MAAM,EAAO,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAErE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAEhD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAI9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAI/D,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AAoE1C,UAAU,kBAAkB,CAAC,YAAY;IACvC,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA;KAAE,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IAC7B,UAAU,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,CAAC,GAAG,IAAI,CAAC;CACpD;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,EAC1C,KAAK,EACL,KAAK,EACL,OAA4B,EAC5B,SAAc,EACd,cAAsC,EACtC,UAA8B,EAC9B,SAAS,GACV,EAAE,kBAAkB,CAAC,YAAY,CAAC,eA0VlC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,EAC9C,KAAK,EACL,KAAK,EACL,aAAa,EACb,WAAW,EACX,aAAa,EACb,oBAAoB,EACpB,uBAAuB,EACvB,YAAY,EACZ,YAAY,EACZ,qBAAqB,EACrB,SAAS,EACT,GAAG,QAAQ,EACZ,EAAE;IACD,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnC,uBAAuB,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACtC,YAAY,EAAE;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACvE,qBAAqB,CAAC,EAAE,IAAI,CAC1B,gCAAgC,CAAC,YAAY,CAAC,EAC9C,OAAO,GAAG,eAAe,GAAG,aAAa,CAC1C,CAAC;CACH,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,eAgFvC;AAED,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,QAAQ,GACT,EAAE;IACD,QAAQ,EAAE,MAAM,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,iBAAiB,CAAC;CAC7B,eAOA"}
|
|
@@ -1,57 +1,38 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@prairielearn/preact-cjs/jsx-runtime";
|
|
2
2
|
import { flexRender } from '@tanstack/react-table';
|
|
3
|
-
import {
|
|
3
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import { useEffect,
|
|
5
|
+
import { useEffect, useMemo, useRef } from 'preact/hooks';
|
|
6
6
|
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|
7
7
|
import Tooltip from 'react-bootstrap/Tooltip';
|
|
8
|
+
import { run } from '@prairielearn/run';
|
|
8
9
|
import { ColumnManager } from './ColumnManager.js';
|
|
9
10
|
import { TanstackTableDownloadButton, } from './TanstackTableDownloadButton.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
|
|
12
|
+
import { useAutoSizeColumns } from './useAutoSizeColumns.js';
|
|
13
|
+
function TableCell({ cell, rowIdx, colIdx, canSort, canFilter, wrapText, handleGridKeyDown, }) {
|
|
14
|
+
return (_jsx("td", { tabIndex: 0, "data-grid-cell-row": rowIdx, "data-grid-cell-col": colIdx, class: clsx(!canSort && !canFilter && 'text-center'), style: {
|
|
15
|
+
display: 'flex',
|
|
16
|
+
width: cell.column.getSize(),
|
|
17
|
+
minWidth: 0,
|
|
18
|
+
maxWidth: cell.column.getSize(),
|
|
19
|
+
flexShrink: 0,
|
|
20
|
+
position: cell.column.getIsPinned() === 'left' ? 'sticky' : undefined,
|
|
21
|
+
left: cell.column.getIsPinned() === 'left' ? cell.column.getStart() : undefined,
|
|
22
|
+
verticalAlign: 'middle',
|
|
23
|
+
}, onKeyDown: (e) => handleGridKeyDown(e, rowIdx, colIdx), children: _jsx("div", { style: {
|
|
24
|
+
display: 'block',
|
|
25
|
+
minWidth: 0,
|
|
26
|
+
maxWidth: '100%',
|
|
27
|
+
overflow: wrapText ? 'visible' : 'hidden',
|
|
28
|
+
textOverflow: wrapText ? undefined : 'ellipsis',
|
|
29
|
+
whiteSpace: wrapText ? 'normal' : 'nowrap',
|
|
30
|
+
flex: '1 1 0%',
|
|
31
|
+
width: 0, // Allow flex to control width, but start from 0
|
|
32
|
+
}, children: flexRender(cell.column.columnDef.cell, cell.getContext()) }) }, cell.id));
|
|
20
33
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const maxSize = header.column.columnDef.maxSize ?? 0;
|
|
24
|
-
const handleKeyDown = (e) => {
|
|
25
|
-
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
26
|
-
e.preventDefault();
|
|
27
|
-
const currentSize = header.getSize();
|
|
28
|
-
const increment = e.shiftKey ? 20 : 5; // Larger increment with Shift key
|
|
29
|
-
const newSize = e.key === 'ArrowLeft'
|
|
30
|
-
? Math.max(minSize, currentSize - increment)
|
|
31
|
-
: Math.min(maxSize, currentSize + increment);
|
|
32
|
-
setColumnSizing((prevSizing) => ({
|
|
33
|
-
...prevSizing,
|
|
34
|
-
[header.column.id]: newSize,
|
|
35
|
-
}));
|
|
36
|
-
}
|
|
37
|
-
else if (e.key === 'Home') {
|
|
38
|
-
e.preventDefault();
|
|
39
|
-
header.column.resetSize();
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
const columnName = typeof header.column.columnDef.header === 'string'
|
|
43
|
-
? header.column.columnDef.header
|
|
44
|
-
: header.column.id;
|
|
45
|
-
return (_jsx("div", { class: "py-1 h-100", style: { position: 'absolute', right: 0, top: 0, width: '4px' }, children: _jsx("div", { role: "separator", "aria-label": `Resize '${columnName}' column`, "aria-valuetext": `${header.getSize()}px`, "aria-orientation": "vertical", "aria-valuemin": minSize, "aria-valuemax": maxSize, "aria-valuenow": header.getSize(),
|
|
46
|
-
// eslint-disable-next-line jsx-a11y-x/no-noninteractive-tabindex
|
|
47
|
-
tabIndex: 0, class: "h-100", style: {
|
|
48
|
-
background: header.column.getIsResizing() ? 'var(--bs-primary)' : 'var(--bs-gray-400)',
|
|
49
|
-
cursor: 'col-resize',
|
|
50
|
-
transition: 'background-color 0.2s',
|
|
51
|
-
}, onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), onKeyDown: handleKeyDown }) }));
|
|
52
|
-
}
|
|
53
|
-
const DefaultNoResultsState = (_jsxs(_Fragment, { children: [_jsx("i", { class: "bi bi-search display-4 mb-2", "aria-hidden": "true" }), _jsx("p", { class: "mb-0", children: "No results found matching your search criteria." })] }));
|
|
54
|
-
const DefaultEmptyState = (_jsxs(_Fragment, { children: [_jsx("i", { class: "bi bi-eye-slash display-4 mb-2", "aria-hidden": "true" }), _jsx("p", { class: "mb-0", children: "No results found." })] }));
|
|
34
|
+
const DefaultNoResultsState = (_jsx(TanstackTableEmptyState, { iconName: "bi-search", children: "No results found matching your search criteria." }));
|
|
35
|
+
const DefaultEmptyState = (_jsx(TanstackTableEmptyState, { iconName: "bi-eye-slash", children: "No results found." }));
|
|
55
36
|
const DEFAULT_FILTER_MAP = {};
|
|
56
37
|
/**
|
|
57
38
|
* A generic component that renders a full-width, resizeable Tanstack Table.
|
|
@@ -62,19 +43,52 @@ const DEFAULT_FILTER_MAP = {};
|
|
|
62
43
|
* @param params.rowHeight - The height of the rows in the table
|
|
63
44
|
* @param params.noResultsState - The no results state for the table
|
|
64
45
|
* @param params.emptyState - The empty state for the table
|
|
46
|
+
* @param params.scrollRef - Optional ref that will be attached to the scroll container element.
|
|
65
47
|
*/
|
|
66
|
-
export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowHeight = 42, noResultsState = DefaultNoResultsState, emptyState = DefaultEmptyState, }) {
|
|
48
|
+
export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowHeight = 42, noResultsState = DefaultNoResultsState, emptyState = DefaultEmptyState, scrollRef, }) {
|
|
67
49
|
const parentRef = useRef(null);
|
|
68
50
|
const tableRef = useRef(null);
|
|
51
|
+
const scrollContainerRef = scrollRef ?? parentRef;
|
|
69
52
|
const rows = [...table.getTopRows(), ...table.getCenterRows()];
|
|
70
53
|
const rowVirtualizer = useVirtualizer({
|
|
71
54
|
count: rows.length,
|
|
72
|
-
getScrollElement: () =>
|
|
55
|
+
getScrollElement: () => scrollContainerRef.current,
|
|
73
56
|
estimateSize: () => rowHeight,
|
|
74
57
|
overscan: 10,
|
|
58
|
+
measureElement: (el) => el?.getBoundingClientRect().height ?? rowHeight,
|
|
59
|
+
});
|
|
60
|
+
const visibleColumns = table.getVisibleLeafColumns();
|
|
61
|
+
const centerColumns = visibleColumns.filter((col) => !col.getIsPinned());
|
|
62
|
+
const columnVirtualizer = useVirtualizer({
|
|
63
|
+
count: centerColumns.length,
|
|
64
|
+
estimateSize: (index) => centerColumns[index]?.getSize(),
|
|
65
|
+
// `useAutoSizeColumns` solves a different problem (happens once when the column set changes)
|
|
66
|
+
// and we don't need to measure the cells themselves, so we can use the default estimateSize.
|
|
67
|
+
getScrollElement: () => scrollContainerRef.current,
|
|
68
|
+
horizontal: true,
|
|
69
|
+
overscan: 3,
|
|
70
|
+
});
|
|
71
|
+
const virtualColumns = columnVirtualizer.getVirtualItems();
|
|
72
|
+
const virtualPaddingLeft = run(() => {
|
|
73
|
+
if (columnVirtualizer && virtualColumns?.length > 0) {
|
|
74
|
+
return virtualColumns[0]?.start ?? 0;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
75
77
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
const virtualPaddingRight = run(() => {
|
|
79
|
+
if (columnVirtualizer && virtualColumns?.length > 0) {
|
|
80
|
+
return (columnVirtualizer.getTotalSize() - (virtualColumns[virtualColumns.length - 1]?.end ?? 0));
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
});
|
|
84
|
+
// Check if any column has wrapping enabled
|
|
85
|
+
const hasWrappedColumns = table.getAllLeafColumns().some((col) => col.columnDef.meta?.wrapText);
|
|
86
|
+
// Create callback for remeasuring after resize
|
|
87
|
+
const handleResizeEnd = useMemo(() => {
|
|
88
|
+
if (!hasWrappedColumns)
|
|
89
|
+
return undefined;
|
|
90
|
+
return () => rowVirtualizer.measure();
|
|
91
|
+
}, [hasWrappedColumns, rowVirtualizer]);
|
|
78
92
|
const getVisibleCells = (row) => [
|
|
79
93
|
...row.getLeftVisibleCells(),
|
|
80
94
|
...row.getCenterVisibleCells(),
|
|
@@ -103,95 +117,47 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
103
117
|
if (!next) {
|
|
104
118
|
return;
|
|
105
119
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
// Only handle arrow keys if we're in the cell itself, not in an interactive element
|
|
121
|
+
const target = e.target;
|
|
122
|
+
if (target.tagName === 'TD') {
|
|
123
|
+
// If we are on the leftmost column, we should allow left scrolling.
|
|
124
|
+
if (colIdx === 0 && e.key === 'ArrowLeft') {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// If we are on the top row, we should allow up scrolling.
|
|
128
|
+
if (rowIdx === 0 && e.key === 'ArrowUp') {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// If we are on the rightmost column, we should allow right scrolling.
|
|
132
|
+
if (colIdx === rowLength - 1 && e.key === 'ArrowRight') {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const selector = `[data-grid-cell-row="${next.row}"][data-grid-cell-col="${next.col}"]`;
|
|
137
|
+
const nextCell = tableRef.current?.querySelector(selector);
|
|
138
|
+
nextCell?.focus();
|
|
118
139
|
}
|
|
119
|
-
e.preventDefault();
|
|
120
140
|
};
|
|
121
|
-
useEffect(() => {
|
|
122
|
-
const selector = `[data-grid-cell-row="${focusedCell.row}"][data-grid-cell-col="${focusedCell.col}"]`;
|
|
123
|
-
const cell = tableRef.current?.querySelector(selector);
|
|
124
|
-
if (!cell)
|
|
125
|
-
return;
|
|
126
|
-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
|
|
127
|
-
cell.focus();
|
|
128
|
-
}, [focusedCell]);
|
|
129
141
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
130
|
-
const [before, after] = virtualRows.length > 0
|
|
131
|
-
? [
|
|
132
|
-
notUndefined(virtualRows[0]).start - rowVirtualizer.options.scrollMargin,
|
|
133
|
-
rowVirtualizer.getTotalSize() - notUndefined(virtualRows.at(-1)).end,
|
|
134
|
-
]
|
|
135
|
-
: [0, 0];
|
|
136
142
|
const headerGroups = table.getHeaderGroups();
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
143
|
+
const leafHeaderGroup = headerGroups[headerGroups.length - 1];
|
|
144
|
+
const leftPinnedHeaders = leafHeaderGroup.headers.filter((header) => header.column.getIsPinned() === 'left');
|
|
145
|
+
const centerHeaders = leafHeaderGroup.headers.filter((header) => !header.column.getIsPinned());
|
|
146
|
+
const isTableResizing = leafHeaderGroup.headers.some((header) => header.column.getIsResizing());
|
|
140
147
|
// We toggle this here instead of in the parent since this component logically manages all UI for the table.
|
|
141
|
-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-manage-parent
|
|
142
148
|
useEffect(() => {
|
|
143
|
-
document.body.classList.toggle('no-user-select', isTableResizing);
|
|
149
|
+
document.body.classList.toggle('pl-ui-no-user-select', isTableResizing);
|
|
144
150
|
}, [isTableResizing]);
|
|
145
|
-
|
|
151
|
+
const hasAutoSized = useAutoSizeColumns(table, tableRef, filters);
|
|
152
|
+
// Re-measure the virtualizer when auto-sizing completes
|
|
146
153
|
useEffect(() => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (!scrollElement)
|
|
150
|
-
return;
|
|
151
|
-
// Find and check all open popovers
|
|
152
|
-
const popovers = document.querySelectorAll('.popover.show');
|
|
153
|
-
popovers.forEach((popover) => {
|
|
154
|
-
// Find the trigger element for this popover
|
|
155
|
-
const triggerElement = document.querySelector(`[aria-describedby="${popover.id}"]`);
|
|
156
|
-
if (!triggerElement)
|
|
157
|
-
return;
|
|
158
|
-
// Check if the trigger element is still visible in the scroll container
|
|
159
|
-
const scrollRect = scrollElement.getBoundingClientRect();
|
|
160
|
-
const triggerRect = triggerElement.getBoundingClientRect();
|
|
161
|
-
// Check if trigger is outside the visible scroll area
|
|
162
|
-
const isOutOfView = triggerRect.bottom < scrollRect.top ||
|
|
163
|
-
triggerRect.top > scrollRect.bottom ||
|
|
164
|
-
triggerRect.right < scrollRect.left ||
|
|
165
|
-
triggerRect.left > scrollRect.right;
|
|
166
|
-
if (isOutOfView) {
|
|
167
|
-
// Use Bootstrap's Popover API to properly hide it
|
|
168
|
-
const popoverInstance = window.bootstrap?.Popover?.getInstance(triggerElement);
|
|
169
|
-
if (popoverInstance) {
|
|
170
|
-
popoverInstance.hide();
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
};
|
|
175
|
-
const scrollElement = parentRef.current;
|
|
176
|
-
if (scrollElement) {
|
|
177
|
-
scrollElement.addEventListener('scroll', handleScroll);
|
|
178
|
-
return () => scrollElement.removeEventListener('scroll', handleScroll);
|
|
179
|
-
}
|
|
180
|
-
}, []);
|
|
181
|
-
// Helper function to get aria-sort value
|
|
182
|
-
const getAriaSort = (sortDirection) => {
|
|
183
|
-
switch (sortDirection) {
|
|
184
|
-
case 'asc':
|
|
185
|
-
return 'ascending';
|
|
186
|
-
case 'desc':
|
|
187
|
-
return 'descending';
|
|
188
|
-
default:
|
|
189
|
-
return 'none';
|
|
154
|
+
if (hasAutoSized) {
|
|
155
|
+
columnVirtualizer.measure();
|
|
190
156
|
}
|
|
191
|
-
};
|
|
157
|
+
}, [columnVirtualizer, hasAutoSized]);
|
|
192
158
|
const displayedCount = table.getRowModel().rows.length;
|
|
193
159
|
const totalCount = table.getCoreRowModel().rows.length;
|
|
194
|
-
return (_jsxs("div", { style: { position: 'relative' }, class: "d-flex flex-column h-100", children: [_jsx("div", { ref:
|
|
160
|
+
return (_jsxs("div", { style: { position: 'relative' }, class: "d-flex flex-column h-100", children: [_jsx("div", { ref: scrollContainerRef, style: {
|
|
195
161
|
position: 'absolute',
|
|
196
162
|
top: 0,
|
|
197
163
|
left: 0,
|
|
@@ -202,70 +168,50 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
202
168
|
}, children: _jsx("div", { ref: tableRef, style: {
|
|
203
169
|
position: 'relative',
|
|
204
170
|
width: `max(${table.getTotalSize()}px, 100%)`,
|
|
205
|
-
}, children: _jsxs("table", { class: "table table-hover mb-0", style: { tableLayout: 'fixed' }, "aria-label": title, role: "grid", children: [_jsx("thead", {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
: flexRender(header.column.columnDef.header, header.getContext()), canSort && (_jsxs("span", { class: "visually-hidden", children: [", ", getAriaSort(sortDirection), ", click to sort"] }))] }), (canSort || canFilter) && (_jsxs("div", { class: "d-flex align-items-center", children: [canSort && (_jsx("button", { type: "button", class: "btn btn-link text-muted p-0", "aria-label": `Sort ${columnName.toLowerCase()}`, title: `Sort ${columnName.toLowerCase()}`, onClick: header.column.getToggleSortingHandler(), children: _jsx(SortIcon, { sortMethod: sortDirection || false }) })), canFilter && filters[header.column.id]?.({ header })] }))] }), tableRect?.width &&
|
|
244
|
-
tableRect.width > table.getTotalSize() &&
|
|
245
|
-
index === headerGroup.headers.length - 1 ? null : (_jsx(ResizeHandle, { header: header, setColumnSizing: table.setColumnSizing }))] }, header.id));
|
|
246
|
-
}) }, headerGroup.id))) }), _jsxs("tbody", { children: [before > 0 && (_jsx("tr", { tabIndex: -1, children: _jsx("td", { colSpan: headerGroups[0].headers.length, style: { height: before } }) })), virtualRows.map((virtualRow) => {
|
|
247
|
-
const row = rows[virtualRow.index];
|
|
248
|
-
const visibleCells = getVisibleCells(row);
|
|
249
|
-
const rowIdx = virtualRow.index;
|
|
250
|
-
return (_jsx("tr", { style: { height: rowHeight }, children: visibleCells.map((cell, colIdx) => {
|
|
171
|
+
}, children: _jsxs("table", { class: "table table-hover mb-0", style: { display: 'grid', tableLayout: 'fixed' }, "aria-label": title, role: "grid", children: [_jsx("thead", { style: {
|
|
172
|
+
display: 'grid',
|
|
173
|
+
position: 'sticky',
|
|
174
|
+
top: 0,
|
|
175
|
+
zIndex: 1,
|
|
176
|
+
}, children: _jsxs("tr", { style: { display: 'flex', width: `${table.getTotalSize()}px` }, children: [leftPinnedHeaders.map((header) => {
|
|
177
|
+
return (_jsx(TanstackTableHeaderCell, { header: header, filters: filters, table: table, handleResizeEnd: handleResizeEnd, isPinned: "left" }, header.id));
|
|
178
|
+
}), virtualPaddingLeft ? (_jsx("th", { style: { display: 'flex', width: virtualPaddingLeft } })) : null, virtualColumns.map((virtualColumn) => {
|
|
179
|
+
const header = centerHeaders[virtualColumn.index];
|
|
180
|
+
if (!header)
|
|
181
|
+
return null;
|
|
182
|
+
return (_jsx(TanstackTableHeaderCell, { header: header, filters: filters, table: table, handleResizeEnd: handleResizeEnd, isPinned: false }, header.id));
|
|
183
|
+
}), virtualPaddingRight ? (_jsx("th", { style: { display: 'flex', width: virtualPaddingRight } })) : null] }, leafHeaderGroup.id) }), _jsx("tbody", { style: {
|
|
184
|
+
display: 'grid',
|
|
185
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
186
|
+
position: 'relative',
|
|
187
|
+
}, children: virtualRows.map((virtualRow) => {
|
|
188
|
+
const row = rows[virtualRow.index];
|
|
189
|
+
const rowIdx = virtualRow.index;
|
|
190
|
+
const leftPinnedCells = row.getLeftVisibleCells();
|
|
191
|
+
const centerCells = row.getCenterVisibleCells();
|
|
192
|
+
let currentColIdx = 0;
|
|
193
|
+
return (_jsxs("tr", { ref: (node) => rowVirtualizer.measureElement(node), "data-index": virtualRow.index, style: {
|
|
194
|
+
display: 'flex',
|
|
195
|
+
position: 'absolute',
|
|
196
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
197
|
+
width: `${table.getTotalSize()}px`,
|
|
198
|
+
}, children: [leftPinnedCells.map((cell) => {
|
|
199
|
+
const colIdx = currentColIdx++;
|
|
200
|
+
const canSort = cell.column.getCanSort();
|
|
201
|
+
const canFilter = cell.column.getCanFilter();
|
|
202
|
+
const wrapText = cell.column.columnDef.meta?.wrapText ?? false;
|
|
203
|
+
return (_jsx(TableCell, { cell: cell, rowIdx: rowIdx, colIdx: colIdx, canSort: canSort, canFilter: canFilter, wrapText: wrapText, handleGridKeyDown: handleGridKeyDown }, cell.id));
|
|
204
|
+
}), virtualPaddingLeft ? (_jsx("td", { style: { display: 'flex', width: virtualPaddingLeft } })) : null, virtualColumns.map((virtualColumn) => {
|
|
205
|
+
const cell = centerCells[virtualColumn.index];
|
|
206
|
+
if (!cell)
|
|
207
|
+
return null;
|
|
208
|
+
const colIdx = currentColIdx++;
|
|
251
209
|
const canSort = cell.column.getCanSort();
|
|
252
210
|
const canFilter = cell.column.getCanFilter();
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
? `max(100%, ${cell.column.getSize()}px)`
|
|
258
|
-
: cell.column.getSize(),
|
|
259
|
-
position: cell.column.getIsPinned() === 'left' ? 'sticky' : undefined,
|
|
260
|
-
left: cell.column.getIsPinned() === 'left'
|
|
261
|
-
? cell.column.getStart()
|
|
262
|
-
: undefined,
|
|
263
|
-
whiteSpace: 'nowrap',
|
|
264
|
-
overflow: 'hidden',
|
|
265
|
-
textOverflow: 'ellipsis',
|
|
266
|
-
}, onFocus: () => setFocusedCell({ row: rowIdx, col: colIdx }), onKeyDown: (e) => handleGridKeyDown(e, rowIdx, colIdx), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
267
|
-
}) }, row.id));
|
|
268
|
-
}), after > 0 && (_jsx("tr", { tabIndex: -1, children: _jsx("td", { colSpan: headerGroups[0].headers.length, style: { height: after } }) }))] })] }) }) }), table.getVisibleLeafColumns().length === 0 || displayedCount === 0 ? (_jsx("div", { children: _jsx("div", { class: "d-flex flex-column justify-content-center align-items-center p-4", style: {
|
|
211
|
+
const wrapText = cell.column.columnDef.meta?.wrapText ?? false;
|
|
212
|
+
return (_jsx(TableCell, { cell: cell, rowIdx: rowIdx, colIdx: colIdx, canSort: canSort, canFilter: canFilter, wrapText: wrapText, handleGridKeyDown: handleGridKeyDown }, cell.id));
|
|
213
|
+
}), virtualPaddingRight ? (_jsx("td", { style: { display: 'flex', width: virtualPaddingRight } })) : null] }, row.id));
|
|
214
|
+
}) })] }) }) }), table.getVisibleLeafColumns().length === 0 || displayedCount === 0 ? (_jsx("div", { children: _jsx("div", { class: "d-flex flex-column justify-content-center align-items-center p-4", style: {
|
|
269
215
|
position: 'absolute',
|
|
270
216
|
top: 0,
|
|
271
217
|
left: 0,
|
|
@@ -283,8 +229,13 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
283
229
|
* @param params
|
|
284
230
|
* @param params.table - The table model
|
|
285
231
|
* @param params.title - The title of the card
|
|
232
|
+
* @param params.className - The class name to apply to the card
|
|
233
|
+
* @param params.style - The style to apply to the card
|
|
234
|
+
* @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
|
|
235
|
+
* @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
|
|
286
236
|
* @param params.headerButtons - The buttons to display in the header
|
|
287
237
|
* @param params.columnManagerButtons - The buttons to display next to the column manager (View button)
|
|
238
|
+
* @param params.columnManagerTopContent - Optional content to display at the top of the column manager (View) dropdown menu
|
|
288
239
|
* @param params.globalFilter - State management for the global filter
|
|
289
240
|
* @param params.globalFilter.value
|
|
290
241
|
* @param params.globalFilter.setValue
|
|
@@ -292,7 +243,7 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
292
243
|
* @param params.tableOptions - Specific options for the table. See {@link TanstackTableProps} for more details.
|
|
293
244
|
* @param params.downloadButtonOptions - Specific options for the download button. See {@link TanstackTableDownloadButtonProps} for more details.
|
|
294
245
|
*/
|
|
295
|
-
export function TanstackTableCard({ table, title, headerButtons, columnManagerButtons, globalFilter, tableOptions, downloadButtonOptions
|
|
246
|
+
export function TanstackTableCard({ table, title, singularLabel, pluralLabel, headerButtons, columnManagerButtons, columnManagerTopContent, globalFilter, tableOptions, downloadButtonOptions, className, ...divProps }) {
|
|
296
247
|
const searchInputRef = useRef(null);
|
|
297
248
|
// Focus the search input when Ctrl+F is pressed
|
|
298
249
|
useEffect(() => {
|
|
@@ -309,11 +260,11 @@ export function TanstackTableCard({ table, title, headerButtons, columnManagerBu
|
|
|
309
260
|
}, []);
|
|
310
261
|
const displayedCount = table.getRowModel().rows.length;
|
|
311
262
|
const totalCount = table.getCoreRowModel().rows.length;
|
|
312
|
-
return (_jsxs("div", { class:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
263
|
+
return (_jsxs("div", { class: clsx('card d-flex flex-column', className), ...divProps, children: [_jsx("div", { class: "card-header bg-primary text-white", children: _jsxs("div", { class: "d-flex align-items-center justify-content-between gap-2", children: [_jsx("div", { children: title }), _jsxs("div", { class: "d-flex gap-2", children: [headerButtons, downloadButtonOptions && (_jsx(TanstackTableDownloadButton, { table: table, pluralLabel: pluralLabel, singularLabel: singularLabel, ...downloadButtonOptions }))] })] }) }), _jsxs("div", { class: "card-body d-flex flex-row flex-wrap flex-grow-0 align-items-center gap-2", children: [_jsxs("div", { class: "position-relative w-100", style: { maxWidth: 'min(400px, 100%)' }, children: [_jsx("input", { ref: searchInputRef, type: "text", class: "form-control pl-ui-tanstack-table-search-input pl-ui-tanstack-table-focusable-shadow", "aria-label": globalFilter.placeholder, placeholder: globalFilter.placeholder, value: globalFilter.value, autoComplete: "off", onInput: (e) => {
|
|
264
|
+
if (!(e.target instanceof HTMLInputElement))
|
|
265
|
+
return;
|
|
266
|
+
globalFilter.setValue(e.target.value);
|
|
267
|
+
} }), globalFilter.value && (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Clear search" }), children: _jsx("button", { type: "button", class: "btn btn-floating-icon", "aria-label": "Clear search", onClick: () => globalFilter.setValue(''), children: _jsx("i", { class: "bi bi-x-circle-fill", "aria-hidden": "true" }) }) }))] }), _jsxs("div", { class: "d-flex flex-wrap flex-row align-items-center gap-2", children: [_jsx(ColumnManager, { table: table, topContent: columnManagerTopContent }), columnManagerButtons] }), _jsxs("div", { class: "ms-auto text-muted text-nowrap", children: ["Showing ", displayedCount, " of ", totalCount, " ", totalCount === 1 ? singularLabel : pluralLabel] })] }), _jsx("div", { class: "flex-grow-1", children: _jsx(TanstackTable, { table: table, title: title, ...tableOptions }) })] }));
|
|
317
268
|
}
|
|
318
269
|
export function TanstackTableEmptyState({ iconName, children, }) {
|
|
319
270
|
return (_jsxs("div", { class: "d-flex flex-column justify-content-center align-items-center text-muted", children: [_jsx("i", { class: clsx('bi', iconName, 'display-4 mb-2'), "aria-hidden": "true" }), _jsx("div", { children: children })] }));
|