@prairielearn/ui 1.3.0 → 1.5.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 +28 -0
- package/README.md +4 -2
- package/dist/components/CategoricalColumnFilter.d.ts +7 -12
- package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
- package/dist/components/CategoricalColumnFilter.js +15 -11
- package/dist/components/CategoricalColumnFilter.js.map +1 -1
- package/dist/components/ColumnManager.d.ts +6 -3
- package/dist/components/ColumnManager.d.ts.map +1 -1
- package/dist/components/ColumnManager.js +98 -18
- package/dist/components/ColumnManager.js.map +1 -1
- package/dist/components/MultiSelectColumnFilter.d.ts +8 -12
- package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -1
- package/dist/components/MultiSelectColumnFilter.js +21 -13
- package/dist/components/MultiSelectColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.d.ts +13 -13
- package/dist/components/NumericInputColumnFilter.d.ts.map +1 -1
- package/dist/components/NumericInputColumnFilter.js +44 -15
- package/dist/components/NumericInputColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.test.d.ts +2 -0
- package/dist/components/NumericInputColumnFilter.test.d.ts.map +1 -0
- package/dist/components/NumericInputColumnFilter.test.js +90 -0
- package/dist/components/NumericInputColumnFilter.test.js.map +1 -0
- package/dist/components/OverlayTrigger.d.ts +78 -0
- package/dist/components/OverlayTrigger.d.ts.map +1 -0
- package/dist/components/OverlayTrigger.js +89 -0
- package/dist/components/OverlayTrigger.js.map +1 -0
- package/dist/components/PresetFilterDropdown.d.ts +19 -0
- package/dist/components/PresetFilterDropdown.d.ts.map +1 -0
- package/dist/components/PresetFilterDropdown.js +93 -0
- package/dist/components/PresetFilterDropdown.js.map +1 -0
- package/dist/components/TanstackTable.d.ts +15 -4
- package/dist/components/TanstackTable.d.ts.map +1 -1
- package/dist/components/TanstackTable.js +148 -197
- package/dist/components/TanstackTable.js.map +1 -1
- package/dist/components/TanstackTableDownloadButton.d.ts +4 -2
- package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
- package/dist/components/TanstackTableDownloadButton.js +4 -3
- package/dist/components/TanstackTableDownloadButton.js.map +1 -1
- package/dist/components/TanstackTableHeaderCell.d.ts +13 -0
- package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -0
- package/dist/components/TanstackTableHeaderCell.js +98 -0
- package/dist/components/TanstackTableHeaderCell.js.map +1 -0
- package/dist/components/{TanstackTable.css → styles.css} +11 -6
- package/dist/components/useAutoSizeColumns.d.ts +17 -0
- package/dist/components/useAutoSizeColumns.d.ts.map +1 -0
- package/dist/components/useAutoSizeColumns.js +99 -0
- package/dist/components/useAutoSizeColumns.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/react-table.d.ts +13 -0
- package/dist/react-table.d.ts.map +1 -0
- package/dist/react-table.js +3 -0
- package/dist/react-table.js.map +1 -0
- package/package.json +2 -2
- package/src/components/CategoricalColumnFilter.tsx +28 -28
- package/src/components/ColumnManager.tsx +222 -46
- package/src/components/MultiSelectColumnFilter.tsx +45 -32
- package/src/components/NumericInputColumnFilter.test.ts +67 -19
- package/src/components/NumericInputColumnFilter.tsx +102 -42
- package/src/components/OverlayTrigger.tsx +168 -0
- package/src/components/PresetFilterDropdown.tsx +155 -0
- package/src/components/TanstackTable.tsx +315 -363
- package/src/components/TanstackTableDownloadButton.tsx +8 -5
- package/src/components/TanstackTableHeaderCell.tsx +207 -0
- package/src/components/{TanstackTable.css → styles.css} +11 -6
- package/src/components/useAutoSizeColumns.tsx +168 -0
- package/src/index.ts +7 -0
- package/src/react-table.ts +17 -0
- package/tsconfig.json +1 -2
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx } from "@prairielearn/preact-cjs/jsx-runtime";
|
|
2
|
+
import { render } from 'preact/compat';
|
|
3
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
4
|
+
import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
|
|
5
|
+
function HiddenMeasurementHeader({ table, columnsToMeasure, filters = {}, }) {
|
|
6
|
+
const headerGroups = table.getHeaderGroups();
|
|
7
|
+
const leafHeaderGroup = headerGroups[headerGroups.length - 1];
|
|
8
|
+
return (_jsx("div", { style: {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
visibility: 'hidden',
|
|
11
|
+
pointerEvents: 'none',
|
|
12
|
+
top: '-9999px',
|
|
13
|
+
}, children: _jsx("table", { class: "table table-hover mb-0", style: { display: 'grid', tableLayout: 'fixed' }, children: _jsx("thead", { style: { display: 'grid' }, children: _jsx("tr", { style: { display: 'flex' }, children: columnsToMeasure.map((col) => {
|
|
14
|
+
const header = leafHeaderGroup.headers.find((h) => h.column.id === col.id);
|
|
15
|
+
if (!header)
|
|
16
|
+
return null;
|
|
17
|
+
return (_jsx(TanstackTableHeaderCell, { header: header, filters: filters, table: table, isPinned: false, measurementMode: true }, header.id));
|
|
18
|
+
}) }) }) }) }));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Custom hook that automatically measures and sets column widths based on header content.
|
|
22
|
+
* Only measures columns that have `meta: { autoSize: true }` and don't have explicit sizes set.
|
|
23
|
+
* User resizes are preserved.
|
|
24
|
+
*
|
|
25
|
+
* @param table - The TanStack Table instance
|
|
26
|
+
* @param tableRef - Ref to the table container element
|
|
27
|
+
* @param filters - Optional filters map for rendering filter components in measurement
|
|
28
|
+
* @returns A boolean indicating whether the initial measurement has completed
|
|
29
|
+
*/
|
|
30
|
+
export function useAutoSizeColumns(table, tableRef, filters) {
|
|
31
|
+
const [hasMeasured, setHasMeasured] = useState(false);
|
|
32
|
+
const measurementContainerRef = useRef(null);
|
|
33
|
+
// Perform measurement
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (hasMeasured || !tableRef.current) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const allColumns = table.getAllLeafColumns();
|
|
39
|
+
const columnsToMeasure = allColumns.filter((col) => col.columnDef.meta?.autoSize);
|
|
40
|
+
if (columnsToMeasure.length === 0) {
|
|
41
|
+
// eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
|
|
42
|
+
setHasMeasured(true);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Wait for next frame to ensure DOM is ready
|
|
46
|
+
const rafId = requestAnimationFrame(() => {
|
|
47
|
+
if (!tableRef.current) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Create or reuse measurement container
|
|
51
|
+
let container = measurementContainerRef.current;
|
|
52
|
+
if (!container) {
|
|
53
|
+
container = document.createElement('div');
|
|
54
|
+
document.body.append(container);
|
|
55
|
+
measurementContainerRef.current = container;
|
|
56
|
+
}
|
|
57
|
+
// Render headers into hidden container
|
|
58
|
+
render(_jsx(HiddenMeasurementHeader, { table: table, columnsToMeasure: columnsToMeasure, filters: filters ?? {} }), container);
|
|
59
|
+
// Force layout calculation
|
|
60
|
+
void container.offsetWidth;
|
|
61
|
+
// Measure each header and build sizing state
|
|
62
|
+
const newSizing = {};
|
|
63
|
+
for (const col of columnsToMeasure) {
|
|
64
|
+
const headerElement = container.querySelector(`th[data-column-id="${col.id}"]`);
|
|
65
|
+
if (headerElement) {
|
|
66
|
+
const measuredWidth = headerElement.scrollWidth;
|
|
67
|
+
const resizeHandlePadding = col.getCanResize() ? 4 : 0;
|
|
68
|
+
const minSize = col.columnDef.minSize ?? 0;
|
|
69
|
+
const maxSize = col.columnDef.maxSize ?? Infinity;
|
|
70
|
+
const finalWidth = Math.max(minSize, Math.min(maxSize, measuredWidth + resizeHandlePadding));
|
|
71
|
+
newSizing[col.id] = finalWidth;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Clear container content by unmounting Preact components
|
|
75
|
+
render(null, container);
|
|
76
|
+
// Apply measurements
|
|
77
|
+
if (Object.keys(newSizing).length > 0) {
|
|
78
|
+
table.setColumnSizing((prev) => ({ ...prev, ...newSizing }));
|
|
79
|
+
}
|
|
80
|
+
setHasMeasured(true);
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
cancelAnimationFrame(rafId);
|
|
84
|
+
};
|
|
85
|
+
}, [table, tableRef, filters, hasMeasured]);
|
|
86
|
+
// Clean up measurement container on unmount
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
return () => {
|
|
89
|
+
const container = measurementContainerRef.current;
|
|
90
|
+
if (container) {
|
|
91
|
+
render(null, container);
|
|
92
|
+
container.remove();
|
|
93
|
+
measurementContainerRef.current = null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
return hasMeasured;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=useAutoSizeColumns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAutoSizeColumns.js","sourceRoot":"","sources":["../../src/components/useAutoSizeColumns.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ColumnSizingState, Header, Table } from '@tanstack/react-table';\nimport type { RefObject } from 'preact';\nimport { render } from 'preact/compat';\nimport { useEffect, useRef, useState } from 'preact/hooks';\nimport type { JSX } from 'preact/jsx-runtime';\n\nimport { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';\n\nfunction HiddenMeasurementHeader<TData>({\n table,\n columnsToMeasure,\n filters = {},\n}: {\n table: Table<TData>;\n columnsToMeasure: { id: string }[];\n filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>;\n}) {\n const headerGroups = table.getHeaderGroups();\n const leafHeaderGroup = headerGroups[headerGroups.length - 1];\n\n return (\n <div\n style={{\n position: 'fixed',\n visibility: 'hidden',\n pointerEvents: 'none',\n top: '-9999px',\n }}\n >\n <table class=\"table table-hover mb-0\" style={{ display: 'grid', tableLayout: 'fixed' }}>\n <thead style={{ display: 'grid' }}>\n <tr style={{ display: 'flex' }}>\n {columnsToMeasure.map((col) => {\n const header = leafHeaderGroup.headers.find((h) => h.column.id === col.id);\n if (!header) return null;\n\n return (\n <TanstackTableHeaderCell\n key={header.id}\n header={header}\n filters={filters}\n table={table}\n isPinned={false}\n measurementMode={true}\n />\n );\n })}\n </tr>\n </thead>\n </table>\n </div>\n );\n}\n\n/**\n * Custom hook that automatically measures and sets column widths based on header content.\n * Only measures columns that have `meta: { autoSize: true }` and don't have explicit sizes set.\n * User resizes are preserved.\n *\n * @param table - The TanStack Table instance\n * @param tableRef - Ref to the table container element\n * @param filters - Optional filters map for rendering filter components in measurement\n * @returns A boolean indicating whether the initial measurement has completed\n */\nexport function useAutoSizeColumns<TData>(\n table: Table<TData>,\n tableRef: RefObject<HTMLDivElement>,\n filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>,\n): boolean {\n const [hasMeasured, setHasMeasured] = useState(false);\n const measurementContainerRef = useRef<HTMLDivElement | null>(null);\n\n // Perform measurement\n useEffect(() => {\n if (hasMeasured || !tableRef.current) {\n return;\n }\n\n const allColumns = table.getAllLeafColumns();\n\n const columnsToMeasure = allColumns.filter((col) => col.columnDef.meta?.autoSize);\n\n if (columnsToMeasure.length === 0) {\n // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect\n setHasMeasured(true);\n return;\n }\n\n // Wait for next frame to ensure DOM is ready\n const rafId = requestAnimationFrame(() => {\n if (!tableRef.current) {\n return;\n }\n\n // Create or reuse measurement container\n let container = measurementContainerRef.current;\n if (!container) {\n container = document.createElement('div');\n document.body.append(container);\n measurementContainerRef.current = container;\n }\n\n // Render headers into hidden container\n render(\n <HiddenMeasurementHeader\n table={table}\n columnsToMeasure={columnsToMeasure}\n filters={filters ?? {}}\n />,\n container,\n );\n\n // Force layout calculation\n void container.offsetWidth;\n\n // Measure each header and build sizing state\n const newSizing: ColumnSizingState = {};\n\n for (const col of columnsToMeasure) {\n const headerElement = container.querySelector(\n `th[data-column-id=\"${col.id}\"]`,\n ) as HTMLElement;\n\n if (headerElement) {\n const measuredWidth = headerElement.scrollWidth;\n const resizeHandlePadding = col.getCanResize() ? 4 : 0;\n const minSize = col.columnDef.minSize ?? 0;\n const maxSize = col.columnDef.maxSize ?? Infinity;\n\n const finalWidth = Math.max(\n minSize,\n Math.min(maxSize, measuredWidth + resizeHandlePadding),\n );\n\n newSizing[col.id] = finalWidth;\n }\n }\n\n // Clear container content by unmounting Preact components\n render(null, container);\n\n // Apply measurements\n if (Object.keys(newSizing).length > 0) {\n table.setColumnSizing((prev) => ({ ...prev, ...newSizing }));\n }\n\n setHasMeasured(true);\n });\n\n return () => {\n cancelAnimationFrame(rafId);\n };\n }, [table, tableRef, filters, hasMeasured]);\n\n // Clean up measurement container on unmount\n useEffect(() => {\n return () => {\n const container = measurementContainerRef.current;\n if (container) {\n render(null, container);\n container.remove();\n measurementContainerRef.current = null;\n }\n };\n }, []);\n\n return hasMeasured;\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import './react-table.js';
|
|
1
2
|
export { TanstackTable, TanstackTableCard, TanstackTableEmptyState, } from './components/TanstackTable.js';
|
|
2
3
|
export { ColumnManager } from './components/ColumnManager.js';
|
|
3
4
|
export { TanstackTableDownloadButton } from './components/TanstackTableDownloadButton.js';
|
|
4
5
|
export { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js';
|
|
5
6
|
export { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';
|
|
6
|
-
export { NumericInputColumnFilter, parseNumericFilter, numericColumnFilterFn, } from './components/NumericInputColumnFilter.js';
|
|
7
|
+
export { NumericInputColumnFilter, parseNumericFilter, numericColumnFilterFn, type NumericColumnFilterValue, } from './components/NumericInputColumnFilter.js';
|
|
7
8
|
export { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';
|
|
9
|
+
export { useAutoSizeColumns } from './components/useAutoSizeColumns.js';
|
|
10
|
+
export { OverlayTrigger, type OverlayTriggerProps } from './components/OverlayTrigger.js';
|
|
11
|
+
export { PresetFilterDropdown } from './components/PresetFilterDropdown.js';
|
|
8
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,wBAAwB,GAC9B,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Augment @tanstack/react-table types
|
|
2
|
+
import './react-table.js';
|
|
1
3
|
export { TanstackTable, TanstackTableCard, TanstackTableEmptyState, } from './components/TanstackTable.js';
|
|
2
4
|
export { ColumnManager } from './components/ColumnManager.js';
|
|
3
5
|
export { TanstackTableDownloadButton } from './components/TanstackTableDownloadButton.js';
|
|
@@ -5,4 +7,7 @@ export { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js
|
|
|
5
7
|
export { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';
|
|
6
8
|
export { NumericInputColumnFilter, parseNumericFilter, numericColumnFilterFn, } from './components/NumericInputColumnFilter.js';
|
|
7
9
|
export { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';
|
|
10
|
+
export { useAutoSizeColumns } from './components/useAutoSizeColumns.js';
|
|
11
|
+
export { OverlayTrigger } from './components/OverlayTrigger.js';
|
|
12
|
+
export { PresetFilterDropdown } from './components/PresetFilterDropdown.js';
|
|
8
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,kBAAkB,CAAC;AAE1B,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAClF,OAAO,EACL,wBAAwB,EACxB,kBAAkB,EAClB,qBAAqB,GAEtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,EAAE,cAAc,EAA4B,MAAM,gCAAgC,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,sCAAsC,CAAC","sourcesContent":["// Augment @tanstack/react-table types\nimport './react-table.js';\n\nexport {\n TanstackTable,\n TanstackTableCard,\n TanstackTableEmptyState,\n} from './components/TanstackTable.js';\nexport { ColumnManager } from './components/ColumnManager.js';\nexport { TanstackTableDownloadButton } from './components/TanstackTableDownloadButton.js';\nexport { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js';\nexport { MultiSelectColumnFilter } from './components/MultiSelectColumnFilter.js';\nexport {\n NumericInputColumnFilter,\n parseNumericFilter,\n numericColumnFilterFn,\n type NumericColumnFilterValue,\n} from './components/NumericInputColumnFilter.js';\nexport { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';\nexport { useAutoSizeColumns } from './components/useAutoSizeColumns.js';\nexport { OverlayTrigger, type OverlayTriggerProps } from './components/OverlayTrigger.js';\nexport { PresetFilterDropdown } from './components/PresetFilterDropdown.js';\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RowData } from '@tanstack/react-table';
|
|
2
|
+
declare module '@tanstack/react-table' {
|
|
3
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
4
|
+
/** If true, the column will wrap text instead of being truncated. */
|
|
5
|
+
wrapText?: boolean;
|
|
6
|
+
/** If set, this will be used as the label for the column in the column manager. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** If true, the column will be automatically sized based on the header content. */
|
|
9
|
+
autoSize?: boolean;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=react-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-table.d.ts","sourceRoot":"","sources":["../src/react-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,OAAO,QAAQ,uBAAuB,CAAC;IAGrC,UAAU,UAAU,CAAC,KAAK,SAAS,OAAO,EAAE,MAAM;QAChD,qEAAqE;QACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,mFAAmF;QACnF,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,mFAAmF;QACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB;CACF;AAGD,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-table.js","sourceRoot":"","sources":["../src/react-table.ts"],"names":[],"mappings":"AAeA,6DAA6D;AAC7D,OAAO,EAAE,CAAC","sourcesContent":["import type { RowData } from '@tanstack/react-table';\n\ndeclare module '@tanstack/react-table' {\n // https://tanstack.com/table/latest/docs/api/core/column-def#meta\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface ColumnMeta<TData extends RowData, TValue> {\n /** If true, the column will wrap text instead of being truncated. */\n wrapText?: boolean;\n /** If set, this will be used as the label for the column in the column manager. */\n label?: string;\n /** If true, the column will be automatically sized based on the header content. */\n autoSize?: boolean;\n }\n}\n\n// eslint-disable-next-line unicorn/require-module-specifiers\nexport {};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"test": "vitest run --coverage"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@prairielearn/browser-utils": "^2.
|
|
20
|
+
"@prairielearn/browser-utils": "^2.6.0",
|
|
21
21
|
"@prairielearn/preact-cjs": "^1.1.6",
|
|
22
22
|
"@tanstack/react-table": "^8.21.3",
|
|
23
23
|
"@tanstack/react-virtual": "^3.13.12",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Column } from '@tanstack/react-table';
|
|
1
2
|
import clsx from 'clsx';
|
|
2
3
|
import { type JSX, useMemo, useState } from 'preact/compat';
|
|
3
4
|
import Dropdown from 'react-bootstrap/Dropdown';
|
|
@@ -14,49 +15,48 @@ function computeSelected<T extends readonly any[]>(
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function defaultRenderValueLabel<T>({ value }: { value: T }) {
|
|
17
|
-
return <span>{String(value)}</span>;
|
|
18
|
+
return <span class="text-nowrap">{String(value)}</span>;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
|
-
* A component that allows the user to filter a categorical column.
|
|
21
|
+
* A component that allows the user to filter a categorical column.
|
|
21
22
|
* The filter mode always defaults to "include".
|
|
22
23
|
*
|
|
23
24
|
* @param params
|
|
24
|
-
* @param params.
|
|
25
|
-
* @param params.columnLabel - The label of the column, e.g. "Status"
|
|
25
|
+
* @param params.column - The TanStack Table column object
|
|
26
26
|
* @param params.allColumnValues - The values to filter by
|
|
27
27
|
* @param params.renderValueLabel - A function that renders the label for a value
|
|
28
|
-
* @param params.columnValuesFilter - The current state of the column filter
|
|
29
|
-
* @param params.setColumnValuesFilter - A function that sets the state of the column filter
|
|
30
28
|
*/
|
|
31
|
-
export function CategoricalColumnFilter<
|
|
32
|
-
|
|
33
|
-
columnLabel,
|
|
29
|
+
export function CategoricalColumnFilter<TData, TValue>({
|
|
30
|
+
column,
|
|
34
31
|
allColumnValues,
|
|
35
32
|
renderValueLabel = defaultRenderValueLabel,
|
|
36
|
-
columnValuesFilter,
|
|
37
|
-
setColumnValuesFilter,
|
|
38
33
|
}: {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
renderValueLabel?: (props: { value: T[number]; isSelected: boolean }) => JSX.Element;
|
|
43
|
-
columnValuesFilter: T[number][];
|
|
44
|
-
setColumnValuesFilter: (value: T[number][]) => void;
|
|
34
|
+
column: Column<TData, TValue>;
|
|
35
|
+
allColumnValues: TValue[] | readonly TValue[];
|
|
36
|
+
renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => JSX.Element;
|
|
45
37
|
}) {
|
|
46
38
|
const [mode, setMode] = useState<'include' | 'exclude'>('include');
|
|
47
39
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
40
|
+
const columnId = column.id;
|
|
41
|
+
|
|
42
|
+
const label =
|
|
43
|
+
column.columnDef.meta?.label ??
|
|
44
|
+
(typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id);
|
|
45
|
+
|
|
46
|
+
const columnValuesFilter = column.getFilterValue() as TValue[] | undefined;
|
|
47
|
+
|
|
48
|
+
const selected = useMemo(() => {
|
|
49
|
+
return computeSelected(allColumnValues, mode, new Set(columnValuesFilter));
|
|
50
|
+
}, [mode, allColumnValues, columnValuesFilter]);
|
|
52
51
|
|
|
53
|
-
const apply = (newMode: 'include' | 'exclude', newSelected: Set<
|
|
52
|
+
const apply = (newMode: 'include' | 'exclude', newSelected: Set<TValue>) => {
|
|
54
53
|
const selected = computeSelected(allColumnValues, newMode, newSelected);
|
|
55
54
|
setMode(newMode);
|
|
56
|
-
|
|
55
|
+
const newValue = Array.from(selected);
|
|
56
|
+
column.setFilterValue(newValue);
|
|
57
57
|
};
|
|
58
58
|
|
|
59
|
-
const toggleSelected = (value:
|
|
59
|
+
const toggleSelected = (value: TValue) => {
|
|
60
60
|
const set = new Set(selected);
|
|
61
61
|
if (set.has(value)) {
|
|
62
62
|
set.delete(value);
|
|
@@ -72,8 +72,8 @@ export function CategoricalColumnFilter<T extends readonly any[]>({
|
|
|
72
72
|
variant="link"
|
|
73
73
|
class="text-muted p-0"
|
|
74
74
|
id={`filter-${columnId}`}
|
|
75
|
-
aria-label={`Filter ${
|
|
76
|
-
title={`Filter ${
|
|
75
|
+
aria-label={`Filter ${label.toLowerCase()}`}
|
|
76
|
+
title={`Filter ${label.toLowerCase()}`}
|
|
77
77
|
>
|
|
78
78
|
<i
|
|
79
79
|
class={clsx('bi', selected.size > 0 ? ['bi-funnel-fill', 'text-primary'] : 'bi-funnel')}
|
|
@@ -83,7 +83,7 @@ export function CategoricalColumnFilter<T extends readonly any[]>({
|
|
|
83
83
|
<Dropdown.Menu class="p-0">
|
|
84
84
|
<div class="p-3 pb-0">
|
|
85
85
|
<div class="d-flex align-items-center justify-content-between mb-2">
|
|
86
|
-
<div class="fw-semibold">{
|
|
86
|
+
<div class="fw-semibold text-nowrap">{label}</div>
|
|
87
87
|
<button
|
|
88
88
|
type="button"
|
|
89
89
|
class={clsx('btn btn-link btn-sm text-decoration-none', {
|
|
@@ -152,7 +152,7 @@ export function CategoricalColumnFilter<T extends readonly any[]>({
|
|
|
152
152
|
id={`${columnId}-${value}`}
|
|
153
153
|
onChange={() => toggleSelected(value)}
|
|
154
154
|
/>
|
|
155
|
-
<label class="form-check-label" for={`${columnId}-${value}`}>
|
|
155
|
+
<label class="form-check-label fw-normal" for={`${columnId}-${value}`}>
|
|
156
156
|
{renderValueLabel({
|
|
157
157
|
value,
|
|
158
158
|
isSelected,
|