@prairielearn/ui 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -2
  3. package/dist/components/CategoricalColumnFilter.d.ts +7 -12
  4. package/dist/components/CategoricalColumnFilter.d.ts.map +1 -1
  5. package/dist/components/CategoricalColumnFilter.js +26 -14
  6. package/dist/components/CategoricalColumnFilter.js.map +1 -1
  7. package/dist/components/ColumnManager.d.ts +6 -2
  8. package/dist/components/ColumnManager.d.ts.map +1 -1
  9. package/dist/components/ColumnManager.js +98 -35
  10. package/dist/components/ColumnManager.js.map +1 -1
  11. package/dist/components/MultiSelectColumnFilter.d.ts +8 -12
  12. package/dist/components/MultiSelectColumnFilter.d.ts.map +1 -1
  13. package/dist/components/MultiSelectColumnFilter.js +21 -13
  14. package/dist/components/MultiSelectColumnFilter.js.map +1 -1
  15. package/dist/components/NumericInputColumnFilter.d.ts +13 -13
  16. package/dist/components/NumericInputColumnFilter.d.ts.map +1 -1
  17. package/dist/components/NumericInputColumnFilter.js +44 -15
  18. package/dist/components/NumericInputColumnFilter.js.map +1 -1
  19. package/dist/components/NumericInputColumnFilter.test.d.ts +2 -0
  20. package/dist/components/NumericInputColumnFilter.test.d.ts.map +1 -0
  21. package/dist/components/NumericInputColumnFilter.test.js +90 -0
  22. package/dist/components/NumericInputColumnFilter.test.js.map +1 -0
  23. package/dist/components/OverlayTrigger.d.ts +78 -0
  24. package/dist/components/OverlayTrigger.d.ts.map +1 -0
  25. package/dist/components/OverlayTrigger.js +89 -0
  26. package/dist/components/OverlayTrigger.js.map +1 -0
  27. package/dist/components/TanstackTable.d.ts +19 -3
  28. package/dist/components/TanstackTable.d.ts.map +1 -1
  29. package/dist/components/TanstackTable.js +159 -219
  30. package/dist/components/TanstackTable.js.map +1 -1
  31. package/dist/components/TanstackTableDownloadButton.d.ts +4 -2
  32. package/dist/components/TanstackTableDownloadButton.d.ts.map +1 -1
  33. package/dist/components/TanstackTableDownloadButton.js +4 -3
  34. package/dist/components/TanstackTableDownloadButton.js.map +1 -1
  35. package/dist/components/TanstackTableHeaderCell.d.ts +13 -0
  36. package/dist/components/TanstackTableHeaderCell.d.ts.map +1 -0
  37. package/dist/components/TanstackTableHeaderCell.js +98 -0
  38. package/dist/components/TanstackTableHeaderCell.js.map +1 -0
  39. package/dist/components/styles.css +58 -0
  40. package/dist/components/useAutoSizeColumns.d.ts +17 -0
  41. package/dist/components/useAutoSizeColumns.d.ts.map +1 -0
  42. package/dist/components/useAutoSizeColumns.js +99 -0
  43. package/dist/components/useAutoSizeColumns.js.map +1 -0
  44. package/dist/index.d.ts +4 -2
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +4 -1
  47. package/dist/index.js.map +1 -1
  48. package/dist/react-table.d.ts +13 -0
  49. package/dist/react-table.d.ts.map +1 -0
  50. package/dist/react-table.js +3 -0
  51. package/dist/react-table.js.map +1 -0
  52. package/package.json +2 -2
  53. package/src/components/CategoricalColumnFilter.tsx +84 -54
  54. package/src/components/ColumnManager.tsx +236 -88
  55. package/src/components/MultiSelectColumnFilter.tsx +45 -32
  56. package/src/components/NumericInputColumnFilter.test.ts +67 -19
  57. package/src/components/NumericInputColumnFilter.tsx +102 -42
  58. package/src/components/OverlayTrigger.tsx +168 -0
  59. package/src/components/TanstackTable.tsx +357 -410
  60. package/src/components/TanstackTableDownloadButton.tsx +8 -5
  61. package/src/components/TanstackTableHeaderCell.tsx +207 -0
  62. package/src/components/styles.css +58 -0
  63. package/src/components/useAutoSizeColumns.tsx +168 -0
  64. package/src/index.ts +10 -1
  65. package/src/react-table.ts +17 -0
  66. package/tsconfig.json +1 -2
  67. package/dist/components/TanstackTable.css +0 -4
  68. package/src/components/TanstackTable.css +0 -4
