Skip to content

fix(AnalyticalTable - subcomponents): fix non-breaking React error & improve performance #7366

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

Merged
merged 2 commits into from
Jun 6, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import type { VirtualItem } from '@tanstack/react-virtual';
import { useIsomorphicLayoutEffect } from '@ui5/webcomponents-react-base';
import { useRef } from 'react';
import type { ReactNode } from 'react';
import { useEffect, useRef } from 'react';
import type { ClassNames } from '../types/index.js';
import type { ClassNames, RowType } from '../types/index.js';

interface RowSubComponent {
subComponentsHeight: Record<string, { rowId: string; subComponentHeight?: number }>;
interface RowSubComponentProps {
subComponentsHeight: Record<string, { rowId: string; subComponentHeight?: number }> | undefined;
virtualRow: VirtualItem;
dispatch: (e: { type: string; payload?: Record<string, unknown> }) => void;
row: Record<string, unknown>;
row: RowType;
rowHeight: number;
children: ReactNode | ReactNode[];
rows: Record<string, unknown>[];
rows: RowType[];
alwaysShowSubComponent: boolean;
rowIndex: number;
classNames: ClassNames;
renderSubComp: boolean;
}

export const RowSubComponent = (props: RowSubComponent) => {
export function RowSubComponent(props: RowSubComponentProps) {
const {
subComponentsHeight,
subComponentsHeight = {},
virtualRow,
dispatch,
row,
Expand All @@ -28,79 +30,76 @@ export const RowSubComponent = (props: RowSubComponent) => {
alwaysShowSubComponent,
rowIndex,
classNames,
renderSubComp,
} = props;
const subComponentRef = useRef(null);
const subComponentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const subComponentHeightObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const target = entry.target.getBoundingClientRect();
if (target) {
// Firefox implements `borderBoxSize` as a single content rect, rather than an array
const borderBoxSize = Array.isArray(entry.borderBoxSize) ? entry.borderBoxSize[0] : entry.borderBoxSize;
// Safari doesn't implement `borderBoxSize`
const subCompHeight = borderBoxSize?.blockSize ?? target.height;
if (subComponentsHeight?.[virtualRow.index]?.subComponentHeight !== subCompHeight && subCompHeight !== 0) {
// use most common sub-component height of first 10 sub-components as default height
if (alwaysShowSubComponent && subComponentsHeight && Object.keys(subComponentsHeight).length === 10) {
const objGroupedByHeight = Object.values(subComponentsHeight).reduce((acc, cur) => {
const count = acc?.[cur.subComponentHeight];
if (typeof count === 'number') {
return { ...acc, [cur.subComponentHeight]: count + 1 };
}
return { ...acc, [cur.subComponentHeight]: 1 };
}, {});
useIsomorphicLayoutEffect(() => {
const subComponentElement = subComponentRef.current;
if (!subComponentElement || !renderSubComp) {
return;
}

const mostUsedHeight = Object.keys(objGroupedByHeight).reduce((a, b) =>
objGroupedByHeight[a] > objGroupedByHeight[b] ? a : b,
);
const estimatedHeights = rows.reduce((acc, cur, index) => {
acc[index] = { subComponentHeight: parseInt(mostUsedHeight), rowId: cur.id };
return acc;
}, {});
dispatch({
type: 'SUB_COMPONENTS_HEIGHT',
payload: { ...estimatedHeights, ...subComponentsHeight },
});
} else {
dispatch({
type: 'SUB_COMPONENTS_HEIGHT',
payload: {
...subComponentsHeight,
[virtualRow.index]: { subComponentHeight: subCompHeight, rowId: row.id },
},
});
}
}
// recalc if row id of row index has changed
if (
subComponentsHeight?.[virtualRow.index]?.rowId != null &&
subComponentsHeight?.[virtualRow.index]?.rowId !== row.id
) {
dispatch({
type: 'SUB_COMPONENTS_HEIGHT',
payload: {
...subComponentsHeight,
[virtualRow.index]: { subComponentHeight: subCompHeight, rowId: row.id },
},
});
const measureAndDispatch = (height: number) => {
const prev: { rowId?: string; subComponentHeight?: number } = subComponentsHeight?.[virtualRow.index] ?? {};
if (height === 0 || (prev.subComponentHeight === height && prev.rowId === row.id)) {
return;
}

// use most common subComponentHeight height of first 10 subcomponents as default height
if (alwaysShowSubComponent && Object.keys(subComponentsHeight).length === 10) {
// create frequency map -> most common height has the highest number
const frequencyMap: Record<number, number> = {};
Object.values(subComponentsHeight).forEach(({ subComponentHeight }) => {
if (subComponentHeight) {
frequencyMap[subComponentHeight] = (frequencyMap[subComponentHeight] || 0) + 1;
}
}
});
});
const mostUsedHeight = Number(Object.entries(frequencyMap).sort((a, b) => b[1] - a[1])[0]?.[0] || 0);
const estimatedHeight: typeof subComponentsHeight = {};
rows.forEach((row, index) => {
estimatedHeight[index] = { subComponentHeight: mostUsedHeight, rowId: row.id };
});
dispatch({ type: 'SUB_COMPONENTS_HEIGHT', payload: { ...estimatedHeight, ...subComponentsHeight } });
} else {
dispatch({
type: 'SUB_COMPONENTS_HEIGHT',
payload: {
...subComponentsHeight,
[virtualRow.index]: { subComponentHeight: height, rowId: row.id },
},
});
}
};

measureAndDispatch(subComponentElement.getBoundingClientRect().height);

const observer = new ResizeObserver(([entry]) => {
measureAndDispatch(entry.borderBoxSize[0].blockSize);
});
if (subComponentRef.current?.firstChild) {
subComponentHeightObserver.observe(subComponentRef.current?.firstChild);
}
observer.observe(subComponentElement);

return () => {
subComponentHeightObserver.disconnect();
observer.disconnect();
};
}, [
subComponentRef.current?.firstChild,
subComponentsHeight,
row.id,
subComponentsHeight?.[virtualRow.index]?.subComponentHeight,
virtualRow.index,
]);
}, [renderSubComp, subComponentsHeight, virtualRow.index, row.id, alwaysShowSubComponent, rows]);

// reset subComponentHeight
useIsomorphicLayoutEffect(() => {
if (!renderSubComp && subComponentsHeight?.[virtualRow.index]?.subComponentHeight) {
dispatch({
type: 'SUB_COMPONENTS_HEIGHT',
payload: {
...subComponentsHeight,
[virtualRow.index]: { subComponentHeight: 0, rowId: row.id },
},
});
}
}, [renderSubComp, subComponentsHeight, virtualRow.index, row.id, dispatch]);

if (!renderSubComp) {
return null;
}

return (
<div
Expand All @@ -117,6 +116,6 @@ export const RowSubComponent = (props: RowSubComponent) => {
{children}
</div>
);
};
}

