diff --git a/packages/core/API.md b/packages/core/API.md
index 18295976f..c2439c17f 100644
--- a/packages/core/API.md
+++ b/packages/core/API.md
@@ -13,14 +13,9 @@ or you can create a portal element yourself using the `createPortal` function fr
```jsx
const portalRef = useRef(null);
<>
- {
- createPortal(
-
,
- document.body
- )
- }
-
->
+ {createPortal(, document.body)}
+
+>;
```
Once you've got that done, the easiest way to use the Data Grid is to give it a fixed size:
@@ -87,10 +82,10 @@ All data grids must set these props. These props are the bare minimum required t
Most data grids will want to set the majority of these props one way or another.
| Name | Description |
-|---------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [fixedShadowX](#fixedshadow) | Enable/disable a shadow behind fixed columns on the X axis. |
| [fixedShadowY](#fixedshadow) | Enable/disable a shadow behind the header(s) on the Y axis. |
-| [freezeColumns](#freezecolumns) | The number of columns which should remain in place when scrolling horizontally. The row marker column, if enabled is always frozen and is not included in this count. |
+| [freezeColumns](#freezecolumns) | The number of columns which should remain in place when scrolling horizontally, or a tuple `[left, right]` to freeze columns on both sides. The row marker column, if enabled is always frozen and is not included in this count. |
| [getCellsForSelection](#getcellsforselection) | Used to fetch large amounts of cells at once. Used for copy/paste, if unset copy will not work. |
| [markdownDivCreateNode](#markdowndivcreatenode) | If specified, it will be used to render Markdown, instead of the default Markdown renderer used by the Grid. You'll want to use this if you need to process your Markdown for security purposes, or if you want to use a renderer with different Markdown features. |
| [onVisibleRegionChanged](#onvisibleregionchanged) | Emits whenever the visible rows/columns changes. |
@@ -192,7 +187,7 @@ Most data grids will want to set the majority of these props one way or another.
| [onRowMoved](#onrowmoved) | Emitted when a row has been dragged to a new location. |
| [preventDiagonalScrolling](#preventdiagonalscrolling) | Prevents diagonal scrolling |
| [rowSelectionMode](#rowselectionmode) | Determines if row selection requires a modifier key to enable multi-selection or not. |
-| [columnSelectionMode](#columnselectionmode) | Determines if column selection requires a modifier key to enable multi-selection or not. |
+| [columnSelectionMode](#columnselectionmode) | Determines if column selection requires a modifier key to enable multi-selection or not. |
| [scrollToEnd](#scrolltoend) | When set to true, the grid will scroll to the end. The ref has a better method to do this and this prop should not be used but it will remain supported for the foreseeable future. |
| [showMinimap](#showminimap) | Shows the interactive minimap of the grid. |
| [validateCell](#validatecell) | When returns false indicates to the user the value will not be accepted. When returns a new GridCell the value is coerced to match. |
@@ -550,11 +545,18 @@ getCellContent: (cell: Item) => GridCell;
## freezeColumns
```ts
-freezeColumns?: number;
+freezeColumns?: number | readonly [left: number, right: number];
```
Set to a positive number to freeze columns on the left side of the grid during horizontal scrolling.
+Alternatively, pass a tuple `[left, right]` where:
+
+- `left` is the number of columns to freeze on the left side
+- `right` is the number of columns to freeze on the right side
+
+Note: The row marker column, if enabled, is always frozen and is not included in the left freeze count.
+
---
## getCellsForSelection
diff --git a/packages/core/src/common/render-state-provider.ts b/packages/core/src/common/render-state-provider.ts
index 55e54b4f1..3366e34a6 100644
--- a/packages/core/src/common/render-state-provider.ts
+++ b/packages/core/src/common/render-state-provider.ts
@@ -35,33 +35,42 @@ export abstract class WindowingTrackerBase {
height: 0,
};
- public freezeCols: number = 0;
+ public columnsLength: number = 0;
+ public freezeCols: number | readonly [number, number] = 0;
public freezeRows: number[] = [];
protected isInWindow = (packed: number) => {
+ const freezeColumnsLeft = typeof this.freezeCols === "number" ? this.freezeCols : this.freezeCols[0];
+ const freezeColumnsRight = typeof this.freezeCols === "number" ? 0 : this.freezeCols[1];
const col = unpackCol(packed);
const row = unpackRow(packed);
const w = this.visibleWindow;
- const colInWindow = (col >= w.x && col <= w.x + w.width) || col < this.freezeCols;
+ const colInWindow =
+ (col >= w.x && col <= w.x + w.width) ||
+ col < freezeColumnsLeft ||
+ col > this.columnsLength - freezeColumnsRight - 1;
+
const rowInWindow = (row >= w.y && row <= w.y + w.height) || this.freezeRows.includes(row);
return colInWindow && rowInWindow;
};
protected abstract clearOutOfWindow: () => void;
- public setWindow(newWindow: Rectangle, freezeCols: number, freezeRows: number[]): void {
+ public setWindow(newWindow: Rectangle, freezeCols: number, freezeRows: number[], columnsLength: number): void {
if (
this.visibleWindow.x === newWindow.x &&
this.visibleWindow.y === newWindow.y &&
this.visibleWindow.width === newWindow.width &&
this.visibleWindow.height === newWindow.height &&
this.freezeCols === freezeCols &&
+ this.columnsLength === columnsLength &&
deepEqual(this.freezeRows, freezeRows)
)
return;
this.visibleWindow = newWindow;
this.freezeCols = freezeCols;
this.freezeRows = freezeRows;
+ this.columnsLength = columnsLength;
this.clearOutOfWindow();
}
}
diff --git a/packages/core/src/common/utils.tsx b/packages/core/src/common/utils.tsx
index aad1b77e2..5f4c026de 100644
--- a/packages/core/src/common/utils.tsx
+++ b/packages/core/src/common/utils.tsx
@@ -282,3 +282,10 @@ export function useDeepMemo(value: T): T {
return ref.current;
}
+
+export function normalizeFreezeColumns(freezeColumns: number | readonly [number, number]): readonly [number, number] {
+ const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0];
+ const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1];
+
+ return [freezeLeftColumns, freezeRightColumns];
+}
diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx
index b86721f9e..9189076ca 100644
--- a/packages/core/src/data-editor/data-editor.tsx
+++ b/packages/core/src/data-editor/data-editor.tsx
@@ -44,7 +44,7 @@ import {
mergeAndRealizeTheme,
} from "../common/styles.js";
import type { DataGridRef } from "../internal/data-grid/data-grid.js";
-import { getScrollBarWidth, useEventListener, whenDefined } from "../common/utils.js";
+import { getScrollBarWidth, useEventListener, normalizeFreezeColumns, whenDefined } from "../common/utils.js";
import {
isGroupEqual,
itemsAreEqual,
@@ -923,6 +923,8 @@ const DataEditorImpl: React.ForwardRefRenderFunction {
if (typeof window === "undefined") return { fontSize: "16px" };
return window.getComputedStyle(document.documentElement);
@@ -1573,9 +1575,13 @@ const DataEditorImpl: React.ForwardRefRenderFunction= columns.length - freezeRightColumns; i--) {
+ frozenRightWidth += columns[i].width;
}
let trailingRowHeight = 0;
const freezeTrailingRowsEffective = freezeTrailingRows + (lastRowSticky ? 1 : 0);
@@ -1588,8 +1594,9 @@ const DataEditorImpl: React.ForwardRefRenderFunction= mangledCols.length - freezeRightColumns))
+ ) {
scrollX = 0;
} else if (
dir === "horizontal" ||
@@ -1664,7 +1675,9 @@ const DataEditorImpl: React.ForwardRefRenderFunction 0) {
freezeRegions.push({
x: region.x - rowMarkerOffset,
@@ -2616,11 +2640,20 @@ const DataEditorImpl: React.ForwardRefRenderFunction 0) {
+ if (freezeLeftColumns > 0) {
freezeRegions.push({
x: 0,
y: rows - freezeTrailingRows,
- width: freezeColumns,
+ width: freezeLeftColumns,
+ height: freezeTrailingRows,
+ });
+ }
+
+ if (freezeRightColumns > 0) {
+ freezeRegions.push({
+ x: columns.length - freezeRightColumns,
+ y: rows - freezeTrailingRows,
+ width: freezeRightColumns,
height: freezeTrailingRows,
});
}
@@ -2635,7 +2668,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction {
+ return [
+ Math.min(mangledCols.length, freezeLeftColumns + (hasRowMarkers ? 1 : 0)),
+ Math.min(mangledCols.length, freezeRightColumns)
+ ] as const;
+ }, [freezeLeftColumns, freezeRightColumns, hasRowMarkers, mangledCols.length])
React.useImperativeHandle(
forwardedRef,
diff --git a/packages/core/src/docs/examples/freeze-columns.stories.tsx b/packages/core/src/docs/examples/freeze-columns.stories.tsx
index fd24a9e5f..b20ecdd4e 100644
--- a/packages/core/src/docs/examples/freeze-columns.stories.tsx
+++ b/packages/core/src/docs/examples/freeze-columns.stories.tsx
@@ -30,14 +30,14 @@ export default {
],
};
-export const FreezeColumns: React.VFC = (p: { freezeColumns: number }) => {
+export const FreezeColumns: React.VFC = (p: { freezeLeftColumns: number, freezeRightColumns: number }) => {
const { cols, getCellContent } = useMockDataGenerator(100);
return (
= (p: { freezeColumns: number }) => {
);
};
(FreezeColumns as any).argTypes = {
- freezeColumns: {
+ freezeLeftColumns: {
+ control: {
+ type: "range",
+ min: 0,
+ max: 10,
+ },
+ },
+ freezeRightColumns: {
control: {
type: "range",
min: 0,
@@ -55,5 +62,6 @@ export const FreezeColumns: React.VFC = (p: { freezeColumns: number }) => {
},
};
(FreezeColumns as any).args = {
- freezeColumns: 1,
+ freezeLeftColumns: 1,
+ freezeRightColumns: 1,
};
diff --git a/packages/core/src/internal/data-grid/data-grid.tsx b/packages/core/src/internal/data-grid/data-grid.tsx
index 338a52e10..5f5325e02 100644
--- a/packages/core/src/internal/data-grid/data-grid.tsx
+++ b/packages/core/src/internal/data-grid/data-grid.tsx
@@ -28,7 +28,13 @@ import {
} from "./data-grid-types.js";
import { CellSet } from "./cell-set.js";
import { SpriteManager, type SpriteMap } from "./data-grid-sprites.js";
-import { direction, getScrollBarWidth, useDebouncedMemo, useEventListener } from "../../common/utils.js";
+import {
+ direction,
+ getScrollBarWidth,
+ useDebouncedMemo,
+ useEventListener,
+ normalizeFreezeColumns,
+} from "../../common/utils.js";
import clamp from "lodash/clamp.js";
import makeRange from "lodash/range.js";
import { drawGrid } from "./render/data-grid-render.js";
@@ -72,7 +78,7 @@ export interface DataGridProps {
readonly accessibilityHeight: number;
- readonly freezeColumns: number;
+ readonly freezeColumns: number | readonly [left: number, right: number];
readonly freezeTrailingRows: number;
readonly hasAppendRow: boolean;
readonly firstColAccessible: boolean;
@@ -403,7 +409,8 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
} = p;
const translateX = p.translateX ?? 0;
const translateY = p.translateY ?? 0;
- const cellXOffset = Math.max(freezeColumns, Math.min(columns.length - 1, cellXOffsetReal));
+ const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns);
+ const cellXOffset = Math.max(freezeLeftColumns, Math.min(columns.length - 1, cellXOffsetReal));
const ref = React.useRef(null);
const windowEventTargetRef = React.useRef(experimental?.eventTarget ?? window);
@@ -451,7 +458,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
const mappedColumns = useMappedColumns(columns, freezeColumns);
const stickyX = React.useMemo(
- () => (fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : 0),
+ () => (fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : [0, 0]),
[mappedColumns, dragAndDropState, fixedShadowX]
);
@@ -526,7 +533,14 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
const y = (posY - rect.top) / scale;
const edgeDetectionBuffer = 5;
- const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, undefined, translateX);
+ const effectiveCols = getEffectiveColumns(
+ mappedColumns,
+ cellXOffset,
+ width,
+ freezeColumns,
+ undefined,
+ translateX
+ );
let button = 0;
let buttons = 0;
@@ -545,7 +559,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
}
// -1 === off right edge
- const col = getColumnIndexForX(x, effectiveCols, translateX);
+ const col = getColumnIndexForX(x, effectiveCols, freezeColumns, width, translateX);
// -1: header or above
// undefined: offbottom
@@ -616,7 +630,11 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
let isEdge = bounds !== undefined && bounds.x + bounds.width - posX <= edgeDetectionBuffer;
const previousCol = col - 1;
- if (posX - bounds.x <= edgeDetectionBuffer && previousCol >= 0) {
+ if (
+ posX - bounds.x <= edgeDetectionBuffer &&
+ previousCol >= 0 &&
+ col < mappedColumns.length - freezeRightColumns
+ ) {
isEdge = true;
bounds = getBoundsForItem(canvas, previousCol, row);
assert(bounds !== undefined);
@@ -723,6 +741,8 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
fillHandle,
selection,
totalHeaderHeight,
+ freezeColumns,
+ freezeRightColumns,
]
);
@@ -1737,7 +1757,14 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
const accessibilityTree = useDebouncedMemo(
() => {
if (width < 50 || experimental?.disableAccessibilityTree === true) return null;
- let effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
+ let effectiveCols = getEffectiveColumns(
+ mappedColumns,
+ cellXOffset,
+ width,
+ freezeColumns,
+ dragAndDropState,
+ translateX
+ );
const colOffset = firstColAccessible ? 0 : -1;
if (!firstColAccessible && effectiveCols[0]?.sourceIndex === 0) {
effectiveCols = effectiveCols.slice(1);
@@ -1871,33 +1898,74 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
onKeyDown,
getBoundsForItem,
onCellFocused,
+ freezeColumns,
],
200
);
- const opacityX =
- freezeColumns === 0 || !fixedShadowX ? 0 : cellXOffset > freezeColumns ? 1 : clamp(-translateX / 100, 0, 1);
+ const opacityXLeft =
+ freezeLeftColumns === 0 || !fixedShadowX
+ ? 0
+ : cellXOffset > freezeLeftColumns
+ ? 1
+ : clamp(-translateX / 100, 0, 1);
+
+ let translateXRight = 0;
+
+ if (eventTargetRef?.current) {
+ translateXRight = eventTargetRef?.current?.scrollLeft + width - eventTargetRef?.current?.scrollWidth;
+ }
+
+ const opacityXRight =
+ freezeRightColumns === 0 || !fixedShadowX
+ ? 0
+ : cellXOffset +
+ getEffectiveColumns(
+ mappedColumns,
+ cellXOffset,
+ width,
+ freezeColumns,
+ dragAndDropState,
+ translateX
+ ).filter(column => !column.sticky).length <
+ columns.length - freezeRightColumns
+ ? 1
+ : clamp(-translateXRight / 100, 0, 1);
const absoluteOffsetY = -cellYOffset * 32 + translateY;
const opacityY = !fixedShadowY ? 0 : clamp(-absoluteOffsetY / 100, 0, 1);
const stickyShadow = React.useMemo(() => {
- if (!opacityX && !opacityY) {
+ if (!opacityXLeft && !opacityY && !opacityXRight) {
return null;
}
- const styleX: React.CSSProperties = {
+ const transition = "opacity 0.2s";
+
+ const styleXLeft: React.CSSProperties = {
position: "absolute",
top: 0,
- left: stickyX,
- width: width - stickyX,
+ left: stickyX[0],
+ width: width - stickyX[0],
height: height,
- opacity: opacityX,
+ opacity: opacityXLeft,
pointerEvents: "none",
- transition: !smoothScrollX ? "opacity 0.2s" : undefined,
+ transition: !smoothScrollX ? transition : undefined,
boxShadow: "inset 13px 0 10px -13px rgba(0, 0, 0, 0.2)",
};
+ const styleXRight: React.CSSProperties = {
+ position: "absolute",
+ top: 0,
+ right: stickyX[1],
+ width: width - stickyX[1],
+ height: height,
+ opacity: opacityXRight,
+ pointerEvents: "none",
+ transition: !smoothScrollX ? transition : undefined,
+ boxShadow: "inset -13px 0 10px -13px rgba(0, 0, 0, 0.2)",
+ };
+
const styleY: React.CSSProperties = {
position: "absolute",
top: totalHeaderHeight,
@@ -1906,17 +1974,28 @@ const DataGrid: React.ForwardRefRenderFunction = (p,
height: height,
opacity: opacityY,
pointerEvents: "none",
- transition: !smoothScrollY ? "opacity 0.2s" : undefined,
+ transition: !smoothScrollY ? transition : undefined,
boxShadow: "inset 0 13px 10px -13px rgba(0, 0, 0, 0.2)",
};
return (
<>
- {opacityX > 0 && }
+ {opacityXLeft > 0 && }
+ {opacityXRight > 0 && }
{opacityY > 0 && }
>
);
- }, [opacityX, opacityY, stickyX, width, smoothScrollX, totalHeaderHeight, height, smoothScrollY]);
+ }, [
+ opacityXLeft,
+ opacityY,
+ stickyX,
+ width,
+ smoothScrollX,
+ totalHeaderHeight,
+ height,
+ smoothScrollY,
+ opacityXRight,
+ ]);
const overlayStyle = React.useMemo(
() => ({
diff --git a/packages/core/src/internal/data-grid/image-window-loader-interface.ts b/packages/core/src/internal/data-grid/image-window-loader-interface.ts
index e32ef93d1..7cadb0fca 100644
--- a/packages/core/src/internal/data-grid/image-window-loader-interface.ts
+++ b/packages/core/src/internal/data-grid/image-window-loader-interface.ts
@@ -3,7 +3,12 @@ import type { Rectangle } from "./data-grid-types.js";
/** @category Types */
export interface ImageWindowLoader {
- setWindow(newWindow: Rectangle, freezeCols: number, freezeRows: number[]): void;
+ setWindow(
+ newWindow: Rectangle,
+ freezeCols: number | readonly [left: number, right: number],
+ freezeRows: number[],
+ columnsLength: number
+ ): void;
loadOrGetImage(url: string, col: number, row: number): HTMLImageElement | ImageBitmap | undefined;
setCallback(imageLoaded: (locations: CellSet) => void): void;
}
diff --git a/packages/core/src/internal/data-grid/render/data-grid-lib.ts b/packages/core/src/internal/data-grid/render/data-grid-lib.ts
index 96eb1d90f..95497f955 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-lib.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-lib.ts
@@ -8,7 +8,7 @@ import {
type Rectangle,
type BaseGridCell,
} from "../data-grid-types.js";
-import { direction } from "../../../common/utils.js";
+import { direction, normalizeFreezeColumns } from "../../../common/utils.js";
import React from "react";
import type { BaseDrawArgs, PrepResult } from "../../../cells/cell-types.js";
import { split as splitText, clearCache } from "canvas-hypertxt";
@@ -17,12 +17,18 @@ import type { FullyDefined } from "../../../common/support.js";
export interface MappedGridColumn extends FullyDefined {
sourceIndex: number;
sticky: boolean;
+ stickyPosition: "left" | "right" | undefined;
}
export function useMappedColumns(
columns: readonly InnerGridColumn[],
- freezeColumns: number
+ freezeColumns: number | readonly [left: number, right: number]
): readonly MappedGridColumn[] {
+ // Extract freeze column counts from the union type parameter. freezeColumnsLeft and freezeColumnsRight
+ // determine which columns should remain sticky at the left and right sides respectively during horizontal scrolling.
+ const freezeColumnsLeft = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0];
+ const freezeColumnsRight = typeof freezeColumns === "number" ? 0 : freezeColumns[1];
+
return React.useMemo(
() =>
columns.map(
@@ -35,7 +41,9 @@ export function useMappedColumns(
menuIcon: c.menuIcon,
overlayIcon: c.overlayIcon,
sourceIndex: i,
- sticky: i < freezeColumns,
+ sticky: i < freezeColumnsLeft || i >= columns.length - freezeColumnsRight,
+ stickyPosition:
+ i < freezeColumnsLeft ? "left" : i >= columns.length - freezeColumnsRight ? "right" : undefined,
indicatorIcon: c.indicatorIcon,
style: c.style,
themeOverride: c.themeOverride,
@@ -50,7 +58,7 @@ export function useMappedColumns(
headerRowMarkerDisabled: c.headerRowMarkerDisabled,
})
),
- [columns, freezeColumns]
+ [columns, freezeColumnsLeft, freezeColumnsRight]
);
}
@@ -174,16 +182,25 @@ export function getStickyWidth(
src: number;
dest: number;
}
-): number {
- let result = 0;
+): [left: number, right: number] {
+ let lWidth = 0;
+ let rWidth = 0;
const remapped = remapForDnDState(columns, dndState);
for (let i = 0; i < remapped.length; i++) {
const c = remapped[i];
- if (c.sticky) result += c.width;
- else break;
+ if (c.sticky) {
+ if (c.stickyPosition === "left") lWidth += c.width;
+ } else break;
}
- return result;
+ for (let i = remapped.length - 1; i >= 0; i--) {
+ const c = remapped[i];
+ if (c.sticky) {
+ if (c.stickyPosition === "right") rWidth += c.width;
+ } else break;
+ }
+
+ return [lWidth, rWidth];
}
export function getFreezeTrailingHeight(
@@ -206,6 +223,7 @@ export function getEffectiveColumns(
columns: readonly MappedGridColumn[],
cellXOffset: number,
width: number,
+ freezeColumns: number | readonly [left: number, right: number],
dndState?: {
src: number;
dest: number;
@@ -214,19 +232,31 @@ export function getEffectiveColumns(
): readonly MappedGridColumn[] {
const mappedCols = remapForDnDState(columns, dndState);
+ const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns);
+
const sticky: MappedGridColumn[] = [];
- for (const c of mappedCols) {
- if (c.sticky) {
- sticky.push(c);
- } else {
- break;
- }
+ for (let i = 0; i < freezeLeftColumns; i++) {
+ sticky.push(mappedCols[i]);
}
+
if (sticky.length > 0) {
for (const c of sticky) {
width -= c.width;
}
}
+
+ const stickyRight: MappedGridColumn[] = [];
+
+ for (let i = mappedCols.length - freezeRightColumns; i < mappedCols.length; i++) {
+ stickyRight.push(mappedCols[i]);
+ }
+
+ if (stickyRight.length > 0) {
+ for (const c of stickyRight) {
+ width -= c.width;
+ }
+ }
+
let endIndex = cellXOffset;
let curX = tx ?? 0;
@@ -242,22 +272,40 @@ export function getEffectiveColumns(
}
}
+ sticky.push(...stickyRight);
+
return sticky;
}
export function getColumnIndexForX(
targetX: number,
effectiveColumns: readonly MappedGridColumn[],
+ freezeColumns: number | readonly [left: number, right: number],
+ width: number,
translateX?: number
): number {
+ const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1];
+
+ let y = width;
+ for (let fc = 0; fc < freezeRightColumns; fc++) {
+ const colIdx = effectiveColumns.length - 1 - fc;
+ const col = effectiveColumns[colIdx];
+ y -= col.width;
+ if (targetX >= y) {
+ return col.sourceIndex;
+ }
+ }
+
let x = 0;
- for (const c of effectiveColumns) {
+ for (let i = 0; i < effectiveColumns.length - freezeRightColumns; i++) {
+ const c = effectiveColumns[i];
const cx = c.sticky ? x : x + (translateX ?? 0);
if (targetX <= cx + c.width) {
return c.sourceIndex;
}
x += c.width;
}
+
return -1;
}
@@ -768,7 +816,7 @@ export function computeBounds(
translateX: number,
translateY: number,
rows: number,
- freezeColumns: number,
+ freezeColumns: number | readonly [left: number, right: number],
freezeTrailingRows: number,
mappedColumns: readonly MappedGridColumn[],
rowHeight: number | ((index: number) => number)
@@ -780,23 +828,31 @@ export function computeBounds(
height: 0,
};
+ const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns);
+ const column = mappedColumns[col];
+
if (col >= mappedColumns.length || row >= rows || row < -2 || col < 0) {
return result;
}
const headerHeight = totalHeaderHeight - groupHeaderHeight;
- if (col >= freezeColumns) {
+ if (col >= freezeLeftColumns && col < mappedColumns.length - freezeRightColumns) {
const dir = cellXOffset > col ? -1 : 1;
- const freezeWidth = getStickyWidth(mappedColumns);
- result.x += freezeWidth + translateX;
+ const [freezeLeftWidth] = getStickyWidth(mappedColumns);
+ result.x += freezeLeftWidth + translateX;
for (let i = cellXOffset; i !== col; i += dir) {
result.x += mappedColumns[dir === 1 ? i : i - 1].width * dir;
}
- } else {
+ } else if (column.stickyPosition === "left") {
for (let i = 0; i < col; i++) {
result.x += mappedColumns[i].width;
}
+ } else if (column.stickyPosition === "right") {
+ result.x = width;
+ for (let i = col; i < mappedColumns.length; i++) {
+ result.x -= mappedColumns[i].width;
+ }
}
result.width = mappedColumns[col].width + 1;
@@ -832,8 +888,8 @@ export function computeBounds(
end++;
}
if (!sticky) {
- const freezeWidth = getStickyWidth(mappedColumns);
- const clip = result.x - freezeWidth;
+ const [freezeLeftWidth] = getStickyWidth(mappedColumns);
+ const clip = result.x - freezeLeftWidth;
if (clip < 0) {
result.x -= clip;
result.width += clip;
diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts b/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts
index 4f5600837..dbf777cfd 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-render.blit.ts
@@ -70,7 +70,7 @@ export function blitLastFrame(
}
deltaX += translateX - last.translateX;
- const stickyWidth = getStickyWidth(effectiveCols);
+ const [stickyLeftWidth, stickyRightWidth] = getStickyWidth(effectiveCols);
if (deltaX !== 0 && deltaY !== 0) {
return {
@@ -81,7 +81,7 @@ export function blitLastFrame(
const freezeTrailingRowsHeight =
freezeTrailingRows > 0 ? getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight) : 0;
- const blitWidth = width - stickyWidth - Math.abs(deltaX);
+ const blitWidth = width - stickyLeftWidth - Math.abs(deltaX) - stickyRightWidth;
const blitHeight = height - totalHeaderHeight - freezeTrailingRowsHeight - Math.abs(deltaY) - 1;
if (blitWidth > 150 && blitHeight > 150) {
@@ -128,28 +128,28 @@ export function blitLastFrame(
// blit X
if (deltaX > 0) {
// pixels moving right
- args.sx = stickyWidth * dpr;
+ args.sx = stickyLeftWidth * dpr;
args.sw = blitWidth * dpr;
- args.dx = (deltaX + stickyWidth) * dpr;
+ args.dx = (deltaX + stickyLeftWidth) * dpr;
args.dw = blitWidth * dpr;
drawRegions.push({
- x: stickyWidth - 1,
+ x: stickyLeftWidth - 1,
y: 0,
width: deltaX + 2, // extra width to account for first col not drawing a left side border
height: height,
});
} else if (deltaX < 0) {
// pixels moving left
- args.sx = (stickyWidth - deltaX) * dpr;
+ args.sx = (stickyLeftWidth - deltaX) * dpr;
args.sw = blitWidth * dpr;
- args.dx = stickyWidth * dpr;
+ args.dx = stickyLeftWidth * dpr;
args.dw = blitWidth * dpr;
drawRegions.push({
- x: width + deltaX,
+ x: width + deltaX - stickyRightWidth,
y: 0,
- width: -deltaX,
+ width: -deltaX + stickyRightWidth,
height: height,
});
}
@@ -157,17 +157,28 @@ export function blitLastFrame(
ctx.setTransform(1, 0, 0, 1, 0, 0);
if (doubleBuffer) {
if (
- stickyWidth > 0 &&
+ stickyLeftWidth > 0 &&
deltaX !== 0 &&
deltaY === 0 &&
(targetScroll === undefined || blitSourceScroll?.[1] !== false)
) {
// When double buffering the freeze columns can be offset by a couple pixels vertically between the two
// buffers. We don't want to redraw them so we need to make sure to copy them between the buffers.
- const w = stickyWidth * dpr;
+ const w = stickyLeftWidth * dpr;
const h = height * dpr;
ctx.drawImage(blitSource, 0, 0, w, h, 0, 0, w, h);
}
+ if (
+ stickyRightWidth > 0 &&
+ deltaX !== 0 &&
+ deltaY === 0 &&
+ (targetScroll === undefined || blitSourceScroll?.[1] !== false)
+ ) {
+ const x = (width - stickyRightWidth) * dpr;
+ const w = stickyRightWidth * dpr;
+ const h = height * dpr;
+ ctx.drawImage(blitSource, x, 0, w, h, x, 0, w, h);
+ }
if (
freezeTrailingRowsHeight > 0 &&
deltaX === 0 &&
@@ -200,7 +211,8 @@ export function blitResizedCol(
height: number,
totalHeaderHeight: number,
effectiveCols: readonly MappedGridColumn[],
- resizedIndex: number
+ resizedIndex: number,
+ freezeTrailingColumns: number
) {
const drawRegions: Rectangle[] = [];
@@ -215,18 +227,27 @@ export function blitResizedCol(
return drawRegions;
}
- walkColumns(effectiveCols, cellYOffset, translateX, translateY, totalHeaderHeight, (c, drawX, _drawY, clipX) => {
- if (c.sourceIndex === resizedIndex) {
- const x = Math.max(drawX, clipX) + 1;
- drawRegions.push({
- x,
- y: 0,
- width: width - x,
- height,
- });
- return true;
+ walkColumns(
+ effectiveCols,
+ width,
+ cellYOffset,
+ translateX,
+ translateY,
+ totalHeaderHeight,
+ freezeTrailingColumns,
+ (c, drawX, _drawY, clipX) => {
+ if (c.sourceIndex === resizedIndex) {
+ const x = Math.max(drawX, clipX) + 1;
+ drawRegions.push({
+ x,
+ y: 0,
+ width: width - x,
+ height,
+ });
+ return true;
+ }
}
- });
+ );
return drawRegions;
}
diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts b/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts
index dc84326c2..36cb36300 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-render.cells.ts
@@ -77,6 +77,7 @@ export function drawCells(
effectiveColumns: readonly MappedGridColumn[],
allColumns: readonly MappedGridColumn[],
height: number,
+ width: number,
totalHeaderHeight: number,
translateX: number,
translateY: number,
@@ -90,6 +91,7 @@ export function drawCells(
isFocused: boolean,
drawFocus: boolean,
freezeTrailingRows: number,
+ freezeTrailingColumns: number,
hasAppendRow: boolean,
drawRegions: readonly Rectangle[],
damage: CellSet | undefined,
@@ -124,16 +126,18 @@ export function drawCells(
walkColumns(
effectiveColumns,
+ width,
cellYOffset,
translateX,
translateY,
totalHeaderHeight,
- (c, drawX, colDrawStartY, clipX, startRow) => {
+ freezeTrailingColumns,
+ (c, drawX, colDrawStartY, clipX, clipXRight, startRow) => {
const diff = Math.max(0, clipX - drawX);
const colDrawX = drawX + diff;
const colDrawY = totalHeaderHeight + 1;
- const colWidth = c.width - diff;
+ const colWidth = c.stickyPosition === "right" ? c.width - diff : Math.min(c.width - diff, width - drawX - clipXRight);
const colHeight = height - totalHeaderHeight - 1;
if (drawRegions.length > 0) {
let found = false;
@@ -295,7 +299,7 @@ export function drawCells(
const bgCell = cell.kind === GridCellKind.Protected ? theme.bgCellMedium : theme.bgCell;
let fill: string | undefined;
- if (isSticky || bgCell !== outerTheme.bgCell) {
+ if (isSticky || bgCell !== outerTheme.bgCell || c.sticky) {
fill = blend(bgCell, fill);
}
@@ -551,11 +555,11 @@ export function drawCell(
partialPrepResult === undefined
? undefined
: {
- deprep: partialPrepResult?.deprep,
- fillStyle: partialPrepResult?.fillStyle,
- font: partialPrepResult?.font,
- renderer: r,
- };
+ deprep: partialPrepResult?.deprep,
+ fillStyle: partialPrepResult?.fillStyle,
+ font: partialPrepResult?.font,
+ renderer: r,
+ };
}
if (needsAnim || animationFrameRequested) enqueue?.(allocatedItem);
diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.header.ts b/packages/core/src/internal/data-grid/render/data-grid-render.header.ts
index 47117f530..9e053452d 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-render.header.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-render.header.ts
@@ -38,7 +38,8 @@ export function drawGridHeaders(
getGroupDetails: GroupDetailsCallback,
damage: CellSet | undefined,
drawHeaderCallback: DrawHeaderCallback | undefined,
- touchMode: boolean
+ touchMode: boolean,
+ freezeTrailingColumns: number
) {
const totalHeaderHeight = headerHeight + groupHeaderHeight;
if (totalHeaderHeight <= 0) return;
@@ -54,80 +55,107 @@ export function drawGridHeaders(
const font = outerTheme.headerFontFull;
// Assinging the context font too much can be expensive, it can be worth it to minimze this
ctx.font = font;
- walkColumns(effectiveCols, 0, translateX, 0, totalHeaderHeight, (c, x, _y, clipX) => {
- if (damage !== undefined && !damage.has([c.sourceIndex, -1])) return;
- const diff = Math.max(0, clipX - x);
- ctx.save();
- ctx.beginPath();
- ctx.rect(x + diff, groupHeaderHeight, c.width - diff, headerHeight);
- ctx.clip();
-
- const groupTheme = getGroupDetails(c.group ?? "").overrideTheme;
- const theme =
- c.themeOverride === undefined && groupTheme === undefined
- ? outerTheme
- : mergeAndRealizeTheme(outerTheme, groupTheme, c.themeOverride);
-
- if (theme.bgHeader !== outerTheme.bgHeader) {
- ctx.fillStyle = theme.bgHeader;
- ctx.fill();
- }
-
- if (theme !== outerTheme) {
- ctx.font = theme.headerFontFull;
- }
- const selected = selection.columns.hasIndex(c.sourceIndex);
- const noHover = dragAndDropState !== undefined || isResizing || c.headerRowMarkerDisabled === true;
- const hoveredBoolean = !noHover && hRow === -1 && hCol === c.sourceIndex;
- const hover = noHover
- ? 0
- : (hoverValues.find(s => s.item[0] === c.sourceIndex && s.item[1] === -1)?.hoverAmount ?? 0);
-
- const hasSelectedCell = selection?.current !== undefined && selection.current.cell[0] === c.sourceIndex;
-
- const bgFillStyle = selected ? theme.accentColor : hasSelectedCell ? theme.bgHeaderHasFocus : theme.bgHeader;
+ walkColumns(
+ effectiveCols,
+ width,
+ 0,
+ translateX,
+ 0,
+ totalHeaderHeight,
+ freezeTrailingColumns,
+ (c, x, _y, clipX, clipXRight) => {
+ if (damage !== undefined && !damage.has([c.sourceIndex, -1])) return;
+ const diff = Math.max(0, clipX - x);
+
+ let rectWidth =
+ c.stickyPosition === "right" ? c.width - diff : Math.min(c.width - diff, width - x - clipXRight);
+
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(x + diff, groupHeaderHeight, rectWidth, headerHeight);
+ ctx.clip();
- const y = enableGroups ? groupHeaderHeight : 0;
- const xOffset = c.sourceIndex === 0 ? 0 : 1;
+ const groupTheme = getGroupDetails(c.group ?? "").overrideTheme;
+ const theme =
+ c.themeOverride === undefined && groupTheme === undefined
+ ? outerTheme
+ : mergeAndRealizeTheme(outerTheme, groupTheme, c.themeOverride);
- if (selected) {
- ctx.fillStyle = bgFillStyle;
- ctx.fillRect(x + xOffset, y, c.width - xOffset, headerHeight);
- } else if (hasSelectedCell || hover > 0) {
- ctx.beginPath();
- ctx.rect(x + xOffset, y, c.width - xOffset, headerHeight);
- if (hasSelectedCell) {
- ctx.fillStyle = theme.bgHeaderHasFocus;
+ if (c.sticky === true) {
+ ctx.fillStyle = theme.bgHeader;
ctx.fill();
}
- if (hover > 0) {
- ctx.globalAlpha = hover;
- ctx.fillStyle = theme.bgHeaderHovered;
+
+ if (theme.bgHeader !== outerTheme.bgHeader) {
+ ctx.fillStyle = theme.bgHeader;
ctx.fill();
- ctx.globalAlpha = 1;
}
- }
- drawHeader(
- ctx,
- x,
- y,
- c.width,
- headerHeight,
- c,
- selected,
- theme,
- hoveredBoolean,
- hoveredBoolean ? hPosX : undefined,
- hoveredBoolean ? hPosY : undefined,
- hasSelectedCell,
- hover,
- spriteManager,
- drawHeaderCallback,
- touchMode
- );
- ctx.restore();
- });
+ if (theme !== outerTheme) {
+ ctx.font = theme.headerFontFull;
+ }
+ const selected = selection.columns.hasIndex(c.sourceIndex);
+ const noHover = dragAndDropState !== undefined || isResizing || c.headerRowMarkerDisabled === true;
+ const hoveredBoolean = !noHover && hRow === -1 && hCol === c.sourceIndex;
+ const hover = noHover
+ ? 0
+ : (hoverValues.find(s => s.item[0] === c.sourceIndex && s.item[1] === -1)?.hoverAmount ?? 0);
+
+ const hasSelectedCell = selection?.current !== undefined && selection.current.cell[0] === c.sourceIndex;
+
+ const bgFillStyle = selected
+ ? theme.accentColor
+ : hasSelectedCell
+ ? theme.bgHeaderHasFocus
+ : theme.bgHeader;
+
+ const y = enableGroups ? groupHeaderHeight : 0;
+ const xOffset = c.sourceIndex === 0 ? 0 : 1;
+
+ if (selected) {
+ ctx.fillStyle = bgFillStyle;
+ ctx.fillRect(x + xOffset, y, c.width - xOffset, headerHeight);
+ } else if (hasSelectedCell || hover > 0) {
+ rectWidth =
+ c.stickyPosition === "right"
+ ? c.width - xOffset
+ : Math.min(c.width - xOffset, width - x - clipXRight);
+
+ ctx.beginPath();
+ ctx.rect(x + xOffset, y, rectWidth, headerHeight);
+ if (hasSelectedCell) {
+ ctx.fillStyle = theme.bgHeaderHasFocus;
+ ctx.fill();
+ }
+ if (hover > 0) {
+ ctx.globalAlpha = hover;
+ ctx.fillStyle = theme.bgHeaderHovered;
+ ctx.fill();
+ ctx.globalAlpha = 1;
+ }
+ }
+
+ drawHeader(
+ ctx,
+ x,
+ y,
+ c.width,
+ headerHeight,
+ c,
+ selected,
+ theme,
+ hoveredBoolean,
+ hoveredBoolean ? hPosX : undefined,
+ hoveredBoolean ? hPosY : undefined,
+ hasSelectedCell,
+ hover,
+ spriteManager,
+ drawHeaderCallback,
+ touchMode
+ );
+ ctx.restore();
+ }
+ );
if (enableGroups) {
drawGroups(
@@ -142,7 +170,8 @@ export function drawGridHeaders(
hoverValues,
verticalBorder,
getGroupDetails,
- damage
+ damage,
+ freezeTrailingColumns
);
}
}
@@ -159,120 +188,128 @@ export function drawGroups(
_hoverValues: HoverValues,
verticalBorder: (col: number) => boolean,
getGroupDetails: GroupDetailsCallback,
- damage: CellSet | undefined
+ damage: CellSet | undefined,
+ freezeTrailingColumns: number
) {
const xPad = 8;
const [hCol, hRow] = hovered?.[0] ?? [];
let finalX = 0;
- walkGroups(effectiveCols, width, translateX, groupHeaderHeight, (span, groupName, x, y, w, h) => {
- if (
- damage !== undefined &&
- !damage.hasItemInRectangle({
- x: span[0],
- y: -2,
- width: span[1] - span[0] + 1,
- height: 1,
- })
- )
- return;
- ctx.save();
- ctx.beginPath();
- ctx.rect(x, y, w, h);
- ctx.clip();
-
- const group = getGroupDetails(groupName);
- const groupTheme =
- group?.overrideTheme === undefined ? theme : mergeAndRealizeTheme(theme, group.overrideTheme);
- const isHovered = hRow === -2 && hCol !== undefined && hCol >= span[0] && hCol <= span[1];
- const fillColor = isHovered
- ? (groupTheme.bgGroupHeaderHovered ?? groupTheme.bgHeaderHovered)
- : (groupTheme.bgGroupHeader ?? groupTheme.bgHeader);
-
- if (fillColor !== theme.bgHeader) {
- ctx.fillStyle = fillColor;
- ctx.fill();
- }
-
- ctx.fillStyle = groupTheme.textGroupHeader ?? groupTheme.textHeader;
- if (group !== undefined) {
- let drawX = x;
- if (group.icon !== undefined) {
- spriteManager.drawSprite(
- group.icon,
- "normal",
- ctx,
- drawX + xPad,
- (groupHeaderHeight - 20) / 2,
- 20,
- groupTheme
- );
- drawX += 26;
- }
- ctx.fillText(
- group.name,
- drawX + xPad,
- groupHeaderHeight / 2 + getMiddleCenterBias(ctx, theme.headerFontFull)
- );
-
- if (group.actions !== undefined && isHovered) {
- const actionBoxes = getActionBoundsForGroup({ x, y, width: w, height: h }, group.actions);
-
- ctx.beginPath();
- const fadeStartX = actionBoxes[0].x - 10;
- const fadeWidth = x + w - fadeStartX;
- ctx.rect(fadeStartX, 0, fadeWidth, groupHeaderHeight);
- const grad = ctx.createLinearGradient(fadeStartX, 0, fadeStartX + fadeWidth, 0);
- const trans = withAlpha(fillColor, 0);
- grad.addColorStop(0, trans);
- grad.addColorStop(10 / fadeWidth, fillColor);
- grad.addColorStop(1, fillColor);
- ctx.fillStyle = grad;
-
+ walkGroups(
+ effectiveCols,
+ width,
+ translateX,
+ groupHeaderHeight,
+ freezeTrailingColumns,
+ (span, groupName, x, y, w, h) => {
+ if (
+ damage !== undefined &&
+ !damage.hasItemInRectangle({
+ x: span[0],
+ y: -2,
+ width: span[1] - span[0] + 1,
+ height: 1,
+ })
+ )
+ return;
+ ctx.save();
+ ctx.beginPath();
+ ctx.rect(x, y, w, h);
+ ctx.clip();
+
+ const group = getGroupDetails(groupName);
+ const groupTheme =
+ group?.overrideTheme === undefined ? theme : mergeAndRealizeTheme(theme, group.overrideTheme);
+ const isHovered = hRow === -2 && hCol !== undefined && hCol >= span[0] && hCol <= span[1];
+ const fillColor = isHovered
+ ? (groupTheme.bgGroupHeaderHovered ?? groupTheme.bgHeaderHovered)
+ : (groupTheme.bgGroupHeader ?? groupTheme.bgHeader);
+
+ if (fillColor !== theme.bgHeader) {
+ ctx.fillStyle = fillColor;
ctx.fill();
+ }
- ctx.globalAlpha = 0.6;
-
- // eslint-disable-next-line prefer-const
- const [mouseX, mouseY] = hovered?.[1] ?? [-1, -1];
- for (let i = 0; i < group.actions.length; i++) {
- const action = group.actions[i];
- const box = actionBoxes[i];
- const actionHovered = pointInRect(box, mouseX + x, mouseY);
- if (actionHovered) {
- ctx.globalAlpha = 1;
- }
+ ctx.fillStyle = groupTheme.textGroupHeader ?? groupTheme.textHeader;
+ if (group !== undefined) {
+ let drawX = x;
+ if (group.icon !== undefined) {
spriteManager.drawSprite(
- action.icon,
+ group.icon,
"normal",
ctx,
- box.x + box.width / 2 - 10,
- box.y + box.height / 2 - 10,
+ drawX + xPad,
+ (groupHeaderHeight - 20) / 2,
20,
groupTheme
);
- if (actionHovered) {
- ctx.globalAlpha = 0.6;
- }
+ drawX += 26;
}
+ ctx.fillText(
+ group.name,
+ drawX + xPad,
+ groupHeaderHeight / 2 + getMiddleCenterBias(ctx, theme.headerFontFull)
+ );
+
+ if (group.actions !== undefined && isHovered) {
+ const actionBoxes = getActionBoundsForGroup({ x, y, width: w, height: h }, group.actions);
+
+ ctx.beginPath();
+ const fadeStartX = actionBoxes[0].x - 10;
+ const fadeWidth = x + w - fadeStartX;
+ ctx.rect(fadeStartX, 0, fadeWidth, groupHeaderHeight);
+ const grad = ctx.createLinearGradient(fadeStartX, 0, fadeStartX + fadeWidth, 0);
+ const trans = withAlpha(fillColor, 0);
+ grad.addColorStop(0, trans);
+ grad.addColorStop(10 / fadeWidth, fillColor);
+ grad.addColorStop(1, fillColor);
+ ctx.fillStyle = grad;
+
+ ctx.fill();
+
+ ctx.globalAlpha = 0.6;
+
+ // eslint-disable-next-line prefer-const
+ const [mouseX, mouseY] = hovered?.[1] ?? [-1, -1];
+ for (let i = 0; i < group.actions.length; i++) {
+ const action = group.actions[i];
+ const box = actionBoxes[i];
+ const actionHovered = pointInRect(box, mouseX + x, mouseY);
+ if (actionHovered) {
+ ctx.globalAlpha = 1;
+ }
+ spriteManager.drawSprite(
+ action.icon,
+ "normal",
+ ctx,
+ box.x + box.width / 2 - 10,
+ box.y + box.height / 2 - 10,
+ 20,
+ groupTheme
+ );
+ if (actionHovered) {
+ ctx.globalAlpha = 0.6;
+ }
+ }
- ctx.globalAlpha = 1;
+ ctx.globalAlpha = 1;
+ }
}
- }
- if (x !== 0 && verticalBorder(span[0])) {
- ctx.beginPath();
- ctx.moveTo(x + 0.5, 0);
- ctx.lineTo(x + 0.5, groupHeaderHeight);
- ctx.strokeStyle = theme.borderColor;
- ctx.lineWidth = 1;
- ctx.stroke();
- }
+ if (x !== 0 && verticalBorder(span[0])) {
+ ctx.beginPath();
+ ctx.moveTo(x + 0.5, 0);
+ ctx.lineTo(x + 0.5, groupHeaderHeight);
+ ctx.strokeStyle = theme.borderColor;
+ ctx.lineWidth = 1;
+ ctx.stroke();
+ }
- ctx.restore();
+ ctx.restore();
- finalX = x + w;
- });
+ finalX = x + w;
+ }
+ );
ctx.beginPath();
ctx.moveTo(finalX + 0.5, 0);
diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts b/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts
index cd4b4cf8d..b1ebf4baf 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-render.lines.ts
@@ -26,6 +26,7 @@ export function drawBlanks(
selectedRows: CompactSelection,
disabledRows: CompactSelection,
freezeTrailingRows: number,
+ freezeTrailingColumns: number,
hasAppendRow: boolean,
drawRegions: readonly Rectangle[],
damage: CellSet | undefined,
@@ -41,11 +42,13 @@ export function drawBlanks(
walkColumns(
effectiveColumns,
+ width,
cellYOffset,
translateX,
translateY,
totalHeaderHeight,
- (c, drawX, colDrawY, clipX, startRow) => {
+ freezeTrailingColumns,
+ (c, drawX, colDrawY, clipX, _clipXRight, startRow) => {
if (c !== effectiveColumns[effectiveColumns.length - 1]) return;
drawX += c.width;
const x = Math.max(drawX, clipX);
@@ -123,18 +126,27 @@ export function overdrawStickyBoundaries(
}
const hColor = theme.horizontalBorderColor ?? theme.borderColor;
const vColor = theme.borderColor;
- const drawX = drawFreezeBorder ? getStickyWidth(effectiveCols) : 0;
+ const [drawXLeft, drawXRight] = drawFreezeBorder ? getStickyWidth(effectiveCols) : [0, 0];
let vStroke: string | undefined;
- if (drawX !== 0) {
+ if (drawXLeft !== 0) {
vStroke = blendCache(vColor, theme.bgCell);
ctx.beginPath();
- ctx.moveTo(drawX + 0.5, 0);
- ctx.lineTo(drawX + 0.5, height);
+ ctx.moveTo(drawXLeft + 0.5, 0);
+ ctx.lineTo(drawXLeft + 0.5, height);
ctx.strokeStyle = vStroke;
ctx.stroke();
}
+ if (drawXRight !== 0) {
+ const hStroke = vColor === hColor && vStroke !== undefined ? vStroke : blendCache(hColor, theme.bgCell);
+ ctx.beginPath();
+ ctx.moveTo(width - drawXRight + 0.5, 0);
+ ctx.lineTo(width - drawXRight + 0.5, height);
+ ctx.strokeStyle = hStroke;
+ ctx.stroke();
+ }
+
if (freezeTrailingRows > 0) {
const hStroke = vColor === hColor && vStroke !== undefined ? vStroke : blendCache(hColor, theme.bgCell);
const h = getFreezeTrailingHeight(rows, freezeTrailingRows, getRowHeight);
@@ -305,6 +317,8 @@ export function drawGridLines(
}
ctx.clip("evenodd");
}
+
+ const effectiveWidth = effectiveCols.reduce((acc, col) => acc + col.width, 0);
const hColor = theme.horizontalBorderColor ?? theme.borderColor;
const vColor = theme.borderColor;
@@ -319,6 +333,7 @@ export function drawGridLines(
for (let index = 0; index < effectiveCols.length; index++) {
const c = effectiveCols[index];
if (c.width === 0) continue;
+ if (effectiveCols[index + 1]?.sticky && effectiveCols[index + 1].stickyPosition !== "left") break;
x += c.width;
const tx = c.sticky ? x : x + translateX;
if (tx >= minX && tx <= maxX && verticalBorder(index + 1)) {
@@ -332,6 +347,25 @@ export function drawGridLines(
}
}
+ const clippedWidth = Math.min(width, effectiveWidth);
+ let rightX = clippedWidth + 0.5;
+ for (let index = effectiveCols.length - 1; index >= 0; index--) {
+ const c = effectiveCols[index];
+ if (c.width === 0) continue;
+ if (!c.sticky) break;
+ rightX -= c.width;
+ const tx = rightX;
+ if (tx >= minX && tx <= maxX && verticalBorder(index + 1)) {
+ toDraw.push({
+ x1: tx,
+ y1: Math.max(groupHeaderHeight, minY),
+ x2: tx,
+ y2: Math.min(height, maxY),
+ color: vColor,
+ });
+ }
+ }
+
let freezeY = height + 0.5;
for (let i = rows - freezeTrailingRows; i < rows; i++) {
const rh = getRowHeight(i);
diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.ts b/packages/core/src/internal/data-grid/render/data-grid-render.ts
index 310c3511d..05d3ed284 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-render.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-render.ts
@@ -12,6 +12,7 @@ import { drawGridHeaders } from "./data-grid-render.header.js";
import { drawGridLines, overdrawStickyBoundaries, drawBlanks, drawExtraRowThemes } from "./data-grid-render.lines.js";
import { blitLastFrame, blitResizedCol, computeCanBlit } from "./data-grid-render.blit.js";
import { drawHighlightRings, drawFillHandle, drawColumnResizeOutline } from "./data-grid.render.rings.js";
+import { normalizeFreezeColumns } from "../../../common/utils.js";
// Future optimization opportunities
// - Create a cache of a buffer used to render the full view of a partially displayed column so that when
@@ -33,35 +34,48 @@ function clipHeaderDamage(
translateX: number,
translateY: number,
cellYOffset: number,
+ freezeTrailingColumns: number,
damage: CellSet | undefined
): void {
if (damage === undefined || damage.size === 0) return;
ctx.beginPath();
- walkGroups(effectiveColumns, width, translateX, groupHeaderHeight, (span, _group, x, y, w, h) => {
- const hasItemInSpan = damage.hasItemInRectangle({
- x: span[0],
- y: -2,
- width: span[1] - span[0] + 1,
- height: 1,
- });
- if (hasItemInSpan) {
- ctx.rect(x, y, w, h);
+ walkGroups(
+ effectiveColumns,
+ width,
+ translateX,
+ groupHeaderHeight,
+ freezeTrailingColumns,
+ (span, _group, x, y, w, h) => {
+ const hasItemInSpan = damage.hasItemInRectangle({
+ x: span[0],
+ y: -2,
+ width: span[1] - span[0] + 1,
+ height: 1,
+ });
+ if (hasItemInSpan) {
+ ctx.rect(x, y, w, h);
+ }
}
- });
+ );
walkColumns(
effectiveColumns,
+ width,
cellYOffset,
translateX,
translateY,
totalHeaderHeight,
- (c, drawX, _colDrawY, clipX) => {
+ freezeTrailingColumns,
+ (c, drawX, _colDrawY, clipX, clipXRight) => {
const diff = Math.max(0, clipX - drawX);
const finalX = drawX + diff + 1;
- const finalWidth = c.width - diff - 1;
+ const finalWidth =
+ c.stickyPosition === "right"
+ ? c.width - diff
+ : Math.min(c.width - diff - 1, width - drawX - clipXRight);
if (damage.has([c.sourceIndex, -1])) {
ctx.rect(finalX, groupHeaderHeight, finalWidth, totalHeaderHeight - groupHeaderHeight);
}
@@ -73,6 +87,7 @@ function clipHeaderDamage(
function getLastRow(
effectiveColumns: readonly MappedGridColumn[],
height: number,
+ width: number,
totalHeaderHeight: number,
translateX: number,
translateY: number,
@@ -80,16 +95,19 @@ function getLastRow(
rows: number,
getRowHeight: (row: number) => number,
freezeTrailingRows: number,
- hasAppendRow: boolean
+ hasAppendRow: boolean,
+ freezeTrailingColumns: number
): number {
let result = 0;
walkColumns(
effectiveColumns,
+ width,
cellYOffset,
translateX,
translateY,
totalHeaderHeight,
- (_c, __drawX, colDrawY, _clipX, startRow) => {
+ freezeTrailingColumns,
+ (_c, __drawX, colDrawY, _clipX, _clipXRight, startRow) => {
walkRowsInCol(
startRow,
colDrawY,
@@ -171,6 +189,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
const doubleBuffer = renderStrategy === "double-buffer";
const dpr = Math.min(maxScaleFactor, Math.ceil(window.devicePixelRatio ?? 1));
+ const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns);
+
// if we are double buffering we need to make sure we can blit. If we can't we need to redraw the whole thing
const canBlit = renderStrategy !== "direct" && computeCanBlit(arg, lastArg);
@@ -253,7 +273,14 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
targetCtx.scale(dpr, dpr);
}
- const effectiveCols = getEffectiveColumns(mappedColumns, cellXOffset, width, dragAndDropState, translateX);
+ const effectiveCols = getEffectiveColumns(
+ mappedColumns,
+ cellXOffset,
+ width,
+ freezeColumns,
+ dragAndDropState,
+ translateX
+ );
let drawRegions: Rectangle[] = [];
@@ -287,7 +314,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
getGroupDetails,
damage,
drawHeaderCallback,
- touchMode
+ touchMode,
+ freezeRightColumns
);
drawGridLines(
@@ -357,6 +385,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
getRowHeight,
getCellContent,
freezeTrailingRows,
+ freezeRightColumns,
hasAppendRow,
fillHandle,
rows
@@ -383,13 +412,13 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
{
x: 0,
y: cellYOffset,
- width: freezeColumns,
+ width: freezeLeftColumns,
height: 300,
},
{
x: 0,
y: -2,
- width: freezeColumns,
+ width: freezeLeftColumns,
height: 2,
},
{
@@ -399,6 +428,18 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
height: freezeTrailingRows,
when: freezeTrailingRows > 0,
},
+ {
+ x: viewRegionWidth - freezeRightColumns,
+ y: cellYOffset,
+ width: freezeRightColumns,
+ height: 300,
+ },
+ {
+ x: viewRegionWidth - freezeRightColumns,
+ y: -2,
+ width: freezeRightColumns,
+ height: 2,
+ },
]);
const doDamage = (ctx: CanvasRenderingContext2D) => {
@@ -407,6 +448,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
effectiveCols,
mappedColumns,
height,
+ width,
totalHeaderHeight,
translateX,
translateY,
@@ -420,6 +462,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
isFocused,
drawFocus,
freezeTrailingRows,
+ freezeRightColumns,
hasAppendRow,
drawRegions,
damage,
@@ -463,6 +506,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
getRowHeight,
getCellContent,
freezeTrailingRows,
+ freezeRightColumns,
hasAppendRow,
fillHandle,
rows
@@ -491,6 +535,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
translateX,
translateY,
cellYOffset,
+ freezeRightColumns,
damage
);
drawHeaderTexture();
@@ -550,7 +595,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
height,
totalHeaderHeight,
effectiveCols,
- resizedCol
+ resizedCol,
+ freezeRightColumns
);
}
@@ -602,6 +648,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
getRowHeight,
getCellContent,
freezeTrailingRows,
+ freezeRightColumns,
hasAppendRow,
fillHandle,
rows
@@ -626,6 +673,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
effectiveCols,
mappedColumns,
height,
+ width,
totalHeaderHeight,
translateX,
translateY,
@@ -639,6 +687,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
isFocused,
drawFocus,
freezeTrailingRows,
+ freezeRightColumns,
hasAppendRow,
drawRegions,
damage,
@@ -675,6 +724,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
selection.rows,
disabledRows,
freezeTrailingRows,
+ freezeRightColumns,
hasAppendRow,
drawRegions,
damage,
@@ -723,7 +773,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
focusRedraw?.();
if (isResizing && resizeIndicator !== "none") {
- walkColumns(effectiveCols, 0, translateX, 0, totalHeaderHeight, (c, x) => {
+ walkColumns(effectiveCols, width, 0, translateX, 0, totalHeaderHeight, freezeRightColumns, (c, x) => {
if (c.sourceIndex === resizeCol) {
drawColumnResizeOutline(
overlayCtx,
@@ -756,6 +806,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
const lastRowDrawn = getLastRow(
effectiveCols,
height,
+ width,
totalHeaderHeight,
translateX,
translateY,
@@ -763,7 +814,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
rows,
getRowHeight,
freezeTrailingRows,
- hasAppendRow
+ hasAppendRow,
+ freezeRightColumns
);
imageLoader?.setWindow(
@@ -774,7 +826,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
height: lastRowDrawn - cellYOffset,
},
freezeColumns,
- Array.from({ length: freezeTrailingRows }, (_, i) => rows - 1 - i)
+ Array.from({ length: freezeTrailingRows }, (_, i) => rows - 1 - i),
+ mappedColumns.length
);
const scrollX = last !== undefined && (cellXOffset !== last.cellXOffset || translateX !== last.translateX);
diff --git a/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts b/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts
index e00ffa4b2..8e2575f6b 100644
--- a/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid-render.walk.ts
@@ -60,29 +60,48 @@ export type WalkColsCallback = (
drawX: number,
drawY: number,
clipX: number,
+ clipXRight: number,
startRow: number
) => boolean | void;
export function walkColumns(
effectiveCols: readonly MappedGridColumn[],
+ width: number,
cellYOffset: number,
translateX: number,
translateY: number,
totalHeaderHeight: number,
+ freezeTrailingColumns: number,
cb: WalkColsCallback
): void {
let x = 0;
let clipX = 0; // this tracks the total width of sticky cols
const drawY = totalHeaderHeight + translateY;
- for (const c of effectiveCols) {
+
+ let clipXRight = 0;
+ for (let i = effectiveCols.length - freezeTrailingColumns; i < effectiveCols.length; i++) {
+ clipXRight += effectiveCols[i].width;
+ }
+
+ for (let i = 0; i < effectiveCols.length - freezeTrailingColumns; i++) {
+ const c = effectiveCols[i];
const drawX = c.sticky ? clipX : x + translateX;
- if (cb(c, drawX, drawY, c.sticky ? 0 : clipX, cellYOffset) === true) {
+ if (cb(c, drawX, drawY, c.sticky ? 0 : clipX, clipXRight, cellYOffset) === true) {
break;
}
x += c.width;
clipX += c.sticky ? c.width : 0;
}
+
+ x = width;
+ for (let fc = 0; fc < freezeTrailingColumns; fc++) {
+ const c = effectiveCols[effectiveCols.length - 1 - fc];
+ const drawX = x - c.width;
+
+ x -= c.width;
+ cb(c, drawX, drawY, clipX, clipXRight, cellYOffset);
+ }
}
// this should not be item, it is [startInclusive, endInclusive]
@@ -100,11 +119,20 @@ export function walkGroups(
width: number,
translateX: number,
groupHeaderHeight: number,
+ freezeTrailingColumns: number,
cb: WalkGroupsCallback
): void {
let x = 0;
let clipX = 0;
- for (let index = 0; index < effectiveCols.length; index++) {
+
+ // Pre-calculate right freeze columns total width
+ let widthRight = 0;
+ for (let i = effectiveCols.length - freezeTrailingColumns; i < effectiveCols.length; i++) {
+ widthRight += effectiveCols[i].width;
+ }
+ const clippedWidth = width - widthRight;
+
+ for (let index = 0; index < effectiveCols.length - freezeTrailingColumns; index++) {
const startCol = effectiveCols[index];
let end = index + 1;
@@ -129,7 +157,7 @@ export function walkGroups(
const t = startCol.sticky ? 0 : translateX;
const localX = x + t;
const delta = startCol.sticky ? 0 : Math.max(0, clipX - localX);
- const w = Math.min(boxWidth - delta, width - (localX + delta));
+ const w = Math.min(boxWidth - delta, clippedWidth - (localX + delta));
cb(
[startCol.sourceIndex, effectiveCols[end - 1].sourceIndex],
startCol.group ?? "",
@@ -141,6 +169,45 @@ export function walkGroups(
x += boxWidth;
}
+
+ let currentWidth = clippedWidth;
+ const rightStartIndex = effectiveCols.length - freezeTrailingColumns;
+
+ for (let index = rightStartIndex; index < effectiveCols.length; index++) {
+ const startCol = effectiveCols[index];
+
+ let end = index + 1;
+ let boxWidth = startCol.width;
+
+ while (
+ end < effectiveCols.length &&
+ isGroupEqual(effectiveCols[end].group, startCol.group) &&
+ effectiveCols[end].sticky === effectiveCols[index].sticky
+ ) {
+ const endCol = effectiveCols[end];
+ boxWidth += endCol.width;
+ end++;
+ index++;
+ if (endCol.sticky) {
+ clipX += endCol.width;
+ }
+ }
+
+ const t = currentWidth + boxWidth;
+ const localX = t - boxWidth;
+ const delta = 0;
+ const w = boxWidth - delta;
+ cb(
+ [startCol.sourceIndex, effectiveCols[end - 1].sourceIndex],
+ startCol.group ?? "",
+ localX + delta,
+ 0,
+ w,
+ groupHeaderHeight
+ );
+
+ currentWidth += w;
+ }
}
export function getSpanBounds(
diff --git a/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts b/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts
index e8f07bc1d..4e4d10866 100644
--- a/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts
+++ b/packages/core/src/internal/data-grid/render/data-grid.render.rings.ts
@@ -7,6 +7,7 @@ import { blend, withAlpha } from "../color-parser.js";
import { hugRectToTarget, intersectRect, rectContains, splitRectIntoRegions } from "../../../common/math.js";
import { getSpanBounds, walkColumns, walkRowsInCol } from "./data-grid-render.walk.js";
import { type Highlight } from "./data-grid-render.cells.js";
+import { normalizeFreezeColumns } from "../../../common/utils.js";
export function drawHighlightRings(
ctx: CanvasRenderingContext2D,
@@ -17,7 +18,7 @@ export function drawHighlightRings(
translateX: number,
translateY: number,
mappedColumns: readonly MappedGridColumn[],
- freezeColumns: number,
+ freezeColumns: number | readonly [left: number, right: number],
headerHeight: number,
groupHeaderHeight: number,
rowHeight: number | ((index: number) => number),
@@ -27,19 +28,25 @@ export function drawHighlightRings(
theme: FullTheme
): (() => void) | undefined {
const highlightRegions = allHighlightRegions?.filter(x => x.style !== "no-outline");
+ const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns);
if (highlightRegions === undefined || highlightRegions.length === 0) return undefined;
- const freezeLeft = getStickyWidth(mappedColumns);
+ const [freezeLeft, freezeRight] = getStickyWidth(mappedColumns);
const freezeBottom = getFreezeTrailingHeight(rows, freezeTrailingRows, rowHeight);
- const splitIndicies = [freezeColumns, 0, mappedColumns.length, rows - freezeTrailingRows] as const;
- const splitLocations = [freezeLeft, 0, width, height - freezeBottom] as const;
+ const splitIndices = [
+ freezeLeftColumns,
+ 0,
+ mappedColumns.length - freezeRightColumns,
+ rows - freezeTrailingRows,
+ ] as const;
+ const splitLocations = [freezeLeft, 0, width - freezeRight, height - freezeBottom] as const;
const drawRects = highlightRegions.map(h => {
const r = h.range;
const style = h.style ?? "dashed";
- return splitRectIntoRegions(r, splitIndicies, width, height, splitLocations).map(arg => {
+ return splitRectIntoRegions(r, splitIndices, width, height, splitLocations).map(arg => {
const rect = arg.rect;
const topLeftBounds = computeBounds(
rect.x,
@@ -187,6 +194,7 @@ export function drawFillHandle(
getRowHeight: (row: number) => number,
getCellContent: (cell: Item) => InnerGridCell,
freezeTrailingRows: number,
+ freezeTrailingColumns: number,
hasAppendRow: boolean,
fillHandle: FillHandle,
rows: number
@@ -223,12 +231,13 @@ export function drawFillHandle(
walkColumns(
effectiveCols,
+ width,
cellYOffset,
translateX,
translateY,
totalHeaderHeight,
- (col, drawX, colDrawY, clipX, startRow) => {
- clipX;
+ freezeTrailingColumns,
+ (col, drawX, colDrawY, clipX, _clipXRight, startRow) => {
if (col.sticky && targetCol > col.sourceIndex) return;
const isBeforeTarget = col.sourceIndex < targetColSpan[0];
diff --git a/packages/core/src/internal/data-grid/render/draw-grid-arg.ts b/packages/core/src/internal/data-grid/render/draw-grid-arg.ts
index 7e3539ee1..057672d18 100644
--- a/packages/core/src/internal/data-grid/render/draw-grid-arg.ts
+++ b/packages/core/src/internal/data-grid/render/draw-grid-arg.ts
@@ -40,7 +40,7 @@ export interface DrawGridArg {
readonly translateY: number;
readonly mappedColumns: readonly MappedGridColumn[];
readonly enableGroups: boolean;
- readonly freezeColumns: number;
+ readonly freezeColumns: number | readonly [left: number, right: number];
readonly dragAndDropState: DragAndDropState | undefined;
readonly theme: FullTheme;
readonly headerHeight: number;
diff --git a/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx b/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx
index dae1ae92b..6402a3208 100644
--- a/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx
+++ b/packages/core/src/internal/scrolling-data-grid/scrolling-data-grid.tsx
@@ -2,6 +2,7 @@ import * as React from "react";
import DataGridDnd, { type DataGridDndProps } from "../data-grid-dnd/data-grid-dnd.js";
import type { Rectangle } from "../data-grid/data-grid-types.js";
import { InfiniteScroller } from "./infinite-scroller.js";
+import { normalizeFreezeColumns } from "../../common/utils.js";
type Props = Omit;
@@ -102,6 +103,8 @@ const GridScroller: React.FunctionComponent = p => {
const lastY = React.useRef();
const lastSize = React.useRef();
+ const [freezeLeftColumns] = normalizeFreezeColumns(freezeColumns);
+
const width = nonGrowWidth + Math.max(0, overscrollX ?? 0);
let height = enableGroups ? headerHeight + groupHeaderHeight : headerHeight;
@@ -130,7 +133,7 @@ const GridScroller: React.FunctionComponent = p => {
args.x = args.x < 0 ? 0 : args.x;
let stickyColWidth = 0;
- for (let i = 0; i < freezeColumns; i++) {
+ for (let i = 0; i < freezeLeftColumns; i++) {
stickyColWidth += columns[i].width;
}
@@ -219,25 +222,13 @@ const GridScroller: React.FunctionComponent = p => {
args.width !== lastSize.current?.[0] ||
args.height !== lastSize.current?.[1]
) {
- onVisibleRegionChanged?.(
- {
- x: cellX,
- y: cellY,
- width: cellRight - cellX,
- height: cellBottom - cellY,
- },
- args.width,
- args.height,
- args.paddingRight ?? 0,
- tx,
- ty
- );
+ onVisibleRegionChanged?.(rect, args.width, args.height, args.paddingRight ?? 0, tx, ty);
last.current = rect;
lastX.current = tx;
lastY.current = ty;
lastSize.current = [args.width, args.height];
}
- }, [columns, rowHeight, rows, onVisibleRegionChanged, freezeColumns, smoothScrollX, smoothScrollY]);
+ }, [columns, rowHeight, rows, onVisibleRegionChanged, freezeLeftColumns, smoothScrollX, smoothScrollY]);
const onScrollUpdate = React.useCallback(
(args: Rectangle & { paddingRight: number }) => {
diff --git a/packages/core/test/data-editor.test.tsx b/packages/core/test/data-editor.test.tsx
index 4a0d97369..23b25cf37 100644
--- a/packages/core/test/data-editor.test.tsx
+++ b/packages/core/test/data-editor.test.tsx
@@ -2273,6 +2273,53 @@ describe("data-editor", () => {
);
});
+
+ test("Freeze area reported with right freeze included", async () => {
+ const spy = vi.fn();
+ vi.useFakeTimers();
+ render(
+ ,
+ {
+ wrapper: Context,
+ }
+ );
+ prep();
+
+ expect(spy).toBeCalledWith(
+ expect.objectContaining({
+ height: 32,
+ width: 9,
+ x: 2,
+ y: 0,
+ }),
+ 0,
+ 0,
+ expect.objectContaining({
+ freezeRegion: {
+ height: 32,
+ width: 2,
+ x: 0,
+ y: 0,
+ },
+ freezeRegions: [
+ {
+ height: 32,
+ width: 2,
+ x: 0,
+ y: 0,
+ },
+ {
+ height: 32,
+ width: 2,
+ x: 9,
+ y: 0,
+ },
+ ],
+ selected: undefined,
+ })
+ );
+ });
+
test("Search close", async () => {
const spy = vi.fn();
vi.useFakeTimers();
diff --git a/packages/core/test/data-grid.test.tsx b/packages/core/test/data-grid.test.tsx
index 33f3c86c2..0036fafdd 100644
--- a/packages/core/test/data-grid.test.tsx
+++ b/packages/core/test/data-grid.test.tsx
@@ -395,4 +395,71 @@ describe("data-grid", () => {
false
);
});
+
+ test("Freeze column simple check with trailing", () => {
+ const spy = vi.fn();
+
+ const basicPropsWithMoreColumns = {
+ ...basicProps,
+ columns: [
+ ...basicProps.columns,
+ {
+ title: "F",
+ width: 150,
+ },
+ {
+ title: "G",
+ width: 150,
+ },
+ {
+ title: "H",
+ width: 150,
+ },
+ {
+ title: "I",
+ width: 150,
+ },
+ {
+ title: "J",
+ width: 150,
+ },
+ {
+ title: "K",
+ width: 150,
+ },
+ {
+ title: "L",
+ width: 150,
+ },
+ ],
+ };
+
+ render();
+
+
+ fireEvent.pointerDown(screen.getByTestId(dataGridCanvasId), {
+ clientX: 950, // Col A
+ clientY: 36 + 32 * 5 + 16, // Row 5 (0 indexed)
+ });
+
+ fireEvent.pointerUp(screen.getByTestId(dataGridCanvasId), {
+ clientX: 950, // Col A
+ clientY: 36 + 32 * 5 + 16, // Row 5 (0 indexed)
+ });
+
+ fireEvent.click(screen.getByTestId(dataGridCanvasId), {
+ clientX: 950, // Col A
+ clientY: 36 + 32 * 5 + 16, // Row 5 (0 indexed)
+ });
+
+ expect(spy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ location: [11, 5],
+ kind: "cell",
+ localEventX: 100,
+ localEventY: 16,
+ }),
+ false
+ );
+ });
});
diff --git a/packages/core/test/image-window-loader.test.ts b/packages/core/test/image-window-loader.test.ts
index 398d0fd33..2abd120ab 100644
--- a/packages/core/test/image-window-loader.test.ts
+++ b/packages/core/test/image-window-loader.test.ts
@@ -19,13 +19,28 @@ describe("ImageWindowLoaderImpl", () => {
};
const freezeCols = 5;
- loader.setWindow(newWindow, freezeCols, []);
+ loader.setWindow(newWindow, freezeCols, [], 10);
// Assuming you modify your class to expose `visibleWindow` and `freezeCols` for testing
expect(loader.visibleWindow).toEqual(newWindow);
expect(loader.freezeCols).toBe(freezeCols);
});
+ it("should set the new columnsLength", () => {
+ const newWindow = {
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ };
+ const freezeCols = 5;
+ const columnsLength = 10;
+
+ loader.setWindow(newWindow, freezeCols, [], columnsLength);
+
+ expect(loader.columnsLength).toBe(columnsLength);
+ });
+
it("should call clearOutOfWindow() if the window or freezeCols changes", () => {
const spyClearOutOfWindow = vi.spyOn(loader, "clearOutOfWindow" as any); // Private method, so using 'as any'
@@ -44,13 +59,13 @@ describe("ImageWindowLoaderImpl", () => {
const freezeCols1 = 5;
const freezeCols2 = 10;
- loader.setWindow(window1, freezeCols1, []);
+ loader.setWindow(window1, freezeCols1, [], 10);
expect(spyClearOutOfWindow).toHaveBeenCalledTimes(1);
- loader.setWindow(window2, freezeCols1, []);
+ loader.setWindow(window2, freezeCols1, [], 10);
expect(spyClearOutOfWindow).toHaveBeenCalledTimes(2);
- loader.setWindow(window2, freezeCols2, []);
+ loader.setWindow(window2, freezeCols2, [], 10);
expect(spyClearOutOfWindow).toHaveBeenCalledTimes(3);
// Cleanup
@@ -68,8 +83,8 @@ describe("ImageWindowLoaderImpl", () => {
};
const freezeCols = 5;
- loader.setWindow(newWindow, freezeCols, []);
- loader.setWindow(newWindow, freezeCols, []);
+ loader.setWindow(newWindow, freezeCols, [], 10);
+ loader.setWindow(newWindow, freezeCols, [], 10);
expect(spyClearOutOfWindow).toHaveBeenCalledTimes(1);
diff --git a/packages/core/test/render-state-provider.test.ts b/packages/core/test/render-state-provider.test.ts
index 82e772bb1..634fa48ce 100644
--- a/packages/core/test/render-state-provider.test.ts
+++ b/packages/core/test/render-state-provider.test.ts
@@ -78,7 +78,7 @@ describe("Data Grid Utility Functions", () => {
it("should update visible window and freeze columns correctly", () => {
renderStateProvider.setValue([0, 30], "state");
renderStateProvider.setValue([1, 0], "state");
- renderStateProvider.setWindow(testRectangle, 1, []);
+ renderStateProvider.setWindow(testRectangle, 1, [], 10);
expect(renderStateProvider.getValue([0, 30])).to.equal("state");
expect(renderStateProvider.getValue([1, 0])).to.equal(undefined);
});
diff --git a/packages/source/src/use-collapsing-groups.ts b/packages/source/src/use-collapsing-groups.ts
index 3b748a506..6a0a245ac 100644
--- a/packages/source/src/use-collapsing-groups.ts
+++ b/packages/source/src/use-collapsing-groups.ts
@@ -25,13 +25,16 @@ export function useCollapsingGroups(props: Props): Result {
theme,
} = props;
+ const freezeColumnsLeft = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0];
+ const freezeColumnsRight = typeof freezeColumns === "number" ? 0 : freezeColumns[1];
+
const gridSelection = gridSelectionIn ?? gridSelectionInner;
const spans = React.useMemo(() => {
const result: [number, number][] = [];
let current: [number, number] = [-1, -1];
let lastGroup: string | undefined;
- for (let i = freezeColumns; i < columnsIn.length; i++) {
+ for (let i = freezeColumnsLeft; i < columnsIn.length - freezeColumnsRight; i++) {
const c = columnsIn[i];
const group = c.group ?? "";
const isCollapsed = collapsed.includes(group);
@@ -53,7 +56,7 @@ export function useCollapsingGroups(props: Props): Result {
}
if (current[0] !== -1) result.push(current);
return result;
- }, [collapsed, columnsIn, freezeColumns]);
+ }, [collapsed, columnsIn, freezeColumnsLeft, freezeColumnsRight]);
const columns = React.useMemo(() => {
if (spans.length === 0) return columnsIn;