Skip to content

Commit

Permalink
Merge pull request #234 from Renumics/bug/resize-correct-columns
Browse files Browse the repository at this point in the history
Bug/resize correct columns
neindochoh authored Sep 18, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 96a5ea8 + cd586ac commit d9591c6
Showing 5 changed files with 168 additions and 118 deletions.
20 changes: 9 additions & 11 deletions src/widgets/DataGrid/Cell/HeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -4,14 +4,15 @@ import Tooltip from '../../../components/ui/Tooltip';
import dataformat from '../../../dataformat';
import { useColorTransferFunction } from '../../../hooks/useColorTransferFunction';
import * as React from 'react';
import { FunctionComponent, useCallback, useMemo } from 'react';
import { FunctionComponent, useCallback, useContext, useMemo } from 'react';
import { HiChevronDown, HiChevronUp } from 'react-icons/hi';
import type { GridChildComponentProps as CellProps } from 'react-window';
import { Dataset, Sorting, useDataset } from '../../../stores/dataset';
import tw from 'twin.macro';
import { useColumn } from '../context/columnContext';
import { useSortByColumn } from '../context/sortingContext';
import RelevanceIndicator from '../RelevanceIndicator';
import { ResizingContext } from '../context/resizeContext';

interface SortingIndicatorProps {
sorting?: Sorting;
@@ -39,17 +40,14 @@ const stopPropagation: React.MouseEventHandler<HTMLButtonElement> = (event) => {
event.stopPropagation();
};

type ItemData = {
onStartResize: (columnIndex: number) => void;
resizedIndex?: number;
};

type Props = CellProps<ItemData>;
type Props = CellProps;

const HeaderCell: FunctionComponent<Props> = ({ data, style, columnIndex }) => {
const HeaderCell: FunctionComponent<Props> = ({ style, columnIndex }) => {
const column = useColumn(columnIndex);
const [columnSorting, sortBy, resetSorting] = useSortByColumn(column.key);

const { startResizing, resizedIndex } = useContext(ResizingContext);

const tags = useDataset(tagsSelector);
const tagColorTransferFunction = useColorTransferFunction(tags, {
kind: 'str',
@@ -130,9 +128,9 @@ const HeaderCell: FunctionComponent<Props> = ({ data, style, columnIndex }) => {
const onStartResize: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(event) => {
stopPropagation(event);
data.onStartResize(columnIndex);
startResizing(columnIndex);
},
[columnIndex, data]
[columnIndex, startResizing]
);

return (
@@ -163,7 +161,7 @@ const HeaderCell: FunctionComponent<Props> = ({ data, style, columnIndex }) => {
onMouseDown={onStartResize}
css={[
tw`h-full w-[3px] border-r bg-none hover:border-r-0 hover:bg-gray-400 transition transform cursor-col-resize`,
data.resizedIndex === columnIndex && tw`bg-gray-400 border-r-0`,
resizedIndex.current === columnIndex && tw`bg-gray-400 border-r-0`,
]}
/>
</div>
123 changes: 29 additions & 94 deletions src/widgets/DataGrid/DataGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'twin.macro';
import TableIcon from '../../icons/Table';
import { Widget } from '../types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeGrid as Grid } from 'react-window';
import { Dataset, Sorting, useDataset } from '../../stores/dataset';
@@ -16,10 +16,8 @@ import HeaderGrid from './HeaderGrid';
import MenuBar from './MenuBar';
import TableGrid, { Ref as TableGridRef } from './TableGrid';
import GridContextMenu from './GridContextMenu';
import columnWidthByType from './columnWidthByType';
import { WidgetContainer, WidgetContent } from '../../lib';

const MIN_COLUMN_WIDTH = 50;
import { ColumnResizeProvider } from './context/resizeContext';

const headerHeight = 24;

@@ -50,73 +48,12 @@ const DataGrid: Widget = () => {
'visibleColumns',
defaultVisibleColumnKeys
);
const [columnWidths, persistColumnWidths] = useWidgetConfig<Record<string, number>>(
'columnWidths',
allColumns.reduce((acc: Record<string, number>, column: DataColumn) => {
acc[column.key] = columnWidthByType[column.type.kind];
return acc;
}, {} as Record<string, number>)
);

const columnWidthsRef = useRef(columnWidths);

useEffect(() => {
columnWidthsRef.current = columnWidths;
}, [columnWidths]);

const resetGridsAfterIndex = useCallback((index: number) => {
tableGrid.current?.resetAfterColumnIndex(index);
headerGrid.current?.resetAfterColumnIndex(index);
}, []);

const [resizedIndex, setResizedIndex] = useState<number>();
const lastResizePosition = useRef<number | null>(null);

const onStartResize = useCallback(
(columnIndex: number) => {
const columnKey = visibleColumns[columnIndex];
const onMouseMoveWhileResize = (event: MouseEvent) => {
if (lastResizePosition.current !== null) {
const delta = event.clientX - lastResizePosition.current;
if (delta > 0 || delta < 0) {
const oldWidths = columnWidthsRef.current;
const newColumnWidth = Math.max(
MIN_COLUMN_WIDTH,
oldWidths[columnKey] + delta
);
const newWidths = {
...oldWidths,
[columnKey]: newColumnWidth,
};
columnWidthsRef.current = newWidths;
resetGridsAfterIndex(columnIndex);
}
} else {
setResizedIndex(columnIndex);
}
lastResizePosition.current = event.clientX;
};

window.addEventListener('mousemove', onMouseMoveWhileResize);

const onMouseUpWhileResize = () => {
window.removeEventListener('mousemove', onMouseMoveWhileResize);
window.removeEventListener('mouseup', onMouseUpWhileResize);
lastResizePosition.current = null;
setResizedIndex(undefined);
persistColumnWidths(columnWidthsRef.current);
};

window.addEventListener('mouseup', onMouseUpWhileResize);
},
[visibleColumns, resetGridsAfterIndex, persistColumnWidths]
);

const columnWidth = useCallback(
(index: number) => columnWidthsRef.current[visibleColumns[index]],
[visibleColumns]
);

const resetVisibleColumns = useCallback(
() => setVisibleColumns(defaultVisibleColumnKeys),
[defaultVisibleColumnKeys, setVisibleColumns]
@@ -145,36 +82,34 @@ const DataGrid: Widget = () => {
setAreOrderedByRelevance={setAreOrderedByRelevance}
resetColumns={resetVisibleColumns}
>
<SortingProvider sorting={sorting} setSorting={setSorting}>
<MenuBar />
<WidgetContent>
<AutoSizer>
{({ width, height }) => (
<div tw="bg-white" style={{ width, height }}>
<GridContextMenu>
<div tw="bg-gray-100 w-full">
<HeaderGrid
width={width - scrollbarWidth}
height={headerHeight}
ref={headerGrid}
columnWidth={columnWidth}
onStartResize={onStartResize}
resizedIndex={resizedIndex}
<ColumnResizeProvider onResize={resetGridsAfterIndex}>
<SortingProvider sorting={sorting} setSorting={setSorting}>
<MenuBar />
<WidgetContent>
<AutoSizer>
{({ width, height }) => (
<div tw="bg-white" style={{ width, height }}>
<GridContextMenu>
<div tw="bg-gray-100 w-full">
<HeaderGrid
width={width - scrollbarWidth}
height={headerHeight}
ref={headerGrid}
/>
</div>
<TableGrid
width={width}
height={height - headerHeight}
onScroll={handleScroll}
ref={tableGrid}
/>
</div>
<TableGrid
width={width}
height={height - headerHeight}
onScroll={handleScroll}
columnWidth={columnWidth}
ref={tableGrid}
/>
</GridContextMenu>
</div>
)}
</AutoSizer>
</WidgetContent>
</SortingProvider>
</GridContextMenu>
</div>
)}
</AutoSizer>
</WidgetContent>
</SortingProvider>
</ColumnResizeProvider>
</ColumnProvider>
</TableViewProvider>
</WidgetContainer>
13 changes: 6 additions & 7 deletions src/widgets/DataGrid/HeaderGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import usePrevious from '../../hooks/usePrevious';
import * as React from 'react';
import { useCallback, useEffect } from 'react';
import { useCallback, useContext, useEffect } from 'react';
import { VariableSizeGrid as Grid } from 'react-window';
import 'twin.macro';
import HeaderCell from './Cell/HeaderCell';
import { useColumnCount, useVisibleColumns } from './context/columnContext';
import { ResizingContext } from './context/resizeContext';

interface Props {
height: number;
width: number;
columnWidth: (index: number) => number;
onStartResize: (columnIndex: number) => void;
resizedIndex?: number;
}

const HeaderGrid: React.ForwardRefRenderFunction<Grid, Props> = (
{ height, width, columnWidth, onStartResize, resizedIndex },
{ height, width },
ref
) => {
const rowHeight = useCallback(() => height, [height]);
const columnCount = useColumnCount();

const { getColumnWidth } = useContext(ResizingContext);

const [displayedColumns] = useVisibleColumns();

const previousColumnKeys = usePrevious(displayedColumns.map(({ key }) => key));
@@ -39,12 +39,11 @@ const HeaderGrid: React.ForwardRefRenderFunction<Grid, Props> = (
return (
<Grid
columnCount={columnCount}
columnWidth={columnWidth}
columnWidth={getColumnWidth}
rowCount={1}
rowHeight={rowHeight}
height={height}
width={width}
itemData={{ onStartResize, resizedIndex }}
style={{
overflow: 'hidden',
}}
15 changes: 9 additions & 6 deletions src/widgets/DataGrid/TableGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import usePrevious from '../../hooks/usePrevious';
import {
ForwardRefRenderFunction,
forwardRef,
ForwardRefRenderFunction,
useCallback,
useContext,
useEffect,
useRef,
useImperativeHandle,
useRef,
} from 'react';
import { VariableSizeGrid as Grid } from 'react-window';
import type { GridOnScrollProps, GridProps } from 'react-window';
import { VariableSizeGrid as Grid } from 'react-window';
import Cell from './Cell';
import CellPlaceholder from './Cell/CellPlaceholder';
import { useColumnCount, useVisibleColumns } from './context/columnContext';
@@ -18,20 +19,20 @@ import useRowCount from './hooks/useRowCount';
import useSort from './hooks/useSort';
import KeyboardControls from './KeyboardControls';
import MouseControls from './MouseControls';
import { ResizingContext } from './context/resizeContext';

interface Props {
width: number;
height: number;
onScroll: GridProps['onScroll'];
columnWidth: (index: number) => number;
}

export type Ref = {
resetAfterColumnIndex: (index: number) => void;
};

const TableGrid: ForwardRefRenderFunction<Ref, Props> = (
{ width, height, onScroll, columnWidth },
{ width, height, onScroll },
fwdRef
) => {
const ref = useRef<Grid>(null);
@@ -46,6 +47,8 @@ const TableGrid: ForwardRefRenderFunction<Ref, Props> = (

const [displayedColumns] = useVisibleColumns();

const { getColumnWidth } = useContext(ResizingContext);

const { getOriginalIndex } = useSort();
const rowCount = useRowCount();

@@ -96,7 +99,7 @@ const TableGrid: ForwardRefRenderFunction<Ref, Props> = (
height={height}
columnCount={columnCount}
rowCount={Math.max(1, rowCount)}
columnWidth={columnWidth}
columnWidth={getColumnWidth}
estimatedColumnWidth={100}
rowHeight={getRowHeight}
itemKey={rowCount ? itemKey : undefined}
115 changes: 115 additions & 0 deletions src/widgets/DataGrid/context/resizeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from 'react';
import { MutableRefObject, useCallback, useMemo, useRef } from 'react';
import { useColumns } from './columnContext';
import columnWidthByType from '../columnWidthByType';
import { useWidgetConfig } from '../../../hooks';

const MIN_COLUMN_WIDTH = 50;

type EditingContextState = {
getColumnWidth: (index: number) => number;
startResizing: (columnIndex: number) => void;
resizedIndex: MutableRefObject<number | undefined>;
};

export const ResizingContext = React.createContext<EditingContextState>({
getColumnWidth: () => MIN_COLUMN_WIDTH,
startResizing: () => undefined,
resizedIndex: { current: undefined },
});

interface Props {
children: React.ReactNode;
onResize: (columnIndex: number) => void;
}

export const ColumnResizeProvider = ({ children, onResize }: Props) => {
const columns = useColumns();

const defaultColumnWidths = useMemo(
() =>
columns.reduce((acc, column) => {
acc[column.key] = columnWidthByType[column.type.kind];
return acc;
}, {} as Record<string, number>),
[columns]
);

const resizedIndex = useRef<number>();
const lastResizePosition = useRef<number | null>(null);

const [columnWidths, _persistColumnWidths] = useWidgetConfig<
Record<string, number>
>('dataGridColumnWidths', {});

const columnWidthsRef = useRef<Record<string, number>>({});

const persistColumnWidths = useCallback(() => {
_persistColumnWidths((oldWidths) => ({
...oldWidths,
...columnWidthsRef.current,
}));
}, [_persistColumnWidths]);

const onStartResize = useCallback(
(columnIndex: number) => {
const columnKey = columns[columnIndex].key;
const onMouseMoveWhileResize = (event: MouseEvent) => {
if (lastResizePosition.current !== null) {
const delta = event.clientX - lastResizePosition.current;
if (delta > 0 || delta < 0) {
const currentWidth =
columnWidthsRef.current[columnKey] ||
defaultColumnWidths[columnKey] ||
MIN_COLUMN_WIDTH;
columnWidthsRef.current[columnKey] = Math.max(
MIN_COLUMN_WIDTH,
currentWidth + delta
);
onResize(columnIndex);
}
} else {
resizedIndex.current = columnIndex;
}
lastResizePosition.current = event.clientX;
};

window.addEventListener('mousemove', onMouseMoveWhileResize);

const onMouseUpWhileResize = () => {
window.removeEventListener('mousemove', onMouseMoveWhileResize);
window.removeEventListener('mouseup', onMouseUpWhileResize);
lastResizePosition.current = null;
resizedIndex.current = undefined;
persistColumnWidths();
};

window.addEventListener('mouseup', onMouseUpWhileResize);
},
[columns, defaultColumnWidths, onResize, persistColumnWidths]
);

const getColumnWidth = useCallback(
(index: number) => {
const columnKey = columns[index]?.key;
if (columnKey) {
return (
columnWidthsRef.current[columnKey] ||
columnWidths[columnKey] ||
defaultColumnWidths[columnKey] ||
MIN_COLUMN_WIDTH
);
}
return 0;
},
[columnWidths, columns, defaultColumnWidths]
);

return (
<ResizingContext.Provider
value={{ getColumnWidth, startResizing: onStartResize, resizedIndex }}
>
{children}
</ResizingContext.Provider>
);
};

0 comments on commit d9591c6

Please sign in to comment.