RowSubComponent.displayName = 'RowSubComponent';
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { Virtualizer } from '@tanstack/react-virtual';
import { clsx } from 'clsx';
import type { MutableRefObject } from 'react';
import { useEffect, useMemo, useRef } from 'react';
import { AnalyticalTableSubComponentsBehavior } from '../../../enums/index.js';
import type {
AnalyticalTablePropTypes,
ClassNames,
Expand Down Expand Up @@ -35,7 +34,6 @@ interface VirtualTableBodyProps {
manualGroupBy?: boolean;
subRowsKey: string;
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
subComponentsBehavior: AnalyticalTablePropTypes['subComponentsBehavior'];
triggerScroll?: TriggerScrollState;
scrollToRef: MutableRefObject<ScrollToRefType>;
rowVirtualizer: Virtualizer<DivWithCustomScrollProp, HTMLElement>;
Expand All @@ -62,7 +60,6 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
manualGroupBy,
subRowsKey,
scrollContainerRef,
subComponentsBehavior,
triggerScroll,
rowVirtualizer,
} = props;
Expand Down Expand Up @@ -161,21 +158,6 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
const isNavigatedCell = typeof markNavigatedRow === 'function' ? markNavigatedRow(row) : false;
const RowSubComponent = typeof renderRowSubComponent === 'function' ? renderRowSubComponent(row) : undefined;

if (
(!RowSubComponent ||
(subComponentsBehavior === AnalyticalTableSubComponentsBehavior.IncludeHeightExpandable &&
!row.isExpanded)) &&
subComponentsHeight &&
subComponentsHeight?.[virtualRow.index]?.subComponentHeight
) {
dispatch({
type: 'SUB_COMPONENTS_HEIGHT',
payload: {
...subComponentsHeight,
[virtualRow.index]: { subComponentHeight: 0, rowId: row.id },
},
});
}
let updatedHeight = rowHeight;
if (
renderRowSubComponent &&
Expand Down Expand Up @@ -205,7 +187,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
height: `${updatedHeight}px`,
}}
>
{RowSubComponent && (row.isExpanded || alwaysShowSubComponent) && (
{typeof renderRowSubComponent === 'function' && (
<SubComponent
subComponentsHeight={subComponentsHeight}
virtualRow={virtualRow}
Expand All @@ -216,6 +198,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
alwaysShowSubComponent={alwaysShowSubComponent}
rowIndex={visibleRowIndex + 1}
classNames={classes}
renderSubComp={RowSubComponent && (row.isExpanded || alwaysShowSubComponent)}
>
{RowSubComponent}
</SubComponent>
Expand Down
4 changes: 2 additions & 2 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -545,12 +545,13 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
adjustTableHeightOnPopIn
? popInRowHeight
: internalRowHeight;

if (includeSubCompRowHeight) {
let initialBodyHeightWithSubComps = 0;
for (let i = 0; i < rowNum; i++) {
if (tableState.subComponentsHeight[i]) {
initialBodyHeightWithSubComps += tableState.subComponentsHeight[i].subComponentHeight + rowHeight;
} else if (rows[i]) {
} else {
initialBodyHeightWithSubComps += rowHeight;
}
}
Expand Down Expand Up @@ -866,7 +867,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
columnVirtualizer={columnVirtualizer}
manualGroupBy={reactTableOptions?.manualGroupBy as boolean | undefined}
subRowsKey={subRowsKey}
subComponentsBehavior={subComponentsBehavior}
triggerScroll={tableState.triggerScroll}
rowVirtualizer={rowVirtualizer}
/>
Expand Down
Loading