@prairielearn/ui 1.2.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 +32 -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 +26 -14
- package/dist/components/CategoricalColumnFilter.js.map +1 -1
- package/dist/components/ColumnManager.d.ts +6 -2
- package/dist/components/ColumnManager.d.ts.map +1 -1
- package/dist/components/ColumnManager.js +98 -35
- 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 +19 -3
- package/dist/components/TanstackTable.d.ts.map +1 -1
- package/dist/components/TanstackTable.js +159 -219
- 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/styles.css +58 -0
- 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 +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- 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 +84 -54
- package/src/components/ColumnManager.tsx +236 -88
- 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 +357 -410
- package/src/components/TanstackTableDownloadButton.tsx +8 -5
- package/src/components/TanstackTableHeaderCell.tsx +207 -0
- package/src/components/styles.css +58 -0
- package/src/components/useAutoSizeColumns.tsx +168 -0
- package/src/index.ts +10 -1
- package/src/react-table.ts +17 -0
- package/tsconfig.json +1 -2
- package/dist/components/TanstackTable.css +0 -4
- package/src/components/TanstackTable.css +0 -4
|
@@ -1,102 +1,86 @@
|
|
|
1
1
|
import { flexRender } from '@tanstack/react-table';
|
|
2
|
-
import {
|
|
3
|
-
import type { Header, Row,
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import type { Cell, Header, Row, Table } from '@tanstack/table-core';
|
|
4
4
|
import clsx from 'clsx';
|
|
5
|
-
import {
|
|
5
|
+
import type { ComponentChildren } from 'preact';
|
|
6
|
+
import { useEffect, useMemo, useRef } from 'preact/hooks';
|
|
6
7
|
import type { JSX } from 'preact/jsx-runtime';
|
|
8
|
+
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|
9
|
+
import Tooltip from 'react-bootstrap/Tooltip';
|
|
10
|
+
|
|
11
|
+
import type { ComponentProps } from '@prairielearn/preact-cjs';
|
|
12
|
+
import { run } from '@prairielearn/run';
|
|
7
13
|
|
|
8
14
|
import { ColumnManager } from './ColumnManager.js';
|
|
9
15
|
import {
|
|
10
16
|
TanstackTableDownloadButton,
|
|
11
17
|
type TanstackTableDownloadButtonProps,
|
|
12
18
|
} from './TanstackTableDownloadButton.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
function ResizeHandle<RowDataModel>({
|
|
25
|
-
header,
|
|
26
|
-
setColumnSizing,
|
|
19
|
+
import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
|
|
20
|
+
import { useAutoSizeColumns } from './useAutoSizeColumns.js';
|
|
21
|
+
|
|
22
|
+
function TableCell<RowDataModel>({
|
|
23
|
+
cell,
|
|
24
|
+
rowIdx,
|
|
25
|
+
colIdx,
|
|
26
|
+
canSort,
|
|
27
|
+
canFilter,
|
|
28
|
+
wrapText,
|
|
29
|
+
handleGridKeyDown,
|
|
27
30
|
}: {
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
cell: Cell<RowDataModel, unknown>;
|
|
32
|
+
rowIdx: number;
|
|
33
|
+
colIdx: number;
|
|
34
|
+
canSort: boolean;
|
|
35
|
+
canFilter: boolean;
|
|
36
|
+
wrapText: boolean;
|
|
37
|
+
handleGridKeyDown: (e: KeyboardEvent, rowIdx: number, colIdx: number) => void;
|
|
30
38
|
}) {
|
|
31
|
-
const minSize = header.column.columnDef.minSize ?? 0;
|
|
32
|
-
const maxSize = header.column.columnDef.maxSize ?? 0;
|
|
33
|
-
const handleKeyDown = (e: KeyboardEvent) => {
|
|
34
|
-
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
const currentSize = header.getSize();
|
|
37
|
-
const increment = e.shiftKey ? 20 : 5; // Larger increment with Shift key
|
|
38
|
-
const newSize =
|
|
39
|
-
e.key === 'ArrowLeft'
|
|
40
|
-
? Math.max(minSize, currentSize - increment)
|
|
41
|
-
: Math.min(maxSize, currentSize + increment);
|
|
42
|
-
|
|
43
|
-
setColumnSizing((prevSizing) => ({
|
|
44
|
-
...prevSizing,
|
|
45
|
-
[header.column.id]: newSize,
|
|
46
|
-
}));
|
|
47
|
-
} else if (e.key === 'Home') {
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
header.column.resetSize();
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const columnName =
|
|
54
|
-
typeof header.column.columnDef.header === 'string'
|
|
55
|
-
? header.column.columnDef.header
|
|
56
|
-
: header.column.id;
|
|
57
|
-
|
|
58
39
|
return (
|
|
59
|
-
<
|
|
60
|
-
{
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
<td
|
|
41
|
+
key={cell.id}
|
|
42
|
+
tabIndex={0}
|
|
43
|
+
data-grid-cell-row={rowIdx}
|
|
44
|
+
data-grid-cell-col={colIdx}
|
|
45
|
+
class={clsx(!canSort && !canFilter && 'text-center')}
|
|
46
|
+
style={{
|
|
47
|
+
display: 'flex',
|
|
48
|
+
width: cell.column.getSize(),
|
|
49
|
+
minWidth: 0,
|
|
50
|
+
maxWidth: cell.column.getSize(),
|
|
51
|
+
flexShrink: 0,
|
|
52
|
+
position: cell.column.getIsPinned() === 'left' ? 'sticky' : undefined,
|
|
53
|
+
left: cell.column.getIsPinned() === 'left' ? cell.column.getStart() : undefined,
|
|
54
|
+
verticalAlign: 'middle',
|
|
55
|
+
}}
|
|
56
|
+
onKeyDown={(e) => handleGridKeyDown(e, rowIdx, colIdx)}
|
|
57
|
+
>
|
|
64
58
|
<div
|
|
65
|
-
role="separator"
|
|
66
|
-
aria-label={`Resize '${columnName}' column`}
|
|
67
|
-
aria-valuetext={`${header.getSize()}px`}
|
|
68
|
-
aria-orientation="vertical"
|
|
69
|
-
aria-valuemin={minSize}
|
|
70
|
-
aria-valuemax={maxSize}
|
|
71
|
-
aria-valuenow={header.getSize()}
|
|
72
|
-
// eslint-disable-next-line jsx-a11y-x/no-noninteractive-tabindex
|
|
73
|
-
tabIndex={0}
|
|
74
|
-
class="h-100"
|
|
75
59
|
style={{
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
60
|
+
display: 'block',
|
|
61
|
+
minWidth: 0,
|
|
62
|
+
maxWidth: '100%',
|
|
63
|
+
overflow: wrapText ? 'visible' : 'hidden',
|
|
64
|
+
textOverflow: wrapText ? undefined : 'ellipsis',
|
|
65
|
+
whiteSpace: wrapText ? 'normal' : 'nowrap',
|
|
66
|
+
flex: '1 1 0%',
|
|
67
|
+
width: 0, // Allow flex to control width, but start from 0
|
|
79
68
|
}}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
</div>
|
|
69
|
+
>
|
|
70
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
71
|
+
</div>
|
|
72
|
+
</td>
|
|
85
73
|
);
|
|
86
74
|
}
|
|
87
75
|
|
|
88
76
|
const DefaultNoResultsState = (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
</>
|
|
77
|
+
<TanstackTableEmptyState iconName="bi-search">
|
|
78
|
+
No results found matching your search criteria.
|
|
79
|
+
</TanstackTableEmptyState>
|
|
93
80
|
);
|
|
94
81
|
|
|
95
82
|
const DefaultEmptyState = (
|
|
96
|
-
|
|
97
|
-
<i class="bi bi-eye-slash display-4 mb-2" aria-hidden="true" />
|
|
98
|
-
<p class="mb-0">No results found.</p>
|
|
99
|
-
</>
|
|
83
|
+
<TanstackTableEmptyState iconName="bi-eye-slash">No results found.</TanstackTableEmptyState>
|
|
100
84
|
);
|
|
101
85
|
|
|
102
86
|
interface TanstackTableProps<RowDataModel> {
|
|
@@ -106,6 +90,7 @@ interface TanstackTableProps<RowDataModel> {
|
|
|
106
90
|
rowHeight?: number;
|
|
107
91
|
noResultsState?: JSX.Element;
|
|
108
92
|
emptyState?: JSX.Element;
|
|
93
|
+
scrollRef?: React.RefObject<HTMLDivElement> | null;
|
|
109
94
|
}
|
|
110
95
|
|
|
111
96
|
const DEFAULT_FILTER_MAP = {};
|
|
@@ -119,6 +104,7 @@ const DEFAULT_FILTER_MAP = {};
|
|
|
119
104
|
* @param params.rowHeight - The height of the rows in the table
|
|
120
105
|
* @param params.noResultsState - The no results state for the table
|
|
121
106
|
* @param params.emptyState - The empty state for the table
|
|
107
|
+
* @param params.scrollRef - Optional ref that will be attached to the scroll container element.
|
|
122
108
|
*/
|
|
123
109
|
export function TanstackTable<RowDataModel>({
|
|
124
110
|
table,
|
|
@@ -127,19 +113,60 @@ export function TanstackTable<RowDataModel>({
|
|
|
127
113
|
rowHeight = 42,
|
|
128
114
|
noResultsState = DefaultNoResultsState,
|
|
129
115
|
emptyState = DefaultEmptyState,
|
|
116
|
+
scrollRef,
|
|
130
117
|
}: TanstackTableProps<RowDataModel>) {
|
|
131
118
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
132
119
|
const tableRef = useRef<HTMLDivElement>(null);
|
|
120
|
+
const scrollContainerRef = scrollRef ?? parentRef;
|
|
121
|
+
|
|
133
122
|
const rows = [...table.getTopRows(), ...table.getCenterRows()];
|
|
134
123
|
const rowVirtualizer = useVirtualizer({
|
|
135
124
|
count: rows.length,
|
|
136
|
-
getScrollElement: () =>
|
|
125
|
+
getScrollElement: () => scrollContainerRef.current,
|
|
137
126
|
estimateSize: () => rowHeight,
|
|
138
127
|
overscan: 10,
|
|
128
|
+
measureElement: (el) => el?.getBoundingClientRect().height ?? rowHeight,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const visibleColumns = table.getVisibleLeafColumns();
|
|
132
|
+
const centerColumns = visibleColumns.filter((col) => !col.getIsPinned());
|
|
133
|
+
|
|
134
|
+
const columnVirtualizer = useVirtualizer({
|
|
135
|
+
count: centerColumns.length,
|
|
136
|
+
estimateSize: (index) => centerColumns[index]?.getSize(),
|
|
137
|
+
// `useAutoSizeColumns` solves a different problem (happens once when the column set changes)
|
|
138
|
+
// and we don't need to measure the cells themselves, so we can use the default estimateSize.
|
|
139
|
+
getScrollElement: () => scrollContainerRef.current,
|
|
140
|
+
horizontal: true,
|
|
141
|
+
overscan: 3,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const virtualColumns = columnVirtualizer.getVirtualItems();
|
|
145
|
+
|
|
146
|
+
const virtualPaddingLeft = run(() => {
|
|
147
|
+
if (columnVirtualizer && virtualColumns?.length > 0) {
|
|
148
|
+
return virtualColumns[0]?.start ?? 0;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const virtualPaddingRight = run(() => {
|
|
154
|
+
if (columnVirtualizer && virtualColumns?.length > 0) {
|
|
155
|
+
return (
|
|
156
|
+
columnVirtualizer.getTotalSize() - (virtualColumns[virtualColumns.length - 1]?.end ?? 0)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
139
160
|
});
|
|
140
161
|
|
|
141
|
-
//
|
|
142
|
-
const
|
|
162
|
+
// Check if any column has wrapping enabled
|
|
163
|
+
const hasWrappedColumns = table.getAllLeafColumns().some((col) => col.columnDef.meta?.wrapText);
|
|
164
|
+
|
|
165
|
+
// Create callback for remeasuring after resize
|
|
166
|
+
const handleResizeEnd = useMemo(() => {
|
|
167
|
+
if (!hasWrappedColumns) return undefined;
|
|
168
|
+
return () => rowVirtualizer.measure();
|
|
169
|
+
}, [hasWrappedColumns, rowVirtualizer]);
|
|
143
170
|
|
|
144
171
|
const getVisibleCells = (row: Row<RowDataModel>) => [
|
|
145
172
|
...row.getLeftVisibleCells(),
|
|
@@ -173,108 +200,57 @@ export function TanstackTable<RowDataModel>({
|
|
|
173
200
|
return;
|
|
174
201
|
}
|
|
175
202
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
203
|
+
// Only handle arrow keys if we're in the cell itself, not in an interactive element
|
|
204
|
+
const target = e.target as HTMLElement;
|
|
205
|
+
if (target.tagName === 'TD') {
|
|
206
|
+
// If we are on the leftmost column, we should allow left scrolling.
|
|
207
|
+
if (colIdx === 0 && e.key === 'ArrowLeft') {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
181
210
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
211
|
+
// If we are on the top row, we should allow up scrolling.
|
|
212
|
+
if (rowIdx === 0 && e.key === 'ArrowUp') {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
186
215
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
216
|
+
// If we are on the rightmost column, we should allow right scrolling.
|
|
217
|
+
if (colIdx === rowLength - 1 && e.key === 'ArrowRight') {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
191
220
|
|
|
192
|
-
|
|
221
|
+
e.preventDefault();
|
|
222
|
+
const selector = `[data-grid-cell-row="${next.row}"][data-grid-cell-col="${next.col}"]`;
|
|
223
|
+
const nextCell = tableRef.current?.querySelector(selector) as HTMLElement | null;
|
|
224
|
+
nextCell?.focus();
|
|
225
|
+
}
|
|
193
226
|
};
|
|
194
227
|
|
|
195
|
-
useEffect(() => {
|
|
196
|
-
const selector = `[data-grid-cell-row="${focusedCell.row}"][data-grid-cell-col="${focusedCell.col}"]`;
|
|
197
|
-
const cell = tableRef.current?.querySelector(selector) as HTMLElement | null;
|
|
198
|
-
if (!cell) return;
|
|
199
|
-
|
|
200
|
-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
|
|
201
|
-
cell.focus();
|
|
202
|
-
}, [focusedCell]);
|
|
203
|
-
|
|
204
228
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
205
|
-
|
|
206
|
-
virtualRows.length > 0
|
|
207
|
-
? [
|
|
208
|
-
notUndefined(virtualRows[0]).start - rowVirtualizer.options.scrollMargin,
|
|
209
|
-
rowVirtualizer.getTotalSize() - notUndefined(virtualRows.at(-1)).end,
|
|
210
|
-
]
|
|
211
|
-
: [0, 0];
|
|
229
|
+
|
|
212
230
|
const headerGroups = table.getHeaderGroups();
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
|
|
232
|
+
const leafHeaderGroup = headerGroups[headerGroups.length - 1];
|
|
233
|
+
|
|
234
|
+
const leftPinnedHeaders = leafHeaderGroup.headers.filter(
|
|
235
|
+
(header) => header.column.getIsPinned() === 'left',
|
|
215
236
|
);
|
|
216
|
-
const
|
|
237
|
+
const centerHeaders = leafHeaderGroup.headers.filter((header) => !header.column.getIsPinned());
|
|
217
238
|
|
|
218
|
-
const
|
|
239
|
+
const isTableResizing = leafHeaderGroup.headers.some((header) => header.column.getIsResizing());
|
|
219
240
|
|
|
220
241
|
// We toggle this here instead of in the parent since this component logically manages all UI for the table.
|
|
221
|
-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-manage-parent
|
|
222
242
|
useEffect(() => {
|
|
223
|
-
document.body.classList.toggle('no-user-select', isTableResizing);
|
|
243
|
+
document.body.classList.toggle('pl-ui-no-user-select', isTableResizing);
|
|
224
244
|
}, [isTableResizing]);
|
|
225
245
|
|
|
226
|
-
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
const handleScroll = () => {
|
|
229
|
-
const scrollElement = parentRef.current;
|
|
230
|
-
if (!scrollElement) return;
|
|
231
|
-
|
|
232
|
-
// Find and check all open popovers
|
|
233
|
-
const popovers = document.querySelectorAll('.popover.show');
|
|
234
|
-
popovers.forEach((popover) => {
|
|
235
|
-
// Find the trigger element for this popover
|
|
236
|
-
const triggerElement = document.querySelector(`[aria-describedby="${popover.id}"]`);
|
|
237
|
-
if (!triggerElement) return;
|
|
238
|
-
|
|
239
|
-
// Check if the trigger element is still visible in the scroll container
|
|
240
|
-
const scrollRect = scrollElement.getBoundingClientRect();
|
|
241
|
-
const triggerRect = triggerElement.getBoundingClientRect();
|
|
242
|
-
|
|
243
|
-
// Check if trigger is outside the visible scroll area
|
|
244
|
-
const isOutOfView =
|
|
245
|
-
triggerRect.bottom < scrollRect.top ||
|
|
246
|
-
triggerRect.top > scrollRect.bottom ||
|
|
247
|
-
triggerRect.right < scrollRect.left ||
|
|
248
|
-
triggerRect.left > scrollRect.right;
|
|
249
|
-
|
|
250
|
-
if (isOutOfView) {
|
|
251
|
-
// Use Bootstrap's Popover API to properly hide it
|
|
252
|
-
const popoverInstance = (window as any).bootstrap?.Popover?.getInstance(triggerElement);
|
|
253
|
-
if (popoverInstance) {
|
|
254
|
-
popoverInstance.hide();
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const scrollElement = parentRef.current;
|
|
261
|
-
if (scrollElement) {
|
|
262
|
-
scrollElement.addEventListener('scroll', handleScroll);
|
|
263
|
-
return () => scrollElement.removeEventListener('scroll', handleScroll);
|
|
264
|
-
}
|
|
265
|
-
}, []);
|
|
246
|
+
const hasAutoSized = useAutoSizeColumns(table, tableRef, filters);
|
|
266
247
|
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return 'ascending';
|
|
272
|
-
case 'desc':
|
|
273
|
-
return 'descending';
|
|
274
|
-
default:
|
|
275
|
-
return 'none';
|
|
248
|
+
// Re-measure the virtualizer when auto-sizing completes
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (hasAutoSized) {
|
|
251
|
+
columnVirtualizer.measure();
|
|
276
252
|
}
|
|
277
|
-
};
|
|
253
|
+
}, [columnVirtualizer, hasAutoSized]);
|
|
278
254
|
|
|
279
255
|
const displayedCount = table.getRowModel().rows.length;
|
|
280
256
|
const totalCount = table.getCoreRowModel().rows.length;
|
|
@@ -282,7 +258,7 @@ export function TanstackTable<RowDataModel>({
|
|
|
282
258
|
return (
|
|
283
259
|
<div style={{ position: 'relative' }} class="d-flex flex-column h-100">
|
|
284
260
|
<div
|
|
285
|
-
ref={
|
|
261
|
+
ref={scrollContainerRef}
|
|
286
262
|
style={{
|
|
287
263
|
position: 'absolute',
|
|
288
264
|
top: 0,
|
|
@@ -301,221 +277,187 @@ export function TanstackTable<RowDataModel>({
|
|
|
301
277
|
}}
|
|
302
278
|
>
|
|
303
279
|
<table
|
|
304
|
-
class="table table-hover mb-0
|
|
305
|
-
style={{ tableLayout: 'fixed' }}
|
|
280
|
+
class="table table-hover mb-0"
|
|
281
|
+
style={{ display: 'grid', tableLayout: 'fixed' }}
|
|
306
282
|
aria-label={title}
|
|
307
283
|
role="grid"
|
|
308
284
|
>
|
|
309
|
-
<thead
|
|
310
|
-
{
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}}
|
|
363
|
-
type="button"
|
|
364
|
-
aria-label={
|
|
365
|
-
canSort
|
|
366
|
-
? `'${columnName}' column, current sort is ${getAriaSort(sortDirection)}`
|
|
367
|
-
: undefined
|
|
368
|
-
}
|
|
369
|
-
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
|
370
|
-
onKeyDown={
|
|
371
|
-
canSort
|
|
372
|
-
? (e) => {
|
|
373
|
-
const handleSort = header.column.getToggleSortingHandler();
|
|
374
|
-
if (e.key === 'Enter' && handleSort) {
|
|
375
|
-
e.preventDefault();
|
|
376
|
-
handleSort(e);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
: undefined
|
|
380
|
-
}
|
|
381
|
-
>
|
|
382
|
-
{header.isPlaceholder
|
|
383
|
-
? null
|
|
384
|
-
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
385
|
-
{canSort && (
|
|
386
|
-
<span class="visually-hidden">
|
|
387
|
-
, {getAriaSort(sortDirection)}, click to sort
|
|
388
|
-
</span>
|
|
389
|
-
)}
|
|
390
|
-
</button>
|
|
391
|
-
|
|
392
|
-
{(canSort || canFilter) && (
|
|
393
|
-
<div class="d-flex align-items-center">
|
|
394
|
-
{canSort && (
|
|
395
|
-
<button
|
|
396
|
-
type="button"
|
|
397
|
-
class="btn btn-link text-muted p-0"
|
|
398
|
-
aria-label={`Sort ${columnName.toLowerCase()}`}
|
|
399
|
-
title={`Sort ${columnName.toLowerCase()}`}
|
|
400
|
-
onClick={header.column.getToggleSortingHandler()}
|
|
401
|
-
>
|
|
402
|
-
<SortIcon sortMethod={sortDirection || false} />
|
|
403
|
-
</button>
|
|
404
|
-
)}
|
|
405
|
-
{canFilter && filters[header.column.id]?.({ header })}
|
|
406
|
-
</div>
|
|
407
|
-
)}
|
|
408
|
-
</div>
|
|
409
|
-
{tableRect?.width &&
|
|
410
|
-
tableRect.width > table.getTotalSize() &&
|
|
411
|
-
index === headerGroup.headers.length - 1 ? null : (
|
|
412
|
-
<ResizeHandle header={header} setColumnSizing={table.setColumnSizing} />
|
|
413
|
-
)}
|
|
414
|
-
</th>
|
|
415
|
-
);
|
|
416
|
-
})}
|
|
417
|
-
</tr>
|
|
418
|
-
))}
|
|
285
|
+
<thead
|
|
286
|
+
style={{
|
|
287
|
+
display: 'grid',
|
|
288
|
+
position: 'sticky',
|
|
289
|
+
top: 0,
|
|
290
|
+
zIndex: 1,
|
|
291
|
+
}}
|
|
292
|
+
>
|
|
293
|
+
<tr
|
|
294
|
+
key={leafHeaderGroup.id}
|
|
295
|
+
style={{ display: 'flex', width: `${table.getTotalSize()}px` }}
|
|
296
|
+
>
|
|
297
|
+
{/* Left pinned columns */}
|
|
298
|
+
{leftPinnedHeaders.map((header) => {
|
|
299
|
+
return (
|
|
300
|
+
<TanstackTableHeaderCell
|
|
301
|
+
key={header.id}
|
|
302
|
+
header={header}
|
|
303
|
+
filters={filters}
|
|
304
|
+
table={table}
|
|
305
|
+
handleResizeEnd={handleResizeEnd}
|
|
306
|
+
isPinned="left"
|
|
307
|
+
/>
|
|
308
|
+
);
|
|
309
|
+
})}
|
|
310
|
+
|
|
311
|
+
{/* Virtual padding for left side of center columns */}
|
|
312
|
+
{virtualPaddingLeft ? (
|
|
313
|
+
<th style={{ display: 'flex', width: virtualPaddingLeft }} />
|
|
314
|
+
) : null}
|
|
315
|
+
|
|
316
|
+
{/* Virtualized center columns */}
|
|
317
|
+
{virtualColumns.map((virtualColumn) => {
|
|
318
|
+
const header = centerHeaders[virtualColumn.index];
|
|
319
|
+
if (!header) return null;
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
<TanstackTableHeaderCell
|
|
323
|
+
key={header.id}
|
|
324
|
+
header={header}
|
|
325
|
+
filters={filters}
|
|
326
|
+
table={table}
|
|
327
|
+
handleResizeEnd={handleResizeEnd}
|
|
328
|
+
isPinned={false}
|
|
329
|
+
/>
|
|
330
|
+
);
|
|
331
|
+
})}
|
|
332
|
+
|
|
333
|
+
{/* Virtual padding for right side of center columns */}
|
|
334
|
+
{virtualPaddingRight ? (
|
|
335
|
+
<th style={{ display: 'flex', width: virtualPaddingRight }} />
|
|
336
|
+
) : null}
|
|
337
|
+
</tr>
|
|
419
338
|
</thead>
|
|
420
|
-
<tbody
|
|
421
|
-
{
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
339
|
+
<tbody
|
|
340
|
+
style={{
|
|
341
|
+
display: 'grid',
|
|
342
|
+
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
343
|
+
position: 'relative',
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
426
346
|
{virtualRows.map((virtualRow) => {
|
|
427
347
|
const row = rows[virtualRow.index];
|
|
428
|
-
const visibleCells = getVisibleCells(row);
|
|
429
348
|
const rowIdx = virtualRow.index;
|
|
349
|
+
const leftPinnedCells = row.getLeftVisibleCells();
|
|
350
|
+
const centerCells = row.getCenterVisibleCells();
|
|
351
|
+
|
|
352
|
+
let currentColIdx = 0;
|
|
430
353
|
|
|
431
354
|
return (
|
|
432
|
-
<tr
|
|
433
|
-
{
|
|
355
|
+
<tr
|
|
356
|
+
key={row.id}
|
|
357
|
+
ref={(node) => rowVirtualizer.measureElement(node)}
|
|
358
|
+
data-index={virtualRow.index}
|
|
359
|
+
style={{
|
|
360
|
+
display: 'flex',
|
|
361
|
+
position: 'absolute',
|
|
362
|
+
transform: `translateY(${virtualRow.start}px)`,
|
|
363
|
+
width: `${table.getTotalSize()}px`,
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
{leftPinnedCells.map((cell) => {
|
|
367
|
+
const colIdx = currentColIdx++;
|
|
434
368
|
const canSort = cell.column.getCanSort();
|
|
435
369
|
const canFilter = cell.column.getCanFilter();
|
|
370
|
+
const wrapText = cell.column.columnDef.meta?.wrapText ?? false;
|
|
436
371
|
|
|
437
372
|
return (
|
|
438
|
-
<
|
|
373
|
+
<TableCell
|
|
439
374
|
key={cell.id}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
style={{
|
|
449
|
-
width:
|
|
450
|
-
cell.column.id === lastColumnId
|
|
451
|
-
? `max(100%, ${cell.column.getSize()}px)`
|
|
452
|
-
: cell.column.getSize(),
|
|
453
|
-
position: cell.column.getIsPinned() === 'left' ? 'sticky' : undefined,
|
|
454
|
-
left:
|
|
455
|
-
cell.column.getIsPinned() === 'left'
|
|
456
|
-
? cell.column.getStart()
|
|
457
|
-
: undefined,
|
|
458
|
-
whiteSpace: 'nowrap',
|
|
459
|
-
overflow: 'hidden',
|
|
460
|
-
textOverflow: 'ellipsis',
|
|
461
|
-
}}
|
|
462
|
-
onFocus={() => setFocusedCell({ row: rowIdx, col: colIdx })}
|
|
463
|
-
onKeyDown={(e) => handleGridKeyDown(e, rowIdx, colIdx)}
|
|
464
|
-
>
|
|
465
|
-
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
466
|
-
</td>
|
|
375
|
+
cell={cell}
|
|
376
|
+
rowIdx={rowIdx}
|
|
377
|
+
colIdx={colIdx}
|
|
378
|
+
canSort={canSort}
|
|
379
|
+
canFilter={canFilter}
|
|
380
|
+
wrapText={wrapText}
|
|
381
|
+
handleGridKeyDown={handleGridKeyDown}
|
|
382
|
+
/>
|
|
467
383
|
);
|
|
468
384
|
})}
|
|
385
|
+
|
|
386
|
+
{virtualPaddingLeft ? (
|
|
387
|
+
<td style={{ display: 'flex', width: virtualPaddingLeft }} />
|
|
388
|
+
) : null}
|
|
389
|
+
|
|
390
|
+
{virtualColumns.map((virtualColumn) => {
|
|
391
|
+
const cell = centerCells[virtualColumn.index];
|
|
392
|
+
if (!cell) return null;
|
|
393
|
+
|
|
394
|
+
const colIdx = currentColIdx++;
|
|
395
|
+
const canSort = cell.column.getCanSort();
|
|
396
|
+
const canFilter = cell.column.getCanFilter();
|
|
397
|
+
const wrapText = cell.column.columnDef.meta?.wrapText ?? false;
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<TableCell
|
|
401
|
+
key={cell.id}
|
|
402
|
+
cell={cell}
|
|
403
|
+
rowIdx={rowIdx}
|
|
404
|
+
colIdx={colIdx}
|
|
405
|
+
canSort={canSort}
|
|
406
|
+
canFilter={canFilter}
|
|
407
|
+
wrapText={wrapText}
|
|
408
|
+
handleGridKeyDown={handleGridKeyDown}
|
|
409
|
+
/>
|
|
410
|
+
);
|
|
411
|
+
})}
|
|
412
|
+
|
|
413
|
+
{virtualPaddingRight ? (
|
|
414
|
+
<td style={{ display: 'flex', width: virtualPaddingRight }} />
|
|
415
|
+
) : null}
|
|
469
416
|
</tr>
|
|
470
417
|
);
|
|
471
418
|
})}
|
|
472
|
-
{after > 0 && (
|
|
473
|
-
<tr tabIndex={-1}>
|
|
474
|
-
<td colSpan={headerGroups[0].headers.length} style={{ height: after }} />
|
|
475
|
-
</tr>
|
|
476
|
-
)}
|
|
477
419
|
</tbody>
|
|
478
420
|
</table>
|
|
479
421
|
</div>
|
|
480
422
|
</div>
|
|
481
|
-
|
|
482
|
-
{table.getVisibleLeafColumns().length === 0 && (
|
|
423
|
+
{table.getVisibleLeafColumns().length === 0 || displayedCount === 0 ? (
|
|
483
424
|
<div>
|
|
484
425
|
<div
|
|
485
|
-
class="d-flex flex-column justify-content-center align-items-center
|
|
426
|
+
class="d-flex flex-column justify-content-center align-items-center p-4"
|
|
486
427
|
style={{
|
|
487
428
|
position: 'absolute',
|
|
488
429
|
top: 0,
|
|
489
430
|
left: 0,
|
|
490
431
|
right: 0,
|
|
491
432
|
bottom: 0,
|
|
492
|
-
|
|
433
|
+
// Allow pointer events (e.g. scrolling) to reach the underlying table.
|
|
434
|
+
pointerEvents: 'none',
|
|
493
435
|
}}
|
|
494
436
|
role="status"
|
|
495
437
|
aria-live="polite"
|
|
496
438
|
>
|
|
497
|
-
<
|
|
498
|
-
|
|
439
|
+
<div
|
|
440
|
+
class="col-lg-6"
|
|
441
|
+
style={{
|
|
442
|
+
// Allow selecting and interacting with the empty state content.
|
|
443
|
+
pointerEvents: 'auto',
|
|
444
|
+
}}
|
|
445
|
+
>
|
|
446
|
+
{table.getVisibleLeafColumns().length === 0 ? (
|
|
447
|
+
<TanstackTableEmptyState iconName="bi-eye-slash">
|
|
448
|
+
No columns selected. Use the View menu to show columns.
|
|
449
|
+
</TanstackTableEmptyState>
|
|
450
|
+
) : displayedCount === 0 ? (
|
|
451
|
+
totalCount > 0 ? (
|
|
452
|
+
noResultsState
|
|
453
|
+
) : (
|
|
454
|
+
emptyState
|
|
455
|
+
)
|
|
456
|
+
) : null}
|
|
457
|
+
</div>
|
|
499
458
|
</div>
|
|
500
459
|
</div>
|
|
501
|
-
)}
|
|
502
|
-
{displayedCount === 0 && (
|
|
503
|
-
<div
|
|
504
|
-
class="d-flex flex-column justify-content-center align-items-center text-muted py-4"
|
|
505
|
-
style={{
|
|
506
|
-
position: 'absolute',
|
|
507
|
-
top: 0,
|
|
508
|
-
left: 0,
|
|
509
|
-
right: 0,
|
|
510
|
-
bottom: 0,
|
|
511
|
-
background: 'var(--bs-body-bg)',
|
|
512
|
-
}}
|
|
513
|
-
role="status"
|
|
514
|
-
aria-live="polite"
|
|
515
|
-
>
|
|
516
|
-
{totalCount > 0 ? noResultsState : emptyState}
|
|
517
|
-
</div>
|
|
518
|
-
)}
|
|
460
|
+
) : null}
|
|
519
461
|
</div>
|
|
520
462
|
);
|
|
521
463
|
}
|
|
@@ -525,8 +467,13 @@ export function TanstackTable<RowDataModel>({
|
|
|
525
467
|
* @param params
|
|
526
468
|
* @param params.table - The table model
|
|
527
469
|
* @param params.title - The title of the card
|
|
470
|
+
* @param params.className - The class name to apply to the card
|
|
471
|
+
* @param params.style - The style to apply to the card
|
|
472
|
+
* @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
|
|
473
|
+
* @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
|
|
528
474
|
* @param params.headerButtons - The buttons to display in the header
|
|
529
475
|
* @param params.columnManagerButtons - The buttons to display next to the column manager (View button)
|
|
476
|
+
* @param params.columnManagerTopContent - Optional content to display at the top of the column manager (View) dropdown menu
|
|
530
477
|
* @param params.globalFilter - State management for the global filter
|
|
531
478
|
* @param params.globalFilter.value
|
|
532
479
|
* @param params.globalFilter.setValue
|
|
@@ -537,42 +484,37 @@ export function TanstackTable<RowDataModel>({
|
|
|
537
484
|
export function TanstackTableCard<RowDataModel>({
|
|
538
485
|
table,
|
|
539
486
|
title,
|
|
487
|
+
singularLabel,
|
|
488
|
+
pluralLabel,
|
|
540
489
|
headerButtons,
|
|
541
490
|
columnManagerButtons,
|
|
491
|
+
columnManagerTopContent,
|
|
542
492
|
globalFilter,
|
|
543
493
|
tableOptions,
|
|
544
|
-
downloadButtonOptions
|
|
494
|
+
downloadButtonOptions,
|
|
495
|
+
className,
|
|
496
|
+
...divProps
|
|
545
497
|
}: {
|
|
546
498
|
table: Table<RowDataModel>;
|
|
547
499
|
title: string;
|
|
500
|
+
singularLabel: string;
|
|
501
|
+
pluralLabel: string;
|
|
548
502
|
headerButtons: JSX.Element;
|
|
549
503
|
columnManagerButtons?: JSX.Element;
|
|
504
|
+
columnManagerTopContent?: JSX.Element;
|
|
550
505
|
globalFilter: {
|
|
551
506
|
value: string;
|
|
552
507
|
setValue: (value: string) => void;
|
|
553
508
|
placeholder: string;
|
|
554
509
|
};
|
|
555
510
|
tableOptions: Partial<Omit<TanstackTableProps<RowDataModel>, 'table'>>;
|
|
556
|
-
downloadButtonOptions?: Omit<
|
|
557
|
-
|
|
511
|
+
downloadButtonOptions?: Omit<
|
|
512
|
+
TanstackTableDownloadButtonProps<RowDataModel>,
|
|
513
|
+
'table' | 'singularLabel' | 'pluralLabel'
|
|
514
|
+
>;
|
|
515
|
+
} & Omit<ComponentProps<'div'>, 'class'>) {
|
|
558
516
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
559
517
|
|
|
560
|
-
// Track screen size for aria-hidden
|
|
561
|
-
const mediaQuery = typeof window !== 'undefined' ? window.matchMedia('(min-width: 768px)') : null;
|
|
562
|
-
const [isMediumOrLarger, setIsMediumOrLarger] = useState(false);
|
|
563
|
-
|
|
564
|
-
useEffect(() => {
|
|
565
|
-
// TODO: This is a workaround to avoid a hydration mismatch.
|
|
566
|
-
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
|
567
|
-
setIsMediumOrLarger(mediaQuery?.matches ?? true);
|
|
568
|
-
}, [mediaQuery]);
|
|
569
|
-
|
|
570
|
-
useEffect(() => {
|
|
571
|
-
const handler = (e: MediaQueryListEvent) => setIsMediumOrLarger(e.matches);
|
|
572
|
-
mediaQuery?.addEventListener('change', handler);
|
|
573
|
-
return () => mediaQuery?.removeEventListener('change', handler);
|
|
574
|
-
}, [mediaQuery]);
|
|
575
|
-
|
|
576
518
|
// Focus the search input when Ctrl+F is pressed
|
|
577
519
|
useEffect(() => {
|
|
578
520
|
function onKeyDown(event: KeyboardEvent) {
|
|
@@ -591,7 +533,7 @@ export function TanstackTableCard<RowDataModel>({
|
|
|
591
533
|
const totalCount = table.getCoreRowModel().rows.length;
|
|
592
534
|
|
|
593
535
|
return (
|
|
594
|
-
<div class=
|
|
536
|
+
<div class={clsx('card d-flex flex-column', className)} {...divProps}>
|
|
595
537
|
<div class="card-header bg-primary text-white">
|
|
596
538
|
<div class="d-flex align-items-center justify-content-between gap-2">
|
|
597
539
|
<div>{title}</div>
|
|
@@ -599,65 +541,70 @@ export function TanstackTableCard<RowDataModel>({
|
|
|
599
541
|
{headerButtons}
|
|
600
542
|
|
|
601
543
|
{downloadButtonOptions && (
|
|
602
|
-
<TanstackTableDownloadButton
|
|
544
|
+
<TanstackTableDownloadButton
|
|
545
|
+
table={table}
|
|
546
|
+
pluralLabel={pluralLabel}
|
|
547
|
+
singularLabel={singularLabel}
|
|
548
|
+
{...downloadButtonOptions}
|
|
549
|
+
/>
|
|
603
550
|
)}
|
|
604
551
|
</div>
|
|
605
552
|
</div>
|
|
606
553
|
</div>
|
|
607
|
-
<div class="card-body d-flex flex-
|
|
608
|
-
<div class="
|
|
609
|
-
<
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
554
|
+
<div class="card-body d-flex flex-row flex-wrap flex-grow-0 align-items-center gap-2">
|
|
555
|
+
<div class="position-relative w-100" style={{ maxWidth: 'min(400px, 100%)' }}>
|
|
556
|
+
<input
|
|
557
|
+
ref={searchInputRef}
|
|
558
|
+
type="text"
|
|
559
|
+
class="form-control pl-ui-tanstack-table-search-input pl-ui-tanstack-table-focusable-shadow"
|
|
560
|
+
aria-label={globalFilter.placeholder}
|
|
561
|
+
placeholder={globalFilter.placeholder}
|
|
562
|
+
value={globalFilter.value}
|
|
563
|
+
autoComplete="off"
|
|
564
|
+
onInput={(e) => {
|
|
565
|
+
if (!(e.target instanceof HTMLInputElement)) return;
|
|
566
|
+
globalFilter.setValue(e.target.value);
|
|
567
|
+
}}
|
|
568
|
+
/>
|
|
569
|
+
{globalFilter.value && (
|
|
570
|
+
<OverlayTrigger overlay={<Tooltip>Clear search</Tooltip>}>
|
|
623
571
|
<button
|
|
624
572
|
type="button"
|
|
625
|
-
class="btn btn-
|
|
573
|
+
class="btn btn-floating-icon"
|
|
626
574
|
aria-label="Clear search"
|
|
627
|
-
title="Clear search"
|
|
628
|
-
data-bs-toggle="tooltip"
|
|
629
575
|
onClick={() => globalFilter.setValue('')}
|
|
630
576
|
>
|
|
631
|
-
<i class="bi bi-x-circle" aria-hidden="true" />
|
|
577
|
+
<i class="bi bi-x-circle-fill" aria-hidden="true" />
|
|
632
578
|
</button>
|
|
633
|
-
</
|
|
634
|
-
{/* We do this instead of CSS properties for the accessibility checker.
|
|
635
|
-
We can't have two elements with the same id of 'column-manager-button'. */}
|
|
636
|
-
{isMediumOrLarger && (
|
|
637
|
-
<>
|
|
638
|
-
<ColumnManager table={table} />
|
|
639
|
-
{columnManagerButtons}
|
|
640
|
-
</>
|
|
641
|
-
)}
|
|
642
|
-
</div>
|
|
643
|
-
{/* We do this instead of CSS properties for the accessibility checker.
|
|
644
|
-
We can't have two elements with the same id of 'column-manager-button'. */}
|
|
645
|
-
{!isMediumOrLarger && (
|
|
646
|
-
<>
|
|
647
|
-
<ColumnManager table={table} />
|
|
648
|
-
{columnManagerButtons}
|
|
649
|
-
</>
|
|
579
|
+
</OverlayTrigger>
|
|
650
580
|
)}
|
|
651
|
-
<div class="flex-lg-grow-1 d-flex flex-row justify-content-end">
|
|
652
|
-
<div class="text-muted text-nowrap">
|
|
653
|
-
Showing {displayedCount} of {totalCount} {title.toLowerCase()}
|
|
654
|
-
</div>
|
|
655
|
-
</div>
|
|
656
581
|
</div>
|
|
657
|
-
<div class="flex-
|
|
658
|
-
<
|
|
582
|
+
<div class="d-flex flex-wrap flex-row align-items-center gap-2">
|
|
583
|
+
<ColumnManager table={table} topContent={columnManagerTopContent} />
|
|
584
|
+
{columnManagerButtons}
|
|
585
|
+
</div>
|
|
586
|
+
<div class="ms-auto text-muted text-nowrap">
|
|
587
|
+
Showing {displayedCount} of {totalCount} {totalCount === 1 ? singularLabel : pluralLabel}
|
|
659
588
|
</div>
|
|
660
589
|
</div>
|
|
590
|
+
<div class="flex-grow-1">
|
|
591
|
+
<TanstackTable table={table} title={title} {...tableOptions} />
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export function TanstackTableEmptyState({
|
|
598
|
+
iconName,
|
|
599
|
+
children,
|
|
600
|
+
}: {
|
|
601
|
+
iconName: `bi-${string}`;
|
|
602
|
+
children: ComponentChildren;
|
|
603
|
+
}) {
|
|
604
|
+
return (
|
|
605
|
+
<div class="d-flex flex-column justify-content-center align-items-center text-muted">
|
|
606
|
+
<i class={clsx('bi', iconName, 'display-4 mb-2')} aria-hidden="true" />
|
|
607
|
+
<div>{children}</div>
|
|
661
608
|
</div>
|
|
662
609
|
);
|
|
663
610
|
}
|