@prairielearn/ui 1.10.0 → 2.0.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 (72) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/CategoricalColumnFilter.d.ts +9 -5
  3. package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
  4. package/dist/components/CategoricalColumnFilter.js +25 -9
  5. package/dist/components/CategoricalColumnFilter.js.map +1 -1
  6. package/dist/components/ColumnManager.d.ts +2 -2
  7. package/dist/components/ColumnManager.d.ts.map +1 -1
  8. package/dist/components/ColumnManager.js +39 -12
  9. package/dist/components/ColumnManager.js.map +1 -1
  10. package/dist/components/MultiSelectColumnFilter.d.ts +9 -6
  11. package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -1
  12. package/dist/components/MultiSelectColumnFilter.js +20 -8
  13. package/dist/components/MultiSelectColumnFilter.js.map +1 -1
  14. package/dist/components/NumericInputColumnFilter.d.ts +2 -2
  15. package/dist/components/NumericInputColumnFilter.d.ts.map +1 -1
  16. package/dist/components/NumericInputColumnFilter.js +27 -6
  17. package/dist/components/NumericInputColumnFilter.js.map +1 -1
  18. package/dist/components/NumericInputColumnFilter.test.d.ts.map +1 -1
  19. package/dist/components/NumericInputColumnFilter.test.js.map +1 -1
  20. package/dist/components/OverlayTrigger.d.ts +1 -1
  21. package/dist/components/OverlayTrigger.d.ts.map +1 -1
  22. package/dist/components/OverlayTrigger.js +4 -3
  23. package/dist/components/OverlayTrigger.js.map +1 -1
  24. package/dist/components/PresetFilterDropdown.d.ts +2 -2
  25. package/dist/components/PresetFilterDropdown.d.ts.map +1 -1
  26. package/dist/components/PresetFilterDropdown.js +11 -5
  27. package/dist/components/PresetFilterDropdown.js.map +1 -1
  28. package/dist/components/TanstackTable.d.ts +7 -9
  29. package/dist/components/TanstackTable.d.ts.map +1 -1
  30. package/dist/components/TanstackTable.js +25 -10
  31. package/dist/components/TanstackTable.js.map +1 -1
  32. package/dist/components/TanstackTableDownloadButton.d.ts +1 -1
  33. package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
  34. package/dist/components/TanstackTableDownloadButton.js +10 -2
  35. package/dist/components/TanstackTableDownloadButton.js.map +1 -1
  36. package/dist/components/TanstackTableHeaderCell.d.ts +3 -3
  37. package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -1
  38. package/dist/components/TanstackTableHeaderCell.js +5 -3
  39. package/dist/components/TanstackTableHeaderCell.js.map +1 -1
  40. package/dist/components/nuqs.d.ts +1 -2
  41. package/dist/components/nuqs.d.ts.map +1 -1
  42. package/dist/components/nuqs.js +5 -5
  43. package/dist/components/nuqs.js.map +1 -1
  44. package/dist/components/nuqs.test.d.ts.map +1 -1
  45. package/dist/components/nuqs.test.js.map +1 -1
  46. package/dist/components/useAutoSizeColumns.d.ts +2 -3
  47. package/dist/components/useAutoSizeColumns.d.ts.map +1 -1
  48. package/dist/components/useAutoSizeColumns.js +11 -7
  49. package/dist/components/useAutoSizeColumns.js.map +1 -1
  50. package/dist/components/useShiftClickCheckbox.d.ts +3 -3
  51. package/dist/components/useShiftClickCheckbox.d.ts.map +1 -1
  52. package/dist/components/useShiftClickCheckbox.js +1 -1
  53. package/dist/components/useShiftClickCheckbox.js.map +1 -1
  54. package/dist/hooks/use-modal-state.d.ts.map +1 -1
  55. package/dist/hooks/use-modal-state.js +1 -1
  56. package/dist/hooks/use-modal-state.js.map +1 -1
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/react-table.d.ts.map +1 -1
  59. package/package.json +13 -9
  60. package/src/components/CategoricalColumnFilter.tsx +28 -21
  61. package/src/components/ColumnManager.tsx +14 -5
  62. package/src/components/MultiSelectColumnFilter.tsx +18 -13
  63. package/src/components/NumericInputColumnFilter.tsx +1 -1
  64. package/src/components/OverlayTrigger.tsx +1 -1
  65. package/src/components/PresetFilterDropdown.tsx +1 -1
  66. package/src/components/TanstackTable.tsx +13 -8
  67. package/src/components/TanstackTableHeaderCell.tsx +3 -3
  68. package/src/components/nuqs.tsx +5 -5
  69. package/src/components/useAutoSizeColumns.tsx +11 -10
  70. package/src/components/useShiftClickCheckbox.tsx +1 -1
  71. package/src/hooks/use-modal-state.ts +1 -1
  72. package/tsconfig.json +1 -1
