@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.
Files changed (71) hide show
  1. package/CHANGELOG.md +28 -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 +15 -11
  6. package/dist/components/CategoricalColumnFilter.js.map +1 -1
  7. package/dist/components/ColumnManager.d.ts +6 -3
  8. package/dist/components/ColumnManager.d.ts.map +1 -1
  9. package/dist/components/ColumnManager.js +98 -18
  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/PresetFilterDropdown.d.ts +19 -0
  28. package/dist/components/PresetFilterDropdown.d.ts.map +1 -0
  29. package/dist/components/PresetFilterDropdown.js +93 -0
  30. package/dist/components/PresetFilterDropdown.js.map +1 -0
  31. package/dist/components/TanstackTable.d.ts +15 -4
  32. package/dist/components/TanstackTable.d.ts.map +1 -1
  33. package/dist/components/TanstackTable.js +148 -197
  34. package/dist/components/TanstackTable.js.map +1 -1
  35. package/dist/components/TanstackTableDownloadButton.d.ts +4 -2
  36. package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
  37. package/dist/components/TanstackTableDownloadButton.js +4 -3
  38. package/dist/components/TanstackTableDownloadButton.js.map +1 -1
  39. package/dist/components/TanstackTableHeaderCell.d.ts +13 -0
  40. package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -0
  41. package/dist/components/TanstackTableHeaderCell.js +98 -0
  42. package/dist/components/TanstackTableHeaderCell.js.map +1 -0
  43. package/dist/components/{TanstackTable.css → styles.css} +11 -6
  44. package/dist/components/useAutoSizeColumns.d.ts +17 -0
  45. package/dist/components/useAutoSizeColumns.d.ts.map +1 -0
  46. package/dist/components/useAutoSizeColumns.js +99 -0
  47. package/dist/components/useAutoSizeColumns.js.map +1 -0
  48. package/dist/index.d.ts +5 -1
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +5 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/react-table.d.ts +13 -0
  53. package/dist/react-table.d.ts.map +1 -0
  54. package/dist/react-table.js +3 -0
  55. package/dist/react-table.js.map +1 -0
  56. package/package.json +2 -2
  57. package/src/components/CategoricalColumnFilter.tsx +28 -28
  58. package/src/components/ColumnManager.tsx +222 -46
  59. package/src/components/MultiSelectColumnFilter.tsx +45 -32
  60. package/src/components/NumericInputColumnFilter.test.ts +67 -19
  61. package/src/components/NumericInputColumnFilter.tsx +102 -42
  62. package/src/components/OverlayTrigger.tsx +168 -0
  63. package/src/components/PresetFilterDropdown.tsx +155 -0
  64. package/src/components/TanstackTable.tsx +315 -363
  65. package/src/components/TanstackTableDownloadButton.tsx +8 -5
  66. package/src/components/TanstackTableHeaderCell.tsx +207 -0
  67. package/src/components/{TanstackTable.css → styles.css} +11 -6
  68. package/src/components/useAutoSizeColumns.tsx +168 -0
  69. package/src/index.ts +7 -0
  70. package/src/react-table.ts +17 -0
  71. 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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","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,GACtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC"}
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,GACtB,MAAM,0CAA0C,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC","sourcesContent":["export {\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} from './components/NumericInputColumnFilter.js';\nexport { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';\n"]}
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,3 @@
1
+ // eslint-disable-next-line unicorn/require-module-specifiers
2
+ export {};
3
+ //# sourceMappingURL=react-table.js.map
@@ -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.0",
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.5.1",
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. State is managed by the parent component.
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.columnId - The ID of the column
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<T extends readonly any[]>({
32
- columnId,
33
- columnLabel,
29
+ export function CategoricalColumnFilter<TData, TValue>({
30
+ column,
34
31
  allColumnValues,
35
32
  renderValueLabel = defaultRenderValueLabel,
36
- columnValuesFilter,
37
- setColumnValuesFilter,
38
33
  }: {
39
- columnId: string;
40
- columnLabel: string;
41
- allColumnValues: T;
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 selected = useMemo(
49
- () => computeSelected(allColumnValues, mode, new Set(columnValuesFilter)),
50
- [mode, columnValuesFilter, allColumnValues],
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<T[number]>) => {
52
+ const apply = (newMode: 'include' | 'exclude', newSelected: Set<TValue>) => {
54
53
  const selected = computeSelected(allColumnValues, newMode, newSelected);
55
54
  setMode(newMode);
56
- setColumnValuesFilter(Array.from(selected));
55
+ const newValue = Array.from(selected);
56
+ column.setFilterValue(newValue);
57
57
  };
58
58
 
59
- const toggleSelected = (value: T[number]) => {
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 ${columnLabel.toLowerCase()}`}
76
- title={`Filter ${columnLabel.toLowerCase()}`}
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">{columnLabel}</div>
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,