@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,5 +1,7 @@
|
|
|
1
1
|
import type { Header, Table } from '@tanstack/table-core';
|
|
2
|
+
import type { ComponentChildren } from 'preact';
|
|
2
3
|
import type { JSX } from 'preact/jsx-runtime';
|
|
4
|
+
import type { ComponentProps } from '@prairielearn/preact-cjs';
|
|
3
5
|
import { type TanstackTableDownloadButtonProps } from './TanstackTableDownloadButton.js';
|
|
4
6
|
interface TanstackTableProps<RowDataModel> {
|
|
5
7
|
table: Table<RowDataModel>;
|
|
@@ -10,6 +12,7 @@ interface TanstackTableProps<RowDataModel> {
|
|
|
10
12
|
rowHeight?: number;
|
|
11
13
|
noResultsState?: JSX.Element;
|
|
12
14
|
emptyState?: JSX.Element;
|
|
15
|
+
scrollRef?: React.RefObject<HTMLDivElement> | null;
|
|
13
16
|
}
|
|
14
17
|
/**
|
|
15
18
|
* A generic component that renders a full-width, resizeable Tanstack Table.
|
|
@@ -20,15 +23,21 @@ interface TanstackTableProps<RowDataModel> {
|
|
|
20
23
|
* @param params.rowHeight - The height of the rows in the table
|
|
21
24
|
* @param params.noResultsState - The no results state for the table
|
|
22
25
|
* @param params.emptyState - The empty state for the table
|
|
26
|
+
* @param params.scrollRef - Optional ref that will be attached to the scroll container element.
|
|
23
27
|
*/
|
|
24
|
-
export declare function TanstackTable<RowDataModel>({ table, title, filters, rowHeight, noResultsState, emptyState, }: TanstackTableProps<RowDataModel>): JSX.Element;
|
|
28
|
+
export declare function TanstackTable<RowDataModel>({ table, title, filters, rowHeight, noResultsState, emptyState, scrollRef, }: TanstackTableProps<RowDataModel>): JSX.Element;
|
|
25
29
|
/**
|
|
26
30
|
* A generic component that wraps the TanstackTable component in a card.
|
|
27
31
|
* @param params
|
|
28
32
|
* @param params.table - The table model
|
|
29
33
|
* @param params.title - The title of the card
|
|
34
|
+
* @param params.className - The class name to apply to the card
|
|
35
|
+
* @param params.style - The style to apply to the card
|
|
36
|
+
* @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
|
|
37
|
+
* @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
|
|
30
38
|
* @param params.headerButtons - The buttons to display in the header
|
|
31
39
|
* @param params.columnManagerButtons - The buttons to display next to the column manager (View button)
|
|
40
|
+
* @param params.columnManagerTopContent - Optional content to display at the top of the column manager (View) dropdown menu
|
|
32
41
|
* @param params.globalFilter - State management for the global filter
|
|
33
42
|
* @param params.globalFilter.value
|
|
34
43
|
* @param params.globalFilter.setValue
|
|
@@ -36,18 +45,25 @@ export declare function TanstackTable<RowDataModel>({ table, title, filters, row
|
|
|
36
45
|
* @param params.tableOptions - Specific options for the table. See {@link TanstackTableProps} for more details.
|
|
37
46
|
* @param params.downloadButtonOptions - Specific options for the download button. See {@link TanstackTableDownloadButtonProps} for more details.
|
|
38
47
|
*/
|
|
39
|
-
export declare function TanstackTableCard<RowDataModel>({ table, title, headerButtons, columnManagerButtons, globalFilter, tableOptions, downloadButtonOptions, }: {
|
|
48
|
+
export declare function TanstackTableCard<RowDataModel>({ table, title, singularLabel, pluralLabel, headerButtons, columnManagerButtons, columnManagerTopContent, globalFilter, tableOptions, downloadButtonOptions, className, ...divProps }: {
|
|
40
49
|
table: Table<RowDataModel>;
|
|
41
50
|
title: string;
|
|
51
|
+
singularLabel: string;
|
|
52
|
+
pluralLabel: string;
|
|
42
53
|
headerButtons: JSX.Element;
|
|
43
54
|
columnManagerButtons?: JSX.Element;
|
|
55
|
+
columnManagerTopContent?: JSX.Element;
|
|
44
56
|
globalFilter: {
|
|
45
57
|
value: string;
|
|
46
58
|
setValue: (value: string) => void;
|
|
47
59
|
placeholder: string;
|
|
48
60
|
};
|
|
49
61
|
tableOptions: Partial<Omit<TanstackTableProps<RowDataModel>, 'table'>>;
|
|
50
|
-
downloadButtonOptions?: Omit<TanstackTableDownloadButtonProps<RowDataModel>, 'table'
|
|
62
|
+
downloadButtonOptions?: Omit<TanstackTableDownloadButtonProps<RowDataModel>, 'table' | 'singularLabel' | 'pluralLabel'>;
|
|
63
|
+
} & Omit<ComponentProps<'div'>, 'class'>): JSX.Element;
|
|
64
|
+
export declare function TanstackTableEmptyState({ iconName, children, }: {
|
|
65
|
+
iconName: `bi-${string}`;
|
|
66
|
+
children: ComponentChildren;
|
|
51
67
|
}): JSX.Element;
|
|
52
68
|
export {};
|
|
53
69
|
//# sourceMappingURL=TanstackTable.d.ts.map
|
|
@@ -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,55 +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
|
+
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
|
|
7
|
+
import Tooltip from 'react-bootstrap/Tooltip';
|
|
8
|
+
import { run } from '@prairielearn/run';
|
|
6
9
|
import { ColumnManager } from './ColumnManager.js';
|
|
7
10
|
import { TanstackTableDownloadButton, } from './TanstackTableDownloadButton.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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));
|
|
18
33
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const maxSize = header.column.columnDef.maxSize ?? 0;
|
|
22
|
-
const handleKeyDown = (e) => {
|
|
23
|
-
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
24
|
-
e.preventDefault();
|
|
25
|
-
const currentSize = header.getSize();
|
|
26
|
-
const increment = e.shiftKey ? 20 : 5; // Larger increment with Shift key
|
|
27
|
-
const newSize = e.key === 'ArrowLeft'
|
|
28
|
-
? Math.max(minSize, currentSize - increment)
|
|
29
|
-
: Math.min(maxSize, currentSize + increment);
|
|
30
|
-
setColumnSizing((prevSizing) => ({
|
|
31
|
-
...prevSizing,
|
|
32
|
-
[header.column.id]: newSize,
|
|
33
|
-
}));
|
|
34
|
-
}
|
|
35
|
-
else if (e.key === 'Home') {
|
|
36
|
-
e.preventDefault();
|
|
37
|
-
header.column.resetSize();
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
const columnName = typeof header.column.columnDef.header === 'string'
|
|
41
|
-
? header.column.columnDef.header
|
|
42
|
-
: header.column.id;
|
|
43
|
-
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(),
|
|
44
|
-
// eslint-disable-next-line jsx-a11y-x/no-noninteractive-tabindex
|
|
45
|
-
tabIndex: 0, class: "h-100", style: {
|
|
46
|
-
background: header.column.getIsResizing() ? 'var(--bs-primary)' : 'var(--bs-gray-400)',
|
|
47
|
-
cursor: 'col-resize',
|
|
48
|
-
transition: 'background-color 0.2s',
|
|
49
|
-
}, onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), onKeyDown: handleKeyDown }) }));
|
|
50
|
-
}
|
|
51
|
-
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." })] }));
|
|
52
|
-
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." }));
|
|
53
36
|
const DEFAULT_FILTER_MAP = {};
|
|
54
37
|
/**
|
|
55
38
|
* A generic component that renders a full-width, resizeable Tanstack Table.
|
|
@@ -60,19 +43,52 @@ const DEFAULT_FILTER_MAP = {};
|
|
|
60
43
|
* @param params.rowHeight - The height of the rows in the table
|
|
61
44
|
* @param params.noResultsState - The no results state for the table
|
|
62
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.
|
|
63
47
|
*/
|
|
64
|
-
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, }) {
|
|
65
49
|
const parentRef = useRef(null);
|
|
66
50
|
const tableRef = useRef(null);
|
|
51
|
+
const scrollContainerRef = scrollRef ?? parentRef;
|
|
67
52
|
const rows = [...table.getTopRows(), ...table.getCenterRows()];
|
|
68
53
|
const rowVirtualizer = useVirtualizer({
|
|
69
54
|
count: rows.length,
|
|
70
|
-
getScrollElement: () =>
|
|
55
|
+
getScrollElement: () => scrollContainerRef.current,
|
|
71
56
|
estimateSize: () => rowHeight,
|
|
72
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;
|
|
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;
|
|
73
83
|
});
|
|
74
|
-
//
|
|
75
|
-
const
|
|
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]);
|
|
76
92
|
const getVisibleCells = (row) => [
|
|
77
93
|
...row.getLeftVisibleCells(),
|
|
78
94
|
...row.getCenterVisibleCells(),
|
|
@@ -101,95 +117,47 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
101
117
|
if (!next) {
|
|
102
118
|
return;
|
|
103
119
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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();
|
|
116
139
|
}
|
|
117
|
-
e.preventDefault();
|
|
118
140
|
};
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
const selector = `[data-grid-cell-row="${focusedCell.row}"][data-grid-cell-col="${focusedCell.col}"]`;
|
|
121
|
-
const cell = tableRef.current?.querySelector(selector);
|
|
122
|
-
if (!cell)
|
|
123
|
-
return;
|
|
124
|
-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-chain-state-updates
|
|
125
|
-
cell.focus();
|
|
126
|
-
}, [focusedCell]);
|
|
127
141
|
const virtualRows = rowVirtualizer.getVirtualItems();
|
|
128
|
-
const [before, after] = virtualRows.length > 0
|
|
129
|
-
? [
|
|
130
|
-
notUndefined(virtualRows[0]).start - rowVirtualizer.options.scrollMargin,
|
|
131
|
-
rowVirtualizer.getTotalSize() - notUndefined(virtualRows.at(-1)).end,
|
|
132
|
-
]
|
|
133
|
-
: [0, 0];
|
|
134
142
|
const headerGroups = table.getHeaderGroups();
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
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());
|
|
138
147
|
// We toggle this here instead of in the parent since this component logically manages all UI for the table.
|
|
139
|
-
// eslint-disable-next-line react-you-might-not-need-an-effect/no-manage-parent
|
|
140
148
|
useEffect(() => {
|
|
141
|
-
document.body.classList.toggle('no-user-select', isTableResizing);
|
|
149
|
+
document.body.classList.toggle('pl-ui-no-user-select', isTableResizing);
|
|
142
150
|
}, [isTableResizing]);
|
|
143
|
-
|
|
151
|
+
const hasAutoSized = useAutoSizeColumns(table, tableRef, filters);
|
|
152
|
+
// Re-measure the virtualizer when auto-sizing completes
|
|
144
153
|
useEffect(() => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (!scrollElement)
|
|
148
|
-
return;
|
|
149
|
-
// Find and check all open popovers
|
|
150
|
-
const popovers = document.querySelectorAll('.popover.show');
|
|
151
|
-
popovers.forEach((popover) => {
|
|
152
|
-
// Find the trigger element for this popover
|
|
153
|
-
const triggerElement = document.querySelector(`[aria-describedby="${popover.id}"]`);
|
|
154
|
-
if (!triggerElement)
|
|
155
|
-
return;
|
|
156
|
-
// Check if the trigger element is still visible in the scroll container
|
|
157
|
-
const scrollRect = scrollElement.getBoundingClientRect();
|
|
158
|
-
const triggerRect = triggerElement.getBoundingClientRect();
|
|
159
|
-
// Check if trigger is outside the visible scroll area
|
|
160
|
-
const isOutOfView = triggerRect.bottom < scrollRect.top ||
|
|
161
|
-
triggerRect.top > scrollRect.bottom ||
|
|
162
|
-
triggerRect.right < scrollRect.left ||
|
|
163
|
-
triggerRect.left > scrollRect.right;
|
|
164
|
-
if (isOutOfView) {
|
|
165
|
-
// Use Bootstrap's Popover API to properly hide it
|
|
166
|
-
const popoverInstance = window.bootstrap?.Popover?.getInstance(triggerElement);
|
|
167
|
-
if (popoverInstance) {
|
|
168
|
-
popoverInstance.hide();
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
};
|
|
173
|
-
const scrollElement = parentRef.current;
|
|
174
|
-
if (scrollElement) {
|
|
175
|
-
scrollElement.addEventListener('scroll', handleScroll);
|
|
176
|
-
return () => scrollElement.removeEventListener('scroll', handleScroll);
|
|
154
|
+
if (hasAutoSized) {
|
|
155
|
+
columnVirtualizer.measure();
|
|
177
156
|
}
|
|
178
|
-
}, []);
|
|
179
|
-
// Helper function to get aria-sort value
|
|
180
|
-
const getAriaSort = (sortDirection) => {
|
|
181
|
-
switch (sortDirection) {
|
|
182
|
-
case 'asc':
|
|
183
|
-
return 'ascending';
|
|
184
|
-
case 'desc':
|
|
185
|
-
return 'descending';
|
|
186
|
-
default:
|
|
187
|
-
return 'none';
|
|
188
|
-
}
|
|
189
|
-
};
|
|
157
|
+
}, [columnVirtualizer, hasAutoSized]);
|
|
190
158
|
const displayedCount = table.getRowModel().rows.length;
|
|
191
159
|
const totalCount = table.getCoreRowModel().rows.length;
|
|
192
|
-
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: {
|
|
193
161
|
position: 'absolute',
|
|
194
162
|
top: 0,
|
|
195
163
|
left: 0,
|
|
@@ -200,92 +168,74 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
200
168
|
}, children: _jsx("div", { ref: tableRef, style: {
|
|
201
169
|
position: 'relative',
|
|
202
170
|
width: `max(${table.getTotalSize()}px, 100%)`,
|
|
203
|
-
}, children: _jsxs("table", { class: "table table-hover mb-0
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
? (e) => {
|
|
233
|
-
const handleSort = header.column.getToggleSortingHandler();
|
|
234
|
-
if (e.key === 'Enter' && handleSort) {
|
|
235
|
-
e.preventDefault();
|
|
236
|
-
handleSort(e);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
: undefined, children: [header.isPlaceholder
|
|
240
|
-
? null
|
|
241
|
-
: 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 &&
|
|
242
|
-
tableRect.width > table.getTotalSize() &&
|
|
243
|
-
index === headerGroup.headers.length - 1 ? null : (_jsx(ResizeHandle, { header: header, setColumnSizing: table.setColumnSizing }))] }, header.id));
|
|
244
|
-
}) }, 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) => {
|
|
245
|
-
const row = rows[virtualRow.index];
|
|
246
|
-
const visibleCells = getVisibleCells(row);
|
|
247
|
-
const rowIdx = virtualRow.index;
|
|
248
|
-
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++;
|
|
249
200
|
const canSort = cell.column.getCanSort();
|
|
250
201
|
const canFilter = cell.column.getCanFilter();
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}, onFocus: () => setFocusedCell({ row: rowIdx, col: colIdx }), onKeyDown: (e) => handleGridKeyDown(e, rowIdx, colIdx), children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id));
|
|
265
|
-
}) }, row.id));
|
|
266
|
-
}), after > 0 && (_jsx("tr", { tabIndex: -1, children: _jsx("td", { colSpan: headerGroups[0].headers.length, style: { height: after } }) }))] })] }) }) }), table.getVisibleLeafColumns().length === 0 && (_jsx("div", { children: _jsxs("div", { class: "d-flex flex-column justify-content-center align-items-center text-muted py-4", style: {
|
|
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++;
|
|
209
|
+
const canSort = cell.column.getCanSort();
|
|
210
|
+
const canFilter = cell.column.getCanFilter();
|
|
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: {
|
|
267
215
|
position: 'absolute',
|
|
268
216
|
top: 0,
|
|
269
217
|
left: 0,
|
|
270
218
|
right: 0,
|
|
271
219
|
bottom: 0,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
bottom: 0,
|
|
279
|
-
background: 'var(--bs-body-bg)',
|
|
280
|
-
}, role: "status", "aria-live": "polite", children: totalCount > 0 ? noResultsState : emptyState }))] }));
|
|
220
|
+
// Allow pointer events (e.g. scrolling) to reach the underlying table.
|
|
221
|
+
pointerEvents: 'none',
|
|
222
|
+
}, role: "status", "aria-live": "polite", children: _jsx("div", { class: "col-lg-6", style: {
|
|
223
|
+
// Allow selecting and interacting with the empty state content.
|
|
224
|
+
pointerEvents: 'auto',
|
|
225
|
+
}, children: table.getVisibleLeafColumns().length === 0 ? (_jsx(TanstackTableEmptyState, { iconName: "bi-eye-slash", children: "No columns selected. Use the View menu to show columns." })) : displayedCount === 0 ? (totalCount > 0 ? (noResultsState) : (emptyState)) : null }) }) })) : null] }));
|
|
281
226
|
}
|
|
282
227
|
/**
|
|
283
228
|
* A generic component that wraps the TanstackTable component in a card.
|
|
284
229
|
* @param params
|
|
285
230
|
* @param params.table - The table model
|
|
286
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"
|
|
287
236
|
* @param params.headerButtons - The buttons to display in the header
|
|
288
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
|
|
289
239
|
* @param params.globalFilter - State management for the global filter
|
|
290
240
|
* @param params.globalFilter.value
|
|
291
241
|
* @param params.globalFilter.setValue
|
|
@@ -293,21 +243,8 @@ export function TanstackTable({ table, title, filters = DEFAULT_FILTER_MAP, rowH
|
|
|
293
243
|
* @param params.tableOptions - Specific options for the table. See {@link TanstackTableProps} for more details.
|
|
294
244
|
* @param params.downloadButtonOptions - Specific options for the download button. See {@link TanstackTableDownloadButtonProps} for more details.
|
|
295
245
|
*/
|
|
296
|
-
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 }) {
|
|
297
247
|
const searchInputRef = useRef(null);
|
|
298
|
-
// Track screen size for aria-hidden
|
|
299
|
-
const mediaQuery = typeof window !== 'undefined' ? window.matchMedia('(min-width: 768px)') : null;
|
|
300
|
-
const [isMediumOrLarger, setIsMediumOrLarger] = useState(false);
|
|
301
|
-
useEffect(() => {
|
|
302
|
-
// TODO: This is a workaround to avoid a hydration mismatch.
|
|
303
|
-
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
|
304
|
-
setIsMediumOrLarger(mediaQuery?.matches ?? true);
|
|
305
|
-
}, [mediaQuery]);
|
|
306
|
-
useEffect(() => {
|
|
307
|
-
const handler = (e) => setIsMediumOrLarger(e.matches);
|
|
308
|
-
mediaQuery?.addEventListener('change', handler);
|
|
309
|
-
return () => mediaQuery?.removeEventListener('change', handler);
|
|
310
|
-
}, [mediaQuery]);
|
|
311
248
|
// Focus the search input when Ctrl+F is pressed
|
|
312
249
|
useEffect(() => {
|
|
313
250
|
function onKeyDown(event) {
|
|
@@ -323,10 +260,13 @@ export function TanstackTableCard({ table, title, headerButtons, columnManagerBu
|
|
|
323
260
|
}, []);
|
|
324
261
|
const displayedCount = table.getRowModel().rows.length;
|
|
325
262
|
const totalCount = table.getCoreRowModel().rows.length;
|
|
326
|
-
return (_jsxs("div", { class:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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 }) })] }));
|
|
268
|
+
}
|
|
269
|
+
export function TanstackTableEmptyState({ iconName, children, }) {
|
|
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 })] }));
|
|
331
271
|
}
|
|
332
272
|
//# sourceMappingURL=TanstackTable.js.map
|