@@ -1 +1 @@
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,EACL,2BAA2B,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,6CAA6C,CAAC;AACrD,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;AAC5E,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,uCAAuC,EACvC,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,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,EACL,2BAA2B,EAC3B,KAAK,oBAAoB,GAC1B,MAAM,6CAA6C,CAAC;AACrD,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;AAC5E,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,uCAAuC,EACvC,yBAAyB,EACzB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,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 {\n TanstackTableDownloadButton,\n type TanstackTableCsvCell,\n} 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';\nexport {\n NuqsAdapter,\n parseAsSortingState,\n parseAsColumnVisibilityStateWithColumns,\n parseAsColumnPinningState,\n parseAsNumericFilter,\n} from './components/nuqs.js';\n\nexport { useModalState } from './hooks/use-modal-state.js';\n"]}
@@ -1 +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"}
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,CAAC;IAGtC,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","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.10.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -12,26 +12,30 @@
12
12
  "./*.css": "./dist/*.css"
13
13
  },
14
14
  "scripts": {
15
- "build": "tsc && tscp",
16
- "dev": "tsc --watch --preserveWatchOutput & tscp --watch",
15
+ "build": "tsgo && tscp",
16
+ "dev": "tsgo --watch --preserveWatchOutput & tscp --watch",
17
17
  "test": "vitest run --coverage"
18
18
  },
19
19
  "dependencies": {
20
- "@prairielearn/browser-utils": "^2.6.0",
21
- "@prairielearn/preact-cjs": "^2.0.0",
20
+ "@prairielearn/browser-utils": "^2.6.1",
22
21
  "@tanstack/react-table": "^8.21.3",
23
- "@tanstack/react-virtual": "^3.13.14",
22
+ "@tanstack/react-virtual": "^3.13.18",
24
23
  "@tanstack/table-core": "^8.21.3",
24
+ "@types/react": "^19.2.8",
25
+ "@types/react-dom": "^19.2.3",
25
26
  "clsx": "^2.1.1",
26
27
  "nuqs": "^2.8.6",
28
+ "react": "^19.2.3",
27
29
  "react-bootstrap": "3.0.0-beta.5",
28
- "use-debounce": "^10.0.6"
30
+ "react-dom": "^19.2.3",
31
+ "use-debounce": "^10.1.0"
29
32
  },
30
33
  "devDependencies": {
31
34
  "@prairielearn/tsconfig": "^0.0.0",
32
- "@types/node": "^22.19.3",
35
+ "@types/node": "^22.19.5",
36
+ "@typescript/native-preview": "^7.0.0-dev.20260106.1",
33
37
  "typescript": "^5.9.3",
34
38
  "typescript-cp": "^0.1.9",
35
- "vitest": "^4.0.16"
39
+ "vitest": "^4.0.17"
36
40
  }
37
41
  }
@@ -1,37 +1,42 @@
1
1
  import type { Column } from '@tanstack/react-table';
2
2
  import clsx from 'clsx';
3
- import { type JSX, useMemo, useState } from 'preact/compat';
3
+ import { type JSX, useMemo, useState } from 'react';
4
4
  import Dropdown from 'react-bootstrap/Dropdown';
5
5
 
