@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.
- package/CHANGELOG.md +12 -0
- package/dist/components/CategoricalColumnFilter.d.ts +9 -5
- package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
- package/dist/components/CategoricalColumnFilter.js +25 -9
- package/dist/components/CategoricalColumnFilter.js.map +1 -1
- package/dist/components/ColumnManager.d.ts +2 -2
- package/dist/components/ColumnManager.d.ts.map +1 -1
- package/dist/components/ColumnManager.js +39 -12
- package/dist/components/ColumnManager.js.map +1 -1
- package/dist/components/MultiSelectColumnFilter.d.ts +9 -6
- package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -1
- package/dist/components/MultiSelectColumnFilter.js +20 -8
- package/dist/components/MultiSelectColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.d.ts +2 -2
- package/dist/components/NumericInputColumnFilter.d.ts.map +1 -1
- package/dist/components/NumericInputColumnFilter.js +27 -6
- package/dist/components/NumericInputColumnFilter.js.map +1 -1
- package/dist/components/NumericInputColumnFilter.test.d.ts.map +1 -1
- package/dist/components/NumericInputColumnFilter.test.js.map +1 -1
- package/dist/components/OverlayTrigger.d.ts +1 -1
- package/dist/components/OverlayTrigger.d.ts.map +1 -1
- package/dist/components/OverlayTrigger.js +4 -3
- package/dist/components/OverlayTrigger.js.map +1 -1
- package/dist/components/PresetFilterDropdown.d.ts +2 -2
- package/dist/components/PresetFilterDropdown.d.ts.map +1 -1
- package/dist/components/PresetFilterDropdown.js +11 -5
- package/dist/components/PresetFilterDropdown.js.map +1 -1
- package/dist/components/TanstackTable.d.ts +7 -9
- package/dist/components/TanstackTable.d.ts.map +1 -1
- package/dist/components/TanstackTable.js +25 -10
- package/dist/components/TanstackTable.js.map +1 -1
- package/dist/components/TanstackTableDownloadButton.d.ts +1 -1
- package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
- package/dist/components/TanstackTableDownloadButton.js +10 -2
- package/dist/components/TanstackTableDownloadButton.js.map +1 -1
- package/dist/components/TanstackTableHeaderCell.d.ts +3 -3
- package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -1
- package/dist/components/TanstackTableHeaderCell.js +5 -3
- package/dist/components/TanstackTableHeaderCell.js.map +1 -1
- package/dist/components/nuqs.d.ts +1 -2
- package/dist/components/nuqs.d.ts.map +1 -1
- package/dist/components/nuqs.js +5 -5
- package/dist/components/nuqs.js.map +1 -1
- package/dist/components/nuqs.test.d.ts.map +1 -1
- package/dist/components/nuqs.test.js.map +1 -1
- package/dist/components/useAutoSizeColumns.d.ts +2 -3
- package/dist/components/useAutoSizeColumns.d.ts.map +1 -1
- package/dist/components/useAutoSizeColumns.js +11 -7
- package/dist/components/useAutoSizeColumns.js.map +1 -1
- package/dist/components/useShiftClickCheckbox.d.ts +3 -3
- package/dist/components/useShiftClickCheckbox.d.ts.map +1 -1
- package/dist/components/useShiftClickCheckbox.js +1 -1
- package/dist/components/useShiftClickCheckbox.js.map +1 -1
- package/dist/hooks/use-modal-state.d.ts.map +1 -1
- package/dist/hooks/use-modal-state.js +1 -1
- package/dist/hooks/use-modal-state.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/react-table.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/components/CategoricalColumnFilter.tsx +28 -21
- package/src/components/ColumnManager.tsx +14 -5
- package/src/components/MultiSelectColumnFilter.tsx +18 -13
- package/src/components/NumericInputColumnFilter.tsx +1 -1
- package/src/components/OverlayTrigger.tsx +1 -1
- package/src/components/PresetFilterDropdown.tsx +1 -1
- package/src/components/TanstackTable.tsx +13 -8
- package/src/components/TanstackTableHeaderCell.tsx +3 -3
- package/src/components/nuqs.tsx +5 -5
- package/src/components/useAutoSizeColumns.tsx +11 -10
- package/src/components/useShiftClickCheckbox.tsx +1 -1
- package/src/hooks/use-modal-state.ts +1 -1
- package/tsconfig.json +1 -1
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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": "
|
|
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": "
|
|
16
|
-
"dev": "
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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 '
|
|
3
|
+
import { type JSX, useMemo, useState } from 'react';
|
|
4
4
|
import Dropdown from 'react-bootstrap/Dropdown';
|
|
5
5
|
|
|
6
|
-
function computeSelected<
|
|
7
|
-
allStatusValues:
|
|
6
|
+
function computeSelected<TValue extends string>(
|
|
7
|
+
allStatusValues: TValue[] | readonly TValue[],
|
|
8
8
|
mode: 'include' | 'exclude',
|
|
9
|
-
selected: Set<
|
|
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
|
|
18
|
-
return <span className="text-nowrap">{
|
|
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
|
|
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,
|
|
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
|
-
|
|
114
|
+
autoComplete="off"
|
|
110
115
|
checked={mode === 'include'}
|
|
111
116
|
onChange={() => apply('include', selected)}
|
|
112
117
|
/>
|
|
113
|
-
<label className="btn btn-outline-primary"
|
|
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
|
-
|
|
130
|
+
autoComplete="off"
|
|
126
131
|
checked={mode === 'exclude'}
|
|
127
132
|
onChange={() => apply('exclude', selected)}
|
|
128
133
|
/>
|
|
129
|
-
<label className="btn btn-outline-primary"
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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"
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 '
|
|
3
|
+
import { type JSX, useMemo } from 'react';
|
|
4
4
|
import Dropdown from 'react-bootstrap/Dropdown';
|
|
5
5
|
|
|
6
|
-
function defaultRenderValueLabel
|
|
7
|
-
return <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 -
|
|
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,
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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"
|
|
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"
|
|
126
|
+
<label className="form-check-label" htmlFor={`${columnId}-empty-filter`}>
|
|
127
127
|
Empty values
|
|
128
128
|
</label>
|
|
129
129
|
</div>
|
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
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:
|
|
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 '
|
|
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:
|
|
124
|
+
const style: CSSProperties = {
|
|
125
125
|
display: 'flex',
|
|
126
126
|
width: headerSize,
|
|
127
127
|
minWidth: 0,
|
package/src/components/nuqs.tsx
CHANGED
|
@@ -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
|
|
8
|
+
import { createContext, use } from 'react';
|
|
9
9
|
|
|
10
10
|
import type { NumericColumnFilterValue } from './NumericInputColumnFilter.js';
|
|
11
11
|
|
|
12
|
-
const AdapterContext =
|
|
12
|
+
const AdapterContext = createContext('');
|
|
13
13
|
|
|
14
14
|
function useExpressAdapterContext(): unstable_AdapterInterface {
|
|
15
|
-
const context =
|
|
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
|
|
37
|
+
<AdapterContext value={search}>
|
|
38
38
|
<NuqsExpressAdapter>{children}</NuqsExpressAdapter>
|
|
39
|
-
</AdapterContext
|
|
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
|
|
3
|
-
import {
|
|
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
|
|
138
|
-
|
|
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 '
|
|
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.
|