Skip to content

Commit 07a9ace

Browse files
authored
feat(AnalyticalTable): add option to always show subcomponents below a row, fix race condition in subcomponent renderer (#1377)
1 parent a7b82c4 commit 07a9ace

File tree

5 files changed

+133
-35
lines changed

5 files changed

+133
-35
lines changed

packages/main/src/components/AnalyticalTable/AnalyticalTable.stories.mdx

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { useRowDisableSelection } from '@ui5/webcomponents-react/lib/AnalyticalT
3030
title="Data Display / AnalyticalTable"
3131
component={AnalyticalTable}
3232
args={{
33-
data: generateData(1000),
33+
data: generateData(500),
3434
columns: [
3535
{
3636
Header: 'Name',
@@ -362,7 +362,7 @@ For more details on this behavior you can double check the [react-table docs](ht
362362
## Tree Table
363363

364364
<Canvas>
365-
<Story name="Tree Table" args={{ data: generateData(20, true), isTreeTable: true }}>
365+
<Story name="Tree Table" args={{ data: generateData(15, true), isTreeTable: true }}>
366366
{(args) => <AnalyticalTable {...args} />}
367367
</Story>
368368
</Canvas>
@@ -471,12 +471,63 @@ const InfiniteScrollTable = (props) => {
471471
## AnalyticalTable with subcomponents
472472

473473
Adding custom subcomponents below table rows can be achieved by setting the `renderRowSubComponent` prop.
474-
The prop expects a function with an optional parameter containing the `row` instance, there you can control which row should display subcomponents.
474+
The prop expects a function with an optional parameter containing the `row` instance, there you can control which row should display subcomponents. If you want to display the subcomponent at the bottom of the row without an expandable container, you can set the `alwaysShowSubComponent` prop to `true`.
475475

476476
**Note:** When `renderRowSubComponent` is set, `grouping` is disabled.
477477

478+
<ArgsTable story="with subcomponents" />
479+
478480
<Canvas>
479-
<Story name="with subcomponents" args={{ data: generateData(100) }}>
481+
<Story
482+
name="with subcomponents"
483+
args={{ data: generateData(50) }}
484+
argTypes={{
485+
columns: { table: { disable: true } },
486+
data: { table: { disable: true } },
487+
title: { table: { disable: true } },
488+
extension: { table: { disable: true } },
489+
minRows: { table: { disable: true } },
490+
visibleRowCountMode: { table: { disable: true } },
491+
visibleRows: { table: { disable: true } },
492+
loading: { table: { disable: true } },
493+
noDataText: { table: { disable: true } },
494+
rowHeight: { table: { disable: true } },
495+
alternateRowColor: { table: { disable: true } },
496+
withRowHighlight: { table: { disable: true } },
497+
highlightField: { table: { disable: true } },
498+
filterable: { table: { disable: true } },
499+
sortable: { table: { disable: true } },
500+
groupable: { table: { disable: true } },
501+
groupBy: { table: { disable: true } },
502+
selectionBehavior: { table: { disable: true } },
503+
selectionMode: { table: { disable: true } },
504+
scaleWidthMode: { table: { disable: true } },
505+
columnOrder: { table: { disable: true } },
506+
infiniteScroll: { table: { disable: true } },
507+
infiniteScrollThreshold: { table: { disable: true } },
508+
onSort: { table: { disable: true } },
509+
onGroup: { table: { disable: true } },
510+
onRowSelected: { table: { disable: true } },
511+
onRowExpandChange: { table: { disable: true } },
512+
onColumnsReordered: { table: { disable: true } },
513+
onLoadMore: { table: { disable: true } },
514+
reactTableOptions: { table: { disable: true } },
515+
tableHooks: { table: { disable: true } },
516+
subRowsKey: { table: { disable: true } },
517+
selectedRowIds: { table: { disable: true } },
518+
isTreeTable: { table: { disable: true } },
519+
overscanCountHorizontal: { table: { disable: true } },
520+
overscanCount: { table: { disable: true } },
521+
NoDataComponent: { table: { disable: true } },
522+
LoadingComponent: { table: { disable: true } },
523+
style: { table: { disable: true } },
524+
className: { table: { disable: true } },
525+
tooltip: { table: { disable: true } },
526+
onRowClick: { table: { disable: true } },
527+
withNavigationHighlight: { table: { disable: true } },
528+
markNavigatedRow: { table: { disable: true } }
529+
}}
530+
>
480531
{(args) => {
481532
const renderRowSubComponent = (row) => {
482533
if (row.id === '0') {
@@ -520,7 +571,14 @@ The prop expects a function with an optional parameter containing the `row` inst
520571
</FlexBox>
521572
);
522573
};
523-
return <AnalyticalTable data={args.data} columns={args.columns} renderRowSubComponent={renderRowSubComponent} />;
574+
return (
575+
<AnalyticalTable
576+
data={args.data}
577+
columns={args.columns}
578+
renderRowSubComponent={renderRowSubComponent}
579+
alwaysShowSubComponent={args.alwaysShowSubComponent}
580+
/>
581+
);
524582
}}
525583
</Story>
526584
</Canvas>
@@ -571,7 +629,14 @@ const TableWithSubcomponents = (props) => {
571629
</FlexBox>
572630
);
573631
};
574-
return <AnalyticalTable data={props.data} columns={props.columns} renderRowSubComponent={renderRowSubComponent} />;
632+
return (
633+
<AnalyticalTable
634+
data={props.data}
635+
columns={props.columns}
636+
renderRowSubComponent={renderRowSubComponent}
637+
alwaysShowSubComponent={false} //default value
638+
/>
639+
);
575640
};
576641
```
577642

@@ -623,6 +688,7 @@ By adding the `visibleRowCountMode` prop and setting it to `TableVisibleRowCount
623688
overscanCountHorizontal: { table: { disable: true } },
624689
overscanCount: { table: { disable: true } },
625690
renderRowSubComponent: { table: { disable: true } },
691+
alwaysShowSubComponent: { table: { disable: true } },
626692
NoDataComponent: { table: { disable: true } },
627693
LoadingComponent: { table: { disable: true } },
628694
style: { table: { disable: true } },
@@ -755,6 +821,7 @@ In the example below you can have a look at this behavior:
755821
overscanCountHorizontal: { table: { disable: true } },
756822
overscanCount: { table: { disable: true } },
757823
renderRowSubComponent: { table: { disable: true } },
824+
alwaysShowSubComponent: { table: { disable: true } },
758825
NoDataComponent: { table: { disable: true } },
759826
LoadingComponent: { table: { disable: true } },
760827
style: { table: { disable: true } },

packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBody.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ interface VirtualTableBodyProps {
2424
popInRowHeight: number;
2525
isRtl: boolean;
2626
markNavigatedRow?: (row?: Record<any, any>) => boolean;
27+
alwaysShowSubComponent: boolean;
28+
dispatch?: (e: { type: string; payload?: any }) => void;
29+
subComponentsHeight?: Record<string, { rowId: string; subComponentHeight?: number }>;
2730
}
2831

2932
export const VirtualTableBody = (props: VirtualTableBodyProps) => {
@@ -45,11 +48,12 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
4548
renderRowSubComponent,
4649
popInRowHeight,
4750
markNavigatedRow,
48-
isRtl
51+
isRtl,
52+
alwaysShowSubComponent,
53+
dispatch,
54+
subComponentsHeight
4955
} = props;
5056

51-
const rowSubComponentsHeight = useRef({});
52-
5357
const itemCount = Math.max(minRows, rows.length);
5458
const overscan = overscanCount ? overscanCount : Math.floor(visibleRows / 2);
5559
const consolidatedParentRef = useConsolidatedRef(parentRef);
@@ -60,16 +64,19 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
6064
parentRef: consolidatedParentRef,
6165
estimateSize: React.useCallback(
6266
(index) => {
63-
if (renderRowSubComponent && rows[index].isExpanded && rowSubComponentsHeight.current.hasOwnProperty(index)) {
64-
return rowHeight + (rowSubComponentsHeight.current?.[index] ?? 0);
67+
if (
68+
renderRowSubComponent &&
69+
(rows[index].isExpanded || alwaysShowSubComponent) &&
70+
subComponentsHeight?.[index]?.rowId === rows[index].id
71+
) {
72+
return rowHeight + (subComponentsHeight?.[index]?.subComponentHeight ?? 0);
6573
}
6674
return rowHeight;
6775
},
68-
[rowHeight, rows, renderRowSubComponent]
76+
[rowHeight, rows, renderRowSubComponent, alwaysShowSubComponent, subComponentsHeight]
6977
),
7078
overscan
7179
});
72-
7380
const columnVirtualizer = useVirtual({
7481
size: visibleColumns.length,
7582
parentRef: tableRef,
@@ -185,8 +192,14 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
185192
{rowVirtualizer.virtualItems.map((virtualRow) => {
186193
const row = rows[virtualRow.index];
187194
const setSubcomponentsRefs = (el) => {
188-
if (el?.offsetHeight) {
189-
rowSubComponentsHeight.current[virtualRow.index] = el.offsetHeight;
195+
if (el?.offsetHeight && subComponentsHeight?.[virtualRow.index]?.subComponentHeight !== el?.offsetHeight) {
196+
dispatch({
197+
type: 'SUB_COMPONENTS_HEIGHT',
198+
payload: {
199+
...subComponentsHeight,
200+
[virtualRow.index]: { subComponentHeight: el?.offsetHeight, rowId: row.id }
201+
}
202+
});
190203
}
191204
};
192205
if (!row) {
@@ -214,7 +227,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
214227
position: 'absolute'
215228
}}
216229
>
217-
{RowSubComponent && row.isExpanded && (
230+
{RowSubComponent && (row.isExpanded || alwaysShowSubComponent) && (
218231
<div
219232
ref={setSubcomponentsRefs}
220233
style={{
@@ -249,7 +262,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
249262
cell.column.id === '__ui5wcr__internal_navigation_column'
250263
) {
251264
contentToRender = 'Cell';
252-
} else if (isTreeTable || RowSubComponent) {
265+
} else if (isTreeTable || (!alwaysShowSubComponent && RowSubComponent)) {
253266
contentToRender = 'Expandable';
254267
} else if (cell.isGrouped) {
255268
contentToRender = 'Grouped';

packages/main/src/components/AnalyticalTable/defaults/Column/Expandable.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@ export const Expandable = (props) => {
5050
paddingLeft
5151
};
5252
const rowProps = row.getToggleRowExpandedProps();
53+
54+
const subComponentExpandable =
55+
typeof webComponentsReactProperties?.renderRowSubComponent === 'function' &&
56+
!!webComponentsReactProperties?.renderRowSubComponent(row) &&
57+
!webComponentsReactProperties.alwaysShowSubComponent;
58+
5359
return (
5460
<>
55-
{columnIndex === 0 && (row.canExpand || !!webComponentsReactProperties.renderRowSubComponent) ? (
61+
{columnIndex === 0 && (row.canExpand || subComponentExpandable) ? (
5662
<span onClick={rowProps.onClick} title={rowProps.title} style={{ ...rowProps.style, ...style }}>
5763
<Icon
5864
name={`${row.isExpanded ? 'navigation-down-arrow' : 'navigation-right-arrow'}`}

packages/main/src/components/AnalyticalTable/index.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ export interface TableProps extends Omit<CommonProps, 'title'> {
277277
* Defines the subcomponent that should be displayed below each row.
278278
*/
279279
renderRowSubComponent?: (row?: any) => ReactNode;
280+
/**
281+
* Defines whether a subcomponent should be rendered as expandable container or directly at the bottom of the row.
282+
*/
283+
alwaysShowSubComponent?: boolean;
280284

281285
// default components
282286
/**
@@ -338,7 +342,8 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
338342
onLoadMore,
339343
extension,
340344
columnOrder,
341-
renderRowSubComponent
345+
renderRowSubComponent,
346+
alwaysShowSubComponent
342347
} = props;
343348

344349
const classes = useStyles();
@@ -401,7 +406,8 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
401406
highlightField,
402407
withNavigationHighlight,
403408
markNavigatedRow,
404-
renderRowSubComponent
409+
renderRowSubComponent,
410+
alwaysShowSubComponent
405411
},
406412
...reactTableOptions
407413
},
@@ -742,8 +748,11 @@ const AnalyticalTable: FC<TableProps> = forwardRef((props: TableProps, ref: Ref<
742748
visibleColumnsWidth={visibleColumnsWidth}
743749
overscanCountHorizontal={overscanCountHorizontal}
744750
renderRowSubComponent={renderRowSubComponent}
751+
alwaysShowSubComponent={alwaysShowSubComponent}
745752
markNavigatedRow={markNavigatedRow}
746753
isRtl={isRtl}
754+
subComponentsHeight={tableState.subComponentsHeight}
755+
dispatch={dispatch}
747756
/>
748757
</VirtualTableBodyContainer>
749758
)}
@@ -805,7 +814,8 @@ AnalyticalTable.defaultProps = {
805814
isTreeTable: false,
806815
alternateRowColor: false,
807816
overscanCountHorizontal: 5,
808-
visibleRowCountMode: TableVisibleRowCountMode.FIXED
817+
visibleRowCountMode: TableVisibleRowCountMode.FIXED,
818+
alwaysShowSubComponent: false
809819
};
810820

811821
export { AnalyticalTable };
Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { actions } from 'react-table';
22

3-
export const stateReducer = (newState, action) => {
3+
export const stateReducer = (prevState, action) => {
44
const { payload } = action;
55

6-
if (newState.isRtl && action.type === actions.columnResizing) {
6+
if (prevState.isRtl && action.type === actions.columnResizing) {
77
const { clientX } = action;
8-
const { startX, columnWidth, headerIdWidths } = newState.columnResizing;
8+
const { startX, columnWidth, headerIdWidths } = prevState.columnResizing;
99

1010
const deltaX = startX - clientX;
1111
const percentageDeltaX = deltaX / columnWidth;
@@ -17,11 +17,11 @@ export const stateReducer = (newState, action) => {
1717
});
1818

1919
return {
20-
...newState,
20+
...prevState,
2121
columnResizing: {
22-
...newState.columnResizing,
22+
...prevState.columnResizing,
2323
columnWidths: {
24-
...newState.columnResizing.columnWidths,
24+
...prevState.columnResizing.columnWidths,
2525
...newColumnWidths
2626
}
2727
}
@@ -30,20 +30,22 @@ export const stateReducer = (newState, action) => {
3030

3131
switch (action.type) {
3232
case 'TABLE_RESIZE':
33-
return { ...newState, tableClientWidth: payload.tableClientWidth };
33+
return { ...prevState, tableClientWidth: payload.tableClientWidth };
3434
case 'VISIBLE_ROWS':
35-
return { ...newState, visibleRows: payload.visibleRows };
35+
return { ...prevState, visibleRows: payload.visibleRows };
3636
case 'TABLE_SCROLLING_ENABLED':
37-
return { ...newState, isScrollable: payload.isScrollable };
37+
return { ...prevState, isScrollable: payload.isScrollable };
3838
case 'SET_SELECTED_ROW_IDS':
39-
return { ...newState, selectedRowIds: payload.selectedRowIds };
39+
return { ...prevState, selectedRowIds: payload.selectedRowIds };
4040
case 'SET_POPIN_COLUMNS':
41-
return { ...newState, popInColumns: payload };
41+
return { ...prevState, popInColumns: payload };
4242
case 'INTERACTIVE_ROWS_HAVE_POPIN':
43-
return { ...newState, interactiveRowsHavePopIn: payload };
43+
return { ...prevState, interactiveRowsHavePopIn: payload };
4444
case 'IS_RTL':
45-
return { ...newState, isRtl: payload.isRtl };
45+
return { ...prevState, isRtl: payload.isRtl };
46+
case 'SUB_COMPONENTS_HEIGHT':
47+
return { ...prevState, subComponentsHeight: payload };
4648
default:
47-
return newState;
49+
return prevState;
4850
}
4951
};

0 commit comments

Comments
 (0)