Skip to content

feat(AnalyticalTable): introduce autoResize column feature (#3196) #5758

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 9 commits into from
May 28, 2024
124 changes: 124 additions & 0 deletions packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,130 @@ describe('AnalyticalTable', () => {
cy.findByText('Name-3').should('not.be.visible');
});

it('autoResize', () => {
let resizeColumns = columns.map((el) => {
return { ...el, autoResizable: true };
});

let dataFixed = data.map((el, i) => {
if (i === 2) return { ...el, name: 'Longer Name Too' };
return el;
});

const resizeSpy = cy.spy().as('resize');

cy.mount(
<AnalyticalTable
data={dataFixed}
columns={resizeColumns}
onAutoResize={(e) => {
resizeSpy(e);
e.preventDefault();
}}
/>
);
cy.wait(100);

cy.get('[data-component-name="AnalyticalTableResizer"]').eq(0).as('resizer1');
cy.get('[data-component-name="AnalyticalTableResizer"]').eq(1).as('resizer2');

cy.get('@resizer2').should('be.visible').dblclick();
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 476);
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 476);

cy.get('@resize').should('have.callCount', 2);

cy.mount(<AnalyticalTable data={dataFixed} columns={resizeColumns} onAutoResize={resizeSpy} />);
cy.wait(100);
cy.get('@resizer2').should('be.visible').dblclick();
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 60);
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 129);

cy.get('@resize').should('have.callCount', 4);

dataFixed = generateMoreData(200);

dataFixed = dataFixed.map((el, i) => {
if (i === 2) return { ...el, name: 'Much Longer Name To Resize Larger For Testing A Larger Auto Resize' };
else if (i > 50) return { ...el, name: 'Short Name' };
return el;
});

const loadMore = cy.spy().as('more');
cy.mount(
<AnalyticalTable
data={dataFixed}
columns={resizeColumns}
onLoadMore={loadMore}
infiniteScroll={true}
infiniteScrollThreshold={0}
onAutoResize={resizeSpy}
/>
);

cy.get('[data-component-name="AnalyticalTableBody"]').scrollTo('bottom');
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 93);

cy.get('@resize').should('have.callCount', 5);

resizeColumns = columns.map((el) => {
return { ...el, autoResizable: false };
});

cy.mount(<AnalyticalTable data={dataFixed} columns={resizeColumns} />);
cy.wait(100);
cy.get('@resizer2').should('be.visible').dblclick();
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 472.75);
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 472.75);

cy.get('@resize').should('have.callCount', 5);

const dataSub = data.map((el, i) => {
if (i === 2) return { ...el, name: 'Longer Name Too' };
return el;
});

resizeColumns = columns.map((el) => {
return { ...el, autoResizable: true };
});

const renderRowSubComponent = () => {
return <div title="subcomponent">SubComponent</div>;
};

cy.mount(
<AnalyticalTable
data={dataSub}
columns={resizeColumns}
renderRowSubComponent={renderRowSubComponent}
onAutoResize={resizeSpy}
/>
);
cy.wait(100);
cy.get('@resizer2').should('be.visible').dblclick();
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 60);
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 165);

cy.get('@resize').should('have.callCount', 7);

const dataResizeTree = [...dataTree];
dataResizeTree[0].subRows[0].name = 'Longer Name To Resize Here';
cy.mount(<AnalyticalTable columns={resizeColumns} data={dataResizeTree} isTreeTable onAutoResize={resizeSpy} />);
cy.wait(100);
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 169);
cy.get('[aria-rowindex="1"] > [aria-colindex="1"] > [title="Expand Node"] > [ui5-button]').click();
cy.get('@resizer1').should('be.visible').dblclick();
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 251);

cy.get('@resize').should('have.callCount', 9);
});

