Skip to content

fix: paginated table in groups scrolled to top on refresh #2291

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
84 changes: 46 additions & 38 deletions src/components/PaginatedTable/PaginatedTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React from 'react';

import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout';

import {usePaginatedTableState} from './PaginatedTableContext';
import {TableChunk} from './TableChunk';
import {TableHead} from './TableHead';
import {DEFAULT_TABLE_ROW_HEIGHT} from './constants';
Expand All @@ -12,12 +11,12 @@ import type {
GetRowClassName,
HandleTableColumnsResize,
PaginatedTableData,
RenderControls,
RenderEmptyDataMessage,
RenderErrorMessage,
SortParams,
} from './types';
import {useScrollBasedChunks} from './useScrollBasedChunks';
import {useTableScroll} from './useTableScroll';

import './PaginatedTable.scss';

Expand All @@ -30,10 +29,10 @@ export interface PaginatedTableProps<T, F> {
columns: Column<T>[];
getRowClassName?: GetRowClassName<T>;
rowHeight?: number;
parentRef: React.RefObject<HTMLElement>;
scrollContainerRef: React.RefObject<HTMLElement>;
tableContainerRef: React.RefObject<HTMLDivElement>;
initialSortParams?: SortParams;
onColumnsResize?: HandleTableColumnsResize;
renderControls?: RenderControls;
renderEmptyDataMessage?: RenderEmptyDataMessage;
renderErrorMessage?: RenderErrorMessage;
containerClassName?: string;
Expand All @@ -52,28 +51,44 @@ export const PaginatedTable = <T, F>({
columns,
getRowClassName,
rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
parentRef,
scrollContainerRef,
tableContainerRef,
initialSortParams,
onColumnsResize,
renderControls,
renderErrorMessage,
renderEmptyDataMessage,
containerClassName,
onDataFetched,
keepCache = true,
}: PaginatedTableProps<T, F>) => {
const initialTotal = initialEntitiesCount || 0;
const initialFound = initialEntitiesCount || 1;
// Get state and setters from context
const {tableState, setSortParams, setTotalEntities, setFoundEntities, setIsInitialLoad} =
usePaginatedTableState();

const {sortParams, foundEntities} = tableState;

const [sortParams, setSortParams] = React.useState<SortParams | undefined>(initialSortParams);
const [totalEntities, setTotalEntities] = React.useState(initialTotal);
const [foundEntities, setFoundEntities] = React.useState(initialFound);
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
// Initialize state with props if available
React.useEffect(() => {
if (initialSortParams) {
setSortParams(initialSortParams);
}

if (initialEntitiesCount) {
setTotalEntities(initialEntitiesCount);
setFoundEntities(initialEntitiesCount);
}
}, [
setSortParams,
setTotalEntities,
setFoundEntities,
initialSortParams,
initialEntitiesCount,
]);

const tableRef = React.useRef<HTMLDivElement>(null);

const activeChunks = useScrollBasedChunks({
parentRef,
scrollContainerRef,
tableRef,
totalItems: foundEntities,
rowHeight,
Expand Down Expand Up @@ -105,18 +120,26 @@ export const PaginatedTable = <T, F>({
onDataFetched?.(data);
}
},
[onDataFetched],
[onDataFetched, setFoundEntities, setIsInitialLoad, setTotalEntities],
);

// reset table on filters change
// Use the extracted table scroll hook
// The hook handles scrolling internally based on dependencies
useTableScroll({
tableContainerRef,
scrollContainerRef,
dependencies: [rawFilters], // Add filters as a dependency to trigger scroll when they change
});

// Reset table on filters change
React.useLayoutEffect(() => {
setTotalEntities(initialTotal);
setFoundEntities(initialFound);
const defaultTotal = initialEntitiesCount || 0;
const defaultFound = initialEntitiesCount || 1;

setTotalEntities(defaultTotal);
setFoundEntities(defaultFound);
setIsInitialLoad(true);
if (parentRef?.current) {
parentRef.current.scrollTo(0, 0);
}
}, [rawFilters, initialFound, initialTotal, parentRef]);
}, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]);

