Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
421d513
[data-table] some accordion performance
ilyabrower Oct 21, 2025
c19b1fa
Merge branch 'release/v16' into UIK-4348/data-table-accordion-perf
ilyabrower Oct 21, 2025
225e284
[data-table] linter fixes
ilyabrower Oct 21, 2025
4e65021
[data-table] fixed accordions
ilyabrower Oct 21, 2025
fa2aa4d
[data-table] accordions perf v2
ilyabrower Oct 22, 2025
ff511eb
[data-table] fixed import path
ilyabrower Oct 22, 2025
84082ec
[chore] Merge remote-tracking branch 'origin/release/v16' into UIK-43…
ilyabrower Oct 22, 2025
aae2419
[data-table] cleanup
ilyabrower Oct 22, 2025
7b729e1
[data-table] UIK-4427
ilyabrower Oct 23, 2025
2e69ee9
[data-table] UIK-4426
ilyabrower Oct 23, 2025
cc1b77e
[data-table] UIK-4424
ilyabrower Oct 23, 2025
6af43e3
[data-table] UIK-4428
ilyabrower Oct 23, 2025
1239c2e
[dropdown-menu] UIK-4429 fixed Addon realization
ilyabrower Oct 23, 2025
e3dfb30
[data-table] UIK-4430
ilyabrower Oct 23, 2025
a7b5182
[data-table] UIK-4431
ilyabrower Oct 23, 2025
926d593
[data-table] update some snapshots
Valeria-Zimnitskaya Oct 23, 2025
8513fbc
[data-table] update toggle test
Valeria-Zimnitskaya Oct 23, 2025
e1740b4
[stories] add props
Valeria-Zimnitskaya Oct 23, 2025
672fc40
[data-table] fixed tests
ilyabrower Oct 23, 2025
1003997
[chore] Merge branch 'UIK-4348/data-table-accordion-perf' of github.c…
ilyabrower Oct 23, 2025
d1b005f
[data-table] UIK-4435
ilyabrower Oct 24, 2025
cda307c
[data-table] UIK-4434 fixed example - for correct expand-collapse in …
ilyabrower Oct 24, 2025
ef34eac
[data-table] UIK-4439
ilyabrower Oct 24, 2025
98874bf
[data-table] fixed accordion rows tempale columns
ilyabrower Oct 24, 2025
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
6 changes: 6 additions & 0 deletions semcore/core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

CHANGELOG.md standards are inspired by [keepachangelog.com](https://keepachangelog.com/en/1.0.0/).

## [16.5.1] - 2025-10-30

### Fixed

- Unnecessary calculations in `sstyled` wrapper.

## [16.5.0] - 2025-10-03

### Changed
Expand Down
25 changes: 15 additions & 10 deletions semcore/core/src/styled/sstyled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,14 @@ function getClassAndVars(styles: any, name: any, props: any) {
);
}

const reshadowMap = new WeakMap();

