Skip to content

Commit c7dad14

Browse files
authored
Merge b845b56 into 828724d
2 parents 828724d + b845b56 commit c7dad14

File tree

6 files changed

+357
-84
lines changed

6 files changed

+357
-84
lines changed

src/components/PaginatedTable/PaginatedTable.tsx

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
RenderErrorMessage,
1616
} from './types';
1717
import {useScrollBasedChunks} from './useScrollBasedChunks';
18+
import {calculateElementOffsetTop} from './utils';
1819

1920
import './PaginatedTable.scss';
2021

@@ -62,13 +63,15 @@ export const PaginatedTable = <T, F>({
6263
const {sortParams, foundEntities} = tableState;
6364

6465
const tableRef = React.useRef<HTMLDivElement>(null);
66+
const [tableOffset, setTableOffset] = React.useState(0);
6567

66-
const activeChunks = useScrollBasedChunks({
68+
const chunkStates = useScrollBasedChunks({
6769
scrollContainerRef,
6870
tableRef,
6971
totalItems: foundEntities,
7072
rowHeight,
7173
chunkSize,
74+
tableOffset,
7275
});
7376

7477
// this prevent situation when filters are new, but active chunks is not yet recalculated (it will be done to the next rendrer, so we bring filters change on the next render too)
@@ -99,6 +102,25 @@ export const PaginatedTable = <T, F>({
99102
[onDataFetched, setFoundEntities, setIsInitialLoad, setTotalEntities],
100103
);
101104

105+
React.useLayoutEffect(() => {
106+
const scrollContainer = scrollContainerRef.current;
107+
const table = tableRef.current;
108+
if (table && scrollContainer) {
109+
setTableOffset(calculateElementOffsetTop(table, scrollContainer));
110+
}
111+
}, [scrollContainerRef.current, tableRef.current, foundEntities]);
112+
113+
// Set will-change: transform on scroll container if not already set
114+
React.useLayoutEffect(() => {
115+
const scrollContainer = scrollContainerRef.current;
116+
if (scrollContainer) {
117+
const computedStyle = window.getComputedStyle(scrollContainer);
118+
if (computedStyle.willChange !== 'transform') {
119+
scrollContainer.style.willChange = 'transform';
120+
}
121+
}
122+
}, [scrollContainerRef.current]);
123+
102124
// Reset table on initialization and filters change
103125
React.useLayoutEffect(() => {
104126
const defaultTotal = initialEntitiesCount || 0;
@@ -110,32 +132,71 @@ export const PaginatedTable = <T, F>({
110132
}, [initialEntitiesCount, setTotalEntities, setFoundEntities, setIsInitialLoad]);
111133

112134
const renderChunks = () => {
113-
return activeChunks.map((isActive, index) => (
114-
<TableChunk<T, F>
115-
key={index}
116-
id={index}
117-
calculatedCount={index === activeChunks.length - 1 ? lastChunkSize : chunkSize}
118-
chunkSize={chunkSize}
119-
rowHeight={rowHeight}
120-
columns={columns}
121-
fetchData={fetchData}
122-
filters={filters}
123-
tableName={tableName}
124-
sortParams={sortParams}
125-
getRowClassName={getRowClassName}
126-
renderErrorMessage={renderErrorMessage}
127-
renderEmptyDataMessage={renderEmptyDataMessage}
128-
onDataFetched={handleDataFetched}
129-
isActive={isActive}
130-
keepCache={keepCache}
131-
/>
132-
));
135+
const chunks: React.ReactElement[] = [];
136+
let i = 0;
137+
138+
while (i < chunkStates.length) {
139+
const chunkState = chunkStates[i];
140+
const shouldRender = chunkState.shouldRender;
141+
const shouldFetch = chunkState.shouldFetch;
142+
const isActive = shouldRender || shouldFetch;
143+
144+
if (isActive) {
145+
// Render active chunk normally
146+
chunks.push(
147+
<TableChunk<T, F>
148+
key={i}
149+
id={i}
150+
calculatedCount={i === chunkStates.length - 1 ? lastChunkSize : chunkSize}
151+
chunkSize={chunkSize}
152+
rowHeight={rowHeight}
153+
columns={columns}
154+
fetchData={fetchData}
155+
filters={filters}
156+
tableName={tableName}
157+
sortParams={sortParams}
158+
getRowClassName={getRowClassName}
159+
renderErrorMessage={renderErrorMessage}
160+
renderEmptyDataMessage={renderEmptyDataMessage}
161+
onDataFetched={handleDataFetched}
162+
shouldFetch={chunkState.shouldFetch}
163+
shouldRender={chunkState.shouldRender}
164+
keepCache={keepCache}
165+
/>,
166+
);
167+
i++;
168+
} else {
169+
// Find consecutive inactive chunks and merge them
170+
const startIndex = i;
171+
let totalHeight = 0;
172+
173+
while (
174+
i < chunkStates.length &&
175+
!chunkStates[i].shouldRender &&
176+
!chunkStates[i].shouldFetch
177+
) {
178+
const currentChunkSize =
179+
i === chunkStates.length - 1 ? lastChunkSize : chunkSize;
180+
totalHeight += currentChunkSize * rowHeight;
181+
i++;
182+
}
183+
184+
// Render merged empty tbody for consecutive inactive chunks
185+
chunks.push(
186+
<tr style={{height: `${totalHeight}px`}} key={`merged-${startIndex}-${i - 1}`}>
187+
<td colSpan={columns.length} style={{padding: 0, border: 'none'}} />
188+
</tr>,
189+
);
190+
}
191+
}
192+
193+
return chunks;
133194
};
134195

135196
const renderTable = () => (
136197
<table className={b('table')}>
137198
<TableHead columns={columns} onSort={setSortParams} onColumnsResize={onColumnsResize} />
138-
{renderChunks()}
199+
<tbody>{renderChunks()}</tbody>
139200
</table>
140201
);
141202

src/components/PaginatedTable/TableChunk.tsx

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ interface TableChunkProps<T, F> {
2929
columns: Column<T>[];
3030
filters?: F;
3131
sortParams?: SortParams;
32-
isActive: boolean;
32+
shouldFetch: boolean;
33+
shouldRender: boolean;
3334
tableName: string;
3435

3536
fetchData: FetchData<T, F>;
@@ -56,7 +57,8 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
5657
renderErrorMessage,
5758
renderEmptyDataMessage,
5859
onDataFetched,
59-
isActive,
60+
shouldFetch,
61+
shouldRender,
6062
keepCache,
6163
}: TableChunkProps<T, F>) {
6264
const [isTimeoutActive, setIsTimeoutActive] = React.useState(true);
@@ -75,7 +77,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
7577
};
7678

7779
tableDataApi.useFetchTableChunkQuery(queryParams, {
78-
skip: isTimeoutActive || !isActive,
80+
skip: isTimeoutActive || !shouldFetch,
7981
pollingInterval: autoRefreshInterval,
8082
refetchOnMountOrArgChange: !keepCache,
8183
});
@@ -85,7 +87,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
8587
React.useEffect(() => {
8688
let timeout = 0;
8789

88-
if (isActive && isTimeoutActive) {
90+
if (shouldFetch && isTimeoutActive) {
8991
timeout = window.setTimeout(() => {
9092
setIsTimeoutActive(false);
9193
}, DEBOUNCE_TIMEOUT);
@@ -94,31 +96,27 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
9496
return () => {
9597
window.clearTimeout(timeout);
9698
};
97-
}, [isActive, isTimeoutActive]);
99+
}, [shouldFetch, isTimeoutActive]);
98100

99101
React.useEffect(() => {
100-
if (currentData && isActive) {
102+
if (currentData) {
101103
onDataFetched({
102104
...currentData,
103105
data: currentData.data as T[],
104106
found: currentData.found || 0,
105107
total: currentData.total || 0,
106108
});
107109
}
108-
}, [currentData, isActive, onDataFetched]);
110+
}, [currentData, onDataFetched]);
109111

110112
const dataLength = currentData?.data?.length || calculatedCount;
111113

112114
const renderContent = () => {
113-
if (!isActive) {
114-
return null;
115-
}
116-
117115
if (!currentData) {
118116
if (error) {
119117
const errorData = error as IResponseError;
120118
return (
121-
<EmptyTableRow columns={columns}>
119+
<EmptyTableRow columns={columns} height={dataLength * rowHeight}>
122120
{renderErrorMessage ? (
123121
renderErrorMessage(errorData)
124122
) : (
@@ -136,7 +134,7 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
136134
// Data is loaded, but there are no entities in the chunk
137135
if (!currentData.data?.length) {
138136
return (
139-
<EmptyTableRow columns={columns}>
137+
<EmptyTableRow columns={columns} height={dataLength * rowHeight}>
140138
{renderEmptyDataMessage ? renderEmptyDataMessage() : i18n('empty')}
141139
</EmptyTableRow>
142140
);
@@ -153,18 +151,11 @@ export const TableChunk = typedMemo(function TableChunk<T, F>({
153151
));
154152
};
155153

156-
return (
157-
<tbody
158-
id={id.toString()}
159-
style={{
160-
height: `${dataLength * rowHeight}px`,
161-
// Default display: table-row-group doesn't work in Safari and breaks the table
162-
// display: block works in Safari, but disconnects thead and tbody cell grids
163-
// Hack to make it work in all cases
164-
display: isActive ? 'table-row-group' : 'block',
165-
}}
166-
>
167-
{renderContent()}
168-
</tbody>
154+
return shouldRender ? (
155+
renderContent()
156+
) : (
157+
<tr style={{height: `${dataLength * rowHeight}px`}}>
158+
<td colSpan={columns.length} style={{padding: 0, border: 'none'}} />
159+
</tr>
169160
);
170161
});

src/components/PaginatedTable/TableRow.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ interface LoadingTableRowProps<T> {
4444

4545
export const LoadingTableRow = typedMemo(function <T>({columns, height}: LoadingTableRowProps<T>) {
4646
return (
47-
<tr className={b('row', {loading: true})}>
47+
<tr className={b('row', {loading: true})} style={{height}}>
4848
{columns.map((column) => {
4949
const resizeable = column.resizeable ?? DEFAULT_RESIZEABLE;
5050

@@ -79,7 +79,7 @@ export const TableRow = <T,>({row, columns, getRowClassName, height}: TableRowPr
7979
const additionalClassName = getRowClassName?.(row);
8080

8181
return (
82-
<tr className={b('row', additionalClassName)}>
82+
<tr className={b('row', additionalClassName)} style={{height}}>
8383
{columns.map((column) => {
8484
const resizeable = column.resizeable ?? DEFAULT_RESIZEABLE;
8585

@@ -103,11 +103,12 @@ export const TableRow = <T,>({row, columns, getRowClassName, height}: TableRowPr
103103
interface EmptyTableRowProps<T> {
104104
columns: Column<T>[];
105105
children?: React.ReactNode;
106+
height: number;
106107
}
107108

108-
export const EmptyTableRow = <T,>({columns, children}: EmptyTableRowProps<T>) => {
109+
export const EmptyTableRow = <T,>({columns, children, height}: EmptyTableRowProps<T>) => {
109110
return (
110-
<tr className={b('row', {empty: true})}>
111+
<tr className={b('row', {empty: true})} style={{height}}>
111112
<td colSpan={columns.length} className={b('td')}>
112113
{children}
113114
</td>

0 commit comments

Comments
 (0)