@@ -6,11 +6,13 @@ export interface TanstackTableDownloadButtonProps<RowDataModel> {
6
6
  table: Table<RowDataModel>;
7
7
  filenameBase: string;
8
8
  mapRowToData: (row: RowDataModel) => Record<string, string | number | null> | null;
9
+ singularLabel: string;
9
10
  pluralLabel: string;
10
11
  }
11
12
  /**
12
13
  * @param params
13
- * @param params.pluralLabel - What you are downloading, e.g. "students"
14
+ * @param params.singularLabel - The singular label for a single row in the table, e.g. "student"
15
+ * @param params.pluralLabel - The plural label for multiple rows in the table, e.g. "students"
14
16
  * @param params.table - The table model
15
17
  * @param params.filenameBase - The base filename for the downloads
16
18
  * @param params.mapRowToData - A function that maps a row to a record where the
@@ -22,6 +24,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
22
24
  table,
23
25
  filenameBase,
24
26
  mapRowToData,
27
+ singularLabel,
25
28
  pluralLabel,
26
29
  }: TanstackTableDownloadButtonProps<RowDataModel>) {
27
30
  const allRows = table.getCoreRowModel().rows.map((row) => row.original);
@@ -91,7 +94,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
91
94
  disabled={selectedRowsJSON.length === 0}
92
95
  onClick={() => downloadJSONAsCSV(selectedRowsJSON, `${filenameBase}_selected.csv`)}
93
96
  >
94
- Selected {pluralLabel} as CSV
97
+ Selected {selectedRowsJSON.length === 1 ? singularLabel : pluralLabel} as CSV
95
98
  </button>
96
99
  </li>
97
100
  <li role="presentation">
@@ -103,7 +106,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
103
106
  disabled={selectedRowsJSON.length === 0}
104
107
  onClick={() => downloadAsJSON(selectedRowsJSON, `${filenameBase}_selected.json`)}
105
108
  >
106
- Selected {pluralLabel} as JSON
109
+ Selected {selectedRowsJSON.length === 1 ? singularLabel : pluralLabel} as JSON
107
110
  </button>
108
111
  </li>
109
112
  <li role="presentation">
@@ -115,7 +118,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
115
118
  disabled={filteredRowsJSON.length === 0}
116
119
  onClick={() => downloadJSONAsCSV(filteredRowsJSON, `${filenameBase}_filtered.csv`)}
117
120
  >
118
- Filtered {pluralLabel} as CSV
121
+ Filtered {filteredRowsJSON.length === 1 ? singularLabel : pluralLabel} as CSV
119
122
  </button>
120
123
  </li>
121
124
  <li role="presentation">
@@ -127,7 +130,7 @@ export function TanstackTableDownloadButton<RowDataModel>({
127
130
  disabled={filteredRowsJSON.length === 0}
128
131
  onClick={() => downloadAsJSON(filteredRowsJSON, `${filenameBase}_filtered.json`)}
129
132
  >
130
- Filtered {pluralLabel} as JSON
133
+ Filtered {filteredRowsJSON.length === 1 ? singularLabel : pluralLabel} as JSON
131
134
  </button>
132
135
  </li>
133
136
  </ul>
@@ -0,0 +1,207 @@
1
+ import { flexRender } from '@tanstack/react-table';
2
+ import type { Header, SortDirection, Table } from '@tanstack/table-core';
3
+ import clsx from 'clsx';
4
+ import type { JSX } from 'preact/jsx-runtime';
5
+
6
+ function SortIcon({ sortMethod }: { sortMethod: false | SortDirection }) {
7
+ if (sortMethod === 'asc') {
8
+ return <i class="bi bi-sort-up-alt" aria-hidden="true" />;
9
+ } else if (sortMethod === 'desc') {
10
+ return <i class="bi bi-sort-down" aria-hidden="true" />;
11
+ } else {
12
+ return <i class="bi bi-arrow-down-up opacity-75 text-muted" aria-hidden="true" />;
13
+ }
14
+ }
15
+
16
+ function ResizeHandle<RowDataModel>({
17
+ header,
18
+ setColumnSizing,
19
+ onResizeEnd,
20
+ }: {
21
+ header: Header<RowDataModel, unknown>;
22
+ setColumnSizing: Table<RowDataModel>['setColumnSizing'];
23
+ onResizeEnd?: () => void;
24
+ }) {
25
+ const minSize = header.column.columnDef.minSize ?? 0;
26
+ const maxSize = header.column.columnDef.maxSize ?? 0;
27
+ const handleKeyDown = (e: KeyboardEvent) => {
28
+ if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
29
+ e.preventDefault();
30
+ const currentSize = header.getSize();
31
+ const increment = e.shiftKey ? 20 : 5; // Larger increment with Shift key
32
+ const newSize =
33
+ e.key === 'ArrowLeft'
34
+ ? Math.max(minSize, currentSize - increment)
35
+ : Math.min(maxSize, currentSize + increment);
36
+
37
+ setColumnSizing((prevSizing) => ({
38
+ ...prevSizing,
39
+ [header.column.id]: newSize,
40
+ }));
41
+ } else if (e.key === 'Home') {
42
+ e.preventDefault();
43
+ header.column.resetSize();
44
+ }
45
+ };
46
+
47
+ const columnName =
48
+ typeof header.column.columnDef.header === 'string'
49
+ ? header.column.columnDef.header
50
+ : header.column.id;
51
+
52
+ return (
53
+ <div class="py-1 h-100" style={{ position: 'absolute', right: 0, top: 0, width: '4px' }}>
54
+ {/* separator role is focusable, so these jsx-a11y-x rules are false positives.
55
+ https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/separator_role#focusable_separator
56
+ */}
57
+ {/* eslint-disable-next-line jsx-a11y-x/no-noninteractive-element-interactions */}
58
+ <div
59
+ role="separator"
60
+ aria-label={`Resize '${columnName}' column`}
61
+ aria-valuetext={`${header.getSize()}px`}
62
+ aria-orientation="vertical"
63
+ aria-valuemin={minSize}
64
+ aria-valuemax={maxSize}
65
+ aria-valuenow={header.getSize()}
66
+ // eslint-disable-next-line jsx-a11y-x/no-noninteractive-tabindex
67
+ tabIndex={0}
68
+ class="h-100"
69
+ style={{
70
+ background: header.column.getIsResizing() ? 'var(--bs-primary)' : 'var(--bs-gray-400)',
71
+ cursor: 'col-resize',
72
+ transition: 'background-color 0.2s',
73
+ }}
74
+ onMouseDown={header.getResizeHandler()}
75
+ onMouseUp={onResizeEnd}
76
+ onTouchStart={header.getResizeHandler()}
77
+ onTouchEnd={onResizeEnd}
78
+ onKeyDown={handleKeyDown}
79
+ />
80
+ </div>
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Helper function to get aria-sort value
86
+ */
87
+ function getAriaSort(sortDirection: false | SortDirection) {
88
+ switch (sortDirection) {
89
+ case 'asc':
90
+ return 'ascending';
91
+ case 'desc':
92
+ return 'descending';
93
+ default:
94
+ return 'none';
95
+ }
96
+ }
97
+
98
+ export function TanstackTableHeaderCell<RowDataModel>({
99
+ header,
100
+ filters,
101
+ table,
102
+ handleResizeEnd,
103
+ isPinned,
104
+ measurementMode = false,
105
+ }: {
106
+ header: Header<RowDataModel, unknown>;
107
+ filters: Record<string, (props: { header: Header<RowDataModel, unknown> }) => JSX.Element>;
108
+ table: Table<RowDataModel>;
109
+ handleResizeEnd?: () => void;
110
+ isPinned: 'left' | false;
111
+ measurementMode?: boolean;
112
+ }) {
113
+ const sortDirection = header.column.getIsSorted();
114
+ const canSort = header.column.getCanSort();
115
+ const canFilter = header.column.getCanFilter();
116
+ const columnName =
117
+ header.column.columnDef.meta?.label ??
118
+ (typeof header.column.columnDef.header === 'string'
119
+ ? header.column.columnDef.header
120
+ : header.column.id);
121
+
122
+ // In measurement mode, we don't want to set the size of the header from tanstack.
123
+ const headerSize = measurementMode ? undefined : header.getSize();
124
+ const style: JSX.CSSProperties = {
125
+ display: 'flex',
126
+ width: headerSize,
127
+ minWidth: 0,
128
+ maxWidth: headerSize,
129
+ flexShrink: 0,
130
+ position: isPinned === 'left' ? 'sticky' : 'relative',
131
+ top: 0,
132
+ zIndex: isPinned === 'left' ? 2 : 1,
133
+ left: isPinned === 'left' ? header.getStart() : undefined,
134
+ boxShadow:
135
+ 'inset 0 calc(-1 * var(--bs-border-width)) 0 0 rgba(0, 0, 0, 1), inset 0 var(--bs-border-width) 0 0 var(--bs-border-color)',
136
+ };
137
+
138
+ const isNormalColumn = canSort || canFilter;
139
+
140
+ return (
141
+ <th
142
+ key={header.id}
143
+ data-column-id={header.column.id}
144
+ class={clsx(isPinned === 'left' && 'bg-light')}
145
+ style={style}
146
+ aria-sort={canSort ? getAriaSort(sortDirection) : undefined}
147
+ role="columnheader"
148
+ >
149
+ <div
150
+ class={clsx(
151
+ 'd-flex align-items-center flex-grow-1',
152
+ isNormalColumn ? 'justify-content-between' : 'justify-content-center',
153
+ )}
154
+ style={{
155
+ minWidth: 0,
156
+ }}
157
+ >
158
+ <div
159
+ class={clsx(
160
+ 'text-nowrap text-start',
161
+ // e.g. checkboxes
162
+ !isNormalColumn && 'd-flex align-items-center justify-content-center',
163
+ )}
164
+ style={{
165
+ minWidth: 0,
166
+ flex: '1 1 0%',
167
+ overflow: 'hidden',
168
+ textOverflow: 'ellipsis',
169
+ background: 'transparent',
170
+ border: 'none',
171
+ }}
172
+ >
173
+ {header.isPlaceholder
174
+ ? null
175
+ : flexRender(header.column.columnDef.header, header.getContext())}
176
+ {canSort && (
177
+ <span class="visually-hidden">, {getAriaSort(sortDirection)}, click to sort</span>
178
+ )}
179
+ </div>
180
+
181
+ {(canSort || canFilter) && (
182
+ <div class="d-flex align-items-center" style={{ flexShrink: 0 }}>
183
+ {canSort && (
184
+ <button
185
+ type="button"
186
+ class="btn btn-link text-muted p-0"
187
+ aria-label={`Sort ${columnName.toLowerCase()}, current sort is ${getAriaSort(sortDirection)}`}
188
+ title={`Sort ${columnName.toLowerCase()}`}
189
+ onClick={header.column.getToggleSortingHandler()}
190
+ >
191
+ <SortIcon sortMethod={sortDirection} />
192
+ </button>
193
+ )}
194
+ {canFilter && filters[header.column.id]?.({ header })}
195
+ </div>
196
+ )}
197
+ </div>
198
+ {header.column.getCanResize() && (
199
+ <ResizeHandle
200
+ header={header}
201
+ setColumnSizing={table.setColumnSizing}
202
+ onResizeEnd={handleResizeEnd}
203
+ />
204
+ )}
205
+ </th>
206
+ );
207
+ }
@@ -0,0 +1,58 @@
1
+ /* Global styles for the PrairieLearn UI components. These should be included in any
2
+ page that uses the PrairieLearn UI components, and class names should not be directly referenced by consumers.
3
+ */
4
+
5
+ body.pl-ui-no-user-select {
6
+ user-select: none;
7
+ -webkit-user-select: none;
8
+ }
9
+
10
+ .pl-ui-tanstack-table-search-input {
11
+ padding-right: 2.5rem;
12
+ }
13
+
14
+ .btn.btn-floating-icon {
15
+ position: absolute;
16
+ right: 0;
17
+ top: 50%;
18
+ transform: translateY(-50%);
19
+ padding: 0.25rem 0.5rem;
20
+ color: var(--bs-secondary);
21
+ opacity: 0.6;
22
+ transition: opacity 0.15s ease-in-out;
23
+ text-decoration: none;
24
+ }
25
+
26
+ .btn.btn-floating-icon:hover {
27
+ opacity: 1;
28
+ color: var(--bs-secondary);
29
+ text-decoration: none;
30
+ }
31
+
32
+ .btn.btn-floating-icon:focus {
33
+ opacity: 1;
34
+ color: var(--bs-secondary);
35
+ text-decoration: none;
36
+ }
37
+
38
+ /* https://react-bootstrap.github.io/docs/getting-started/theming/#new-variants-and-sizes */
39
+ .btn.btn-tanstack-table {
40
+ --bs-btn-color: var(--bs-body-color);
41
+ --bs-btn-bg: var(--bs-input-bg);
42
+ --bs-btn-border-color: var(--bs-border-color);
43
+ --bs-btn-hover-color: var(--bs-body-color);
44
+ --bs-btn-hover-bg: var(--bs-border-color);
45
+ --bs-btn-hover-border-color: var(--bs-border-color);
46
+ --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
47
+ --bs-btn-active-color: var(--bs-body-color);
48
+ --bs-btn-active-bg: var(--bs-border-color);
49
+ --bs-btn-active-border-color: var(--bs-border-color);
50
+ --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
51
+ --bs-btn-disabled-color: var(--bs-body-color);
52
+ --bs-btn-disabled-bg: var(--bs-secondary-bg);
53
+ --bs-btn-disabled-border-color: var(--bs-border-color);
54
+ }
55
+
56
+ :is(.btn.btn-tanstack-table, .pl-ui-tanstack-table-focusable-shadow):not(:focus-visible) {
57
+ box-shadow: var(--bs-box-shadow-sm);
58
+ }
@@ -0,0 +1,168 @@
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, useRef, useState } from 'preact/hooks';
5
+ import type { JSX } from 'preact/jsx-runtime';
6
+
7
+ import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
8
+
9
+ function HiddenMeasurementHeader<TData>({
10
+ table,
11
+ columnsToMeasure,
12
+ filters = {},
13
+ }: {
14
+ table: Table<TData>;
15
+ columnsToMeasure: { id: string }[];
16
+ filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>;
17
+ }) {
18
+ const headerGroups = table.getHeaderGroups();
19
+ const leafHeaderGroup = headerGroups[headerGroups.length - 1];
20
+
21
+ return (
22
+ <div
23
+ style={{
24
+ position: 'fixed',
25
+ visibility: 'hidden',
26
+ pointerEvents: 'none',
27
+ top: '-9999px',
28
+ }}
29
+ >
30
+ <table class="table table-hover mb-0" style={{ display: 'grid', tableLayout: 'fixed' }}>
31
+ <thead style={{ display: 'grid' }}>
32
+ <tr style={{ display: 'flex' }}>
33
+ {columnsToMeasure.map((col) => {
34
+ const header = leafHeaderGroup.headers.find((h) => h.column.id === col.id);
35
+ if (!header) return null;
36
+
37
+ return (
38
+ <TanstackTableHeaderCell
39
+ key={header.id}
40
+ header={header}
41
+ filters={filters}
42
+ table={table}
43
+ isPinned={false}
44
+ measurementMode={true}
45
+ />
46
+ );
47
+ })}
48
+ </tr>
49
+ </thead>
50
+ </table>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Custom hook that automatically measures and sets column widths based on header content.
57
+ * Only measures columns that have `meta: { autoSize: true }` and don't have explicit sizes set.
58
+ * User resizes are preserved.
59
+ *
60
+ * @param table - The TanStack Table instance
61
+ * @param tableRef - Ref to the table container element
62
+ * @param filters - Optional filters map for rendering filter components in measurement
63
+ * @returns A boolean indicating whether the initial measurement has completed
64
+ */
65
+ export function useAutoSizeColumns<TData>(
66
+ table: Table<TData>,
67
+ tableRef: RefObject<HTMLDivElement>,
68
+ filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>,
69
+ ): boolean {
70
+ const [hasMeasured, setHasMeasured] = useState(false);
71
+ const measurementContainerRef = useRef<HTMLDivElement | null>(null);
72
+
73
+ // Perform measurement
74
+ useEffect(() => {
75
+ if (hasMeasured || !tableRef.current) {
76
+ return;
77
+ }
78
+
79
+ const allColumns = table.getAllLeafColumns();
80
+
81
+ const columnsToMeasure = allColumns.filter((col) => col.columnDef.meta?.autoSize);
82
+
83
+ if (columnsToMeasure.length === 0) {
84
+ // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
85
+ setHasMeasured(true);
86
+ return;
87
+ }
88
+
89
+ // Wait for next frame to ensure DOM is ready
90
+ const rafId = requestAnimationFrame(() => {
91
+ if (!tableRef.current) {
92
+ return;
93
+ }
94
+
95
+ // Create or reuse measurement container
96
+ let container = measurementContainerRef.current;
97
+ if (!container) {
98
+ container = document.createElement('div');
99
+ document.body.append(container);
100
+ measurementContainerRef.current = container;
101
+ }
102
+
103
+ // Render headers into hidden container
104
+ render(
105
+ <HiddenMeasurementHeader
106
+ table={table}
107
+ columnsToMeasure={columnsToMeasure}
108
+ filters={filters ?? {}}
109
+ />,
110
+ container,
111
+ );
112
+
113
+ // Force layout calculation
114
+ void container.offsetWidth;
115
+
116
+ // Measure each header and build sizing state
117
+ const newSizing: ColumnSizingState = {};
118
+
119
+ for (const col of columnsToMeasure) {
120
+ const headerElement = container.querySelector(
121
+ `th[data-column-id="${col.id}"]`,
122
+ ) as HTMLElement;
123
+
124
+ if (headerElement) {
125
+ const measuredWidth = headerElement.scrollWidth;
126
+ const resizeHandlePadding = col.getCanResize() ? 4 : 0;
127
+ const minSize = col.columnDef.minSize ?? 0;
128
+ const maxSize = col.columnDef.maxSize ?? Infinity;
129
+
130
+ const finalWidth = Math.max(
131
+ minSize,
132
+ Math.min(maxSize, measuredWidth + resizeHandlePadding),
133
+ );
134
+
135
+ newSizing[col.id] = finalWidth;
136
+ }
137
+ }
138
+
139
+ // Clear container content by unmounting Preact components
140
+ render(null, container);
141
+
142
+ // Apply measurements
143
+ if (Object.keys(newSizing).length > 0) {
144
+ table.setColumnSizing((prev) => ({ ...prev, ...newSizing }));
145
+ }
146
+
147
+ setHasMeasured(true);
148
+ });
149
+
150
+ return () => {
151
+ cancelAnimationFrame(rafId);
152
+ };
153
+ }, [table, tableRef, filters, hasMeasured]);
154
+
155
+ // Clean up measurement container on unmount
156
+ useEffect(() => {
157
+ return () => {
158
+ const container = measurementContainerRef.current;
159
+ if (container) {
160
+ render(null, container);
161
+ container.remove();
162
+ measurementContainerRef.current = null;
163
+ }
164
+ };
165
+ }, []);
166
+
167
+ return hasMeasured;
168
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,11 @@
1
- export { TanstackTable, TanstackTableCard } from './components/TanstackTable.js';
1
+ // Augment @tanstack/react-table types
2
+ import './react-table.js';
3
+
4
+ export {
5
+ TanstackTable,
6
+ TanstackTableCard,
7
+ TanstackTableEmptyState,
8
+ } from './components/TanstackTable.js';
2
9
  export { ColumnManager } from './components/ColumnManager.js';
3
10
  export { TanstackTableDownloadButton } from './components/TanstackTableDownloadButton.js';
4
11
  export { CategoricalColumnFilter } from './components/CategoricalColumnFilter.js';
@@ -7,5 +14,7 @@ export {
7
14
  NumericInputColumnFilter,
8
15
  parseNumericFilter,
9
16
  numericColumnFilterFn,
17
+ type NumericColumnFilterValue,
10
18
  } from './components/NumericInputColumnFilter.js';
11
19
  export { useShiftClickCheckbox } from './components/useShiftClickCheckbox.js';
20
+ export { OverlayTrigger, type OverlayTriggerProps } from './components/OverlayTrigger.js';
@@ -0,0 +1,17 @@
1
+ import type { RowData } from '@tanstack/react-table';
2
+
3
+ declare module '@tanstack/react-table' {
4
+ // https://tanstack.com/table/latest/docs/api/core/column-def#meta
5
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
6
+ interface ColumnMeta<TData extends RowData, TValue> {
7
+ /** If true, the column will wrap text instead of being truncated. */
8
+ wrapText?: boolean;
9
+ /** If set, this will be used as the label for the column in the column manager. */
10
+ label?: string;
11
+ /** If true, the column will be automatically sized based on the header content. */
12
+ autoSize?: boolean;
13
+ }
14
+ }
15
+
16
+ // eslint-disable-next-line unicorn/require-module-specifiers
17
+ export {};
package/tsconfig.json CHANGED
@@ -6,6 +6,5 @@
6
6
  "types": ["node"],
7
7
  "jsx": "react-jsx",
8
8
  "jsxImportSource": "@prairielearn/preact-cjs"
9
- },
10
- "include": ["src/*"]
9
+ }
11
10
  }
@@ -1,4 +0,0 @@
1
- body.no-user-select {
2
- user-select: none;
3
- -webkit-user-select: none;
4
- }
@@ -1,4 +0,0 @@
1
- body.no-user-select {
2
- user-select: none;
3
- -webkit-user-select: none;
4
- }