function reshadowToShadow(obj: any) {
return Object.entries(obj).reduce((style: any, [name, value]) => {
if (reshadowMap.has(obj)) {
return reshadowMap.get(obj);
}

const shadowStyle = Object.entries(obj).reduce((style: any, [name, value]) => {
let n = name;
if (name.startsWith('__')) {
n = name.replace(/^__/, '');
Expand All @@ -110,6 +116,10 @@ function reshadowToShadow(obj: any) {
style[n] = value;
return style;
}, {});

reshadowMap.set(obj, shadowStyle);

return shadowStyle;
}

function sstyled(styles = {}): ((ReactNode: any) => React.ReactNode) & {
Expand All @@ -119,21 +129,16 @@ function sstyled(styles = {}): ((ReactNode: any) => React.ReactNode) & {
return {
cn(name, props) {
const [classes, style] = getClassAndVars(reshadowToShadow(styles), name, props);
const extraProps = {};

if (Object.keys(classes).length) {
// @ts-ignore
extraProps.className = cn(props.className, classes);
props.className = cn(props.className, classes);
}

if (Object.keys(style).length) {
// @ts-ignore
extraProps.style = Object.assign(style, props.style);
props.style = Object.assign(style, props.style);
}
return {
...props,
...extraProps,
};

return props;
},
};
}
Expand Down
1 change: 1 addition & 0 deletions semcore/data-table/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CHANGELOG.md standards are inspired by [keepachangelog.com](https://keepachangel

### Fixed

- Low performance when opening an accordion with a large number of rows.
- Keyboard interaction after mouse clicking in Safari.

## [16.4.1] - 2025-10-17
Expand Down
211 changes: 211 additions & 0 deletions semcore/data-table/src/components/AccordionRows/AccordionRows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { Box } from '@semcore/base-components';
import { sstyled } from '@semcore/core';
import trottle from '@semcore/core/lib/utils/rafTrottle';
import React from 'react';

import styles from './style.shadow.css';
import type { CellRenderProps } from '../Body/Body.types';
import { Row } from '../Body/Row';
import type { DTRow, DTRows } from '../Body/Row.types';
import type { DataTableData, DataTableProps, DataRowItem, DTUse } from '../DataTable/DataTable.types';
import type { DTColumn } from '../Head/Column.types';

type AccordionRowsProps<Data extends DataTableData, UniqKeyType> = {
accordionId: string;
expanded: boolean;
expandedForAnimation: boolean;

tableRef: React.RefObject<HTMLDivElement>;

use: DTUse;
columns: DTColumn[];
row: DTRow<UniqKeyType> | DTRow<UniqKeyType>[];
rows: DTRows<UniqKeyType>;
flatRows: DTRow<UniqKeyType>[];
rowIndex: number; // from 0
gridRowIndex: number; // from 1 + 1 (or 2 if it has group) header
accordionDuration: number | [number, number];
accordionAnimationRows: number;
sideIndents: 'wide' | undefined;
getFixedStyle: (
cell: Pick<DTColumn, 'name' | 'fixed'>,
) => [side: 'left' | 'right', style: string | number] | [side: undefined, style: undefined];

renderCell: ((props: CellRenderProps<Data[number], UniqKeyType>) => React.ReactNode | Record<string, any>) | undefined;
rawData: DataRowItem[];
shadowVertical: '' | 'end' | 'start' | 'median' | undefined;
variant: DataTableProps<any, any, any>['variant'];
limit: DataTableProps<any, any, any>['limit'];
} & {
'aria-level': number;
};

type State = {
maxHeight: number;
};

export class AccordionRows<Data extends DataTableData, UniqKeyType> extends React.PureComponent<AccordionRowsProps<Data, UniqKeyType>, State> {
accordionRowsRef = React.createRef<HTMLDivElement>();

tableObserver: ResizeObserver;
tableWidth: number = 0;

state: State = {
maxHeight: 0,
};

constructor(props: AccordionRowsProps<Data, UniqKeyType>) {
super(props);

this.tableObserver = new ResizeObserver(this.handleTableResize);
}

componentDidMount(): void {
setTimeout(() => {
this.calculateGridSettings();
}, 500); // need this for calculate widths after Header render.

if (this.props.tableRef.current) {
this.tableWidth = this.props.tableRef.current.getBoundingClientRect().width;
this.tableObserver.observe(this.props.tableRef.current);
}
}

componentWillUnmount(): void {
this.tableObserver.disconnect();
}

componentDidUpdate(prevProps: Readonly<AccordionRowsProps<Data, UniqKeyType>>): void {
const { expanded, rows, expandedForAnimation } = this.props;

if (prevProps.expanded !== expanded && expanded) {
this.setState({
maxHeight: 2000, // some value, more than real window height
});
}
if (prevProps.rows !== rows && this.accordionRowsRef.current) {
this.setState({
maxHeight: this.accordionRowsRef.current.scrollHeight,
});
}
if (prevProps.expandedForAnimation !== expandedForAnimation && expandedForAnimation && !expanded) {
this.setState({ maxHeight: 0 });
}
}

render(): React.ReactNode {
const SAccordionRows = Box;

const {
accordionId,
rows,
expanded,
expandedForAnimation,
getFixedStyle,
columns,
rowIndex,
'aria-level': ariaLevel,
gridRowIndex,
use,
shadowVertical,
accordionDuration,
accordionAnimationRows,
variant,
flatRows,
sideIndents,
renderCell,
rawData,
limit,
} = this.props;

return sstyled(styles)(
<SAccordionRows
id={accordionId}
role='rowgroup'
aria-hidden={!expanded}
ref={this.accordionRowsRef}
hMax={`${this.state.maxHeight}px`}
// @ts-ignore
duration={`${accordionDuration}ms`}
gridRow={`${gridRowIndex + 1} / ${gridRowIndex + 1 + rows.length}`}
>
{(expanded || expandedForAnimation) && rows.map((subrow, i) => {
return (
<Row
key={i}
// @ts-ignore
row={subrow}
columns={columns}
rows={rows}
rowIndex={rowIndex}
aria-hidden={!expanded}
aria-posinset={i + 1}
aria-level={ariaLevel + 1}
gridRowIndex={gridRowIndex + 1 + i}
isAccordionRow={true}
accordionIndex={i}
getFixedStyle={getFixedStyle}
animationExpand={expanded}
accordionRowIndex={i}
use={use}
shadowVertical={shadowVertical}
accordionDuration={accordionDuration}
accordionAnimationRows={accordionAnimationRows}
variant={variant}
flatRows={flatRows}
sideIndents={sideIndents}
renderCell={renderCell}
rawData={rawData}
limit={limit}
/>
);
})}
</SAccordionRows>,
);
}

private handleTableResize = trottle(() => {
this.calculateGridSettings();
});

private calculateGridSettings() {
const { tableRef } = this.props;
const tableElement = tableRef.current;
const currentWidth = tableElement?.getBoundingClientRect().width;

if (currentWidth === this.tableWidth) {
return;
}

this.tableWidth = currentWidth ?? 0;
const tableStyles = tableElement?.style;
const header = tableElement?.querySelector('[data-ui-name="DataTable.Head"]');
const accordionRows = this.accordionRowsRef.current;

if (tableStyles && header && accordionRows) {
let gridTemplateAreas = '';

for (let i = 0; i < tableStyles.length; i++) {
const key = tableStyles[i];
if (key.startsWith('--gridTemplateAreas')) {
gridTemplateAreas = tableStyles.getPropertyValue(key);
accordionRows.style.setProperty(key, gridTemplateAreas);
}
}

const gridTemplateColumns: string[] = [];
gridTemplateAreas.split(' ').forEach((templateArea) => {
const headerCell = header.querySelector(`[role="columnheader"][name="${templateArea}"]`);
const width = headerCell?.getBoundingClientRect().width;

if (width === undefined) {
gridTemplateColumns.push('auto');
} else {
gridTemplateColumns.push(`${width}px`);
}
});

accordionRows.style.setProperty('grid-template-columns', gridTemplateColumns.join(' '));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
SAccordionRows {
display: grid;
grid-column: 1 / -1;
grid-row: var(--gridRow);
overflow: clip;
transition-property: max-height;
transition-duration: var(--duration);
transition-timing-function: linear;
}
28 changes: 14 additions & 14 deletions semcore/data-table/src/components/Body/Body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ class BodyRoot<Data extends DataTableData, UniqKeyType> extends Component<DataTa
}
};

handleComponentRef = (row: DTRow<UniqKeyType>) => (component: RowRoot<Data, UniqKeyType> | null) => {
requestAnimationFrame(() => {
if (component) {
this.rowsComponentsMap.set(row[UNIQ_ROW_KEY], component);
} else {
this.rowsComponentsMap.delete(row[UNIQ_ROW_KEY]);
}
});
};

getRowProps(props: { row: DTRow<UniqKeyType>; mergedRow?: boolean }): RowPropsInner<Data, UniqKeyType> {
const {
use,
Expand All @@ -90,6 +100,7 @@ class BodyRoot<Data extends DataTableData, UniqKeyType> extends Component<DataTa
onSelectRow,
getFixedStyle,
accordionDuration,
accordionAnimationRows,
getI18nText,
renderCell,
tableRef,
Expand Down Expand Up @@ -128,6 +139,7 @@ class BodyRoot<Data extends DataTableData, UniqKeyType> extends Component<DataTa
getFixedStyle,
mergedRow: props.mergedRow,
accordionDuration,
accordionAnimationRows,
flatRows,
getI18nText,
renderCell,
Expand Down Expand Up @@ -302,13 +314,7 @@ class BodyRoot<Data extends DataTableData, UniqKeyType> extends Component<DataTa
key={item[UNIQ_ROW_KEY]?.toString()}
row={item}
mergedRow={i > 0 ? true : false}
componentRef={(component: RowRoot<Data, UniqKeyType> | null) => {
if (component) {
this.rowsComponentsMap.set(item[UNIQ_ROW_KEY], component);
} else {
this.rowsComponentsMap.delete(item[UNIQ_ROW_KEY]);
}
}}
componentRef={this.handleComponentRef(item)}
/>
);
})}
Expand All @@ -320,13 +326,7 @@ class BodyRoot<Data extends DataTableData, UniqKeyType> extends Component<DataTa
key={row[UNIQ_ROW_KEY]?.toString()}
row={row}
ref={virtualScroll ? this.handleRef(this.startIndex + index, row) : undefined}
componentRef={(component: RowRoot<Data, UniqKeyType> | null) => {
if (component) {
this.rowsComponentsMap.set(row[UNIQ_ROW_KEY], component);
} else {
this.rowsComponentsMap.delete(row[UNIQ_ROW_KEY]);
}
}}
componentRef={this.handleComponentRef(row)}
/>
);
})}
Expand Down
9 changes: 5 additions & 4 deletions semcore/data-table/src/components/Body/Body.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Intergalactic } from '@semcore/core';
import type * as React from 'react';

