@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -2
  3. package/dist/components/CategoricalColumnFilter.d.ts +7 -12
  4. package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
  5. package/dist/components/CategoricalColumnFilter.js +26 -14
  6. package/dist/components/CategoricalColumnFilter.js.map +1 -1
  7. package/dist/components/ColumnManager.d.ts +6 -2
  8. package/dist/components/ColumnManager.d.ts.map +1 -1
  9. package/dist/components/ColumnManager.js +98 -35
  10. package/dist/components/ColumnManager.js.map +1 -1
  11. package/dist/components/MultiSelectColumnFilter.d.ts +8 -12
  12. package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -1
  13. package/dist/components/MultiSelectColumnFilter.js +21 -13
  14. package/dist/components/MultiSelectColumnFilter.js.map +1 -1
  15. package/dist/components/NumericInputColumnFilter.d.ts +13 -13
  16. package/dist/components/NumericInputColumnFilter.d.ts.map +1 -1
  17. package/dist/components/NumericInputColumnFilter.js +44 -15
  18. package/dist/components/NumericInputColumnFilter.js.map +1 -1
  19. package/dist/components/NumericInputColumnFilter.test.d.ts +2 -0
  20. package/dist/components/NumericInputColumnFilter.test.d.ts.map +1 -0
  21. package/dist/components/NumericInputColumnFilter.test.js +90 -0
  22. package/dist/components/NumericInputColumnFilter.test.js.map +1 -0
  23. package/dist/components/OverlayTrigger.d.ts +78 -0
  24. package/dist/components/OverlayTrigger.d.ts.map +1 -0
  25. package/dist/components/OverlayTrigger.js +89 -0
  26. package/dist/components/OverlayTrigger.js.map +1 -0
  27. package/dist/components/TanstackTable.d.ts +19 -3
  28. package/dist/components/TanstackTable.d.ts.map +1 -1
  29. package/dist/components/TanstackTable.js +159 -219
  30. package/dist/components/TanstackTable.js.map +1 -1
  31. package/dist/components/TanstackTableDownloadButton.d.ts +4 -2
  32. package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
  33. package/dist/components/TanstackTableDownloadButton.js +4 -3
  34. package/dist/components/TanstackTableDownloadButton.js.map +1 -1
  35. package/dist/components/TanstackTableHeaderCell.d.ts +13 -0
  36. package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -0
  37. package/dist/components/TanstackTableHeaderCell.js +98 -0
  38. package/dist/components/TanstackTableHeaderCell.js.map +1 -0
  39. package/dist/components/styles.css +58 -0
  40. package/dist/components/useAutoSizeColumns.d.ts +17 -0
  41. package/dist/components/useAutoSizeColumns.d.ts.map +1 -0
  42. package/dist/components/useAutoSizeColumns.js +99 -0
  43. package/dist/components/useAutoSizeColumns.js.map +1 -0
  44. package/dist/index.d.ts +4 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/react-table.d.ts +13 -0
  49. package/dist/react-table.d.ts.map +1 -0
  50. package/dist/react-table.js +3 -0
  51. package/dist/react-table.js.map +1 -0
  52. package/package.json +2 -2
  53. package/src/components/CategoricalColumnFilter.tsx +84 -54
  54. package/src/components/ColumnManager.tsx +236 -88
  55. package/src/components/MultiSelectColumnFilter.tsx +45 -32
  56. package/src/components/NumericInputColumnFilter.test.ts +67 -19
  57. package/src/components/NumericInputColumnFilter.tsx +102 -42
  58. package/src/components/OverlayTrigger.tsx +168 -0
  59. package/src/components/TanstackTable.tsx +357 -410
  60. package/src/components/TanstackTableDownloadButton.tsx +8 -5
  61. package/src/components/TanstackTableHeaderCell.tsx +207 -0
  62. package/src/components/styles.css +58 -0
  63. package/src/components/useAutoSizeColumns.tsx +168 -0
  64. package/src/index.ts +10 -1
  65. package/src/react-table.ts +17 -0
  66. package/tsconfig.json +1 -2
  67. package/dist/components/TanstackTable.css +0 -4
  68. 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'> | null;
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,EAAE,MAAM,EAAsB,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAG9E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAG9C,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,kCAAkC,CAAC;AA0F1C,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;CAC1B;AAID;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,YAAY,EAAE,EAC1C,KAAK,EACL,KAAK,EACL,OAA4B,EAC5B,SAAc,EACd,cAAsC,EACtC,UAA8B,GAC/B,EAAE,kBAAkB,CAAC,YAAY,CAAC,eAuYlC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,EAC9C,KAAK,EACL,KAAK,EACL,aAAa,EACb,oBAAoB,EACpB,YAAY,EACZ,YAAY,EACZ,qBAA4B,GAC7B,EAAE;IACD,KAAK,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,GAAG,CAAC,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC;IACnC,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,CAAC,gCAAgC,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9F,eA0GA"}
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, Fragment as _Fragment, jsxs as _jsxs } from "@prairielearn/preact-cjs/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@prairielearn/preact-cjs/jsx-runtime";
2
2
  import { flexRender } from '@tanstack/react-table';
