From 9c9d981d9da44cb1c7b70ed85fa2ce3e3458643a Mon Sep 17 00:00:00 2001 From: Rahul Yadav <52163880+rahulyadav5524@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:46:46 +0530 Subject: [PATCH 01/13] Update release.js.yml --- .github/workflows/release.js.yml | 72 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.js.yml b/.github/workflows/release.js.yml index 4924ac7b0..e49555160 100644 --- a/.github/workflows/release.js.yml +++ b/.github/workflows/release.js.yml @@ -28,39 +28,39 @@ jobs: token: ${{ secrets.NPM_TOKEN }} access: public package: ./packages/core/package.json - publish-cells: - defaults: - run: - working-directory: ./packages/cells - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20.10.0 - - name: Bootstrap - run: npm install && npm run build - working-directory: . - - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.NPM_TOKEN }} - access: public - package: ./packages/cells/package.json - publish-source: - defaults: - run: - working-directory: ./packages/source - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20.10.0 - - name: Bootstrap - run: npm install && npm run build - working-directory: . - - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.NPM_TOKEN }} - access: public - package: ./packages/source/package.json + # publish-cells: + # defaults: + # run: + # working-directory: ./packages/cells + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-node@v4 + # with: + # node-version: 20.10.0 + # - name: Bootstrap + # run: npm install && npm run build + # working-directory: . + # - uses: JS-DevTools/npm-publish@v1 + # with: + # token: ${{ secrets.NPM_TOKEN }} + # access: public + # package: ./packages/cells/package.json + # publish-source: + # defaults: + # run: + # working-directory: ./packages/source + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # - uses: actions/setup-node@v4 + # with: + # node-version: 20.10.0 + # - name: Bootstrap + # run: npm install && npm run build + # working-directory: . + # - uses: JS-DevTools/npm-publish@v1 + # with: + # token: ${{ secrets.NPM_TOKEN }} + # access: public + # package: ./packages/source/package.json From 705f23f0a742c1994a2ceeee8554ac2ee22f9c4d Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Mon, 1 Jul 2024 17:14:26 +0530 Subject: [PATCH 02/13] Added feature to right side freeze columnj --- packages/core/src/data-editor/data-editor.tsx | 69 ++++++++--- .../docs/examples/freeze-columns.stories.tsx | 16 ++- .../core/src/internal/data-grid/data-grid.tsx | 116 +++++++++++++----- .../image-window-loader-interface.ts | 2 +- .../data-grid/render/data-grid-lib.ts | 81 ++++++++---- .../data-grid/render/data-grid-render.blit.ts | 52 ++++---- .../render/data-grid-render.cells.ts | 4 + .../render/data-grid-render.header.ts | 5 +- .../render/data-grid-render.lines.ts | 38 +++++- .../data-grid/render/data-grid-render.ts | 46 +++++-- .../data-grid/render/data-grid-render.walk.ts | 15 ++- .../render/data-grid.render.rings.ts | 13 +- .../data-grid/render/draw-grid-arg.ts | 2 +- .../scrolling-data-grid.tsx | 14 +-- 14 files changed, 347 insertions(+), 126 deletions(-) diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx index 32a42a625..b94d488aa 100644 --- a/packages/core/src/data-editor/data-editor.tsx +++ b/packages/core/src/data-editor/data-editor.tsx @@ -913,6 +913,9 @@ const DataEditorImpl: React.ForwardRefRenderFunction { if (typeof window === "undefined") return { fontSize: "16px" }; return window.getComputedStyle(document.documentElement); @@ -1563,9 +1566,13 @@ const DataEditorImpl: React.ForwardRefRenderFunction= mangledCols.length - freezeRightColumns; i--) { + frozenRightWidth += columns[i].width; } let trailingRowHeight = 0; const freezeTrailingRowsEffective = freezeTrailingRows + (lastRowSticky ? 1 : 0); @@ -1578,8 +1585,8 @@ const DataEditorImpl: React.ForwardRefRenderFunction= mangledCols.length - freezeRightColumns)) + ) { scrollX = 0; } else if ( dir === "horizontal" || @@ -1654,7 +1665,9 @@ const DataEditorImpl: React.ForwardRefRenderFunction 0) { freezeRegions.push({ x: region.x - rowMarkerOffset, @@ -2590,11 +2614,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, }); } @@ -2609,7 +2642,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction = (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 883c541eb..d63210774 100644 --- a/packages/core/src/internal/data-grid/data-grid.tsx +++ b/packages/core/src/internal/data-grid/data-grid.tsx @@ -72,7 +72,7 @@ export interface DataGridProps { readonly accessibilityHeight: number; - readonly freezeColumns: number; + readonly freezeColumns: number | [left: number, right: number]; readonly freezeTrailingRows: number; readonly hasAppendRow: boolean; readonly firstColAccessible: boolean; @@ -403,7 +403,9 @@ 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 = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + 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 +453,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] ); @@ -478,7 +480,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, translateX, translateY, rows, - freezeColumns, + freezeLeftColumns, freezeTrailingRows, mappedColumns, rowHeight @@ -506,7 +508,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, translateX, translateY, rows, - freezeColumns, + freezeLeftColumns, freezeTrailingRows, mappedColumns, rowHeight, @@ -526,7 +528,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 +554,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 @@ -723,6 +732,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, fillHandle, selection, totalHeaderHeight, + freezeColumns, ] ); @@ -969,14 +979,14 @@ const DataGrid: React.ForwardRefRenderFunction = (p, const cursor = isDragging ? "grabbing" : canDrag || isResizing - ? "col-resize" - : overFill || isFilling - ? "crosshair" - : cursorOverride !== undefined - ? cursorOverride - : headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered - ? "pointer" - : "default"; + ? "col-resize" + : overFill || isFilling + ? "crosshair" + : cursorOverride !== undefined + ? cursorOverride + : headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered + ? "pointer" + : "default"; const style = React.useMemo( () => ({ @@ -1239,13 +1249,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, } } }, - [ - eventTargetRef, - getMouseArgsForPosition, - isOverHeaderElement, - onHeaderMenuClick, - onHeaderIndicatorClick, - ] + [eventTargetRef, getMouseArgsForPosition, isOverHeaderElement, onHeaderMenuClick, onHeaderIndicatorClick] ); useEventListener("click", onClickImpl, windowEventTarget, false); @@ -1738,7 +1742,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); @@ -1872,33 +1883,59 @@ 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); + + const opacityXRight = + freezeRightColumns === 0 || !fixedShadowX + ? 0 + : cellXOffset + width < columns.length - freezeRightColumns + ? 1 + : clamp((translateX - (columns.length - freezeRightColumns - width) * 32) / 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, @@ -1907,17 +1944,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..6495a1ca3 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,7 @@ 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 | [left: number, right: number], freezeRows: 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..8fd5bb604 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 @@ -17,12 +17,16 @@ 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 | [left: number, right: number] ): readonly MappedGridColumn[] { + const freezeColumnsLeft = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeColumnsRight = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + return React.useMemo( () => columns.map( @@ -35,7 +39,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 +56,7 @@ export function useMappedColumns( headerRowMarkerDisabled: c.headerRowMarkerDisabled, }) ), - [columns, freezeColumns] + [columns, freezeColumnsLeft, freezeColumnsRight] ); } @@ -174,16 +180,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 +221,7 @@ export function getEffectiveColumns( columns: readonly MappedGridColumn[], cellXOffset: number, width: number, + freezeColumns: number | [left: number, right: number], dndState?: { src: number; dest: number; @@ -214,14 +230,14 @@ export function getEffectiveColumns( ): readonly MappedGridColumn[] { const mappedCols = remapForDnDState(columns, dndState); + const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + 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; @@ -242,22 +258,42 @@ export function getEffectiveColumns( } } + for (let i = mappedCols.length - freezeRightColumns; i < mappedCols.length; i++) { + sticky.push(mappedCols[i]); + } + return sticky; } export function getColumnIndexForX( targetX: number, effectiveColumns: readonly MappedGridColumn[], + freezeColumns: number | [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 +804,7 @@ export function computeBounds( translateX: number, translateY: number, rows: number, - freezeColumns: number, + freezeColumns: number | [left: number, right: number], freezeTrailingRows: number, mappedColumns: readonly MappedGridColumn[], rowHeight: number | ((index: number) => number) @@ -780,16 +816,19 @@ export function computeBounds( height: 0, }; + const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + if (col >= mappedColumns.length || row >= rows || row < -2 || col < 0) { return result; } const headerHeight = totalHeaderHeight - groupHeaderHeight; - if (col >= freezeColumns) { + if (col >= freezeLeftColumns) { const dir = cellXOffset > col ? -1 : 1; - const freezeWidth = getStickyWidth(mappedColumns); - result.x += freezeWidth + translateX; + const [freezeLeftWidth, freezeRightWidth] = getStickyWidth(mappedColumns); + result.x += freezeLeftWidth + translateX; for (let i = cellXOffset; i !== col; i += dir) { result.x += mappedColumns[dir === 1 ? i : i - 1].width * dir; } @@ -832,8 +871,8 @@ export function computeBounds( end++; } if (!sticky) { - const freezeWidth = getStickyWidth(mappedColumns); - const clip = result.x - freezeWidth; + const [freezeLeftWidth, freezeRightWidth] = 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..a6c9806d2 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,22 +128,22 @@ 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({ @@ -157,14 +157,14 @@ 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); } @@ -200,7 +200,8 @@ export function blitResizedCol( height: number, totalHeaderHeight: number, effectiveCols: readonly MappedGridColumn[], - resizedIndex: number + resizedIndex: number, + freezeTrailingColumns: number ) { const drawRegions: Rectangle[] = []; @@ -215,18 +216,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..fed1f21f4 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,10 +126,12 @@ export function drawCells( walkColumns( effectiveColumns, + width, cellYOffset, translateX, translateY, totalHeaderHeight, + freezeTrailingColumns, (c, drawX, colDrawStartY, clipX, startRow) => { const diff = Math.max(0, clipX - drawX); 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..daf594b85 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,7 +55,7 @@ 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) => { + walkColumns(effectiveCols, width, 0, translateX, 0, totalHeaderHeight, freezeTrailingColumns, (c, x, _y, clipX) => { if (damage !== undefined && !damage.has([c.sourceIndex, -1])) return; const diff = Math.max(0, clipX - x); ctx.save(); 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..c1a58191e 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,10 +42,12 @@ export function drawBlanks( walkColumns( effectiveColumns, + width, cellYOffset, translateX, translateY, totalHeaderHeight, + freezeTrailingColumns, (c, drawX, colDrawY, clipX, startRow) => { if (c !== effectiveColumns[effectiveColumns.length - 1]) return; drawX += c.width; @@ -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); @@ -332,6 +344,24 @@ export function drawGridLines( } } + // let rightX = 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 = c.sticky ? rightX : rightX + translateX; + // 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..b1592eb44 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 @@ -33,6 +33,7 @@ function clipHeaderDamage( translateX: number, translateY: number, cellYOffset: number, + freezeTrailingColumns: number, damage: CellSet | undefined ): void { if (damage === undefined || damage.size === 0) return; @@ -53,10 +54,12 @@ function clipHeaderDamage( walkColumns( effectiveColumns, + width, cellYOffset, translateX, translateY, totalHeaderHeight, + freezeTrailingColumns, (c, drawX, _colDrawY, clipX) => { const diff = Math.max(0, clipX - drawX); @@ -73,6 +76,7 @@ function clipHeaderDamage( function getLastRow( effectiveColumns: readonly MappedGridColumn[], height: number, + width: number, totalHeaderHeight: number, translateX: number, translateY: number, @@ -80,15 +84,18 @@ 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, + freezeTrailingColumns, (_c, __drawX, colDrawY, _clipX, startRow) => { walkRowsInCol( startRow, @@ -171,6 +178,9 @@ 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 = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + // 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 +263,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 +304,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getGroupDetails, damage, drawHeaderCallback, - touchMode + touchMode, + freezeRightColumns ); drawGridLines( @@ -357,6 +375,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getRowHeight, getCellContent, freezeTrailingRows, + freezeRightColumns, hasAppendRow, fillHandle, rows @@ -383,13 +402,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, }, { @@ -407,6 +426,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { effectiveCols, mappedColumns, height, + width, totalHeaderHeight, translateX, translateY, @@ -420,6 +440,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { isFocused, drawFocus, freezeTrailingRows, + freezeRightColumns, hasAppendRow, drawRegions, damage, @@ -463,6 +484,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getRowHeight, getCellContent, freezeTrailingRows, + freezeRightColumns, hasAppendRow, fillHandle, rows @@ -491,6 +513,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { translateX, translateY, cellYOffset, + freezeRightColumns, damage ); drawHeaderTexture(); @@ -550,7 +573,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { height, totalHeaderHeight, effectiveCols, - resizedCol + resizedCol, + freezeRightColumns ); } @@ -602,6 +626,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { getRowHeight, getCellContent, freezeTrailingRows, + freezeRightColumns, hasAppendRow, fillHandle, rows @@ -626,6 +651,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { effectiveCols, mappedColumns, height, + width, totalHeaderHeight, translateX, translateY, @@ -639,6 +665,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { isFocused, drawFocus, freezeTrailingRows, + freezeRightColumns, hasAppendRow, drawRegions, damage, @@ -675,6 +702,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { selection.rows, disabledRows, freezeTrailingRows, + freezeRightColumns, hasAppendRow, drawRegions, damage, @@ -723,7 +751,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 +784,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { const lastRowDrawn = getLastRow( effectiveCols, height, + width, totalHeaderHeight, translateX, translateY, @@ -763,7 +792,8 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { rows, getRowHeight, freezeTrailingRows, - hasAppendRow + hasAppendRow, + freezeRightColumns ); imageLoader?.setWindow( 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..d56cba716 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 @@ -65,16 +65,20 @@ export type WalkColsCallback = ( 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) { + + 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) { break; @@ -83,6 +87,15 @@ export function walkColumns( 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, cellYOffset); + } } // this should not be item, it is [startInclusive, endInclusive] 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..31c0be305 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 @@ -17,7 +17,7 @@ export function drawHighlightRings( translateX: number, translateY: number, mappedColumns: readonly MappedGridColumn[], - freezeColumns: number, + freezeColumns: number | [left: number, right: number], headerHeight: number, groupHeaderHeight: number, rowHeight: number | ((index: number) => number), @@ -27,19 +27,21 @@ export function drawHighlightRings( theme: FullTheme ): (() => void) | undefined { const highlightRegions = allHighlightRegions?.filter(x => x.style !== "no-outline"); + const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; 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 splitIndices = [freezeLeftColumns, 0, mappedColumns.length, rows - freezeTrailingRows] as const; const splitLocations = [freezeLeft, 0, width, 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 +189,7 @@ export function drawFillHandle( getRowHeight: (row: number) => number, getCellContent: (cell: Item) => InnerGridCell, freezeTrailingRows: number, + freezeTrailingColumns: number, hasAppendRow: boolean, fillHandle: FillHandle, rows: number @@ -223,10 +226,12 @@ export function drawFillHandle( walkColumns( effectiveCols, + width, cellYOffset, translateX, translateY, totalHeaderHeight, + freezeTrailingColumns, (col, drawX, colDrawY, clipX, startRow) => { clipX; if (col.sticky && targetCol > col.sourceIndex) return; 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..b352d7fe2 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 | [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 841eeefbe..d9ce19ad9 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 @@ -102,6 +102,9 @@ const GridScroller: React.FunctionComponent = p => { const lastY = React.useRef(); const lastSize = React.useRef(); + const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const freezeRightColumns = typeof freezeColumns === "number"? 0 : freezeColumns[1]; + 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; } @@ -220,12 +223,7 @@ const GridScroller: React.FunctionComponent = p => { args.height !== lastSize.current?.[1] ) { onVisibleRegionChanged?.( - { - x: cellX, - y: cellY, - width: cellRight - cellX, - height: cellBottom - cellY, - }, + rect, args.width, args.height, args.paddingRight ?? 0, @@ -237,7 +235,7 @@ const GridScroller: React.FunctionComponent = p => { 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 }) => { From f2b977f62ef021fd56936efb791af85cc9c55836 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Mon, 1 Jul 2024 17:17:09 +0530 Subject: [PATCH 03/13] revert workflow file --- .github/workflows/release.js.yml | 72 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release.js.yml b/.github/workflows/release.js.yml index e49555160..4924ac7b0 100644 --- a/.github/workflows/release.js.yml +++ b/.github/workflows/release.js.yml @@ -28,39 +28,39 @@ jobs: token: ${{ secrets.NPM_TOKEN }} access: public package: ./packages/core/package.json - # publish-cells: - # defaults: - # run: - # working-directory: ./packages/cells - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # with: - # node-version: 20.10.0 - # - name: Bootstrap - # run: npm install && npm run build - # working-directory: . - # - uses: JS-DevTools/npm-publish@v1 - # with: - # token: ${{ secrets.NPM_TOKEN }} - # access: public - # package: ./packages/cells/package.json - # publish-source: - # defaults: - # run: - # working-directory: ./packages/source - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # with: - # node-version: 20.10.0 - # - name: Bootstrap - # run: npm install && npm run build - # working-directory: . - # - uses: JS-DevTools/npm-publish@v1 - # with: - # token: ${{ secrets.NPM_TOKEN }} - # access: public - # package: ./packages/source/package.json + publish-cells: + defaults: + run: + working-directory: ./packages/cells + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.10.0 + - name: Bootstrap + run: npm install && npm run build + working-directory: . + - uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.NPM_TOKEN }} + access: public + package: ./packages/cells/package.json + publish-source: + defaults: + run: + working-directory: ./packages/source + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.10.0 + - name: Bootstrap + run: npm install && npm run build + working-directory: . + - uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.NPM_TOKEN }} + access: public + package: ./packages/source/package.json From bf5af23114ea247828323a572de079ba379e1109 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Tue, 2 Jul 2024 15:27:12 +0530 Subject: [PATCH 04/13] fixed selection issue and highlight fixes --- packages/core/src/data-editor/data-editor.tsx | 5 +-- .../core/src/internal/data-grid/data-grid.tsx | 7 +++- .../data-grid/render/data-grid-lib.ts | 16 ++++++--- .../data-grid/render/data-grid-render.blit.ts | 15 ++++++-- .../render/data-grid-render.cells.ts | 2 +- .../render/data-grid-render.header.ts | 5 +++ .../render/data-grid-render.lines.ts | 35 ++++++++++--------- .../data-grid/render/data-grid-render.ts | 12 +++++++ .../render/data-grid.render.rings.ts | 9 +++-- .../scrolling-data-grid.tsx | 10 +----- 10 files changed, 77 insertions(+), 39 deletions(-) diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx index b94d488aa..4660f6f4a 100644 --- a/packages/core/src/data-editor/data-editor.tsx +++ b/packages/core/src/data-editor/data-editor.tsx @@ -913,7 +913,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction { @@ -1585,7 +1585,8 @@ const DataEditorImpl: 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); @@ -733,6 +737,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, selection, totalHeaderHeight, freezeColumns, + freezeRightColumns, ] ); 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 8fd5bb604..5b075231f 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 @@ -279,7 +279,7 @@ export function getColumnIndexForX( const colIdx = effectiveColumns.length - 1 - fc; const col = effectiveColumns[colIdx]; y -= col.width; - if (targetX <= y) { + if (targetX >= y) { return col.sourceIndex; } } @@ -818,6 +818,7 @@ export function computeBounds( const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + const column = mappedColumns[col]; if (col >= mappedColumns.length || row >= rows || row < -2 || col < 0) { return result; @@ -825,17 +826,22 @@ export function computeBounds( const headerHeight = totalHeaderHeight - groupHeaderHeight; - if (col >= freezeLeftColumns) { + if (col >= freezeLeftColumns && col < mappedColumns.length - freezeRightColumns) { const dir = cellXOffset > col ? -1 : 1; - const [freezeLeftWidth, freezeRightWidth] = getStickyWidth(mappedColumns); + 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; @@ -871,7 +877,7 @@ export function computeBounds( end++; } if (!sticky) { - const [freezeLeftWidth, freezeRightWidth] = getStickyWidth(mappedColumns); + const [freezeLeftWidth] = getStickyWidth(mappedColumns); const clip = result.x - freezeLeftWidth; if (clip < 0) { result.x -= 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 a6c9806d2..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 @@ -147,9 +147,9 @@ export function blitLastFrame( args.dw = blitWidth * dpr; drawRegions.push({ - x: width + deltaX, + x: width + deltaX - stickyRightWidth, y: 0, - width: -deltaX, + width: -deltaX + stickyRightWidth, height: height, }); } @@ -168,6 +168,17 @@ export function blitLastFrame( 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 && 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 fed1f21f4..a5cd1a46d 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 @@ -299,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); } 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 daf594b85..f1b2c62c0 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 @@ -69,6 +69,11 @@ export function drawGridHeaders( ? outerTheme : mergeAndRealizeTheme(outerTheme, groupTheme, c.themeOverride); + if (c.sticky) { + ctx.fillStyle = theme.bgHeader; + ctx.fill(); + } + if (theme.bgHeader !== outerTheme.bgHeader) { ctx.fillStyle = theme.bgHeader; ctx.fill(); 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 c1a58191e..a4e0fb8aa 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 @@ -331,6 +331,7 @@ export function drawGridLines( for (let index = 0; index < effectiveCols.length; index++) { const c = effectiveCols[index]; if (c.width === 0) continue; + if (c.sticky && c.stickyPosition !== "left") break; x += c.width; const tx = c.sticky ? x : x + translateX; if (tx >= minX && tx <= maxX && verticalBorder(index + 1)) { @@ -344,23 +345,23 @@ export function drawGridLines( } } - // let rightX = 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 = c.sticky ? rightX : rightX + translateX; - // 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 rightX = width + 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++) { 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 b1592eb44..853e415e2 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 @@ -418,6 +418,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) => { 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 31c0be305..766319b38 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 @@ -34,8 +34,13 @@ export function drawHighlightRings( const [freezeLeft, freezeRight] = getStickyWidth(mappedColumns); const freezeBottom = getFreezeTrailingHeight(rows, freezeTrailingRows, rowHeight); - const splitIndices = [freezeLeftColumns, 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; 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 d9ce19ad9..63af6dfdf 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 @@ -103,7 +103,6 @@ const GridScroller: React.FunctionComponent = p => { const lastSize = React.useRef(); const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; - const freezeRightColumns = typeof freezeColumns === "number"? 0 : freezeColumns[1]; const width = nonGrowWidth + Math.max(0, overscrollX ?? 0); @@ -222,14 +221,7 @@ const GridScroller: React.FunctionComponent = p => { args.width !== lastSize.current?.[0] || args.height !== lastSize.current?.[1] ) { - onVisibleRegionChanged?.( - rect, - 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; From 90d596ba3231207b1e2ddb35b5ccdfc62ca2a210 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Sun, 22 Jun 2025 17:16:26 +0400 Subject: [PATCH 05/13] freeze right columns: calculations, hovers, groupings and shadows --- .../core/src/internal/data-grid/data-grid.tsx | 23 +- .../data-grid/render/data-grid-lib.ts | 17 +- .../render/data-grid-render.cells.ts | 14 +- .../render/data-grid-render.header.ts | 367 ++++++++++-------- .../render/data-grid-render.lines.ts | 4 +- .../data-grid/render/data-grid-render.ts | 71 ++-- .../data-grid/render/data-grid-render.walk.ts | 50 ++- .../render/data-grid.render.rings.ts | 35 +- packages/core/test/data-editor.test.tsx | 47 +++ packages/core/test/data-grid.test.tsx | 67 ++++ packages/source/src/use-collapsing-groups.ts | 11 +- 11 files changed, 465 insertions(+), 241 deletions(-) diff --git a/packages/core/src/internal/data-grid/data-grid.tsx b/packages/core/src/internal/data-grid/data-grid.tsx index e75e98fff..9e7f84889 100644 --- a/packages/core/src/internal/data-grid/data-grid.tsx +++ b/packages/core/src/internal/data-grid/data-grid.tsx @@ -480,7 +480,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, translateX, translateY, rows, - freezeLeftColumns, + freezeColumns, freezeTrailingRows, mappedColumns, rowHeight @@ -508,7 +508,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, translateX, translateY, rows, - freezeLeftColumns, + freezeColumns, freezeTrailingRows, mappedColumns, rowHeight, @@ -1900,12 +1900,27 @@ const DataGrid: React.ForwardRefRenderFunction = (p, ? 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 + width < columns.length - freezeRightColumns + : cellXOffset + + getEffectiveColumns( + mappedColumns, + cellXOffset, + width, + freezeColumns, + dragAndDropState, + translateX + ).filter(column => !column.sticky).length < + columns.length - freezeRightColumns ? 1 - : clamp((translateX - (columns.length - freezeRightColumns - width) * 32) / 100, 0, 1); + : clamp(-translateXRight / 100, 0, 1); const absoluteOffsetY = -cellYOffset * 32 + translateY; const opacityY = !fixedShadowY ? 0 : clamp(-absoluteOffsetY / 100, 0, 1); 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 5b075231f..eef07c677 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 @@ -243,6 +243,19 @@ export function getEffectiveColumns( 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; @@ -258,9 +271,7 @@ export function getEffectiveColumns( } } - for (let i = mappedCols.length - freezeRightColumns; i < mappedCols.length; i++) { - sticky.push(mappedCols[i]); - } + sticky.push(...stickyRight); return sticky; } 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 a5cd1a46d..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 @@ -132,12 +132,12 @@ export function drawCells( translateY, totalHeaderHeight, freezeTrailingColumns, - (c, drawX, colDrawStartY, clipX, startRow) => { + (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; @@ -555,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 f1b2c62c0..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 @@ -55,85 +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, width, 0, translateX, 0, totalHeaderHeight, freezeTrailingColumns, (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 (c.sticky) { - ctx.fillStyle = theme.bgHeader; - ctx.fill(); - } - - 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( @@ -148,7 +170,8 @@ export function drawGridHeaders( hoverValues, verticalBorder, getGroupDetails, - damage + damage, + freezeTrailingColumns ); } } @@ -165,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 a4e0fb8aa..d142cd023 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 @@ -48,7 +48,7 @@ export function drawBlanks( translateY, totalHeaderHeight, freezeTrailingColumns, - (c, drawX, colDrawY, clipX, startRow) => { + (c, drawX, colDrawY, clipX, _clipXRight, startRow) => { if (c !== effectiveColumns[effectiveColumns.length - 1]) return; drawX += c.width; const x = Math.max(drawX, clipX); @@ -331,7 +331,7 @@ export function drawGridLines( for (let index = 0; index < effectiveCols.length; index++) { const c = effectiveCols[index]; if (c.width === 0) continue; - if (c.sticky && c.stickyPosition !== "left") break; + 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)) { 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 853e415e2..71f1c4da0 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 @@ -40,17 +40,24 @@ function clipHeaderDamage( 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, @@ -60,11 +67,11 @@ function clipHeaderDamage( translateY, totalHeaderHeight, freezeTrailingColumns, - (c, drawX, _colDrawY, clipX) => { + (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); // c.width - diff - 1; if (damage.has([c.sourceIndex, -1])) { ctx.rect(finalX, groupHeaderHeight, finalWidth, totalHeaderHeight - groupHeaderHeight); } @@ -96,7 +103,7 @@ function getLastRow( translateY, totalHeaderHeight, freezeTrailingColumns, - (_c, __drawX, colDrawY, _clipX, startRow) => { + (_c, __drawX, colDrawY, _clipX, _clipXRight, startRow) => { walkRowsInCol( startRow, colDrawY, @@ -624,25 +631,25 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { // the overdraw may have nuked out our focus ring right edge. const focusRedraw = drawFocus ? drawFillHandle( - targetCtx, - width, - height, - cellYOffset, - translateX, - translateY, - effectiveCols, - mappedColumns, - theme, - totalHeaderHeight, - selection, - getRowHeight, - getCellContent, - freezeTrailingRows, - freezeRightColumns, - hasAppendRow, - fillHandle, - rows - ) + targetCtx, + width, + height, + cellYOffset, + translateX, + translateY, + effectiveCols, + mappedColumns, + theme, + totalHeaderHeight, + selection, + getRowHeight, + getCellContent, + freezeTrailingRows, + freezeRightColumns, + hasAppendRow, + fillHandle, + rows + ) : undefined; targetCtx.fillStyle = theme.bgCell; 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 d56cba716..13ddaf128 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,6 +60,7 @@ export type WalkColsCallback = ( drawX: number, drawY: number, clipX: number, + clipXRight: number, startRow: number ) => boolean | void; @@ -76,11 +77,12 @@ export function walkColumns( let x = 0; let clipX = 0; // this tracks the total width of sticky cols const drawY = totalHeaderHeight + translateY; + const clipXRight = freezeTrailingColumns === 0 ? 0 : effectiveCols.slice(-freezeTrailingColumns).reduce((acc, col) => acc + col.width, 0); 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; } @@ -94,7 +96,7 @@ export function walkColumns( const drawX = x - c.width; x -= c.width; - cb(c, drawX, drawY, clipX, cellYOffset); + cb(c, drawX, drawY, clipX, clipXRight, cellYOffset); } } @@ -113,11 +115,17 @@ 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++) { + + const effectiveColsRight = freezeTrailingColumns === 0 ? [] : effectiveCols.slice(-freezeTrailingColumns); + const widthRight = effectiveColsRight.reduce((acc, col) => acc + col.width, 0); + width -= widthRight; + + for (let index = 0; index < effectiveCols.length - freezeTrailingColumns; index++) { const startCol = effectiveCols[index]; let end = index + 1; @@ -154,6 +162,42 @@ export function walkGroups( x += boxWidth; } + + for (let index = 0; index < effectiveColsRight.length; index++) { + const startCol = effectiveColsRight[index]; + + let end = index + 1; + let boxWidth = startCol.width; + + while ( + end < effectiveColsRight.length && + isGroupEqual(effectiveColsRight[end].group, startCol.group) && + effectiveColsRight[end].sticky === effectiveColsRight[index].sticky + ) { + const endCol = effectiveColsRight[end]; + boxWidth += endCol.width; + end++; + index++; + if (endCol.sticky) { + clipX += endCol.width; + } + } + + const t = width + boxWidth; + const localX = t - boxWidth; + const delta = 0; + const w = boxWidth - delta; + cb( + [startCol.sourceIndex, effectiveColsRight[end - 1].sourceIndex], + startCol.group ?? "", + localX + delta, + 0, + w, + groupHeaderHeight + ); + + width += 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 766319b38..b60d489f9 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 @@ -69,22 +69,22 @@ export function drawHighlightRings( rect.width === 1 && rect.height === 1 ? topLeftBounds : computeBounds( - rect.x + rect.width - 1, - rect.y + rect.height - 1, - width, - height, - groupHeaderHeight, - headerHeight + groupHeaderHeight, - cellXOffset, - cellYOffset, - translateX, - translateY, - rows, - freezeColumns, - freezeTrailingRows, - mappedColumns, - rowHeight - ); + rect.x + rect.width - 1, + rect.y + rect.height - 1, + width, + height, + groupHeaderHeight, + headerHeight + groupHeaderHeight, + cellXOffset, + cellYOffset, + translateX, + translateY, + rows, + freezeColumns, + freezeTrailingRows, + mappedColumns, + rowHeight + ); if (rect.x + rect.width >= mappedColumns.length) { bottomRightBounds.width -= 1; @@ -237,8 +237,7 @@ export function drawFillHandle( translateY, totalHeaderHeight, freezeTrailingColumns, - (col, drawX, colDrawY, clipX, startRow) => { - clipX; + (col, drawX, colDrawY, clipX, _clipXRight, startRow) => { if (col.sticky && targetCol > col.sourceIndex) return; const isBeforeTarget = col.sourceIndex < targetColSpan[0]; diff --git a/packages/core/test/data-editor.test.tsx b/packages/core/test/data-editor.test.tsx index 161e2d764..45cb3645b 100644 --- a/packages/core/test/data-editor.test.tsx +++ b/packages/core/test/data-editor.test.tsx @@ -2276,6 +2276,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..4f19fa648 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.mouseDown(screen.getByTestId(dataGridCanvasId), { + clientX: 950, // Col A + clientY: 36 + 32 * 5 + 16, // Row 5 (0 indexed) + }); + + fireEvent.mouseUp(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/source/src/use-collapsing-groups.ts b/packages/source/src/use-collapsing-groups.ts index 3b748a506..1b3df6d96 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 as number; 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; @@ -118,8 +121,8 @@ export function useCollapsingGroups(props: Props): Result { name: group, overrideTheme: collapsed.includes(group ?? "") ? { - bgHeader: theme.bgHeaderHasFocus, - } + bgHeader: theme.bgHeaderHasFocus, + } : undefined, }; }, From 4bd0842b4d0bd8ed5838ae690dc6d752167592e9 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Thu, 3 Jul 2025 18:09:39 +0400 Subject: [PATCH 06/13] added an inline comment explaining freezeColumnsLeft and freezeColumnsRight --- packages/core/src/internal/data-grid/render/data-grid-lib.ts | 2 ++ 1 file changed, 2 insertions(+) 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 eef07c677..ebf4e063c 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 @@ -24,6 +24,8 @@ export function useMappedColumns( columns: readonly InnerGridColumn[], freezeColumns: number | [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]; From de49e6c011c0ad396b07d26d04a08d8ca93374f3 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Thu, 3 Jul 2025 18:11:04 +0400 Subject: [PATCH 07/13] added readonly flag to freezeColumns tuple prop --- .../core/src/internal/data-grid/data-grid.tsx | 2 +- .../image-window-loader-interface.ts | 6 +++- .../data-grid/render/data-grid-lib.ts | 8 ++--- .../render/data-grid.render.rings.ts | 34 +++++++++---------- .../data-grid/render/draw-grid-arg.ts | 2 +- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/packages/core/src/internal/data-grid/data-grid.tsx b/packages/core/src/internal/data-grid/data-grid.tsx index 9e7f84889..04392a0a6 100644 --- a/packages/core/src/internal/data-grid/data-grid.tsx +++ b/packages/core/src/internal/data-grid/data-grid.tsx @@ -72,7 +72,7 @@ export interface DataGridProps { readonly accessibilityHeight: number; - readonly freezeColumns: number | [left: number, right: number]; + readonly freezeColumns: number | readonly [left: number, right: number]; readonly freezeTrailingRows: number; readonly hasAppendRow: boolean; readonly firstColAccessible: boolean; 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 6495a1ca3..a06c1033f 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,11 @@ import type { Rectangle } from "./data-grid-types.js"; /** @category Types */ export interface ImageWindowLoader { - setWindow(newWindow: Rectangle, freezeCols: number | [left: number, right: number], freezeRows: number[]): void; + setWindow( + newWindow: Rectangle, + freezeCols: number | readonly [left: number, right: number], + freezeRows: 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 ebf4e063c..bf10cf4dd 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 @@ -22,7 +22,7 @@ export interface MappedGridColumn extends FullyDefined { export function useMappedColumns( columns: readonly InnerGridColumn[], - freezeColumns: number | [left: number, right: 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. @@ -223,7 +223,7 @@ export function getEffectiveColumns( columns: readonly MappedGridColumn[], cellXOffset: number, width: number, - freezeColumns: number | [left: number, right: number], + freezeColumns: number | readonly [left: number, right: number], dndState?: { src: number; dest: number; @@ -281,7 +281,7 @@ export function getEffectiveColumns( export function getColumnIndexForX( targetX: number, effectiveColumns: readonly MappedGridColumn[], - freezeColumns: number | [left: number, right: number], + freezeColumns: number | readonly [left: number, right: number], width: number, translateX?: number ): number { @@ -817,7 +817,7 @@ export function computeBounds( translateX: number, translateY: number, rows: number, - freezeColumns: number | [left: number, right: number], + freezeColumns: number | readonly [left: number, right: number], freezeTrailingRows: number, mappedColumns: readonly MappedGridColumn[], rowHeight: number | ((index: number) => number) 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 b60d489f9..723ad4a23 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 @@ -17,7 +17,7 @@ export function drawHighlightRings( translateX: number, translateY: number, mappedColumns: readonly MappedGridColumn[], - freezeColumns: number | [left: number, right: number], + freezeColumns: number | readonly [left: number, right: number], headerHeight: number, groupHeaderHeight: number, rowHeight: number | ((index: number) => number), @@ -69,22 +69,22 @@ export function drawHighlightRings( rect.width === 1 && rect.height === 1 ? topLeftBounds : computeBounds( - rect.x + rect.width - 1, - rect.y + rect.height - 1, - width, - height, - groupHeaderHeight, - headerHeight + groupHeaderHeight, - cellXOffset, - cellYOffset, - translateX, - translateY, - rows, - freezeColumns, - freezeTrailingRows, - mappedColumns, - rowHeight - ); + rect.x + rect.width - 1, + rect.y + rect.height - 1, + width, + height, + groupHeaderHeight, + headerHeight + groupHeaderHeight, + cellXOffset, + cellYOffset, + translateX, + translateY, + rows, + freezeColumns, + freezeTrailingRows, + mappedColumns, + rowHeight + ); if (rect.x + rect.width >= mappedColumns.length) { bottomRightBounds.width -= 1; 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 b352d7fe2..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 | [left: number, right: number]; + readonly freezeColumns: number | readonly [left: number, right: number]; readonly dragAndDropState: DragAndDropState | undefined; readonly theme: FullTheme; readonly headerHeight: number; From eeebf214009c865b303ddfb168581f286012b233 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Thu, 3 Jul 2025 18:12:54 +0400 Subject: [PATCH 08/13] fix scrollTo issue not finding a column --- packages/core/src/data-editor/data-editor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx index 4660f6f4a..969df22ef 100644 --- a/packages/core/src/data-editor/data-editor.tsx +++ b/packages/core/src/data-editor/data-editor.tsx @@ -1323,7 +1323,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction= mangledCols.length - freezeRightColumns; i--) { + for (let i = columns.length - 1; i >= columns.length - 1 - freezeRightColumns; i--) { frozenRightWidth += columns[i].width; } let trailingRowHeight = 0; @@ -3978,7 +3978,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction { return typeof verticalBorder === "boolean" ? verticalBorder - : verticalBorder?.(col - rowMarkerOffset) ?? true; + : (verticalBorder?.(col - rowMarkerOffset) ?? true); }, [rowMarkerOffset, verticalBorder] ); From d8493f075895e6149cf7afaf52568efea7bc82f6 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Tue, 8 Jul 2025 16:39:02 +0400 Subject: [PATCH 09/13] fix image loader doesn't take into account freezeColumns new api --- .../core/src/common/render-state-provider.ts | 15 ++++-- .../image-window-loader-interface.ts | 3 +- .../data-grid/render/data-grid-render.ts | 46 ++++++++++--------- .../core/test/image-window-loader.test.ts | 27 ++++++++--- .../core/test/render-state-provider.test.ts | 2 +- 5 files changed, 61 insertions(+), 32 deletions(-) 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/internal/data-grid/image-window-loader-interface.ts b/packages/core/src/internal/data-grid/image-window-loader-interface.ts index a06c1033f..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 @@ -6,7 +6,8 @@ export interface ImageWindowLoader { setWindow( newWindow: Rectangle, freezeCols: number | readonly [left: number, right: number], - freezeRows: 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-render.ts b/packages/core/src/internal/data-grid/render/data-grid-render.ts index 71f1c4da0..05190a69e 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 @@ -71,7 +71,10 @@ function clipHeaderDamage( const diff = Math.max(0, clipX - drawX); const finalX = drawX + diff + 1; - const finalWidth = c.stickyPosition === "right" ? c.width - diff : Math.min(c.width - diff - 1, width - drawX - clipXRight); // 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); } @@ -631,25 +634,25 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) { // the overdraw may have nuked out our focus ring right edge. const focusRedraw = drawFocus ? drawFillHandle( - targetCtx, - width, - height, - cellYOffset, - translateX, - translateY, - effectiveCols, - mappedColumns, - theme, - totalHeaderHeight, - selection, - getRowHeight, - getCellContent, - freezeTrailingRows, - freezeRightColumns, - hasAppendRow, - fillHandle, - rows - ) + targetCtx, + width, + height, + cellYOffset, + translateX, + translateY, + effectiveCols, + mappedColumns, + theme, + totalHeaderHeight, + selection, + getRowHeight, + getCellContent, + freezeTrailingRows, + freezeRightColumns, + hasAppendRow, + fillHandle, + rows + ) : undefined; targetCtx.fillStyle = theme.bgCell; @@ -823,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/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); }); From 28c644497ffd93da3dabf9e3ccfbc0783229e64b Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Tue, 15 Jul 2025 10:49:28 +0400 Subject: [PATCH 10/13] fix vertical lines calculation issue --- .../src/internal/data-grid/render/data-grid-render.lines.ts | 3 +++ 1 file changed, 3 insertions(+) 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 d142cd023..ccd598f3a 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 @@ -317,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; @@ -345,6 +347,7 @@ export function drawGridLines( } } + width = Math.min(width, effectiveWidth); let rightX = width + 0.5; for (let index = effectiveCols.length - 1; index >= 0; index--) { const c = effectiveCols[index]; From baa35c8f4b73cebf5af89ade36fa98f43f213208 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Fri, 8 Aug 2025 12:18:27 +0400 Subject: [PATCH 11/13] add utility function to extract left and right freeze, minor fixes --- packages/core/src/common/utils.tsx | 7 +++++++ packages/core/src/data-editor/data-editor.tsx | 7 +++---- packages/core/src/internal/data-grid/data-grid.tsx | 11 ++++++++--- .../src/internal/data-grid/render/data-grid-lib.ts | 8 +++----- .../src/internal/data-grid/render/data-grid-render.ts | 4 ++-- .../data-grid/render/data-grid.render.rings.ts | 4 ++-- .../scrolling-data-grid/scrolling-data-grid.tsx | 3 ++- packages/source/src/use-collapsing-groups.ts | 6 +++--- 8 files changed, 30 insertions(+), 20 deletions(-) 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 969df22ef..c340cc26d 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, @@ -913,8 +913,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction { if (typeof window === "undefined") return { fontSize: "16px" }; @@ -1571,7 +1570,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction= columns.length - 1 - freezeRightColumns; i--) { + for (let i = columns.length - 1; i >= columns.length - freezeRightColumns; i--) { frozenRightWidth += columns[i].width; } let trailingRowHeight = 0; diff --git a/packages/core/src/internal/data-grid/data-grid.tsx b/packages/core/src/internal/data-grid/data-grid.tsx index 04392a0a6..33dda876b 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"; @@ -403,8 +409,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, } = p; const translateX = p.translateX ?? 0; const translateY = p.translateY ?? 0; - const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; - const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns); const cellXOffset = Math.max(freezeLeftColumns, Math.min(columns.length - 1, cellXOffsetReal)); const ref = React.useRef(null); 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 bf10cf4dd..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"; @@ -232,8 +232,7 @@ export function getEffectiveColumns( ): readonly MappedGridColumn[] { const mappedCols = remapForDnDState(columns, dndState); - const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; - const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns); const sticky: MappedGridColumn[] = []; for (let i = 0; i < freezeLeftColumns; i++) { @@ -829,8 +828,7 @@ export function computeBounds( height: 0, }; - const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; - const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns); const column = mappedColumns[col]; if (col >= mappedColumns.length || row >= rows || row < -2 || col < 0) { 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 05190a69e..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 @@ -188,8 +189,7 @@ 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 = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; - const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[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); 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 723ad4a23..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, @@ -27,8 +28,7 @@ export function drawHighlightRings( theme: FullTheme ): (() => void) | undefined { const highlightRegions = allHighlightRegions?.filter(x => x.style !== "no-outline"); - const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; - const freezeRightColumns = typeof freezeColumns === "number" ? 0 : freezeColumns[1]; + const [freezeLeftColumns, freezeRightColumns] = normalizeFreezeColumns(freezeColumns); if (highlightRegions === undefined || highlightRegions.length === 0) return undefined; 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 63af6dfdf..2ba1bfb45 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,7 +103,7 @@ const GridScroller: React.FunctionComponent = p => { const lastY = React.useRef(); const lastSize = React.useRef(); - const freezeLeftColumns = typeof freezeColumns === "number" ? freezeColumns : freezeColumns[0]; + const [freezeLeftColumns] = normalizeFreezeColumns(freezeColumns); const width = nonGrowWidth + Math.max(0, overscrollX ?? 0); diff --git a/packages/source/src/use-collapsing-groups.ts b/packages/source/src/use-collapsing-groups.ts index 1b3df6d96..6a0a245ac 100644 --- a/packages/source/src/use-collapsing-groups.ts +++ b/packages/source/src/use-collapsing-groups.ts @@ -34,7 +34,7 @@ export function useCollapsingGroups(props: Props): Result { const result: [number, number][] = []; let current: [number, number] = [-1, -1]; let lastGroup: string | undefined; - for (let i = freezeColumnsLeft as number; i < columnsIn.length - freezeColumnsRight; i++) { + for (let i = freezeColumnsLeft; i < columnsIn.length - freezeColumnsRight; i++) { const c = columnsIn[i]; const group = c.group ?? ""; const isCollapsed = collapsed.includes(group); @@ -121,8 +121,8 @@ export function useCollapsingGroups(props: Props): Result { name: group, overrideTheme: collapsed.includes(group ?? "") ? { - bgHeader: theme.bgHeaderHasFocus, - } + bgHeader: theme.bgHeaderHasFocus, + } : undefined, }; }, From eddeb95a02961e6e5cffeed01474f38f57423ce4 Mon Sep 17 00:00:00 2001 From: Mihran Margaryan Date: Wed, 20 Aug 2025 12:23:38 +0400 Subject: [PATCH 12/13] apply minor code review impovements --- .../render/data-grid-render.lines.ts | 4 +- .../data-grid/render/data-grid-render.walk.ts | 38 ++++++++++++------- 2 files changed, 26 insertions(+), 16 deletions(-) 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 ccd598f3a..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 @@ -347,8 +347,8 @@ export function drawGridLines( } } - width = Math.min(width, effectiveWidth); - let rightX = width + 0.5; + 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; 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 13ddaf128..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 @@ -77,7 +77,11 @@ export function walkColumns( let x = 0; let clipX = 0; // this tracks the total width of sticky cols const drawY = totalHeaderHeight + translateY; - const clipXRight = freezeTrailingColumns === 0 ? 0 : effectiveCols.slice(-freezeTrailingColumns).reduce((acc, col) => acc + col.width, 0); + + 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]; @@ -121,9 +125,12 @@ export function walkGroups( let x = 0; let clipX = 0; - const effectiveColsRight = freezeTrailingColumns === 0 ? [] : effectiveCols.slice(-freezeTrailingColumns); - const widthRight = effectiveColsRight.reduce((acc, col) => acc + col.width, 0); - width -= widthRight; + // 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]; @@ -150,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 ?? "", @@ -163,18 +170,21 @@ export function walkGroups( x += boxWidth; } - for (let index = 0; index < effectiveColsRight.length; index++) { - const startCol = effectiveColsRight[index]; + 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 < effectiveColsRight.length && - isGroupEqual(effectiveColsRight[end].group, startCol.group) && - effectiveColsRight[end].sticky === effectiveColsRight[index].sticky + end < effectiveCols.length && + isGroupEqual(effectiveCols[end].group, startCol.group) && + effectiveCols[end].sticky === effectiveCols[index].sticky ) { - const endCol = effectiveColsRight[end]; + const endCol = effectiveCols[end]; boxWidth += endCol.width; end++; index++; @@ -183,12 +193,12 @@ export function walkGroups( } } - const t = width + boxWidth; + const t = currentWidth + boxWidth; const localX = t - boxWidth; const delta = 0; const w = boxWidth - delta; cb( - [startCol.sourceIndex, effectiveColsRight[end - 1].sourceIndex], + [startCol.sourceIndex, effectiveCols[end - 1].sourceIndex], startCol.group ?? "", localX + delta, 0, @@ -196,7 +206,7 @@ export function walkGroups( groupHeaderHeight ); - width += w; + currentWidth += w; } } From efd83c60ae01d2a969a1e6324c1f2e89866b6526 Mon Sep 17 00:00:00 2001 From: Sassoun Derderian Date: Fri, 22 Aug 2025 14:11:49 +0400 Subject: [PATCH 13/13] fix: ensure freeze columns cannot exceed total column count Fix test to perform click with the pointer events. --- packages/core/src/data-editor/data-editor.tsx | 9 +++++++-- packages/core/test/data-grid.test.tsx | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx index c340cc26d..39f55528d 100644 --- a/packages/core/src/data-editor/data-editor.tsx +++ b/packages/core/src/data-editor/data-editor.tsx @@ -4000,7 +4000,12 @@ 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, @@ -4268,7 +4273,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction { render(); - fireEvent.mouseDown(screen.getByTestId(dataGridCanvasId), { + fireEvent.pointerDown(screen.getByTestId(dataGridCanvasId), { clientX: 950, // Col A clientY: 36 + 32 * 5 + 16, // Row 5 (0 indexed) }); - fireEvent.mouseUp(screen.getByTestId(dataGridCanvasId), { + fireEvent.pointerUp(screen.getByTestId(dataGridCanvasId), { clientX: 950, // Col A clientY: 36 + 32 * 5 + 16, // Row 5 (0 indexed) });