Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

port-changes - frozen columns #1

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ function DataGrid<R, SR, K extends Key>(
colOverscanEndIdx,
templateColumns,
layoutCssVars,
totalFrozenColumnWidth
totalFrozenColumnWidth,
rightFrozenColumnCount,
totalRightFrozenColumnWidth
} = useCalculatedColumns({
rawColumns,
defaultColumnOptions,
Expand Down Expand Up @@ -424,7 +426,8 @@ function DataGrid<R, SR, K extends Key>(
rowOverscanEndIdx,
rows,
topSummaryRows,
bottomSummaryRows
bottomSummaryRows,
rightFrozenColumnCount
});

const { gridTemplateColumns, handleColumnResize } = useColumnWidths(
Expand Down
57 changes: 46 additions & 11 deletions src/hooks/useCalculatedColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,15 @@ export function useCalculatedColumns<R, SR>({
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<R, SR>[];
readonly colSpanColumns: readonly CalculatedColumn<R, SR>[];
readonly lastFrozenColumnIndex: number;
readonly rightFrozenColumnCount: number;
readonly headerRowsCount: number;
} => {
let lastFrozenColumnIndex = -1;
let rightFrozenColumnCount = 0;
let headerRowsCount = 1;
const columns: MutableCalculatedColumn<R, SR>[] = [];

Expand All @@ -85,13 +87,15 @@ export function useCalculatedColumns<R, SR>({
}

const frozen = rawColumn.frozen ?? false;
const rightFrozen = rawColumn.rightFrozen ?? false;

const column: MutableCalculatedColumn<R, SR> = {
...rawColumn,
parent,
idx: 0,
level: 0,
frozen,
rightFrozen,
width: rawColumn.width ?? defaultWidth,
minWidth: rawColumn.minWidth ?? defaultMinWidth,
maxWidth: rawColumn.maxWidth ?? defaultMaxWidth,
Expand All @@ -107,13 +111,17 @@ export function useCalculatedColumns<R, SR>({
lastFrozenColumnIndex++;
}

if (rightFrozen) {
rightFrozenColumnCount++;
}

if (level > headerRowsCount) {
headerRowsCount = level;
}
}
}

columns.sort(({ key: aKey, frozen: frozenA }, { key: bKey, frozen: frozenB }) => {
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;
Expand All @@ -125,6 +133,13 @@ export function useCalculatedColumns<R, SR>({
}
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

// Sort other columns last:
Expand All @@ -145,6 +160,7 @@ export function useCalculatedColumns<R, SR>({
columns,
colSpanColumns,
lastFrozenColumnIndex,
rightFrozenColumnCount,
headerRowsCount
};
}, [
Expand All @@ -158,15 +174,17 @@ export function useCalculatedColumns<R, SR>({
defaultDraggable
]);

const { templateColumns, layoutCssVars, totalFrozenColumnWidth, columnMetrics } = useMemo((): {
const { templateColumns, layoutCssVars, totalFrozenColumnWidth, totalRightFrozenColumnWidth, columnMetrics } = useMemo((): {
templateColumns: readonly string[];
layoutCssVars: Readonly<Record<string, string>>;
totalFrozenColumnWidth: number;
totalRightFrozenColumnWidth: number;
columnMetrics: ReadonlyMap<CalculatedColumn<R, SR>, ColumnMetric>;
} => {
const columnMetrics = new Map<CalculatedColumn<R, SR>, ColumnMetric>();
let left = 0;
let totalFrozenColumnWidth = 0;
let totalRightFrozenColumnWidth = 0;
const templateColumns: string[] = [];

for (const column of columns) {
Expand All @@ -191,24 +209,38 @@ export function useCalculatedColumns<R, SR>({

const layoutCssVars: Record<string, string> = {};

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) {
return [0, columns.length - 1];
}
// 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) {
Expand All @@ -217,7 +249,7 @@ export function useCalculatedColumns<R, SR>({

// 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
Expand All @@ -229,7 +261,7 @@ export function useCalculatedColumns<R, SR>({

// 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.
Expand All @@ -240,7 +272,7 @@ export function useCalculatedColumns<R, SR>({
}

const colOverscanStartIdx = max(firstUnfrozenColumnIdx, colVisibleStartIdx - 1);
const colOverscanEndIdx = min(lastColIdx, colVisibleEndIdx + 1);
const colOverscanEndIdx = min(lastUnfrozonColumnIdx, colVisibleEndIdx + 1);

return [colOverscanStartIdx, colOverscanEndIdx];
}, [
Expand All @@ -249,6 +281,7 @@ export function useCalculatedColumns<R, SR>({
lastFrozenColumnIndex,
scrollLeft,
totalFrozenColumnWidth,
totalRightFrozenColumnWidth,
viewportWidth,
enableVirtualization
]);
Expand All @@ -262,7 +295,9 @@ export function useCalculatedColumns<R, SR>({
layoutCssVars,
headerRowsCount,
lastFrozenColumnIndex,
totalFrozenColumnWidth
totalFrozenColumnWidth,
rightFrozenColumnCount,
totalRightFrozenColumnWidth
};
}

Expand All @@ -283,4 +318,4 @@ function updateColumnParent<R, SR>(
parent.colSpan += 1;
updateColumnParent(parent, index, level - 1);
}
}
}
9 changes: 8 additions & 1 deletion src/hooks/useViewportColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface ViewportColumnsArgs<R, SR> {
colOverscanStartIdx: number;
colOverscanEndIdx: number;
lastFrozenColumnIndex: number;
rightFrozenColumnCount: number;
rowOverscanStartIdx: number;
rowOverscanEndIdx: number;
}
Expand All @@ -25,6 +26,7 @@ export function useViewportColumns<R, SR>({
colOverscanStartIdx,
colOverscanEndIdx,
lastFrozenColumnIndex,
rightFrozenColumnCount,
rowOverscanStartIdx,
rowOverscanEndIdx
}: ViewportColumnsArgs<R, SR>) {
Expand Down Expand Up @@ -110,6 +112,11 @@ export function useViewportColumns<R, SR>({
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]);
}
}
16 changes: 16 additions & 0 deletions src/style/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,19 @@ 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}`;
2 changes: 2 additions & 0 deletions src/style/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
38 changes: 17 additions & 21 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,19 @@ export interface Column<TRow, TSummaryRow = unknown> {
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<number | string>;
/**
* Minimum column width in px
* @default '50px'
*/
/** Minimum column width in px. */
readonly minWidth?: Maybe<number>;
/** Maximum column width in px. */
readonly maxWidth?: Maybe<number>;
readonly cellClass?: Maybe<string | ((row: TRow) => Maybe<string>)>;
readonly headerCellClass?: Maybe<string>;
readonly summaryCellClass?: Maybe<string | ((row: TSummaryRow) => Maybe<string>)>;
/** Render function used to render the content of cells */
readonly renderCell?: Maybe<(props: RenderCellProps<TRow, TSummaryRow>) => ReactNode>;
/** Render function used to render the content of the column's header cell */
readonly renderHeaderCell?: Maybe<(props: RenderHeaderCellProps<TRow, TSummaryRow>) => ReactNode>;
/** Render function used to render the content of cells */
readonly renderCell?: Maybe<(props: RenderCellProps<TRow, TSummaryRow>) => ReactNode>;
/** Render function used to render the content of summary cells */
readonly renderSummaryCell?: Maybe<
(props: RenderSummaryCellProps<TSummaryRow, TRow>) => ReactNode
Expand All @@ -45,6 +39,8 @@ export interface Column<TRow, TSummaryRow = unknown> {
readonly colSpan?: Maybe<(args: ColSpanArgs<TRow, TSummaryRow>) => Maybe<number>>;
/** Determines whether column is frozen or not */
readonly frozen?: Maybe<boolean>;
/** Determines whether column is right frozen or not */
readonly rightFrozen?: Maybe<boolean>;
/** Enable resizing of a column */
readonly resizable?: Maybe<boolean>;
/** Enable sorting of a column */
Expand Down Expand Up @@ -149,10 +145,10 @@ export interface RenderHeaderCellProps<TRow, TSummaryRow = unknown> {

export interface CellRendererProps<TRow, TSummaryRow>
extends Pick<RenderRowProps<TRow, TSummaryRow>, 'row' | 'rowIdx' | 'selectCell'>,
Omit<
React.HTMLAttributes<HTMLDivElement>,
'children' | 'onClick' | 'onDoubleClick' | 'onContextMenu'
> {
Omit<
React.HTMLAttributes<HTMLDivElement>,
'children' | 'onClick' | 'onDoubleClick' | 'onContextMenu'
> {
column: CalculatedColumn<TRow, TSummaryRow>;
colSpan: number | undefined;
isCopied: boolean;
Expand Down Expand Up @@ -209,10 +205,10 @@ export interface CellSelectArgs<TRow, TSummaryRow = unknown> {

export interface BaseRenderRowProps<TRow, TSummaryRow = unknown>
extends Omit<React.HTMLAttributes<HTMLDivElement>, 'style' | 'children'>,
Pick<
DataGridProps<TRow, TSummaryRow>,
'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu'
> {
Pick<
DataGridProps<TRow, TSummaryRow>,
'onCellClick' | 'onCellDoubleClick' | 'onCellContextMenu'
> {
viewportColumns: readonly CalculatedColumn<TRow, TSummaryRow>[];
rowIdx: number;
selectedCellIdx: number | undefined;
Expand Down Expand Up @@ -304,7 +300,7 @@ export interface RenderSortPriorityProps {
priority: number | undefined;
}

export interface RenderSortStatusProps extends RenderSortIconProps, RenderSortPriorityProps {}
export interface RenderSortStatusProps extends RenderSortIconProps, RenderSortPriorityProps { }

export interface RenderCheckboxProps
extends Pick<
Expand All @@ -316,11 +312,11 @@ export interface RenderCheckboxProps
}

export interface Renderers<TRow, TSummaryRow> {
renderCell?: Maybe<(key: Key, props: CellRendererProps<TRow, TSummaryRow>) => ReactNode>;
renderCheckbox?: Maybe<(props: RenderCheckboxProps) => ReactNode>;
renderRow?: Maybe<(key: Key, props: RenderRowProps<TRow, TSummaryRow>) => ReactNode>;
renderSortStatus?: Maybe<(props: RenderSortStatusProps) => ReactNode>;
renderCell?: Maybe<(key: Key, props: CellRendererProps<TRow, TSummaryRow>) => ReactNode>;
noRowsFallback?: Maybe<ReactNode>;
}

export type Direction = 'ltr' | 'rtl';
export type Direction = 'ltr' | 'rtl';
2 changes: 1 addition & 1 deletion src/utils/domUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ export function stopPropagation(event: React.SyntheticEvent) {
}

export function scrollIntoView(element: Maybe<Element>) {
element?.scrollIntoView({ inline: 'nearest', block: 'nearest' });
element?.scrollIntoView({ inline: 'center', block: 'nearest' });
}
Loading