3
- import { notUndefined, useVirtualizer } from '@tanstack/react-virtual';
3
+ import { useVirtualizer } from '@tanstack/react-virtual';
4
4
  import clsx from 'clsx';
5
- import { useEffect, useRef, useState } from 'preact/hooks';
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
- function SortIcon({ sortMethod }) {
9
- if (sortMethod === 'asc') {
10
- return _jsx("i", { class: "bi bi-sort-up-alt", "aria-hidden": "true" });
11
- }
12
- else if (sortMethod === 'desc') {
13
- return _jsx("i", { class: "bi bi-sort-down", "aria-hidden": "true" });
14
- }
15
- else {
16
- return _jsx("i", { class: "bi bi-arrow-down-up opacity-75 text-muted", "aria-hidden": "true" });
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
- function ResizeHandle({ header, setColumnSizing, }) {
20
- const minSize = header.column.columnDef.minSize ?? 0;
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: () => parentRef.current,
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
- // Track focused cell for grid navigation
75
- const [focusedCell, setFocusedCell] = useState({ row: 0, col: 0 });
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
- setFocusedCell({ row: next.row, col: next.col });
105
- // If we are on the leftmost column, we should allow left scrolling.
106
- if (colIdx === 0 && e.key === 'ArrowLeft') {
107
- return;
108
- }
109
- // If we are on the top row, we should allow up scrolling.
110
- if (rowIdx === 0 && e.key === 'ArrowUp') {
111
- return;
112
- }
113
- // If we are on the rightmost column, we should allow right scrolling.
114
- if (colIdx === rowLength - 1 && e.key === 'ArrowRight') {
115
- return;
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 isTableResizing = headerGroups.some((headerGroup) => headerGroup.headers.some((header) => header.column.getIsResizing()));
136
- const lastColumnId = table.getAllLeafColumns()[table.getAllLeafColumns().length - 1].id;
137
- const tableRect = tableRef.current?.getBoundingClientRect();
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
- // Dismiss popovers when their triggering element scrolls out of view
151
+ const hasAutoSized = useAutoSizeColumns(table, tableRef, filters);
152
+ // Re-measure the virtualizer when auto-sizing completes
144
153
  useEffect(() => {
145
- const handleScroll = () => {
146
- const scrollElement = parentRef.current;
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: parentRef, style: {
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 border border-top-0", style: { tableLayout: 'fixed' }, "aria-label": title, role: "grid", children: [_jsx("thead", { children: headerGroups.map((headerGroup) => (_jsx("tr", { children: headerGroup.headers.map((header, index) => {
204
- const isPinned = header.column.getIsPinned();
205
- const sortDirection = header.column.getIsSorted();
206
- const canSort = header.column.getCanSort();
207
- const canFilter = header.column.getCanFilter();
208
- const columnName = typeof header.column.columnDef.header === 'string'
209
- ? header.column.columnDef.header
210
- : header.column.id;
211
- const style = {
212
- width: header.column.id === lastColumnId
213
- ? `max(100%, ${header.getSize()}px)`
214
- : header.getSize(),
215
- position: 'sticky',
216
- top: 0,
217
- zIndex: isPinned === 'left' ? 2 : 1,
218
- left: isPinned === 'left' ? header.getStart() : undefined,
219
- boxShadow: 'inset 0 calc(-1 * var(--bs-border-width)) 0 0 rgba(0, 0, 0, 1), inset 0 var(--bs-border-width) 0 0 var(--bs-border-color)',
220
- };
221
- return (_jsxs("th", { class: clsx(isPinned === 'left' && 'bg-light'), style: style, "aria-sort": canSort ? getAriaSort(sortDirection) : undefined, role: "columnheader", children: [_jsxs("div", { class: clsx('d-flex align-items-center', canSort || canFilter
222
- ? 'justify-content-between'
223
- : 'justify-content-center'), children: [_jsxs("button", { class: clsx('text-nowrap text-start', canSort || canFilter ? 'flex-grow-1' : ''), style: {
224
- cursor: canSort ? 'pointer' : 'default',
225
- overflow: 'hidden',
226
- textOverflow: 'ellipsis',
227
- background: 'transparent',
228
- border: 'none',
229
- }, type: "button", "aria-label": canSort
230
- ? `'${columnName}' column, current sort is ${getAriaSort(sortDirection)}`
231
- : undefined, onClick: canSort ? header.column.getToggleSortingHandler() : undefined, onKeyDown: canSort
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
- return (_jsx("td", {
252
- // You can tab to the most-recently focused cell.
253
- tabIndex: focusedCell.row === rowIdx && focusedCell.col === colIdx ? 0 : -1, "data-grid-cell-row": rowIdx, "data-grid-cell-col": colIdx, class: clsx(!canSort && !canFilter && 'text-center'), style: {
254
- width: cell.column.id === lastColumnId
255
- ? `max(100%, ${cell.column.getSize()}px)`
256
- : cell.column.getSize(),
257
- position: cell.column.getIsPinned() === 'left' ? 'sticky' : undefined,
258
- left: cell.column.getIsPinned() === 'left'
259
- ? cell.column.getStart()
260
- : undefined,
261
- whiteSpace: 'nowrap',
262
- overflow: 'hidden',
263
- textOverflow: 'ellipsis',
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
- background: 'var(--bs-body-bg)',
273
- }, role: "status", "aria-live": "polite", children: [_jsx("i", { class: "bi bi-eye-slash display-4 mb-2", "aria-hidden": "true" }), _jsx("p", { class: "mb-0", children: "No columns selected. Use the View menu to show columns." })] }) })), displayedCount === 0 && (_jsx("div", { class: "d-flex flex-column justify-content-center align-items-center text-muted py-4", style: {
274
- position: 'absolute',
275
- top: 0,
276
- left: 0,
277
- right: 0,
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 = null, }) {
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: "card d-flex flex-column h-100", 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, ...downloadButtonOptions }))] })] }) }), _jsxs("div", { class: "card-body d-flex flex-column", children: [_jsxs("div", { class: "d-flex flex-row flex-wrap align-items-center mb-3 gap-2", children: [_jsxs("div", { class: "flex-grow-1 flex-lg-grow-0 col-xl-6 col-lg-7 d-flex flex-row gap-2", children: [_jsxs("div", { class: "input-group", children: [_jsx("input", { ref: searchInputRef, type: "text", class: "form-control", "aria-label": globalFilter.placeholder, placeholder: globalFilter.placeholder, value: globalFilter.value, onInput: (e) => {
327
- if (!(e.target instanceof HTMLInputElement))
328
- return;
329
- globalFilter.setValue(e.target.value);
330
- } }), _jsx("button", { type: "button", class: "btn btn-outline-secondary", "aria-label": "Clear search", title: "Clear search", "data-bs-toggle": "tooltip", onClick: () => globalFilter.setValue(''), children: _jsx("i", { class: "bi bi-x-circle", "aria-hidden": "true" }) })] }), isMediumOrLarger && (_jsxs(_Fragment, { children: [_jsx(ColumnManager, { table: table }), columnManagerButtons] }))] }), !isMediumOrLarger && (_jsxs(_Fragment, { children: [_jsx(ColumnManager, { table: table }), columnManagerButtons] })), _jsx("div", { class: "flex-lg-grow-1 d-flex flex-row justify-content-end", children: _jsxs("div", { class: "text-muted text-nowrap", children: ["Showing ", displayedCount, " of ", totalCount, " ", title.toLowerCase()] }) })] }), _jsx("div", { class: "flex-grow-1", children: _jsx(TanstackTable, { table: table, title: title, ...tableOptions }) })] })] }));
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