const renderChunks = () => {
return activeChunks.map((isActive, index) => (
Expand Down Expand Up @@ -148,24 +171,9 @@ export const PaginatedTable = <T, F>({
</table>
);

const renderContent = () => {
if (renderControls) {
return (
<TableWithControlsLayout>
<TableWithControlsLayout.Controls>
{renderControls({inited: !isInitialLoad, totalEntities, foundEntities})}
</TableWithControlsLayout.Controls>
<TableWithControlsLayout.Table>{renderTable()}</TableWithControlsLayout.Table>
</TableWithControlsLayout>
);
}

return renderTable();
};

return (
<div ref={tableRef} className={b(null, containerClassName)}>
{renderContent()}
{renderTable()}
</div>
);
};
98 changes: 98 additions & 0 deletions src/components/PaginatedTable/PaginatedTableContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React from 'react';

import type {PaginatedTableState} from './types';

// Default state for the table
const defaultTableState: PaginatedTableState = {
sortParams: undefined,
totalEntities: 0,
foundEntities: 0,
isInitialLoad: true,
};

// Context type definition
interface PaginatedTableStateContextType {
// State
tableState: PaginatedTableState;

// Granular setters
setSortParams: (params: PaginatedTableState['sortParams']) => void;
setTotalEntities: (total: number) => void;
setFoundEntities: (found: number) => void;
setIsInitialLoad: (isInitial: boolean) => void;
}

// Creating the context with default values
export const PaginatedTableStateContext = React.createContext<PaginatedTableStateContextType>({
tableState: defaultTableState,
setSortParams: () => undefined,
setTotalEntities: () => undefined,
setFoundEntities: () => undefined,
setIsInitialLoad: () => undefined,
});

// Provider component props
interface PaginatedTableStateProviderProps {
children: React.ReactNode;
initialState?: Partial<PaginatedTableState>;
}

// Provider component
export const PaginatedTableProvider = ({
children,
initialState = {},
}: PaginatedTableStateProviderProps) => {
// Use individual state variables for each field
const [sortParams, setSortParams] = React.useState<PaginatedTableState['sortParams']>(
initialState.sortParams ?? defaultTableState.sortParams,
);
const [totalEntities, setTotalEntities] = React.useState<number>(
initialState.totalEntities ?? defaultTableState.totalEntities,
);
const [foundEntities, setFoundEntities] = React.useState<number>(
initialState.foundEntities ?? defaultTableState.foundEntities,
);
const [isInitialLoad, setIsInitialLoad] = React.useState<boolean>(
initialState.isInitialLoad ?? defaultTableState.isInitialLoad,
);

// Construct tableState from individual state variables
const tableState = React.useMemo(
() => ({
sortParams,
totalEntities,
foundEntities,
isInitialLoad,
}),
[sortParams, totalEntities, foundEntities, isInitialLoad],
);

// Create the context value with the constructed tableState and direct setters
const contextValue = React.useMemo(
() => ({
tableState,
setSortParams,
setTotalEntities,
setFoundEntities,
setIsInitialLoad,
}),
[tableState, setSortParams, setTotalEntities, setFoundEntities, setIsInitialLoad],
);

return (
<PaginatedTableStateContext.Provider value={contextValue}>
{children}
</PaginatedTableStateContext.Provider>
);
};

// Custom hook for consuming the context
export const usePaginatedTableState = () => {
const context = React.useContext(PaginatedTableStateContext);

if (context === undefined) {
throw new Error('usePaginatedTableState must be used within a PaginatedTableStateProvider');
}

return context;
};
5 changes: 4 additions & 1 deletion src/components/PaginatedTable/ResizeablePaginatedTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {useTableResize} from '../../utils/hooks/useTableResize';
import type {PaginatedTableProps} from './PaginatedTable';
import {PaginatedTable} from './PaginatedTable';
import {b} from './shared';
import type {Column} from './types';
import type {Column, PaginatedTableState} from './types';

function updateColumnsWidth<T>(columns: Column<T>[], columnsWidthSetup: ColumnWidthByName) {
return columns.map((column) => {
Expand All @@ -16,11 +16,13 @@ function updateColumnsWidth<T>(columns: Column<T>[], columnsWidthSetup: ColumnWi
interface ResizeablePaginatedTableProps<T, F>
extends Omit<PaginatedTableProps<T, F>, 'onColumnsResize'> {
columnsWidthLSKey: string;
onStateChange?: (state: PaginatedTableState) => void;
}

export function ResizeablePaginatedTable<T, F>({
columnsWidthLSKey,
columns,
tableContainerRef,
...props
}: ResizeablePaginatedTableProps<T, F>) {
const [tableColumnsWidth, setTableColumnsWidth] = useTableResize(columnsWidthLSKey);
Expand All @@ -29,6 +31,7 @@ export function ResizeablePaginatedTable<T, F>({

return (
<PaginatedTable
tableContainerRef={tableContainerRef}
columns={updatedColumns}
onColumnsResize={setTableColumnsWidth}
containerClassName={b('resizeable-table-container')}
Expand Down
7 changes: 7 additions & 0 deletions src/components/PaginatedTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ export type FetchData<T, F = undefined, E = {}> = (

export type OnError = (error?: IResponseError) => void;

export interface PaginatedTableState {
sortParams?: SortParams;
totalEntities: number;
foundEntities: number;
isInitialLoad: boolean;
}

interface ControlsParams {
totalEntities: number;
foundEntities: number;
Expand Down
12 changes: 6 additions & 6 deletions src/components/PaginatedTable/useScrollBasedChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import {calculateElementOffsetTop, rafThrottle} from './utils';

interface UseScrollBasedChunksProps {
parentRef: React.RefObject<HTMLElement>;
scrollContainerRef: React.RefObject<HTMLElement>;
tableRef: React.RefObject<HTMLElement>;
totalItems: number;
rowHeight: number;
Expand All @@ -14,7 +14,7 @@ interface UseScrollBasedChunksProps {
const DEFAULT_OVERSCAN_COUNT = 1;

export const useScrollBasedChunks = ({
parentRef,
scrollContainerRef,
tableRef,
totalItems,
rowHeight,
Expand All @@ -32,7 +32,7 @@ export const useScrollBasedChunks = ({
);

const calculateVisibleRange = React.useCallback(() => {
const container = parentRef?.current;
const container = scrollContainerRef?.current;
const table = tableRef.current;
if (!container || !table) {
return null;
Expand All @@ -49,7 +49,7 @@ export const useScrollBasedChunks = ({
Math.max(chunksCount - 1, 0),
);
return {start, end};
}, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);
}, [scrollContainerRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);

const updateVisibleChunks = React.useCallback(() => {
const newRange = calculateVisibleRange();
Expand Down Expand Up @@ -80,7 +80,7 @@ export const useScrollBasedChunks = ({
}, [updateVisibleChunks]);

React.useEffect(() => {
const container = parentRef?.current;
const container = scrollContainerRef?.current;
if (!container) {
return undefined;
}
Expand All @@ -91,7 +91,7 @@ export const useScrollBasedChunks = ({
return () => {
container.removeEventListener('scroll', throttledHandleScroll);
};
}, [handleScroll, parentRef]);
}, [handleScroll, scrollContainerRef]);

return React.useMemo(() => {
// boolean array that represents active chunks
Expand Down
Loading
Loading