Skip to content

Commit f4971ee

Browse files
authored
fix: paginated table in groups scrolled to top on refresh (#2291)
1 parent 1a1a3f2 commit f4971ee

File tree

73 files changed

+1647
-961
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

73 files changed

+1647
-961
lines changed

src/components/PaginatedTable/PaginatedTable.tsx

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22

3-
import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout';
4-
3+
import {usePaginatedTableState} from './PaginatedTableContext';
54
import {TableChunk} from './TableChunk';
65
import {TableHead} from './TableHead';
76
import {DEFAULT_TABLE_ROW_HEIGHT} from './constants';
@@ -12,7 +11,6 @@ import type {
1211
GetRowClassName,
1312
HandleTableColumnsResize,
1413
PaginatedTableData,
15-
RenderControls,
1614
RenderEmptyDataMessage,
1715
RenderErrorMessage,
1816
SortParams,
@@ -30,10 +28,9 @@ export interface PaginatedTableProps<T, F> {
3028
columns: Column<T>[];
3129
getRowClassName?: GetRowClassName<T>;
3230
rowHeight?: number;
33-
parentRef: React.RefObject<HTMLElement>;
31+
scrollContainerRef: React.RefObject<HTMLElement>;
3432
initialSortParams?: SortParams;
3533
onColumnsResize?: HandleTableColumnsResize;
36-
renderControls?: RenderControls;
3734
renderEmptyDataMessage?: RenderEmptyDataMessage;
3835
renderErrorMessage?: RenderErrorMessage;
3936
containerClassName?: string;
@@ -52,28 +49,43 @@ export const PaginatedTable = <T, F>({
5249
columns,
5350
getRowClassName,
5451
rowHeight = DEFAULT_TABLE_ROW_HEIGHT,
55-
parentRef,
52+
scrollContainerRef,
5653
initialSortParams,
5754
onColumnsResize,
58-
renderControls,
5955
renderErrorMessage,
6056
renderEmptyDataMessage,
6157
containerClassName,
6258
onDataFetched,
6359
keepCache = true,
6460
}: PaginatedTableProps<T, F>) => {
65-
const initialTotal = initialEntitiesCount || 0;
66-
const initialFound = initialEntitiesCount || 1;
61+
// Get state and setters from context
62+
const {tableState, setSortParams, setTotalEntities, setFoundEntities, setIsInitialLoad} =
63+
usePaginatedTableState();
64+
65+
const {sortParams, foundEntities} = tableState;
66+
67+
// Initialize state with props if available
68+
React.useEffect(() => {
69+
if (initialSortParams) {
70+
setSortParams(initialSortParams);
71+
}
6772

68-
const [sortParams, setSortParams] = React.useState<SortParams | undefined>(initialSortParams);
69-
const [totalEntities, setTotalEntities] = React.useState(initialTotal);
70-
const [foundEntities, setFoundEntities] = React.useState(initialFound);
71-
const [isInitialLoad, setIsInitialLoad] = React.useState(true);
73+
if (initialEntitiesCount) {
74+
setTotalEntities(initialEntitiesCount);
75+
setFoundEntities(initialEntitiesCount);
76+
}
77+
}, [
78+
setSortParams,
79+
setTotalEntities,
80+
setFoundEntities,
81+
initialSortParams,
82+
initialEntitiesCount,
83+
]);
7284

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

7587
const activeChunks = useScrollBasedChunks({
76-
parentRef,
88+
scrollContainerRef,
7789
tableRef,
7890
totalItems: foundEntities,
7991
rowHeight,
@@ -105,18 +117,18 @@ export const PaginatedTable = <T, F>({
105117
onDataFetched?.(data);
106118
}
107119
},
108-
[onDataFetched],
120+
[onDataFetched, setFoundEntities, setIsInitialLoad, setTotalEntities],
109121
);
110122

111-
// reset table on filters change
123+
// Reset table on filters change
112124
React.useLayoutEffect(() => {
113-
setTotalEntities(initialTotal);
114-
setFoundEntities(initialFound);
125+
const defaultTotal = initialEntitiesCount || 0;
126+
const defaultFound = initialEntitiesCount || 1;
127+
128+
setTotalEntities(defaultTotal);
129+
setFoundEntities(defaultFound);
115130
setIsInitialLoad(true);
116-
if (parentRef?.current) {
117-
parentRef.current.scrollTo(0, 0);
118-
}
119-
}, [rawFilters, initialFound, initialTotal, parentRef]);
131+
}, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]);
120132

121133
const renderChunks = () => {
122134
return activeChunks.map((isActive, index) => (
@@ -148,24 +160,9 @@ export const PaginatedTable = <T, F>({
148160
</table>
149161
);
150162

151-
const renderContent = () => {
152-
if (renderControls) {
153-
return (
154-
<TableWithControlsLayout>
155-
<TableWithControlsLayout.Controls>
156-
{renderControls({inited: !isInitialLoad, totalEntities, foundEntities})}
157-
</TableWithControlsLayout.Controls>
158-
<TableWithControlsLayout.Table>{renderTable()}</TableWithControlsLayout.Table>
159-
</TableWithControlsLayout>
160-
);
161-
}
162-
163-
return renderTable();
164-
};
165-
166163
return (
167164
<div ref={tableRef} className={b(null, containerClassName)}>
168-
{renderContent()}
165+
{renderTable()}
169166
</div>
170167
);
171168
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React from 'react';
2+
3+
import type {PaginatedTableState} from './types';
4+
5+
// Default state for the table
6+
const defaultTableState: PaginatedTableState = {
7+
sortParams: undefined,
8+
totalEntities: 0,
9+
foundEntities: 0,
10+
isInitialLoad: true,
11+
};
12+
13+
// Context type definition
14+
interface PaginatedTableStateContextType {
15+
// State
16+
tableState: PaginatedTableState;
17+
18+
// Granular setters
19+
setSortParams: (params: PaginatedTableState['sortParams']) => void;
20+
setTotalEntities: (total: number) => void;
21+
setFoundEntities: (found: number) => void;
22+
setIsInitialLoad: (isInitial: boolean) => void;
23+
}
24+
25+
// Creating the context with default values
26+
export const PaginatedTableStateContext = React.createContext<PaginatedTableStateContextType>({
27+
tableState: defaultTableState,
28+
setSortParams: () => undefined,
29+
setTotalEntities: () => undefined,
30+
setFoundEntities: () => undefined,
31+
setIsInitialLoad: () => undefined,
32+
});
33+
34+
// Provider component props
35+
interface PaginatedTableStateProviderProps {
36+
children: React.ReactNode;
37+
initialState?: Partial<PaginatedTableState>;
38+
}
39+
40+
// Provider component
41+
export const PaginatedTableProvider = ({
42+
children,
43+
initialState = {},
44+
}: PaginatedTableStateProviderProps) => {
45+
// Use individual state variables for each field
46+
const [sortParams, setSortParams] = React.useState<PaginatedTableState['sortParams']>(
47+
initialState.sortParams ?? defaultTableState.sortParams,
48+
);
49+
const [totalEntities, setTotalEntities] = React.useState<number>(
50+
initialState.totalEntities ?? defaultTableState.totalEntities,
51+
);
52+
const [foundEntities, setFoundEntities] = React.useState<number>(
53+
initialState.foundEntities ?? defaultTableState.foundEntities,
54+
);
55+
const [isInitialLoad, setIsInitialLoad] = React.useState<boolean>(
56+
initialState.isInitialLoad ?? defaultTableState.isInitialLoad,
57+
);
58+
59+
// Construct tableState from individual state variables
60+
const tableState = React.useMemo(
61+
() => ({
62+
sortParams,
63+
totalEntities,
64+
foundEntities,
65+
isInitialLoad,
66+
}),
67+
[sortParams, totalEntities, foundEntities, isInitialLoad],
68+
);
69+
70+
// Create the context value with the constructed tableState and direct setters
71+
const contextValue = React.useMemo(
72+
() => ({
73+
tableState,
74+
setSortParams,
75+
setTotalEntities,
76+
setFoundEntities,
77+
setIsInitialLoad,
78+
}),
79+
[tableState, setSortParams, setTotalEntities, setFoundEntities, setIsInitialLoad],
80+
);
81+
82+
return (
83+
<PaginatedTableStateContext.Provider value={contextValue}>
84+
{children}
85+
</PaginatedTableStateContext.Provider>
86+
);
87+
};
88+
89+
// Custom hook for consuming the context
90+
export const usePaginatedTableState = () => {
91+
const context = React.useContext(PaginatedTableStateContext);
92+
93+
if (context === undefined) {
94+
throw new Error('usePaginatedTableState must be used within a PaginatedTableStateProvider');
95+
}
96+
97+
return context;
98+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import React from 'react';
2+
3+
import {TableWithControlsLayout} from '../TableWithControlsLayout/TableWithControlsLayout';
4+
import type {TableProps} from '../TableWithControlsLayout/TableWithControlsLayout';
5+
6+
import {PaginatedTableProvider} from './PaginatedTableContext';
7+
import type {PaginatedTableState} from './types';
8+
9+
export interface PaginatedTableWithLayoutProps {
10+
controls: React.ReactNode;
11+
table: React.ReactNode;
12+
tableProps?: TableProps;
13+
error?: React.ReactNode;
14+
initialState?: Partial<PaginatedTableState>;
15+
fullHeight?: boolean;
16+
}
17+
18+
export const PaginatedTableWithLayout = ({
19+
controls,
20+
table,
21+
tableProps,
22+
error,
23+
initialState,
24+
fullHeight = true,
25+
}: PaginatedTableWithLayoutProps) => (
26+
<PaginatedTableProvider initialState={initialState}>
27+
<TableWithControlsLayout fullHeight={fullHeight}>
28+
<TableWithControlsLayout.Controls>{controls}</TableWithControlsLayout.Controls>
29+
{error}
30+
<TableWithControlsLayout.Table {...(tableProps || {})}>
31+
{table}
32+
</TableWithControlsLayout.Table>
33+
</TableWithControlsLayout>
34+
</PaginatedTableProvider>
35+
);

src/components/PaginatedTable/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,14 @@ export type FetchData<T, F = undefined, E = {}> = (
5858

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

61-
interface ControlsParams {
61+
export interface PaginatedTableState {
62+
sortParams?: SortParams;
6263
totalEntities: number;
6364
foundEntities: number;
64-
inited: boolean;
65+
isInitialLoad: boolean;
6566
}
6667

67-
export type RenderControls = (params: ControlsParams) => React.ReactNode;
68+
export type RenderControls = () => React.ReactNode;
6869
export type RenderEmptyDataMessage = () => React.ReactNode;
6970
export type RenderErrorMessage = (error: IResponseError) => React.ReactNode;
7071

src/components/PaginatedTable/useScrollBasedChunks.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import {calculateElementOffsetTop, rafThrottle} from './utils';
44

55
interface UseScrollBasedChunksProps {
6-
parentRef: React.RefObject<HTMLElement>;
6+
scrollContainerRef: React.RefObject<HTMLElement>;
77
tableRef: React.RefObject<HTMLElement>;
88
totalItems: number;
99
rowHeight: number;
@@ -14,7 +14,7 @@ interface UseScrollBasedChunksProps {
1414
const DEFAULT_OVERSCAN_COUNT = 1;
1515

1616
export const useScrollBasedChunks = ({
17-
parentRef,
17+
scrollContainerRef,
1818
tableRef,
1919
totalItems,
2020
rowHeight,
@@ -32,7 +32,7 @@ export const useScrollBasedChunks = ({
3232
);
3333

3434
const calculateVisibleRange = React.useCallback(() => {
35-
const container = parentRef?.current;
35+
const container = scrollContainerRef?.current;
3636
const table = tableRef.current;
3737
if (!container || !table) {
3838
return null;
@@ -49,7 +49,7 @@ export const useScrollBasedChunks = ({
4949
Math.max(chunksCount - 1, 0),
5050
);
5151
return {start, end};
52-
}, [parentRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);
52+
}, [scrollContainerRef, tableRef, rowHeight, chunkSize, overscanCount, chunksCount]);
5353

5454
const updateVisibleChunks = React.useCallback(() => {
5555
const newRange = calculateVisibleRange();
@@ -80,7 +80,7 @@ export const useScrollBasedChunks = ({
8080
}, [updateVisibleChunks]);
8181

8282
React.useEffect(() => {
83-
const container = parentRef?.current;
83+
const container = scrollContainerRef?.current;
8484
if (!container) {
8585
return undefined;
8686
}
@@ -91,7 +91,7 @@ export const useScrollBasedChunks = ({
9191
return () => {
9292
container.removeEventListener('scroll', throttledHandleScroll);
9393
};
94-
}, [handleScroll, parentRef]);
94+
}, [handleScroll, scrollContainerRef]);
9595

9696
return React.useMemo(() => {
9797
// boolean array that represents active chunks

src/components/TableWithControlsLayout/TableWithControlsLayout.scss

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
@use '../../styles/mixins.scss';
22

33
.ydb-table-with-controls-layout {
4-
--data-table-sticky-top-offset: 62px;
4+
// Total height of all fixed elements above table for sticky header positioning
5+
--data-table-sticky-header-offset: 62px;
56

67
display: inline-block;
78

89
box-sizing: border-box;
910
min-width: 100%;
1011

12+
&_full-height {
13+
min-height: calc(100% - var(--sticky-tabs-height, 0px));
14+
}
15+
1116
&__controls-wrapper {
1217
z-index: 3;
1318

@@ -33,11 +38,11 @@
3338
}
3439

3540
.ydb-paginated-table__head {
36-
top: var(--data-table-sticky-top-offset, 62px);
41+
top: var(--data-table-sticky-header-offset, 62px);
3742
}
3843

3944
.data-table__sticky_moving {
4045
// Place table head right after controls
41-
top: var(--data-table-sticky-top-offset, 62px) !important;
46+
top: var(--data-table-sticky-header-offset, 62px) !important;
4247
}
4348
}

0 commit comments

Comments
 (0)