diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dcfc94696..cf9335bf9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,15 @@ -name: CI +name: CI and Publish to GitHub Packages on: push: branches: - - main - pull_request: + - port-changes jobs: test: runs-on: ubuntu-latest strategy: matrix: - react: [18, 19] + react: [18] fail-fast: false steps: - uses: actions/checkout@v4 @@ -18,48 +17,40 @@ jobs: with: node-version: 23 check-latest: true - - name: set up react 19 - if: matrix.react == 19 - run: | - node ./.github/workflows/patch-react19.js - cat package.json - - name: npm install - run: npm i + - name: Install dependencies + run: npm install - 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 + - name: Build package run: | node --run build node --run build:types - name: Build website run: node --run build:website - - name: Install Playwright Browsers - run: npx playwright install chromium - - name: Test - run: node --run test - timeout-minutes: 4 - - name: Upload coverage - if: matrix.react == 18 - uses: codecov/codecov-action@v5 + + publish: + needs: test # Only run if tests pass + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} - - name: Deploy gh-pages - if: matrix.react == 18 && 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://adazzle:${{secrets.GITHUB_TOKEN}}@github.com/adazzle/react-data-grid.git + node-version: 23 + registry-url: 'https://npm.pkg.github.com' + + - name: Install dependencies + run: npm install + + - name: Build the package + run: npm run build + + - name: Publish to GitHub Packages + run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} diff --git a/package.json b/package.json index 697f9e69ab..d71570d064 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "react-data-grid", - "version": "7.0.0-beta.47", + "name": "@port-labs/port-react-data-grid", + "version": "7.0.0-port.1", + "description": "Forked version of react-data-grid with custom changes by Port Labs", "license": "MIT", - "description": "Feature-rich and customizable data grid React component", "keywords": [ "react", "data grid" diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index f5efe623c4..bb2f992b3d 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -324,7 +324,9 @@ function DataGrid( colOverscanEndIdx, templateColumns, layoutCssVars, - totalFrozenColumnWidth + totalFrozenColumnWidth, + rightFrozenColumnCount, + totalRightFrozenColumnWidth } = useCalculatedColumns({ rawColumns, defaultColumnOptions, @@ -424,7 +426,8 @@ function DataGrid( rowOverscanEndIdx, rows, topSummaryRows, - bottomSummaryRows + bottomSummaryRows, + rightFrozenColumnCount }); const { gridTemplateColumns, handleColumnResize } = useColumnWidths( @@ -1078,7 +1081,6 @@ function DataGrid( const isGroupRowFocused = selectedPosition.idx === -1 && selectedPosition.rowIdx !== minRowIdx - 1; - return ( // biome-ignore lint/a11y/useValidAriaProps: aria-description is a valid prop
( 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 6fbf3c770a..7fd246ac4a 100644 --- a/src/HeaderCell.tsx +++ b/src/HeaderCell.tsx @@ -35,7 +35,12 @@ export const resizeHandleClassname = 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 92a3417ab9..d8c84975d4 100644 --- a/src/hooks/useCalculatedColumns.ts +++ b/src/hooks/useCalculatedColumns.ts @@ -52,13 +52,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[] = []; @@ -85,6 +93,7 @@ export function useCalculatedColumns({ } const frozen = rawColumn.frozen ?? false; + const rightFrozen = rawColumn.rightFrozen ?? false; const column: MutableCalculatedColumn = { ...rawColumn, @@ -92,6 +101,7 @@ export function useCalculatedColumns({ idx: 0, level: 0, frozen, + rightFrozen, width: rawColumn.width ?? defaultWidth, minWidth: rawColumn.minWidth ?? defaultMinWidth, maxWidth: rawColumn.maxWidth ?? defaultMaxWidth, @@ -107,29 +117,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) => { @@ -145,6 +171,7 @@ export function useCalculatedColumns({ columns, colSpanColumns, lastFrozenColumnIndex, + rightFrozenColumnCount, headerRowsCount }; }, [ @@ -158,15 +185,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) { @@ -191,13 +226,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) { @@ -205,10 +257,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) { @@ -217,7 +270,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 @@ -229,7 +282,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. @@ -240,7 +293,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]; }, [ @@ -249,8 +302,10 @@ export function useCalculatedColumns({ lastFrozenColumnIndex, scrollLeft, totalFrozenColumnWidth, + totalRightFrozenColumnWidth, viewportWidth, - enableVirtualization + enableVirtualization, + rightFrozenColumnCount ]); return { @@ -262,7 +317,9 @@ export function useCalculatedColumns({ layoutCssVars, headerRowsCount, lastFrozenColumnIndex, - totalFrozenColumnWidth + totalFrozenColumnWidth, + rightFrozenColumnCount, + totalRightFrozenColumnWidth }; } diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 701b719ec6..0c5f76cb76 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) { @@ -110,6 +112,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 02afec16a0..9ccdae9646 100644 --- a/src/style/cell.ts +++ b/src/style/cell.ts @@ -45,3 +45,18 @@ 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}`; diff --git a/src/style/core.ts b/src/style/core.ts index 1d3040c5c1..ead4cdc6ef 100644 --- a/src/style/core.ts +++ b/src/style/core.ts @@ -45,9 +45,11 @@ const root = css` --rdg-selection-color: #66afe9; --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); &: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 ebd01a9671..e99c33f6f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,25 +13,19 @@ 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 px - * @default '50px' - */ + /** Minimum column width in px. */ readonly minWidth?: Maybe; /** Maximum column width in px. */ readonly maxWidth?: Maybe; readonly cellClass?: Maybe Maybe)>; readonly headerCellClass?: Maybe; readonly summaryCellClass?: Maybe Maybe)>; - /** Render function used to render the content of cells */ - readonly renderCell?: Maybe<(props: RenderCellProps) => ReactNode>; /** Render function used to render the content of the column's header cell */ readonly renderHeaderCell?: Maybe<(props: RenderHeaderCellProps) => ReactNode>; + /** Render function used to render the content of cells */ + readonly renderCell?: Maybe<(props: RenderCellProps) => ReactNode>; /** Render function used to render the content of summary cells */ readonly renderSummaryCell?: Maybe< (props: RenderSummaryCellProps) => ReactNode @@ -45,6 +39,8 @@ export interface Column { readonly colSpan?: Maybe<(args: ColSpanArgs) => Maybe>; /** Determines whether column is frozen or not */ readonly frozen?: Maybe; + /** Determines whether column is right frozen or not */ + readonly rightFrozen?: Maybe; /** Enable resizing of a column */ readonly resizable?: Maybe; /** Enable sorting of a column */ @@ -316,10 +312,10 @@ export interface RenderCheckboxProps } export interface Renderers { - renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>; renderRow?: Maybe<(key: Key, props: RenderRowProps) => ReactNode>; renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>; + renderCell?: Maybe<(key: Key, props: CellRendererProps) => ReactNode>; noRowsFallback?: Maybe; } diff --git a/src/utils/styleUtils.ts b/src/utils/styleUtils.ts index 53a3becc57..d08fd598ce 100644 --- a/src/utils/styleUtils.ts +++ b/src/utils/styleUtils.ts @@ -2,7 +2,7 @@ import type { CSSProperties } from 'react'; import clsx from 'clsx'; import type { CalculatedColumn, CalculatedColumnOrColumnGroup } 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 } as unknown as CSSProperties; @@ -41,7 +41,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 }; } @@ -52,7 +53,8 @@ export function getCellClassname( return clsx( cellClassname, { - [cellFrozenClassname]: column.frozen + [cellFrozenClassname]: column.frozen, + [cellRightFrozenClassname]: column.rightFrozen }, ...extraClasses ); diff --git a/website/routes/CommonFeatures.lazy.tsx b/website/routes/CommonFeatures.lazy.tsx index f7f066b2bf..46203a7e3f 100644 --- a/website/routes/CommonFeatures.lazy.tsx +++ b/website/routes/CommonFeatures.lazy.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 (