6
- function computeSelected<T extends readonly any[]>(
7
- allStatusValues: T,
6
+ function computeSelected<TValue extends string>(
7
+ allStatusValues: TValue[] | readonly TValue[],
8
8
  mode: 'include' | 'exclude',
9
- selected: Set<T[number]>,
10
- ) {
9
+ selected: Set<TValue>,
10
+ ): Set<TValue> {
11
11
  if (mode === 'include') {
12
12
  return selected;
13
13
  }
14
14
  return new Set(allStatusValues.filter((s) => !selected.has(s)));
15
15
  }
16
16
 
17
- function defaultRenderValueLabel<T>({ value }: { value: T }) {
18
- return <span className="text-nowrap">{String(value)}</span>;
17
+ function defaultRenderValueLabel({ value }: { value: string }) {
18
+ return <span className="text-nowrap">{value}</span>;
19
19
  }
20
+
20
21
  /**
21
22
  * A component that allows the user to filter a categorical column.
22
23
  * The filter mode always defaults to "include".
23
24
  *
25
+ * The filter options (`allColumnValues`) are strings (or string subtypes like
26
+ * enums). The column's `filterFn` is responsible for mapping these string
27
+ * values to the actual column data (e.g., mapping "Unassigned" to `null`).
28
+ *
24
29
  * @param params
25
30
  * @param params.column - The TanStack Table column object
26
- * @param params.allColumnValues - The values to filter by
31
+ * @param params.allColumnValues - The string values to display as filter options
27
32
  * @param params.renderValueLabel - A function that renders the label for a value
28
33
  */
29
- export function CategoricalColumnFilter<TData, TValue>({
34
+ export function CategoricalColumnFilter<TData, TValue extends string = string>({
30
35
  column,
31
36
  allColumnValues,
32
37
  renderValueLabel = defaultRenderValueLabel,
33
38
  }: {
34
- column: Column<TData, TValue>;
39
+ column: Column<TData, unknown>;
35
40
  allColumnValues: TValue[] | readonly TValue[];
36
41
  renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => JSX.Element;
37
42
  }) {
@@ -94,7 +99,7 @@ export function CategoricalColumnFilter<TData, TValue>({
94
99
  // Use `visibility` instead of conditional rendering to avoid layout shift.
95
100
  invisible: selected.size === 0 && mode === 'include',
96
101
  })}
97
- onClick={() => apply('include', new Set())}
102
+ onClick={() => apply('include', new Set<TValue>())}
98
103
  >
99
104
  Clear
100
105
  </button>
@@ -106,11 +111,11 @@ export function CategoricalColumnFilter<TData, TValue>({
106
111
  className="btn-check"
107
112
  name={`filter-${columnId}-options`}
108
113
  id={`filter-${columnId}-include`}
109
- autocomplete="off"
114
+ autoComplete="off"
110
115
  checked={mode === 'include'}
111
116
  onChange={() => apply('include', selected)}
112
117
  />
113
- <label className="btn btn-outline-primary" for={`filter-${columnId}-include`}>
118
+ <label className="btn btn-outline-primary" htmlFor={`filter-${columnId}-include`}>
114
119
  <span className="text-nowrap">
115
120
  {mode === 'include' && <i className="bi bi-check-lg me-1" aria-hidden="true" />}
116
121
  Include
@@ -122,11 +127,11 @@ export function CategoricalColumnFilter<TData, TValue>({
122
127
  className="btn-check"
123
128
  name={`filter-${columnId}-options`}
124
129
  id={`filter-${columnId}-exclude`}
125
- autocomplete="off"
130
+ autoComplete="off"
126
131
  checked={mode === 'exclude'}
127
132
  onChange={() => apply('exclude', selected)}
128
133
  />
129
- <label className="btn btn-outline-primary" for={`filter-${columnId}-exclude`}>
134
+ <label className="btn btn-outline-primary" htmlFor={`filter-${columnId}-exclude`}>
130
135
  <span className="text-nowrap">
131
136
  {mode === 'exclude' && <i className="bi bi-check-lg me-1" aria-hidden="true" />}
132
137
  Exclude
@@ -137,11 +142,13 @@ export function CategoricalColumnFilter<TData, TValue>({
137
142
 
138
143
  <div
139
144
  className="list-group list-group-flush"
140
- style={{
141
- // This is needed to prevent the last item's background from covering
142
- // the dropdown's border radius.
143
- '--bs-list-group-bg': 'transparent',
144
- }}
145
+ style={
146
+ {
147
+ // This is needed to prevent the last item's background from covering
148
+ // the dropdown's border radius.
149
+ '--bs-list-group-bg': 'transparent',
150
+ } as React.CSSProperties
151
+ }
145
152
  >
146
153
  {allColumnValues.map((value) => {
147
154
  const isSelected = selected.has(value);
@@ -155,7 +162,7 @@ export function CategoricalColumnFilter<TData, TValue>({
155
162
  id={`${columnId}-${value}`}
156
163
  onChange={() => toggleSelected(value)}
157
164
  />
158
- <label className="form-check-label fw-normal" for={`${columnId}-${value}`}>
165
+ <label className="form-check-label fw-normal" htmlFor={`${columnId}-${value}`}>
159
166
  {renderValueLabel({
160
167
  value,
161
168
  isSelected,
@@ -1,6 +1,6 @@
1
1
  import { type Column, type Table } from '@tanstack/react-table';
2
2
  import clsx from 'clsx';
3
- import { type JSX, useEffect, useRef, useState } from 'preact/compat';
3
+ import { type JSX, useEffect, useRef, useState } from 'react';
4
4
  import Button from 'react-bootstrap/Button';
5
5
  import Dropdown from 'react-bootstrap/Dropdown';
6
6
 
@@ -75,13 +75,21 @@ function ColumnGroupItem<RowDataModel>({
75
75
  getIsOnPinningBoundary: (columnId: string) => boolean;
76
76
  }) {
77
77
  const [isExpanded, setIsExpanded] = useState(false);
78
+ const checkboxRef = useRef<HTMLInputElement>(null);
78
79
 
79
80
  const leafColumns = column.getLeafColumns();
80
81
  const visibleLeafColumns = leafColumns.filter((c) => c.getIsVisible());
81
82
  const isAllVisible = visibleLeafColumns.length === leafColumns.length;
82
83
  const isSomeVisible = visibleLeafColumns.length > 0 && !isAllVisible;
83
84
 
84
- const handleToggleVisibility = (e: Event) => {
85
+ // Set indeterminate state via ref since it's a DOM property, not an HTML attribute
86
+ useEffect(() => {
87
+ if (checkboxRef.current) {
88
+ checkboxRef.current.indeterminate = isSomeVisible;
89
+ }
90
+ }, [isSomeVisible]);
91
+
92
+ const handleToggleVisibility = (e: React.ChangeEvent<HTMLInputElement>) => {
85
93
  e.preventDefault();
86
94
  e.stopPropagation();
87
95
  const targetVisibility = !isAllVisible;
@@ -102,10 +110,10 @@ function ColumnGroupItem<RowDataModel>({
102
110
  <div className="px-2 py-1 d-flex align-items-center justify-content-between">
103
111
  <div className="d-flex align-items-center flex-grow-1">
104
112
  <input
113
+ ref={checkboxRef}
105
114
  type="checkbox"
106
115
  className="form-check-input flex-shrink-0"
107
116
  checked={isAllVisible}
108
- indeterminate={isSomeVisible}
109
117
  aria-label={`Toggle visibility for group '${header}'`}
110
118
  onChange={handleToggleVisibility}
111
119
  />
@@ -297,9 +305,10 @@ export function ColumnManager<RowDataModel>({
297
305
  autoClose="outside"
298
306
  show={dropdownOpen}
299
307
  onToggle={(isOpen, _meta) => setDropdownOpen(isOpen)}
300
- onFocusOut={(e: FocusEvent) => {
308
+ onBlur={(e: React.FocusEvent) => {
301
309
  // Since we aren't using role="menu", we need to manually close the dropdown when focus leaves.
302
- if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
310
+ // `relatedTarget` is the element gaining focus.
311
+ if (menuRef.current && !menuRef.current.contains(e.relatedTarget)) {
303
312
  setDropdownOpen(false);
304
313
  }
305
314
  }}
@@ -1,28 +1,31 @@
1
1
  import type { Column } from '@tanstack/table-core';
2
2
  import clsx from 'clsx';
3
- import { type JSX, useMemo } from 'preact/compat';
3
+ import { type JSX, useMemo } from 'react';
4
4
  import Dropdown from 'react-bootstrap/Dropdown';
5
5
 
6
- function defaultRenderValueLabel<T>({ value }: { value: T }) {
7
- return <span>{String(value)}</span>;
6
+ function defaultRenderValueLabel({ value }: { value: string }) {
7
+ return <span>{value}</span>;
8
8
  }
9
9
 
10
10
  /**
11
11
  * A component that allows the user to filter a column containing arrays of values.
12
12
  * Uses AND logic: rows must contain ALL selected values to match.
13
13
  *
14
+ * The filter options (`allColumnValues`) are strings (or string subtypes like
15
+ * enums). The column's `filterFn` is responsible for mapping these string
16
+ * values to the actual column data.
17
+ *
14
18
  * @param params
15
19
  * @param params.column - The TanStack Table column object
16
- * @param params.allColumnValues - All possible values that can appear in the column filter
20
+ * @param params.allColumnValues - The string values to display as filter options
17
21
  * @param params.renderValueLabel - A function that renders the label for a value
18
22
  */
19
- export function MultiSelectColumnFilter<TData, TValue>({
23
+ export function MultiSelectColumnFilter<TData, TValue extends string = string>({
20
24
  column,
21
25
  allColumnValues,
22
26
  renderValueLabel = defaultRenderValueLabel,
23
27
  }: {
24
- column: Column<TData, TValue>;
25
- /** In some cases, the filter values are not the same as the column values, but `TValue` is a good estimation. */
28
+ column: Column<TData, unknown>;
26
29
  allColumnValues: TValue[];
27
30
  renderValueLabel?: (props: { value: TValue; isSelected: boolean }) => JSX.Element;
28
31
  }) {
@@ -81,11 +84,13 @@ export function MultiSelectColumnFilter<TData, TValue>({
81
84
 
82
85
  <div
83
86
  className="list-group list-group-flush"
84
- style={{
85
- // This is needed to prevent the last item's background from covering
86
- // the dropdown's border radius.
87
- '--bs-list-group-bg': 'transparent',
88
- }}
87
+ style={
88
+ {
89
+ // This is needed to prevent the last item's background from covering
90
+ // the dropdown's border radius.
91
+ '--bs-list-group-bg': 'transparent',
92
+ } as React.CSSProperties
93
+ }
89
94
  >
90
95
  {allColumnValues.map((value) => {
91
96
  const isSelected = selected.has(value);
@@ -99,7 +104,7 @@ export function MultiSelectColumnFilter<TData, TValue>({
99
104
  id={`${columnId}-${value}`}
100
105
  onChange={() => toggleSelected(value)}
101
106
  />
102
- <label className="form-check-label fw-normal" for={`${columnId}-${value}`}>
107
+ <label className="form-check-label fw-normal" htmlFor={`${columnId}-${value}`}>
103
108
  {renderValueLabel({
104
109
  value,
105
110
  isSelected,
@@ -123,7 +123,7 @@ export function NumericInputColumnFilter<TData, TValue>({
123
123
  );
124
124
  }}
125
125
  />
126
- <label className="form-check-label" for={`${columnId}-empty-filter`}>
126
+ <label className="form-check-label" htmlFor={`${columnId}-empty-filter`}>
127
127
  Empty values
128
128
  </label>
129
129
  </div>
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from 'preact/compat';
1
+ import { useEffect, useRef } from 'react';
2
2
  import {
3
3
  // eslint-disable-next-line no-restricted-imports
4
4
  OverlayTrigger as BootstrapOverlayTrigger,
@@ -1,5 +1,5 @@
1
1
  import type { ColumnFiltersState, Table } from '@tanstack/react-table';
2
- import { useMemo } from 'preact/compat';
2
+ import { useMemo } from 'react';
3
3
  import { ButtonGroup, Dropdown } from 'react-bootstrap';
4
4
 
5
5
  /**
@@ -2,14 +2,19 @@ import { flexRender } from '@tanstack/react-table';
2
2
  import { useVirtualizer } from '@tanstack/react-virtual';
3
3
  import type { Cell, Header, Row, Table } from '@tanstack/table-core';
4
4
  import clsx from 'clsx';
5
- import type { ComponentChildren } from 'preact';
6
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
7
- import type { JSX } from 'preact/jsx-runtime';
5
+ import {
6
+ type ComponentProps,
7
+ type JSX,
8
+ type ReactNode,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
8
14
  import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
9
15
  import Tooltip from 'react-bootstrap/Tooltip';
10
16
  import { useDebouncedCallback } from 'use-debounce';
11
17
 
12
- import type { ComponentProps } from '@prairielearn/preact-cjs';
13
18
  import { run } from '@prairielearn/run';
14
19
 
15
20
  import { ColumnManager } from './ColumnManager.js';
@@ -35,7 +40,7 @@ function TableCell<RowDataModel>({
35
40
  canSort: boolean;
36
41
  canFilter: boolean;
37
42
  wrapText: boolean;
38
- handleGridKeyDown: (e: KeyboardEvent, rowIdx: number, colIdx: number) => void;
43
+ handleGridKeyDown: (e: React.KeyboardEvent, rowIdx: number, colIdx: number) => void;
39
44
  }) {
40
45
  return (
41
46
  <td
@@ -91,7 +96,7 @@ interface TanstackTableProps<RowDataModel> {
91
96
  rowHeight?: number;
92
97
  noResultsState?: JSX.Element;
93
98
  emptyState?: JSX.Element;
94
- scrollRef?: React.RefObject<HTMLDivElement> | null;
99
+ scrollRef?: React.RefObject<HTMLDivElement | null> | null;
95
100
  }
96
101
 
97
102
  const DEFAULT_FILTER_MAP = {};
@@ -174,7 +179,7 @@ export function TanstackTable<RowDataModel>({
174
179
  ...row.getCenterVisibleCells(),
175
180
  ];
176
181
 
177
- const handleGridKeyDown = (e: KeyboardEvent, rowIdx: number, colIdx: number) => {
182
+ const handleGridKeyDown = (e: React.KeyboardEvent, rowIdx: number, colIdx: number) => {
178
183
  const rowLength = getVisibleCells(rows[rowIdx]).length;
179
184
  const adjacentCells: Record<KeyboardEvent['key'], { row: number; col: number }> = {
180
185
  ArrowDown: {
@@ -630,7 +635,7 @@ export function TanstackTableEmptyState({
630
635
  children,
631
636
  }: {
632
637
  iconName: `bi-${string}`;
633
- children: ComponentChildren;
638
+ children: ReactNode;
634
639
  }) {
635
640
  return (
636
641
  <div className="d-flex flex-column justify-content-center align-items-center text-muted">
@@ -1,7 +1,7 @@
1
1
  import { flexRender } from '@tanstack/react-table';
2
2
  import type { Header, SortDirection, Table } from '@tanstack/table-core';
3
3
  import clsx from 'clsx';
4
- import type { JSX } from 'preact/jsx-runtime';
4
+ import type { CSSProperties, JSX } from 'react';
5
5
 
6
6
  function SortIcon({ sortMethod }: { sortMethod: false | SortDirection }) {
7
7
  if (sortMethod === 'asc') {
@@ -24,7 +24,7 @@ function ResizeHandle<RowDataModel>({
24
24
  }) {
25
25
  const minSize = header.column.columnDef.minSize ?? 0;
26
26
  const maxSize = header.column.columnDef.maxSize ?? 0;
27
- const handleKeyDown = (e: KeyboardEvent) => {
27
+ const handleKeyDown = (e: React.KeyboardEvent) => {
28
28
  if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
29
29
  e.preventDefault();
30
30
  const currentSize = header.getSize();
@@ -121,7 +121,7 @@ export function TanstackTableHeaderCell<RowDataModel>({
121
121
 
122
122
  // In measurement mode, we don't want to set the size of the header from tanstack.
123
123
  const headerSize = measurementMode ? undefined : header.getSize();
124
- const style: JSX.CSSProperties = {
124
+ const style: CSSProperties = {
125
125
  display: 'flex',
126
126
  width: headerSize,
127
127
  minWidth: 0,
@@ -5,14 +5,14 @@ import {
5
5
  unstable_createAdapterProvider,
6
6
  } from 'nuqs/adapters/custom';
7
7
  import { NuqsAdapter as NuqsReactAdapter } from 'nuqs/adapters/react';
8
- import React from 'preact/compat';
8
+ import { createContext, use } from 'react';
9
9
 
10
10
  import type { NumericColumnFilterValue } from './NumericInputColumnFilter.js';
11
11
 
12
- const AdapterContext = React.createContext('');
12
+ const AdapterContext = createContext('');
13
13
 
14
14
  function useExpressAdapterContext(): unstable_AdapterInterface {
15
- const context = React.useContext(AdapterContext);
15
+ const context = use(AdapterContext);
16
16
 
17
17
  return {
18
18
  searchParams: new URLSearchParams(context),
@@ -34,9 +34,9 @@ export function NuqsAdapter({ children, search }: { children: React.ReactNode; s
34
34
  if (typeof location === 'undefined') {
35
35
  // We're rendering on the server.
36
36
  return (
37
- <AdapterContext.Provider value={search}>
37
+ <AdapterContext value={search}>
38
38
  <NuqsExpressAdapter>{children}</NuqsExpressAdapter>
39
- </AdapterContext.Provider>
39
+ </AdapterContext>
40
40
  );
41
41
  }
42
42
 
@@ -1,8 +1,6 @@
1
1
  import type { ColumnSizingState, Header, Table } from '@tanstack/react-table';
2
- import type { RefObject } from 'preact';
3
- import { render } from 'preact/compat';
4
- import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
5
- import type { JSX } from 'preact/jsx-runtime';
2
+ import { type JSX, type RefObject, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { type Root, createRoot } from 'react-dom/client';
6
4
 
7
5
  import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
8
6
 
@@ -64,10 +62,11 @@ function HiddenMeasurementHeader<TData>({
64
62
  */
65
63
  export function useAutoSizeColumns<TData>(
66
64
  table: Table<TData>,
67
- tableRef: RefObject<HTMLDivElement>,
65
+ tableRef: RefObject<HTMLDivElement | null>,
68
66
  filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>,
69
67
  ): boolean {
70
68
  const measurementContainerRef = useRef<HTMLDivElement | null>(null);
69
+ const measurementRootRef = useRef<Root | null>(null);
71
70
 
72
71
  // Compute columns that need measuring
73
72
  const columnsToMeasure = useMemo(() => {
@@ -96,16 +95,16 @@ export function useAutoSizeColumns<TData>(
96
95
  container = document.createElement('div');
97
96
  document.body.append(container);
98
97
  measurementContainerRef.current = container;
98
+ measurementRootRef.current = createRoot(container);
99
99
  }
100
100
 
101
101
  // Render headers into hidden container
102
- render(
102
+ measurementRootRef.current?.render(
103
103
  <HiddenMeasurementHeader
104
104
  table={table}
105
105
  columnsToMeasure={columnsToMeasure}
106
106
  filters={filters ?? {}}
107
107
  />,
108
- container,
109
108
  );
110
109
 
111
110
  // Force layout calculation
@@ -134,8 +133,9 @@ export function useAutoSizeColumns<TData>(
134
133
  }
135
134
  }
136
135
 
137
- // Clear container content by unmounting Preact components
138
- render(null, container);
136
+ // Clear container content by unmounting React components
137
+ measurementRootRef.current?.unmount();
138
+ measurementRootRef.current = null;
139
139
 
140
140
  // Apply measurements
141
141
  if (Object.keys(newSizing).length > 0) {
@@ -153,9 +153,10 @@ export function useAutoSizeColumns<TData>(
153
153
  // Clean up measurement container on unmount
154
154
  useEffect(() => {
155
155
  return () => {
156
+ measurementRootRef.current?.unmount();
157
+ measurementRootRef.current = null;
156
158
  const container = measurementContainerRef.current;
157
159
  if (container) {
158
- render(null, container);
159
160
  container.remove();
160
161
  measurementContainerRef.current = null;
161
162
  }
@@ -1,5 +1,5 @@
1
1
  import type { Row, Table } from '@tanstack/react-table';
2
- import { type MouseEvent, useCallback, useState } from 'preact/compat';
2
+ import { type MouseEvent, useCallback, useState } from 'react';
3
3
 
4
4
  /**
5
5
  * A hook that provides shift-click range selection functionality for table checkboxes.
@@ -1,4 +1,4 @@
1
- import { useState } from 'preact/hooks';
1
+ import { useState } from 'react';
2
2
 
3
3
  /**
4
4
  * A hook to manage the state of a modal dialog that's rendered with a certain set of data.
package/tsconfig.json CHANGED
@@ -5,6 +5,6 @@
5
5
  "rootDir": "./src",
6
6
  "types": ["node"],
7
7
  "jsx": "react-jsx",
8
- "jsxImportSource": "@prairielearn/preact-cjs"
8
+ "lib": ["ES2022", "DOM", "DOM.Iterable"]
9
9
  }
10
10
  }