Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 11 additions & 25 deletions semcore/data-table/__tests__/data-table-accordion.browser-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,7 @@ test.describe('Accordion in table', () => {
const htmlContent = await e2eStandToHtml(standPath, 'en', {
accordionMode: 'independent',
onAccordionToggle: `function(type, rowId, rowIndex) {
console.log("Accordion " + type + " for row #" + rowIndex);
}`,
console.log("Accordion " + type + " for row #" + rowIndex);}`,
});

await page.setContent(htmlContent);
Expand Down Expand Up @@ -412,10 +411,6 @@ test.describe('Accordion in table', () => {
await expect(page.locator('[data-ui-name="ButtonLink"]').nth(1)).toBeFocused();
await page.keyboard.press('Enter');
await locators.rowTableInTable(page, 2, 9).waitFor({ state: 'hidden' });
await page.waitForEvent('console', {
predicate: (msg) => msg.type() === 'log' && msg.text() === 'Accordion close for row #1',
timeout: 200,
});
expect(messages.length).toBe(1);
expect(messages).toEqual(['Accordion close for row #1']);
messages = [];
Expand All @@ -432,7 +427,7 @@ test.describe('Accordion in table', () => {
});

test('Verify table in table mouse navigation when accordionMode=independent', async ({ page, browserName }) => {
if (browserName === 'webkit') return;// disabled for websbkit because works in debug mode but lags in docker
if (browserName === 'webkit') return;// disabled for webkit because works in debug mode but lags in docker
let messages: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'log' && msg.text().startsWith('Accordion')) {
Expand Down Expand Up @@ -470,10 +465,6 @@ test.describe('Accordion in table', () => {
messages = [];
await locators.toggle(page).first().click();
await locators.rowTableInTable(page, 2, 3).waitFor({ state: 'hidden' });
await page.waitForEvent('console', {
predicate: (msg) => msg.type() === 'log' && msg.text() === 'Accordion close for row #0',
timeout: 500,
});
expect(messages.length).toBe(1);
expect(messages).toEqual(['Accordion close for row #0']);
});
Expand All @@ -483,10 +474,6 @@ test.describe('Accordion in table', () => {
const row = locators.row(page, 3);
await row.getByRole('gridcell').first().click();
await locators.rowTableInTable(page, 2, 6).waitFor({ state: 'hidden' });
await page.waitForEvent('console', {
predicate: (msg) => msg.type() === 'log' && msg.text() === 'Accordion close for row #1',
timeout: 200,
});
expect(messages.length).toBe(1);
expect(messages).toEqual(['Accordion close for row #1']);
});
Expand Down Expand Up @@ -548,14 +535,15 @@ test.describe('Accordion in table', () => {
await page.keyboard.press('ArrowUp');
await page.keyboard.press('ArrowUp');
await page.keyboard.press('ArrowUp');
const waitForAccordionClose = page.waitForEvent('console', {
predicate: (msg) => msg.type() === 'log' && msg.text() === 'Accordion close for row #0',
timeout: 1000,
});
await page.keyboard.press('Enter');
await page.waitForTimeout(100);
await locators.rowTableInTable(page, 2, 6).waitFor({ state: 'visible' });
await page.waitForEvent('console', {
predicate: (msg) => msg.type() === 'log' && msg.text() === 'Accordion close for row #0',
timeout: 200,
});
expect(messages.length).toBe(2);
await waitForAccordionClose;

expect(messages).toEqual(['Accordion open for row #1',
'Accordion close for row #0']);
for (let i = 0; i < 6; i++) await page.keyboard.press('ArrowDown');
Expand All @@ -574,10 +562,7 @@ test.describe('Accordion in table', () => {
for (let i = 0; i < 6; i++) await page.keyboard.press('ArrowUp');
await page.keyboard.press('Enter');
await locators.rowTableInTable(page, 2, 6).waitFor({ state: 'hidden' });
await page.waitForEvent('console', {
predicate: (msg) => msg.type() === 'log' && msg.text() === 'Accordion close for row #1',
timeout: 200,
});

expect(messages.length).toBe(1);
expect(messages).toEqual(['Accordion close for row #1']);
messages = [];
Expand Down Expand Up @@ -864,14 +849,15 @@ test.describe('Accordion in table', () => {
const htmlContent = await e2eStandToHtml(standPath, 'en');

await page.setContent(htmlContent);
await new Promise((resolve) => setTimeout(resolve, 1000)); // need this for AccordionRows grid calculations after rendering
const tableInTableRow = page.locator('[aria-rowindex="5"][aria-level="2"]');
await page.keyboard.press('Tab');
await page.keyboard.press('Enter');
await tableInTableRow.waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot();
await page.keyboard.press('ArrowRight');
await page.keyboard.press('ArrowRight');
await page.waitForTimeout(100);
await expect(page).toHaveScreenshot();
if (browserName === 'webkit')
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.01 });
else await expect(page).toHaveScreenshot();
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 142 additions & 0 deletions semcore/data-table/src/components/AccordionRows/AccordionRows.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { Box } from '@semcore/base-components';
import { sstyled } from '@semcore/core';
import trottle from '@semcore/core/lib/utils/rafTrottle';
import React from 'react';

import type { CellRenderProps } from '../Body/Body.types';
import { Row } from '../Body/Row';
import type { DTRow, DTRows } from '../Body/Row.types';
import styles from '../Body/style.shadow.css';
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>();

state: State = {
maxHeight: 0,
};

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>,
);
}
}
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
Loading
Loading