@prairielearn/ui 2.0.0 → 3.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
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# @prairielearn/ui
|
|
2
2
|
|
|
3
|
+
## 3.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 3914bb4: Upgrade to Node 24
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- f1da6ea: Make `useAutoSizeColumns` compatible with React 18+
|
|
12
|
+
- @prairielearn/browser-utils@2.6.2
|
|
13
|
+
|
|
3
14
|
## 2.0.0
|
|
4
15
|
|
|
5
16
|
### Major Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoSizeColumns.d.ts","sourceRoot":"","sources":["../../src/components/useAutoSizeColumns.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,MAAM,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,SAAS,EAAwC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useAutoSizeColumns.d.ts","sourceRoot":"","sources":["../../src/components/useAutoSizeColumns.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,MAAM,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAC9E,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,SAAS,EAAwC,MAAM,OAAO,CAAC;AAoDvF;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EACnB,QAAQ,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;CAAE,KAAK,GAAG,CAAC,OAAO,CAAC,GACnF,OAAO,CAwGT","sourcesContent":["import type { ColumnSizingState, Header, Table } from '@tanstack/react-table';\nimport { type JSX, type RefObject, useEffect, useMemo, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { type Root, createRoot } from 'react-dom/client';\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 className=\"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 | null>,\n filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>,\n): boolean {\n const measurementContainerRef = useRef<HTMLDivElement | null>(null);\n const measurementRootRef = useRef<Root | null>(null);\n\n // Compute columns that need measuring\n const columnsToMeasure = useMemo(() => {\n const allColumns = table.getAllLeafColumns();\n return allColumns.filter((col) => col.columnDef.meta?.autoSize);\n }, [table]);\n\n // Initialize hasMeasured to true if there's nothing to measure\n const [hasMeasured, setHasMeasured] = useState(() => columnsToMeasure.length === 0);\n\n // Perform measurement\n useEffect(() => {\n if (hasMeasured || !tableRef.current || columnsToMeasure.length === 0) {\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 measurementRootRef.current = createRoot(container);\n }\n\n // Render headers into hidden container. We need to use `flushSync` to ensure\n // that it's rendered synchronously before we measure.\n // eslint-disable-next-line @eslint-react/dom/no-flush-sync\n flushSync(() => {\n measurementRootRef.current?.render(\n <HiddenMeasurementHeader\n table={table}\n columnsToMeasure={columnsToMeasure}\n filters={filters ?? {}}\n />,\n );\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 React components\n measurementRootRef.current?.unmount();\n measurementRootRef.current = null;\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, columnsToMeasure]);\n\n // Clean up measurement container on unmount\n useEffect(() => {\n return () => {\n measurementRootRef.current?.unmount();\n measurementRootRef.current = null;\n const container = measurementContainerRef.current;\n if (container) {\n container.remove();\n measurementContainerRef.current = null;\n }\n };\n }, []);\n\n return hasMeasured;\n}\n"]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
3
4
|
import { createRoot } from 'react-dom/client';
|
|
4
5
|
import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
|
|
5
6
|
function HiddenMeasurementHeader({ table, columnsToMeasure, filters = {}, }) {
|
|
@@ -55,8 +56,12 @@ export function useAutoSizeColumns(table, tableRef, filters) {
|
|
|
55
56
|
measurementContainerRef.current = container;
|
|
56
57
|
measurementRootRef.current = createRoot(container);
|
|
57
58
|
}
|
|
58
|
-
// Render headers into hidden container
|
|
59
|
-
|
|
59
|
+
// Render headers into hidden container. We need to use `flushSync` to ensure
|
|
60
|
+
// that it's rendered synchronously before we measure.
|
|
61
|
+
// eslint-disable-next-line @eslint-react/dom/no-flush-sync
|
|
62
|
+
flushSync(() => {
|
|
63
|
+
measurementRootRef.current?.render(_jsx(HiddenMeasurementHeader, { table: table, columnsToMeasure: columnsToMeasure, filters: filters ?? {} }));
|
|
64
|
+
});
|
|
60
65
|
// Force layout calculation
|
|
61
66
|
void container.offsetWidth;
|
|
62
67
|
// Measure each header and build sizing state
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAutoSizeColumns.js","sourceRoot":"","sources":["../../src/components/useAutoSizeColumns.tsx"],"names":[],"mappings":"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
|
|
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 JSX, type RefObject, useEffect, useMemo, useRef, useState } from 'react';\nimport { flushSync } from 'react-dom';\nimport { type Root, createRoot } from 'react-dom/client';\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 className=\"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 | null>,\n filters?: Record<string, (props: { header: Header<TData, unknown> }) => JSX.Element>,\n): boolean {\n const measurementContainerRef = useRef<HTMLDivElement | null>(null);\n const measurementRootRef = useRef<Root | null>(null);\n\n // Compute columns that need measuring\n const columnsToMeasure = useMemo(() => {\n const allColumns = table.getAllLeafColumns();\n return allColumns.filter((col) => col.columnDef.meta?.autoSize);\n }, [table]);\n\n // Initialize hasMeasured to true if there's nothing to measure\n const [hasMeasured, setHasMeasured] = useState(() => columnsToMeasure.length === 0);\n\n // Perform measurement\n useEffect(() => {\n if (hasMeasured || !tableRef.current || columnsToMeasure.length === 0) {\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 measurementRootRef.current = createRoot(container);\n }\n\n // Render headers into hidden container. We need to use `flushSync` to ensure\n // that it's rendered synchronously before we measure.\n // eslint-disable-next-line @eslint-react/dom/no-flush-sync\n flushSync(() => {\n measurementRootRef.current?.render(\n <HiddenMeasurementHeader\n table={table}\n columnsToMeasure={columnsToMeasure}\n filters={filters ?? {}}\n />,\n );\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 React components\n measurementRootRef.current?.unmount();\n measurementRootRef.current = null;\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, columnsToMeasure]);\n\n // Clean up measurement container on unmount\n useEffect(() => {\n return () => {\n measurementRootRef.current?.unmount();\n measurementRootRef.current = null;\n const container = measurementContainerRef.current;\n if (container) {\n container.remove();\n measurementContainerRef.current = null;\n }\n };\n }, []);\n\n return hasMeasured;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/PrairieLearn/PrairieLearn.git",
|
|
8
8
|
"directory": "packages/ui"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=24.0.0"
|
|
12
|
+
},
|
|
10
13
|
"exports": {
|
|
11
14
|
".": "./dist/index.js",
|
|
12
15
|
"./*.css": "./dist/*.css"
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
"test": "vitest run --coverage"
|
|
18
21
|
},
|
|
19
22
|
"dependencies": {
|
|
20
|
-
"@prairielearn/browser-utils": "^2.6.
|
|
23
|
+
"@prairielearn/browser-utils": "^2.6.2",
|
|
21
24
|
"@tanstack/react-table": "^8.21.3",
|
|
22
25
|
"@tanstack/react-virtual": "^3.13.18",
|
|
23
26
|
"@tanstack/table-core": "^8.21.3",
|
|
@@ -32,7 +35,7 @@
|
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
35
|
-
"@types/node": "^
|
|
38
|
+
"@types/node": "^24.10.9",
|
|
36
39
|
"@typescript/native-preview": "^7.0.0-dev.20260106.1",
|
|
37
40
|
"typescript": "^5.9.3",
|
|
38
41
|
"typescript-cp": "^0.1.9",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ColumnSizingState, Header, Table } from '@tanstack/react-table';
|
|
2
2
|
import { type JSX, type RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { flushSync } from 'react-dom';
|
|
3
4
|
import { type Root, createRoot } from 'react-dom/client';
|
|
4
5
|
|
|
5
6
|
import { TanstackTableHeaderCell } from './TanstackTableHeaderCell.js';
|
|
@@ -98,14 +99,18 @@ export function useAutoSizeColumns<TData>(
|
|
|
98
99
|
measurementRootRef.current = createRoot(container);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
// Render headers into hidden container
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
// Render headers into hidden container. We need to use `flushSync` to ensure
|
|
103
|
+
// that it's rendered synchronously before we measure.
|
|
104
|
+
// eslint-disable-next-line @eslint-react/dom/no-flush-sync
|
|
105
|
+
flushSync(() => {
|
|
106
|
+
measurementRootRef.current?.render(
|
|
107
|
+
<HiddenMeasurementHeader
|
|
108
|
+
table={table}
|
|
109
|
+
columnsToMeasure={columnsToMeasure}
|
|
110
|
+
filters={filters ?? {}}
|
|
111
|
+
/>,
|
|
112
|
+
);
|
|
113
|
+
});
|
|
109
114
|
|
|
110
115
|
// Force layout calculation
|
|
111
116
|
void container.offsetWidth;
|