diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8a755ee0a9..0000000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: CI -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - uses: actions/setup-node@v6 - with: - node-version: 25 - check-latest: true - - - name: Install dependencies - run: npm i - - - name: Biome - run: node --run biome:ci - - - name: Typecheck - run: node --run typecheck - - - name: ESLint - run: node --run eslint - - - name: Prettier - run: node --run prettier:check - - - name: Bundle - run: node --run build - - - name: Build website - run: node --run build:website - - - name: Install Playwright Browsers - run: npx playwright install chromium firefox - - - name: Test - run: node --run test - timeout-minutes: 4 - - - name: Visual regression test - run: node --run visual - - - name: Upload coverage - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - - - name: Deploy gh-pages - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: | - git config --global user.email 'action@github.com' - git config --global user.name 'GitHub Action' - git fetch origin gh-pages - git worktree add gh-pages gh-pages - cd gh-pages - git rm -r . - mv ../dist/* . - touch .nojekyll - git add . - git commit -m "gh-pages deployment" || echo "Nothing to commit" - git push -f https://comcast:${{secrets.GITHUB_TOKEN}}@github.com/Comcast/react-data-grid.git diff --git a/package.json b/package.json index b461ad0a7d..0351aedd8c 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,15 @@ { - "name": "react-data-grid", - "version": "7.0.0-beta.59", + "name": "@port-labs/port-react-data-grid", + "version": "1.0.3-beta.1", "license": "MIT", - "description": "Feature-rich and customizable data grid React component", + "description": "Forked version of @adazzle/react-data-grid with custom changes by Port Labs", "keywords": [ "react", "data grid" ], "repository": { - "type": "git", - "url": "git+https://github.com/Comcast/react-data-grid.git" + "type": "git" }, - "homepage": "https://github.com/Comcast/react-data-grid#readme", - "bugs": "https://github.com/Comcast/react-data-grid/issues", "type": "module", "exports": { "./lib/styles.css": "./lib/styles.css", diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index b8532c13fd..4a2239b644 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -363,7 +363,9 @@ export function DataGrid(props: DataGridPr colOverscanEndIdx, templateColumns, layoutCssVars, - totalFrozenColumnWidth + totalFrozenColumnWidth, + rightFrozenColumnCount, + totalRightFrozenColumnWidth } = useCalculatedColumns({ rawColumns, defaultColumnOptions, @@ -460,7 +462,8 @@ export function DataGrid(props: DataGridPr rowOverscanEndIdx, rows, topSummaryRows, - bottomSummaryRows + bottomSummaryRows, + rightFrozenColumnCount }); const { gridTemplateColumns, handleColumnResize } = useColumnWidths( @@ -507,7 +510,9 @@ export function DataGrid(props: DataGridPr const cell = getCellToScroll(gridRef.current!); if (cell === null) return; - if (shouldScroll) { + const isFrozen = cell.classList.contains('rdg-cell-frozen'); + + if (!isFrozen && shouldScroll) { scrollIntoView(cell); } @@ -1179,7 +1184,6 @@ export function DataGrid(props: DataGridPr const isGroupRowFocused = selectedPosition.idx === -1 && selectedPosition.rowIdx !== minRowIdx - 1; - return (
(props: DataGridPr selectedPosition.idx > lastFrozenColumnIndex || scrollToPosition?.idx !== undefined ? `${totalFrozenColumnWidth}px` : undefined, + scrollPaddingInlineEnd: + rightFrozenColumnCount < maxRowIdx || scrollToPosition?.idx !== undefined + ? `${totalRightFrozenColumnWidth}px` + : undefined, scrollPaddingBlock: isRowIdxWithinViewportBounds(selectedPosition.rowIdx) || scrollToPosition?.rowIdx !== undefined diff --git a/src/HeaderCell.tsx b/src/HeaderCell.tsx index 8da9ed3e90..2d2d0d0a31 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -37,7 +37,12 @@ const resizeHandle = css` inset-block-start: 0; inset-inline-end: 0; inset-block-end: 0; - inline-size: 10px; + inline-size: 4px; + + &:hover { + background: var(--rdg-selection-color); + padding: 4px 0; + } } `; diff --git a/src/hooks/useCalculatedColumns.ts b/src/hooks/useCalculatedColumns.ts index 0bbc33d0c9..324276ade1 100644 --- a/src/hooks/useCalculatedColumns.ts +++ b/src/hooks/useCalculatedColumns.ts @@ -54,13 +54,21 @@ export function useCalculatedColumns({ const defaultResizable = defaultColumnOptions?.resizable ?? false; const defaultDraggable = defaultColumnOptions?.draggable ?? false; - const { columns, colSpanColumns, lastFrozenColumnIndex, headerRowsCount } = useMemo((): { + const { + columns, + colSpanColumns, + lastFrozenColumnIndex, + rightFrozenColumnCount, + headerRowsCount + } = useMemo((): { readonly columns: readonly CalculatedColumn[]; readonly colSpanColumns: readonly CalculatedColumn[]; readonly lastFrozenColumnIndex: number; + readonly rightFrozenColumnCount: number; readonly headerRowsCount: number; } => { let lastFrozenColumnIndex = -1; + let rightFrozenColumnCount = 0; let headerRowsCount = 1; const columns: MutableCalculatedColumn[] = []; @@ -87,6 +95,7 @@ export function useCalculatedColumns({ } const frozen = rawColumn.frozen ?? false; + const rightFrozen = rawColumn.rightFrozen ?? false; const column: MutableCalculatedColumn = { ...rawColumn, @@ -94,6 +103,7 @@ export function useCalculatedColumns({ idx: 0, level: 0, frozen, + rightFrozen, width: rawColumn.width ?? defaultWidth, minWidth: rawColumn.minWidth ?? defaultMinWidth, maxWidth: rawColumn.maxWidth ?? defaultMaxWidth, @@ -110,29 +120,45 @@ export function useCalculatedColumns({ lastFrozenColumnIndex++; } + if (rightFrozen) { + rightFrozenColumnCount++; + } + if (level > headerRowsCount) { headerRowsCount = level; } } } - columns.sort(({ key: aKey, frozen: frozenA }, { key: bKey, frozen: frozenB }) => { - // Sort select column first: - if (aKey === SELECT_COLUMN_KEY) return -1; - if (bKey === SELECT_COLUMN_KEY) return 1; + columns.sort( + ( + { key: aKey, frozen: frozenA, rightFrozen: rightFrozenA }, + { key: bKey, frozen: frozenB, rightFrozen: rightFrozenB } + ) => { + // Sort select column first: + if (aKey === SELECT_COLUMN_KEY) return -1; + if (bKey === SELECT_COLUMN_KEY) return 1; + + // Sort frozen columns second: + if (frozenA) { + if (frozenB) return 0; + return -1; + } + if (frozenB) return 1; - // Sort frozen columns second: - if (frozenA) { - if (frozenB) return 0; - return -1; - } - if (frozenB) return 1; + // Sort right frozen columns second: + if (rightFrozenA) { + if (rightFrozenB) return 0; + return 1; + } + if (rightFrozenB) return -1; - // TODO: sort columns to keep them grouped if they have a parent + // TODO: sort columns to keep them grouped if they have a parent - // Sort other columns last: - return 0; - }); + // Sort other columns last: + return 0; + } + ); const colSpanColumns: CalculatedColumn[] = []; columns.forEach((column, idx) => { @@ -148,6 +174,7 @@ export function useCalculatedColumns({ columns, colSpanColumns, lastFrozenColumnIndex, + rightFrozenColumnCount, headerRowsCount }; }, [ @@ -162,15 +189,23 @@ export function useCalculatedColumns({ defaultDraggable ]); - const { templateColumns, layoutCssVars, totalFrozenColumnWidth, columnMetrics } = useMemo((): { + const { + templateColumns, + layoutCssVars, + totalFrozenColumnWidth, + totalRightFrozenColumnWidth, + columnMetrics + } = useMemo((): { templateColumns: readonly string[]; layoutCssVars: Readonly>; totalFrozenColumnWidth: number; + totalRightFrozenColumnWidth: number; columnMetrics: ReadonlyMap, ColumnMetric>; } => { const columnMetrics = new Map, ColumnMetric>(); let left = 0; let totalFrozenColumnWidth = 0; + let totalRightFrozenColumnWidth = 0; const templateColumns: string[] = []; for (const column of columns) { @@ -195,13 +230,30 @@ export function useCalculatedColumns({ const layoutCssVars: Record = {}; + if (rightFrozenColumnCount !== 0) { + let rightEnd = 0; + for (let i = columns.length - 1; i >= columns.length - rightFrozenColumnCount; i--) { + const column = columns[i]; + const columnMetric = columnMetrics.get(column)!; + totalRightFrozenColumnWidth += columnMetric.width; + layoutCssVars[`--rdg-frozen-right-${column.idx}`] = `${rightEnd}px`; + rightEnd += columnMetric.width; + } + } + for (let i = 0; i <= lastFrozenColumnIndex; i++) { const column = columns[i]; layoutCssVars[`--rdg-frozen-left-${column.idx}`] = `${columnMetrics.get(column)!.left}px`; } - return { templateColumns, layoutCssVars, totalFrozenColumnWidth, columnMetrics }; - }, [getColumnWidth, columns, lastFrozenColumnIndex]); + return { + templateColumns, + layoutCssVars, + totalFrozenColumnWidth, + totalRightFrozenColumnWidth, + columnMetrics + }; + }, [getColumnWidth, columns, lastFrozenColumnIndex, rightFrozenColumnCount]); const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { if (!enableVirtualization) { @@ -209,10 +261,11 @@ export function useCalculatedColumns({ } // get the viewport's left side and right side positions for non-frozen columns const viewportLeft = scrollLeft + totalFrozenColumnWidth; - const viewportRight = scrollLeft + viewportWidth; + const viewportRight = scrollLeft + viewportWidth - totalRightFrozenColumnWidth; // get first and last non-frozen column indexes const lastColIdx = columns.length - 1; const firstUnfrozenColumnIdx = min(lastFrozenColumnIndex + 1, lastColIdx); + const lastUnfrozonColumnIdx = min(columns.length - rightFrozenColumnCount - 1, lastColIdx); // skip rendering non-frozen columns if the frozen columns cover the entire viewport if (viewportLeft >= viewportRight) { @@ -221,7 +274,7 @@ export function useCalculatedColumns({ // get the first visible non-frozen column index let colVisibleStartIdx = firstUnfrozenColumnIdx; - while (colVisibleStartIdx < lastColIdx) { + while (colVisibleStartIdx < lastUnfrozonColumnIdx) { const { left, width } = columnMetrics.get(columns[colVisibleStartIdx])!; // if the right side of the columnn is beyond the left side of the available viewport, // then it is the first column that's at least partially visible @@ -233,7 +286,7 @@ export function useCalculatedColumns({ // get the last visible non-frozen column index let colVisibleEndIdx = colVisibleStartIdx; - while (colVisibleEndIdx < lastColIdx) { + while (colVisibleEndIdx < lastUnfrozonColumnIdx) { const { left, width } = columnMetrics.get(columns[colVisibleEndIdx])!; // if the right side of the column is beyond or equal to the right side of the available viewport, // then it the last column that's at least partially visible, as the previous column's right side is not beyond the viewport. @@ -244,7 +297,7 @@ export function useCalculatedColumns({ } const colOverscanStartIdx = max(firstUnfrozenColumnIdx, colVisibleStartIdx - 1); - const colOverscanEndIdx = min(lastColIdx, colVisibleEndIdx + 1); + const colOverscanEndIdx = min(lastUnfrozonColumnIdx, colVisibleEndIdx + 1); return [colOverscanStartIdx, colOverscanEndIdx]; }, [ @@ -253,8 +306,10 @@ export function useCalculatedColumns({ lastFrozenColumnIndex, scrollLeft, totalFrozenColumnWidth, + totalRightFrozenColumnWidth, viewportWidth, - enableVirtualization + enableVirtualization, + rightFrozenColumnCount ]); return { @@ -266,7 +321,9 @@ export function useCalculatedColumns({ layoutCssVars, headerRowsCount, lastFrozenColumnIndex, - totalFrozenColumnWidth + totalFrozenColumnWidth, + rightFrozenColumnCount, + totalRightFrozenColumnWidth }; } diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index c79d085399..7195e3d7eb 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -12,6 +12,7 @@ interface ViewportColumnsArgs { colOverscanStartIdx: number; colOverscanEndIdx: number; lastFrozenColumnIndex: number; + rightFrozenColumnCount: number; rowOverscanStartIdx: number; rowOverscanEndIdx: number; } @@ -25,6 +26,7 @@ export function useViewportColumns({ colOverscanStartIdx, colOverscanEndIdx, lastFrozenColumnIndex, + rightFrozenColumnCount, rowOverscanStartIdx, rowOverscanEndIdx }: ViewportColumnsArgs) { @@ -109,6 +111,11 @@ export function useViewportColumns({ viewportColumns.push(column); } + for (let colIdx = columns.length - rightFrozenColumnCount; colIdx < columns.length; colIdx++) { + const column = columns[colIdx]; + viewportColumns.push(column); + } + return viewportColumns; - }, [startIdx, colOverscanEndIdx, columns]); + }, [startIdx, colOverscanEndIdx, columns, rightFrozenColumnCount]); } diff --git a/src/style/cell.ts b/src/style/cell.ts index 207d468f56..425cff42d3 100644 --- a/src/style/cell.ts +++ b/src/style/cell.ts @@ -46,6 +46,21 @@ export const cellFrozen = css` export const cellFrozenClassname = `rdg-cell-frozen ${cellFrozen}`; +export const cellRightFrozen = css` + @layer rdg.Cell { + position: sticky; + /* Should have a higher value than 0 to show up above unfrozen cells */ + z-index: 1; + right: 0; + /* Add box-shadow on the last frozen cell */ + &:nth-child(1 of &) { + box-shadow: var(--rdg-cell-right-frozen-box-shadow); + } + } +`; + +export const cellRightFrozenClassname = `rdg-cell-frozen ${cellRightFrozen}`; + const cellDragHandle = css` @layer rdg.DragHandle { --rdg-drag-handle-size: 8px; diff --git a/src/style/core.ts b/src/style/core.ts index a2bbf2022b..0105d1a0f7 100644 --- a/src/style/core.ts +++ b/src/style/core.ts @@ -17,6 +17,7 @@ const root = css` --rdg-selection-color: hsl(207, 75%, 66%); --rdg-font-size: 14px; --rdg-cell-frozen-box-shadow: 2px 0 5px -2px rgba(136, 136, 136, 0.3); + --rdg-cell-right-frozen-box-shadow: -2px 0 5px -2px rgba(136, 136, 136, 0.3); --rdg-border-width: 1px; --rdg-summary-border-width: calc(var(--rdg-border-width) * 2); --rdg-color: light-dark(#000, #ddd); @@ -40,6 +41,7 @@ const root = css` &:dir(rtl) { --rdg-cell-frozen-box-shadow: -2px 0 5px -2px rgba(136, 136, 136, 0.3); + --rdg-cell-right-frozen-box-shadow: 2px 0 5px -2px rgba(136, 136, 136, 0.3); } display: grid; diff --git a/src/types.ts b/src/types.ts index f635ed2d0e..aba464995b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,10 +13,7 @@ export interface Column { readonly name: string | ReactElement; /** A unique key to distinguish each column */ readonly key: string; - /** - * Column width. If not specified, it will be determined automatically based on grid width and specified widths of other columns - * @default 'auto' - */ + /** Column width. If not specified, it will be determined automatically based on grid width and specified widths of other columns */ readonly width?: Maybe; /** * Minimum column width in pixels @@ -48,6 +45,8 @@ export interface Column { readonly colSpan?: Maybe<(args: ColSpanArgs) => Maybe>; /** Determines whether column is frozen */ readonly frozen?: Maybe; + /** Determines whether column is right frozen or not */ + readonly rightFrozen?: Maybe; /** Enable resizing of the column */ readonly resizable?: Maybe; /** Enable sorting of the column */ @@ -89,6 +88,7 @@ export interface CalculatedColumn extends Column) => ReactNode; readonly renderHeaderCell: (props: RenderHeaderCellProps) => ReactNode; } diff --git a/src/utils/styleUtils.ts b/src/utils/styleUtils.ts index 3de29e06e0..dac7260390 100644 --- a/src/utils/styleUtils.ts +++ b/src/utils/styleUtils.ts @@ -1,7 +1,7 @@ import type { CSSProperties } from 'react'; import type { CalculatedColumn, CalculatedColumnOrColumnGroup, Maybe } from '../types'; -import { cellClassname, cellFrozenClassname } from '../style/cell'; +import { cellClassname, cellFrozenClassname, cellRightFrozenClassname } from '../style/cell'; export function getRowStyle(rowIdx: number): CSSProperties { return { '--rdg-grid-row-start': rowIdx }; @@ -40,7 +40,8 @@ export function getCellStyle( return { gridColumnStart: index, gridColumnEnd: index + colSpan, - insetInlineStart: column.frozen ? `var(--rdg-frozen-left-${column.idx})` : undefined + insetInlineStart: column.frozen ? `var(--rdg-frozen-left-${column.idx})` : undefined, + insetInlineEnd: column.rightFrozen ? `var(--rdg-frozen-right-${column.idx})` : undefined }; } @@ -73,7 +74,8 @@ export function getCellClassname( return classnames( cellClassname, { - [cellFrozenClassname]: column.frozen + [cellFrozenClassname]: column.frozen, + [cellRightFrozenClassname]: column.rightFrozen }, ...extraClasses ); diff --git a/website/routes/CommonFeatures.tsx b/website/routes/CommonFeatures.tsx index 0d934a526c..054dc8e24b 100644 --- a/website/routes/CommonFeatures.tsx +++ b/website/routes/CommonFeatures.tsx @@ -214,7 +214,8 @@ function getColumns( }, { key: 'account', - name: 'Account' + name: 'Account', + rightFrozen: true }, { key: 'version', @@ -224,6 +225,7 @@ function getColumns( { key: 'available', name: 'Available', + rightFrozen: true, renderCell({ row, onRowChange, tabIndex }) { return (