Skip to content

Commit 18f3180

Browse files
committed
Feat(AnalyticalTable): introduce autoResize column feature (#3196)
A functionality by which the cell width gets auto adjusted when we double click the data-resizer columns. Implemented for text.
1 parent 5acb2e9 commit 18f3180

File tree

7 files changed

+204
-54
lines changed

7 files changed

+204
-54
lines changed

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,65 @@ describe('AnalyticalTable', () => {
239239
cy.findByText('Name-3').should('not.be.visible');
240240
});
241241

242+
it('autoResize', () => {
243+
let resizeColumns = columns.map((el) => {
244+
return { ...el, autoResizable: true };
245+
});
246+
247+
let dataFixed = data.map((el, i) => {
248+
if (i === 2) return { ...el, name: 'Much Longer Name To Resize Larger For Testing A Larger Auto Resize' };
249+
return el;
250+
});
251+
252+
cy.mount(<AnalyticalTable data={dataFixed} columns={resizeColumns} onAutoResize={(e) => e.preventDefault()} />);
253+
cy.wait(200);
254+
cy.get('[data-cy="data-resizer-1"]').should('be.visible').dblclick();
255+
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 476);
256+
cy.get('[data-cy="data-resizer-0"]').should('be.visible').dblclick();
257+
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 476);
258+
259+
cy.mount(<AnalyticalTable data={dataFixed} columns={resizeColumns} />);
260+
cy.wait(200);
261+
cy.get('[data-cy="data-resizer-1"]').should('be.visible').dblclick();
262+
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 60);
263+
cy.get('[data-cy="data-resizer-0"]').should('be.visible').dblclick();
264+
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 451);
265+
266+
dataFixed = generateMoreData(200);
267+
268+
dataFixed = dataFixed.map((el, i) => {
269+
if (i === 2) return { ...el, name: 'Much Longer Name To Resize Larger For Testing A Larger Auto Resize' };
270+
else if (i > 50) return { ...el, name: 'Short Name' };
271+
return el;
272+
});
273+
274+
const loadMore = cy.spy().as('more');
275+
cy.mount(
276+
<AnalyticalTable
277+
data={dataFixed}
278+
columns={resizeColumns}
279+
onLoadMore={loadMore}
280+
infiniteScroll={true}
281+
infiniteScrollThreshold={0}
282+
/>
283+
);
284+
285+
cy.get('[data-component-name="AnalyticalTableBody"]').scrollTo('bottom');
286+
cy.get('[data-cy="data-resizer-0"]').should('be.visible').dblclick();
287+
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 94);
288+
289+
resizeColumns = columns.map((el) => {
290+
return { ...el, autoResizable: false };
291+
});
292+
293+
cy.mount(<AnalyticalTable data={dataFixed} columns={resizeColumns} />);
294+
cy.wait(200);
295+
cy.get('[data-cy="data-resizer-1"]').should('be.visible').dblclick();
296+
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 472.75);
297+
cy.get('[data-cy="data-resizer-0"]').should('be.visible').dblclick();
298+
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 472.75);
299+
});
300+
242301
it('scrollTo', () => {
243302
interface ScrollTableProps {
244303
scrollFn: string;

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@ const meta = {
3232
{
3333
Header: 'Name',
3434
headerTooltip: 'Full Name', // A more extensive description!
35-
accessor: 'name' // String-based value accessors!
35+
accessor: 'name', // String-based value accessors!
36+
autoResizable: true // Double clicking the resize bar auto resizes the column!
3637
},
3738
{
3839
Header: 'Age',
3940
accessor: 'age',
41+
autoResizable: true,
4042
hAlign: TextAlign.End,
4143
disableGroupBy: true,
4244
disableSortBy: false,
@@ -45,12 +47,14 @@ const meta = {
4547
},
4648
{
4749
Header: 'Friend Name',
48-
accessor: 'friend.name'
50+
accessor: 'friend.name',
51+
autoResizable: true
4952
},
5053
{
5154
Header: () => <span>Friend Age</span>,
5255
headerLabel: 'Friend Age',
5356
accessor: 'friend.age',
57+
autoResizable: true,
5458
hAlign: TextAlign.End,
5559
filter: (rows, accessor, filterValue) => {
5660
if (filterValue === 'all') {

packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderContainer.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface ColumnHeaderContainerProps {
1717
columnVirtualizer: Virtualizer<DivWithCustomScrollProp, Element>;
1818
uniqueId: string;
1919
showVerticalEndBorder: boolean;
20+
onAutoResize: (e: React.MouseEvent<HTMLDivElement, globalThis.MouseEvent>, accessor: string) => void;
2021
}
2122

2223
export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderContainerProps>((props, ref) => {
@@ -30,7 +31,8 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
3031
portalContainer,
3132
columnVirtualizer,
3233
uniqueId,
33-
showVerticalEndBorder
34+
showVerticalEndBorder,
35+
onAutoResize
3436
} = props;
3537

3638
useStylesheet(styleData, 'Resizer');
@@ -65,8 +67,14 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
6567
<div
6668
{...column.getResizerProps()}
6769
data-resizer
70+
data-cy={`data-resizer-${index}`}
6871
className={classNames.resizer}
6972
style={resizerDirectionStyle}
73+
onDoubleClick={(e) => {
74+
if (column.autoResizable) {
75+
onAutoResize(e, rest.id);
76+
}
77+
}}
7078
/>
7179
)}
7280
<ColumnHeader

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

Lines changed: 5 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { Virtualizer } from '@tanstack/react-virtual';
2-
import { useVirtualizer } from '@tanstack/react-virtual';
32
import { clsx } from 'clsx';
43
import type { MutableRefObject, ReactNode } from 'react';
5-
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
4+
import React, { useEffect, useMemo, useRef } from 'react';
65
import { AnalyticalTableSubComponentsBehavior } from '../../../enums/index.js';
76
import type {
87
AnalyticalTablePropTypes,
@@ -18,15 +17,10 @@ interface VirtualTableBodyProps {
1817
classes: Record<string, string>;
1918
prepareRow: (row: unknown) => void;
2019
rows: Record<string, any>[];
21-
itemCount: number;
22-
scrollToRef: MutableRefObject<ScrollToRefType>;
2320
isTreeTable: boolean;
2421
internalRowHeight: number;
25-
visibleRows: number;
2622
alternateRowColor: boolean;
27-
overscanCount: number;
2823
visibleColumns: Record<string, unknown>[];
29-
parentRef: MutableRefObject<HTMLDivElement>;
3024
renderRowSubComponent: (row?: Record<string, unknown>) => ReactNode;
3125
popInRowHeight: number;
3226
isRtl: boolean;
@@ -40,26 +34,20 @@ interface VirtualTableBodyProps {
4034
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
4135
subComponentsBehavior: AnalyticalTablePropTypes['subComponentsBehavior'];
4236
triggerScroll?: TriggerScrollState;
37+
scrollToRef: MutableRefObject<ScrollToRefType>;
38+
rowVirtualizer: Virtualizer<DivWithCustomScrollProp, HTMLElement>;
4339
}
4440

45-
const measureElement = (el: HTMLElement) => {
46-
return el.offsetHeight;
47-
};
48-
4941
export const VirtualTableBody = (props: VirtualTableBodyProps) => {
5042
const {
5143
alternateRowColor,
5244
classes,
5345
prepareRow,
5446
rows,
55-
itemCount,
5647
scrollToRef,
5748
isTreeTable,
5849
internalRowHeight,
59-
visibleRows,
60-
overscanCount,
6150
visibleColumns,
62-
parentRef,
6351
renderRowSubComponent,
6452
popInRowHeight,
6553
markNavigatedRow,
@@ -72,33 +60,13 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
7260
subRowsKey,
7361
scrollContainerRef,
7462
subComponentsBehavior,
75-
triggerScroll
63+
triggerScroll,
64+
rowVirtualizer
7665
} = props;
7766

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

82-
const rowVirtualizer = useVirtualizer({
83-
count: itemCount,
84-
getScrollElement: () => parentRef.current,
85-
estimateSize: useCallback(
86-
(index) => {
87-
if (
88-
renderRowSubComponent &&
89-
(rows[index]?.isExpanded || alwaysShowSubComponent) &&
90-
subComponentsHeight?.[index]?.rowId === rows[index]?.id
91-
) {
92-
return rowHeight + (subComponentsHeight?.[index]?.subComponentHeight ?? 0);
93-
}
94-
return rowHeight;
95-
},
96-
[rowHeight, rows, renderRowSubComponent, alwaysShowSubComponent, subComponentsHeight]
97-
),
98-
overscan,
99-
measureElement,
100-
indexAttribute: 'data-virtual-row-index'
101-
});
10270
scrollToRef.current = {
10371
...scrollToRef.current,
10472
scrollToOffset: rowVirtualizer.scrollToOffset,

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

Lines changed: 104 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
149149
onRowSelect,
150150
onSort,
151151
onTableScroll,
152+
onAutoResize,
152153
LoadingComponent,
153154
NoDataComponent,
154155
additionalEmptyRowsCount = 0,
@@ -655,6 +656,105 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
655656
);
656657
};
657658

659+
const handleOnAutoResize = (e, accessor) => {
660+
// Helpers
661+
interface canvasHolderProps {
662+
canvas?: HTMLCanvasElement;
663+
}
664+
const canvasHolder: canvasHolderProps = { canvas: undefined };
665+
// Text Width Analysis
666+
function getTextWidth(text: string, font: string) {
667+
// Reusing the canvas is more efficient
668+
const canvas = canvasHolder.canvas || (canvasHolder.canvas = document.createElement('canvas'));
669+
const context = canvas.getContext('2d');
670+
context.font = font;
671+
const metrics = context.measureText(text);
672+
return metrics.width;
673+
}
674+
675+
function getCssStyle(element: Element, prop: string) {
676+
return window.getComputedStyle(element, null).getPropertyValue(prop);
677+
}
678+
679+
function getCanvasFont(el: Element = document.body) {
680+
const fontWeight = getCssStyle(el, 'font-weight') || 'normal';
681+
const fontSize = getCssStyle(el, 'font-size') || '12px';
682+
const fontFamily = getCssStyle(el, 'font-family') || 'Times New Roman';
683+
684+
return `${fontWeight} ${fontSize} ${fontFamily}`;
685+
}
686+
687+
const findWidth = (text: string, el: Element) => {
688+
let font: string | undefined;
689+
if (!font) font = getCanvasFont(el);
690+
return getTextWidth(text, font);
691+
};
692+
// End Helpers
693+
let largest = 0;
694+
// Currently Including Overscan
695+
const items = rowVirtualizer.getVirtualItems();
696+
const [start, end] = [items[0].index, items[items.length - 1].index];
697+
698+
for (let i = start; i < end; i++) {
699+
// Use the classname for the span where the text lives AnalyticalTable.module.css.js
700+
const collection = document.getElementsByClassName(clsx(classNames.tableText));
701+
const current = findWidth(rows[i].values[accessor], collection[0]);
702+
largest = current > largest ? current : largest;
703+
}
704+
// Assign padding
705+
largest = Math.ceil(largest + 20);
706+
// Smallest column allowed is 60px
707+
largest = largest < 60 ? 60 : largest;
708+
onAutoResize(
709+
enrichEventWithDetails(e, {
710+
accessor,
711+
width: largest
712+
})
713+
);
714+
if (e.defaultPrevented) {
715+
return;
716+
}
717+
dispatch({
718+
type: 'DOUBLE_CLICK_RESIZE',
719+
payload: { [accessor]: largest }
720+
});
721+
};
722+
723+
const measureElement = (el: HTMLElement) => {
724+
return el.offsetHeight;
725+
};
726+
727+
const overscan = overscanCount ? overscanCount : Math.floor(visibleRows / 2);
728+
const rHeight = popInRowHeight !== internalRowHeight ? popInRowHeight : internalRowHeight;
729+
730+
const itemCount =
731+
Math.max(
732+
minRows,
733+
rows.length,
734+
visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows ? internalVisibleRowCount : 0
735+
) + (!tableState.isScrollable ? additionalEmptyRowsCount : 0);
736+
737+
const rowVirtualizer = useVirtualizer({
738+
count: itemCount,
739+
getScrollElement: () => parentRef.current,
740+
estimateSize: useCallback(
741+
(index) => {
742+
if (
743+
renderRowSubComponent &&
744+
(rows[index]?.isExpanded || alwaysShowSubComponent) &&
745+
tableState.subComponentsHeight?.[index]?.rowId === rows[index]?.id
746+
) {
747+
return rHeight + (tableState.subComponentsHeight?.[index]?.subComponentHeight ?? 0);
748+
}
749+
return rHeight;
750+
},
751+
[rHeight, rows, renderRowSubComponent, alwaysShowSubComponent, tableState.subComponentsHeight]
752+
),
753+
overscan,
754+
measureElement,
755+
indexAttribute: 'data-virtual-row-index'
756+
});
757+
658758
return (
659759
<>
660760
<div
@@ -723,6 +823,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
723823
columnVirtualizer={columnVirtualizer}
724824
uniqueId={uniqueId}
725825
showVerticalEndBorder={showVerticalEndBorder}
826+
onAutoResize={handleOnAutoResize}
726827
/>
727828
)
728829
);
@@ -760,23 +861,11 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
760861
classes={classNames}
761862
prepareRow={prepareRow}
762863
rows={rows}
763-
itemCount={
764-
Math.max(
765-
minRows,
766-
rows.length,
767-
visibleRowCountMode === AnalyticalTableVisibleRowCountMode.AutoWithEmptyRows
768-
? internalVisibleRowCount
769-
: 0
770-
) + (!tableState.isScrollable ? additionalEmptyRowsCount : 0)
771-
}
772864
scrollToRef={scrollToRef}
773865
isTreeTable={isTreeTable}
774866
internalRowHeight={internalRowHeight}
775867
popInRowHeight={popInRowHeight}
776-
visibleRows={internalVisibleRowCount}
777868
alternateRowColor={alternateRowColor}
778-
overscanCount={overscanCount}
779-
parentRef={parentRef}
780869
visibleColumns={visibleColumns}
781870
renderRowSubComponent={renderRowSubComponent}
782871
alwaysShowSubComponent={alwaysShowSubComponent}
@@ -789,6 +878,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
789878
subRowsKey={subRowsKey}
790879
subComponentsBehavior={subComponentsBehavior}
791880
triggerScroll={tableState.triggerScroll}
881+
rowVirtualizer={rowVirtualizer}
792882
/>
793883
</VirtualTableBodyContainer>
794884
)}
@@ -870,7 +960,8 @@ AnalyticalTable.defaultProps = {
870960
isTreeTable: false,
871961
alternateRowColor: false,
872962
overscanCountHorizontal: 5,
873-
visibleRowCountMode: AnalyticalTableVisibleRowCountMode.Fixed
963+
visibleRowCountMode: AnalyticalTableVisibleRowCountMode.Fixed,
964+
onAutoResize: () => {}
874965
};
875966

876967
export { AnalyticalTable };

0 commit comments

Comments
 (0)