it('scrollTo', () => {
interface ScrollTableProps {
scrollFn: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,10 @@ import * as ComponentStories from './AnalyticalTable.stories';

**Required Attributes**

| Attribute | Type | Description |
| ---------- | ------------------------------------------------- | ----------------------------------------- |
| `accessor` | `string OR ((row: any, rowIndex: number) => any)` | |
| `id` | `string` | Only required if `accessor` is a function |
| Attribute | Type | Description |
| ---------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `accessor` | `string OR ((row: any, rowIndex: number) => any)` | This `string`/`function` is used to build the data model for your column.<br /><br />**Note**: You can also specify deeply nested values with accessors like `info.hobby` or even `address[0].street`<br /> |
| `id` | `string` | Defines the unique ID for the column. It is used by reference in things like sorting, grouping, filtering etc.<br /><br />**Note:** If no `accessor` is set, or the `accessor` is a function, the `id` property has to be set. |

**Optional Properties**

Expand Down Expand Up @@ -192,6 +192,7 @@ import * as ComponentStories from './AnalyticalTable.stories';
| `PopInHeader` | `string OR ComponentType` | Custom pop-in header renderer. If set, the table will call that component for every column that is "popped-in" and pass the table instance as prop. |
| `disableDragAndDrop` | `boolean` | Defines if the column is reorderable by dragging and dropping columns. |
| `enableMultiSort` | `boolean` | Defines whether this column should allow multi-sort.<br /><br /> **Note:** When sorting by a column that does not allow multiple sorting, only the current column is sorted and all other sorted columns are reset. |
| `autoResizable` | `boolean` | Defines whether double-clicking a columns resizer will automatically resize the column to fit the largest cell content of visible rows.<br /><br />**Note:** Only default text content is supported by this option, for custom content it might work as well, but we recommend checking the behavior carefully as the logic can't account for all possible implementations. |

<br />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ const meta = {
{
Header: 'Name',
headerTooltip: 'Full Name', // A more extensive description!
accessor: 'name' // String-based value accessors!
accessor: 'name', // String-based value accessors!
autoResizable: true // Double clicking the resize bar auto resizes the column!
},
{
Header: 'Age',
accessor: 'age',
autoResizable: true,
hAlign: TextAlign.End,
disableGroupBy: true,
disableSortBy: false,
Expand All @@ -45,12 +47,14 @@ const meta = {
},
{
Header: 'Friend Name',
accessor: 'friend.name'
accessor: 'friend.name',
autoResizable: true
},
{
Header: () => <span>Friend Age</span>,
headerLabel: 'Friend Age',
accessor: 'friend.age',
autoResizable: true,
hAlign: TextAlign.End,
filter: (rows, accessor, filterValue) => {
if (filterValue === 'all') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
<div
{...column.getResizerProps()}
data-resizer
data-component-name="AnalyticalTableResizer"
className={classNames.resizer}
style={resizerDirectionStyle}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type { Virtualizer } from '@tanstack/react-virtual';
import { useVirtualizer } from '@tanstack/react-virtual';
import { clsx } from 'clsx';
import type { MutableRefObject, ReactNode } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import { AnalyticalTableSubComponentsBehavior } from '../../../enums/index.js';
import type {
AnalyticalTablePropTypes,
Expand All @@ -18,15 +17,10 @@ interface VirtualTableBodyProps {
classes: Record<string, string>;
prepareRow: (row: unknown) => void;
rows: Record<string, any>[];
itemCount: number;
scrollToRef: MutableRefObject<ScrollToRefType>;
isTreeTable: boolean;
internalRowHeight: number;
visibleRows: number;
alternateRowColor: boolean;
overscanCount: number;
visibleColumns: Record<string, unknown>[];
parentRef: MutableRefObject<HTMLDivElement>;
renderRowSubComponent: (row?: Record<string, unknown>) => ReactNode;
popInRowHeight: number;
isRtl: boolean;
Expand All @@ -40,26 +34,20 @@ interface VirtualTableBodyProps {
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
subComponentsBehavior: AnalyticalTablePropTypes['subComponentsBehavior'];
triggerScroll?: TriggerScrollState;
scrollToRef: MutableRefObject<ScrollToRefType>;
rowVirtualizer: Virtualizer<DivWithCustomScrollProp, HTMLElement>;
}

const measureElement = (el: HTMLElement) => {
return el.offsetHeight;
};

export const VirtualTableBody = (props: VirtualTableBodyProps) => {
const {
alternateRowColor,
classes,
prepareRow,
rows,
itemCount,
scrollToRef,
isTreeTable,
internalRowHeight,
visibleRows,
overscanCount,
visibleColumns,
parentRef,
renderRowSubComponent,
popInRowHeight,
markNavigatedRow,
Expand All @@ -72,33 +60,13 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
subRowsKey,
scrollContainerRef,
subComponentsBehavior,
triggerScroll
triggerScroll,
rowVirtualizer
} = props;

const overscan = overscanCount ? overscanCount : Math.floor(visibleRows / 2);
const rowHeight = popInRowHeight !== internalRowHeight ? popInRowHeight : internalRowHeight;
const lastNonEmptyRow = useRef(null);

const rowVirtualizer = useVirtualizer({
count: itemCount,
getScrollElement: () => parentRef.current,
estimateSize: useCallback(
(index) => {
if (
renderRowSubComponent &&
(rows[index]?.isExpanded || alwaysShowSubComponent) &&
subComponentsHeight?.[index]?.rowId === rows[index]?.id
) {
return rowHeight + (subComponentsHeight?.[index]?.subComponentHeight ?? 0);
}
return rowHeight;
},
[rowHeight, rows, renderRowSubComponent, alwaysShowSubComponent, subComponentsHeight]
),
overscan,
measureElement,
indexAttribute: 'data-virtual-row-index'
});
scrollToRef.current = {
...scrollToRef.current,
scrollToOffset: rowVirtualizer.scrollToOffset,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React from 'react';

export const Cell = ({ cell: { value = '', isGrouped }, row, webComponentsReactProperties }) => {
export const Cell = ({ cell: { value = '', isGrouped }, column, row, webComponentsReactProperties }) => {
let cellContent = `${value ?? ''}`;
if (isGrouped) {
cellContent += ` (${row.subRows.length})`;
}
return (
<span title={cellContent} className={webComponentsReactProperties.classes.tableText}>
<span
title={cellContent}
className={webComponentsReactProperties.classes.tableText}
data-column-id-cell-text={column.id}
>
{cellContent}
</span>
);
Expand Down
Loading
Loading