diff --git a/AGENTS.md b/AGENTS.md index 3f2a0fa850..60297f11da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ website/ # demo site (Vite + TanStack Router) - **Dual class names** — components apply both a semantic class (`rdg-cell`) and a generated hash. Preserve both. - **Light/dark mode** — handled via CSS `light-dark()` + `color-scheme`, not JS. - **Accessibility first** — ARIA attributes (e.g., `aria-colindex`, `aria-rowindex`, `aria-selected`, roles) are required. Tests query by role. -- **Build** — `vp pack` bundles the library to `lib/`, then lints the packed package with publint; `ecij` plugin prefixes classes with `rdg-{version}-` (dots→dashes) to avoid cross-version conflicts. +- **Build** — `vp pack` bundles the library to `lib/`, then lints the packed package with publint; `ecij` plugin prefixes classes with `rdg-{version}-` (dots→dashes) to avoid cross-version conflicts. React Compiler compiles every component and hook, and `vp pack` fails in CI and agent sessions when it can't compile one. - **Workflows** — pin every action to a full commit SHA with its version in a comment (Dependabot updates both), pass expressions to `run` steps through `env` instead of inlining `${{ }}`, grant each job only the `permissions` it uses, and check out with `persist-credentials: false`. Jobs that install dependencies only get read permissions: publishing, pushing, and editing PRs happen in separate jobs that receive build output as artifacts and never install the project's dependencies. - **Releases** — the header of `.github/workflows/publish.yml` explains how to publish. Its read-only `build` job hands `lib/` to the `publish` job, so the project's dependencies never run with the `id-token: write` permission that npm's trusted publishing relies on, and a separate `release` job creates the GitHub release. diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 76c1cf90f8..ca3f54e1c1 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -503,7 +503,6 @@ export function DataGrid(props: DataGridPr const selectHeaderRowLatest = useLatestFunc(selectHeaderRow); const selectRowLatest = useLatestFunc(selectRow); const updateRowLatest = useLatestFunc(updateRow); - // oxlint-disable-next-line react/immutability const setPositionLatest = useLatestFunc(setPosition); const selectHeaderCellLatest = useLatestFunc(selectHeaderCell); @@ -530,7 +529,7 @@ export function DataGrid(props: DataGridPr setScrollToPosition({ idx: scrollToIdx, rowIdx: scrollToRowIdx }); } }, - setActivePosition: setPosition + setActivePosition: setPositionLatest })); /** diff --git a/src/EditCell.tsx b/src/EditCell.tsx index 548b74de56..1da46e4f63 100644 --- a/src/EditCell.tsx +++ b/src/EditCell.tsx @@ -50,6 +50,14 @@ export default function EditCell({ const cancelScheduledTaskRef = useRef<() => void>(undefined); const commitOnOutsideClick = column.editorOptions?.commitOnOutsideClick ?? true; + function onClose(commitChanges = false, shouldFocus = true) { + if (commitChanges) { + onRowChange(row, true, shouldFocus); + } else { + closeEditor(shouldFocus); + } + } + // We need to prevent the `useLayoutEffect` from cleaning up between re-renders, // as `onWindowCaptureMouseDown` might otherwise miss valid mousedown events. // To that end, we instead access the latest props via useEffectEvent. @@ -57,6 +65,14 @@ export default function EditCell({ onClose(true, false); }); + function cancelTask() { + captureEventRef.current = undefined; + cancelScheduledTaskRef.current?.(); + cancelScheduledTaskRef.current = undefined; + } + + const cancelTaskEffect = useEffectEvent(cancelTask); + useLayoutEffect(() => { if (!commitOnOutsideClick) return; @@ -104,18 +120,10 @@ export default function EditCell({ win.removeEventListener('mousedown', onWindowCaptureMouseDown, { capture: true }); win.removeEventListener('mousedown', onWindowMouseDown); } - cancelTask(); + cancelTaskEffect(); }; }, [commitOnOutsideClick]); - // TODO: report React Compiler bug - // oxlint-disable-next-line react/invariant - function cancelTask() { - captureEventRef.current = undefined; - cancelScheduledTaskRef.current?.(); - cancelScheduledTaskRef.current = undefined; - } - function handleKeyDown(event: React.KeyboardEvent) { if (onKeyDown) { const cellEvent = createCellEvent(event); @@ -145,14 +153,6 @@ export default function EditCell({ } } - function onClose(commitChanges = false, shouldFocus = true) { - if (commitChanges) { - onRowChange(row, true, shouldFocus); - } else { - closeEditor(shouldFocus); - } - } - function onEditorRowChange(row: R, commitChangesAndFocus = false) { onRowChange(row, commitChangesAndFocus, commitChangesAndFocus); } diff --git a/src/GroupCell.tsx b/src/GroupCell.tsx index 8354f167c0..206f05377d 100644 --- a/src/GroupCell.tsx +++ b/src/GroupCell.tsx @@ -13,8 +13,9 @@ interface GroupCellProps { column: CalculatedColumn; row: GroupRow; isCellActive: boolean; - groupColumnIndex: number; isGroupByColumn: boolean; + // Only the group-by column of the row's level renders the group cell and is clickable + isLevelMatching: boolean; } function GroupCell({ @@ -25,8 +26,8 @@ function GroupCell({ isCellActive, column, row, - groupColumnIndex, isGroupByColumn, + isLevelMatching, toggleGroup: toggleGroupWrapper }: GroupCellProps) { const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellActive); @@ -35,9 +36,6 @@ function GroupCell({ toggleGroupWrapper(id); } - // Only make the cell clickable if the group level matches - const isLevelMatching = isGroupByColumn && groupColumnIndex === column.idx; - return (
({ ...props }: GroupedRowProps) { const isPositionOnRow = activeCellIdx === -1; - - let idx = row.level; + // the group-by columns follow the `groupBy` order, so each level has its own column + const levelColumnKey = groupBy[row.level]; function handleSelectGroup() { setActivePosition({ idx: -1, rowIdx }, { shouldFocus: true }); @@ -75,29 +74,21 @@ function GroupedRow({ style={{ gridRowStart }} {...props} > - {mapViewportColumns(activeCellIdx, undefined, (column, isCellActive, colSpan, index) => { - // Select is always the first column - if (index === 0 && column.key === SELECT_COLUMN_KEY) { - // oxlint-disable-next-line react/immutability - idx += 1; - } - - return ( - - ); - })} + {mapViewportColumns(activeCellIdx, undefined, (column, isCellActive) => ( + + ))}
); diff --git a/src/hooks/useCalculatedColumns.ts b/src/hooks/useCalculatedColumns.ts index 433371c36e..fbb7f2808b 100644 --- a/src/hooks/useCalculatedColumns.ts +++ b/src/hooks/useCalculatedColumns.ts @@ -136,7 +136,7 @@ export function useCalculatedColumns({ columns.push(column); if (isStartFrozen(frozen)) { - lastStartFrozenColumnIndex++; + lastStartFrozenColumnIndex += 1; } if (level > headerRowsCount) { diff --git a/src/hooks/useColumnWidths.ts b/src/hooks/useColumnWidths.ts index 5a2388504d..fdc8560300 100644 --- a/src/hooks/useColumnWidths.ts +++ b/src/hooks/useColumnWidths.ts @@ -65,7 +65,9 @@ export function useColumnWidths( for (const key of columnsToMeasure) { const measuredWidth = measureColumnWidth(gridRef, key); - hasChanges ||= measuredWidth !== columnWidths.get(key)?.width; + if (measuredWidth !== columnWidths.get(key)?.width) { + hasChanges = true; + } if (measuredWidth === undefined) { newColumnWidths.delete(key); } else { diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index ca149b4425..c277571998 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -50,7 +50,7 @@ export function useViewportRows({ repeatCount = 1; } else if (currentHeight === currentRowHeight) { // If the current row height is the same as the previous one, increment the repeat count - repeatCount++; + repeatCount += 1; } else { if (repeatCount > 1) { gridTemplateRows += `repeat(${repeatCount}, ${currentHeight}px) `; diff --git a/vite.config.ts b/vite.config.ts index 415f693cc5..97757b6b39 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -53,6 +53,15 @@ async function getTscPath(): Promise { return getExePath(); } +// shared by Vite and `vp pack` +const reactPlugin = react({ + // warn about the code React Compiler can't compile, which fails `vp pack` with `failOnWarn` + compiler: { reportDiagnostics: true }, + // `vp pack` passes the declaration files it generates through the plugin, + // and the compiler's TypeScript transform would strip all of their declarations + exclude: [/\/node_modules\//, /\.d\.ts$/] +}); + const actionTimeout = 2000; const viewport = { width: 1920, height: 1080 } as const; const playwrightOptions: PlaywrightProviderOptions = { @@ -190,7 +199,7 @@ export default defineConfig({ autoCodeSplitting: true }) ), - react({ compiler: true }) + reactPlugin ]), server: { @@ -239,10 +248,7 @@ export default defineConfig({ // between multiple versions of RDG on the same page classPrefix: `rdg-${pkg.version.replaceAll('.', '-')}-` }), - react({ - compiler: true, - exclude: [/node_modules/, /\.d\.ts$/] - }) + reactPlugin ] }, @@ -677,7 +683,7 @@ copy( 'react/static-components': 'warn', 'react/style-prop-object': 'warn', 'react/syntax': 'warn', - 'react/todo': 'off', + 'react/todo': 'warn', 'react/unsupported-syntax': 'warn', 'react/use-memo': 'warn', 'react/void-dom-elements-no-children': 'warn',