import type { CellPropsInner, Theme } from './Cell.types';
import type { DTRow } from './Row.types';
import type { DataTableCellProps, Theme } from './Cell.types';
import type { DTRow, RowPropsInner } from './Row.types';
import type { ACCORDION } from '../DataTable/DataTable';
import type { DataRowItem, DTUse, VirtualScroll, DataTableProps, DataTableData } from '../DataTable/DataTable.types';
import type { DTColumn } from '../Head/Column.types';
Expand Down Expand Up @@ -85,15 +85,16 @@ export type BodyPropsInner<Data extends DataTableData, UniqKeyType> = DataTableB
getFixedStyle: (
cell: Pick<DTColumn, 'name' | 'fixed'>,
) => [side: 'left' | 'right', style: string | number] | [side: undefined, style: undefined];
accordionDuration?: DataTableProps<any, any, any>['accordionDuration'];
onCellClick: CellPropsInner<Data, UniqKeyType>['onClick'];
accordionDuration: number | [number, number];
onCellClick: DataTableCellProps<Data, UniqKeyType>['onClick'];
rawData: DataRowItem[];
accordionMode?: DataTableProps<any, any, any>['accordionMode'];
shadowVertical?: '' | 'end' | 'start' | 'median';
renderCellOverlay?: () => React.ReactNode;
limit?: DataTableProps<any, any, any>['limit'];
variant?: DataTableProps<any, any, any>['variant'];
totalRows?: number;
accordionAnimationRows: RowPropsInner<Data, UniqKeyType>['accordionAnimationRows'];
};

export type DataTableBodyType = (<
Expand Down
Loading
Loading