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', 135);

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', 99);

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', 173);

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', 177);
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', 259);

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 @@ -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,21 @@ interface VirtualTableBodyProps {
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
subComponentsBehavior: AnalyticalTablePropTypes['subComponentsBehavior'];
triggerScroll?: TriggerScrollState;
scrollToRef: MutableRefObject<ScrollToRefType>;
rowVirtualizer: Virtualizer<DivWithCustomScrollProp, HTMLElement>;
uniqueId: string;
}

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 +61,14 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
subRowsKey,
scrollContainerRef,
subComponentsBehavior,
triggerScroll
triggerScroll,
rowVirtualizer,
uniqueId
} = 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 Expand Up @@ -129,6 +99,7 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
<div
ref={scrollContainerRef}
data-component-name="AnalyticalTableBodyScrollableContainer"
data-react-id={uniqueId}
style={{
position: 'relative',
height: `${rowVirtualizer.getTotalSize()}px`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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-id={column.id}>
{cellContent}
</span>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base';
import { DEFAULT_COLUMN_WIDTH } from '../defaults/Column/index.js';
import type { ReactTableHooks } from '../types/index.js';
import { CELL_PADDING_PX } from './useDynamicColumnWidths.js';

function setResizerProps(...params) {
const [props, { instance, header }] = params;
const { dispatch, virtualRowsRange, rows, webComponentsReactProperties, state } = instance;
const { uniqueId, onAutoResize } = webComponentsReactProperties;
const { autoResizable, id: accessor } = header;

if (!document || !autoResizable || !rows.length || !virtualRowsRange) {
return props;
}

return {
...props,
onDoubleClick: (e) => {
let largest = getMeasureMax(accessor, uniqueId, virtualRowsRange, state.isRtl);
largest = largest > DEFAULT_COLUMN_WIDTH ? largest : DEFAULT_COLUMN_WIDTH;
onAutoResize(
enrichEventWithDetails(e, {
accessor,
width: largest
})
);
if (e.defaultPrevented) {
return;
}
dispatch({
type: 'AUTO_RESIZE',
payload: { [accessor]: largest }
});
}
};
}

function getMeasureMax(accessor, uniqueId, virtualRowsRange, isRtl) {
let maxWidth = 0;
const firstRowQuery = `[data-component-name="AnalyticalTableBodyScrollableContainer"][data-react-id="${uniqueId}"] [data-virtual-row-index="${virtualRowsRange.startIndex}"]`;
const rowsDOM = document.querySelectorAll(`${firstRowQuery}, ${firstRowQuery} ~ [data-virtual-row-index]`);
const start = virtualRowsRange.startIndex;
const end = virtualRowsRange.endIndex;

for (let i = 0; i <= end - start; i++) {
const cellTextElement: HTMLSpanElement = rowsDOM[i]?.querySelector(`[data-id="${accessor}"]`);
let currWidth = 0;
if (!cellTextElement) {
continue;
}
const computedStyle = getComputedStyle(cellTextElement);
currWidth += cellTextElement.scrollWidth;
// cannot use `offsetLeft` for RTL direction
currWidth += !isRtl
? cellTextElement.offsetLeft
: cellTextElement.parentElement.getBoundingClientRect().right - cellTextElement.getBoundingClientRect().right;
currWidth += parseFloat(computedStyle.marginInlineEnd);
currWidth += parseFloat(computedStyle.borderInlineEndWidth);
currWidth += CELL_PADDING_PX;

maxWidth = maxWidth > currWidth ? maxWidth : currWidth;
}

return Math.ceil(maxWidth);
}

export const useAutoResize = (hooks: ReactTableHooks) => {
hooks.getResizerProps.push(setResizerProps);
};

useAutoResize.pluginName = 'useAutoResize';
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { AnalyticalTableColumnDefinition, ReactTableHooks } from '../types/

const ROW_SAMPLE_SIZE = 20;
const MAX_WIDTH = 700;
const CELL_PADDING_PX = 18; /* padding left and right 0.5rem each (16px) + borders (1px) + buffer (1px) */
export const CELL_PADDING_PX = 18; /* padding left and right 0.5rem each (16px) + borders (1px) + buffer (1px) */

function findLongestString(str1, str2) {
if (typeof str1 !== 'string' || typeof str2 !== 'string') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ const getCellProps = (cellProps, { cell: { column }, instance }) => {
{
className,
style,
tabIndex: -1
tabIndex: -1,
id: `${cellProps.key}-${instance?.webComponentsReactProperties.uniqueId}`
}
];
};
Expand Down
Loading
Loading