Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 1 addition & 2 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,6 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(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);

Expand All @@ -530,7 +529,7 @@ export function DataGrid<R, SR = unknown, K extends Key = Key>(props: DataGridPr
setScrollToPosition({ idx: scrollToIdx, rowIdx: scrollToRowIdx });
}
},
setActivePosition: setPosition
setActivePosition: setPositionLatest
}));

/**
Expand Down
39 changes: 23 additions & 16 deletions src/EditCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ function canUsePostTask({ scheduler }: typeof globalThis) {
return typeof scheduler === 'object' && typeof scheduler.postTask === 'function';
}

// Declared outside of the component, so the `useLayoutEffect` cleanup
// doesn't depend on a function that is recreated on every render
function cancelTask(
captureEventRef: React.RefObject<MouseEvent | undefined>,
cancelScheduledTaskRef: React.RefObject<(() => void) | undefined>
) {
captureEventRef.current = undefined;
cancelScheduledTaskRef.current?.();
cancelScheduledTaskRef.current = undefined;
}

const cellEditing = css`
@layer rdg.EditCell {
padding: 0;
Expand All @@ -50,6 +61,14 @@ export default function EditCell<R, SR>({
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.
Expand Down Expand Up @@ -104,16 +123,12 @@ export default function EditCell<R, SR>({
win.removeEventListener('mousedown', onWindowCaptureMouseDown, { capture: true });
win.removeEventListener('mousedown', onWindowMouseDown);
}
cancelTask();
cancelTask(captureEventRef, cancelScheduledTaskRef);
};
}, [commitOnOutsideClick]);

// TODO: report React Compiler bug
// oxlint-disable-next-line react/invariant
function cancelTask() {
captureEventRef.current = undefined;
cancelScheduledTaskRef.current?.();
cancelScheduledTaskRef.current = undefined;
function handleMouseDownCapture() {
cancelTask(captureEventRef, cancelScheduledTaskRef);
}

function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
Expand Down Expand Up @@ -145,14 +160,6 @@ export default function EditCell<R, SR>({
}
}

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);
}
Expand All @@ -175,7 +182,7 @@ export default function EditCell<R, SR>({
className={className}
style={getCellStyle(column, colSpan)}
onKeyDown={handleKeyDown}
onMouseDownCapture={cancelTask}
onMouseDownCapture={handleMouseDownCapture}
>
{column.renderEditCell != null && (
<>
Expand Down
8 changes: 3 additions & 5 deletions src/GroupCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ interface GroupCellProps<R, SR> {
column: CalculatedColumn<R, SR>;
row: GroupRow<R>;
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<R, SR>({
Expand All @@ -25,8 +26,8 @@ function GroupCell<R, SR>({
isCellActive,
column,
row,
groupColumnIndex,
isGroupByColumn,
isLevelMatching,
toggleGroup: toggleGroupWrapper
}: GroupCellProps<R, SR>) {
const { tabIndex, childTabIndex, onFocus } = useRovingTabIndex(isCellActive);
Expand All @@ -35,9 +36,6 @@ function GroupCell<R, SR>({
toggleGroupWrapper(id);
}

// Only make the cell clickable if the group level matches
const isLevelMatching = isGroupByColumn && groupColumnIndex === column.idx;

return (
<div
key={column.key}
Expand Down
43 changes: 17 additions & 26 deletions src/GroupedRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { css } from 'ecij';
import { RowSelectionContext, type RowSelectionContextValue } from './hooks';
import { classnames } from './utils';
import type { BaseRenderRowProps, GroupRow, Omit } from './types';
import { SELECT_COLUMN_KEY } from './Columns';
import GroupCell from './GroupCell';
import { cell, cellFrozenBase } from './style/cell';
import { rowClassname, rowActiveClassname } from './style/row';
Expand Down Expand Up @@ -43,8 +42,8 @@ function GroupedRow<R, SR>({
...props
}: GroupedRowProps<R, SR>) {
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 });
Expand Down Expand Up @@ -75,29 +74,21 @@ function GroupedRow<R, SR>({
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 (
<GroupCell
key={column.key}
id={row.id}
groupKey={row.groupKey}
childRows={row.childRows}
isExpanded={row.isExpanded}
isCellActive={isCellActive}
column={column}
row={row}
groupColumnIndex={idx}
toggleGroup={toggleGroup}
isGroupByColumn={groupBy.includes(column.key)}
/>
);
})}
{mapViewportColumns(activeCellIdx, undefined, (column, isCellActive) => (
<GroupCell
key={column.key}
id={row.id}
groupKey={row.groupKey}
childRows={row.childRows}
isExpanded={row.isExpanded}
isCellActive={isCellActive}
column={column}
row={row}
toggleGroup={toggleGroup}
isGroupByColumn={groupBy.includes(column.key)}
isLevelMatching={column.key === levelColumnKey}
/>
))}
</div>
</RowSelectionContext>
);
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useCalculatedColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export function useCalculatedColumns<R, SR>({
columns.push(column);

if (isStartFrozen(frozen)) {
lastStartFrozenColumnIndex++;
lastStartFrozenColumnIndex += 1;
}

if (level > headerRowsCount) {
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/useColumnWidths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export function useColumnWidths<R, SR>(

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 {
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useViewportRows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function useViewportRows<R>({
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) `;
Expand Down
18 changes: 12 additions & 6 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ async function getTscPath(): Promise<string> {
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 = {
Expand Down Expand Up @@ -178,7 +187,7 @@ export default defineConfig({
autoCodeSplitting: true
})
),
react({ compiler: true })
reactPlugin
]),

server: {
Expand Down Expand Up @@ -227,10 +236,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
]
},

Expand Down Expand Up @@ -658,7 +664,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',
Expand Down
Loading