Skip to content

Commit 7a3a145

Browse files
authored
refactor(AnalyticalTable): attach scroll methods via callback ref (#7446)
1 parent 0d755b3 commit 7a3a145

File tree

6 files changed

+122
-132
lines changed

6 files changed

+122
-132
lines changed

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

Lines changed: 28 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,16 @@ describe('AnalyticalTable', () => {
256256
});
257257

258258
it('autoResize', () => {
259+
function doubleClickResizer(selector: string, columnName: string, outerWidth: number) {
260+
cy.get(selector)
261+
.realHover()
262+
.should('have.css', 'background-color', cssVarToRgb('--sapContent_DragAndDropActiveColor'))
263+
.dblclick()
264+
// fallback
265+
.realClick({ clickCount: 2 });
266+
cy.get(`[data-column-id="${columnName}"]`).invoke('outerWidth').should('equal', outerWidth);
267+
}
268+
259269
let resizeColumns = columns.map((el) => {
260270
return { ...el, autoResizable: true };
261271
});
@@ -277,26 +287,19 @@ describe('AnalyticalTable', () => {
277287
}}
278288
/>,
279289
);
280-
cy.wait(100);
281290

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

285-
cy.get('@resizer2').should('be.visible').dblclick();
286-
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 476);
287-
cy.get('@resizer1').should('be.visible').dblclick();
288-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 476);
289-
290-
cy.get('@resize').should('have.callCount', 2);
294+
doubleClickResizer('@resizer2', 'age', 476);
295+
doubleClickResizer('@resizer1', 'name', 476);
296+
// doubled call count because of fallback
297+
cy.get('@resize').should('have.callCount', 4);
291298

292299
cy.mount(<AnalyticalTable data={dataFixed} columns={resizeColumns} onAutoResize={resizeSpy} />);
293-
cy.wait(100);
294-
cy.get('@resizer2').should('be.visible').dblclick();
295-
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 60);
296-
cy.get('@resizer1').should('be.visible').dblclick();
297-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 129);
298-
299-
cy.get('@resize').should('have.callCount', 4);
300+
doubleClickResizer('@resizer2', 'age', 60);
301+
doubleClickResizer('@resizer1', 'name', 129);
302+
cy.get('@resize').should('have.callCount', 8);
300303

301304
dataFixed = generateMoreData(200);
302305

@@ -319,23 +322,19 @@ describe('AnalyticalTable', () => {
319322
);
320323

321324
cy.get('[data-component-name="AnalyticalTableBody"]').scrollTo('bottom');
322-
cy.get('@resizer1').should('be.visible').dblclick();
323-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 93);
324-
325-
cy.get('@resize').should('have.callCount', 5);
325+
doubleClickResizer('@resizer1', 'name', 93);
326+
cy.get('@resize').should('have.callCount', 10);
326327

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

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

338-
cy.get('@resize').should('have.callCount', 5);
337+
cy.get('@resize').should('have.callCount', 10);
339338

340339
const dataSub = data.map((el, i) => {
341340
if (i === 2) return { ...el, name: 'Longer Name Too' };
@@ -358,25 +357,17 @@ describe('AnalyticalTable', () => {
358357
onAutoResize={resizeSpy}
359358
/>,
360359
);
361-
cy.wait(100);
362-
cy.get('@resizer2').should('be.visible').dblclick();
363-
cy.get('[data-column-id="age"]').invoke('outerWidth').should('equal', 60);
364-
cy.get('@resizer1').should('be.visible').dblclick();
365-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 165);
366-
367-
cy.get('@resize').should('have.callCount', 7);
360+
doubleClickResizer('@resizer2', 'age', 60);
361+
doubleClickResizer('@resizer1', 'name', 165);
362+
cy.get('@resize').should('have.callCount', 14);
368363

369364
const dataResizeTree = [...dataTree];
370365
dataResizeTree[0].subRows[0].name = 'Longer Name To Resize Here';
371366
cy.mount(<AnalyticalTable columns={resizeColumns} data={dataResizeTree} isTreeTable onAutoResize={resizeSpy} />);
372-
cy.wait(100);
373-
cy.get('@resizer1').should('be.visible').dblclick();
374-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 169);
367+
doubleClickResizer('@resizer1', 'name', 169);
375368
cy.get('[aria-rowindex="1"] > [aria-colindex="1"] > [title="Expand Node"] > [ui5-button]').click();
376-
cy.get('@resizer1').should('be.visible').dblclick();
377-
cy.get('[data-column-id="name"]').invoke('outerWidth').should('equal', 251);
378-
379-
cy.get('@resize').should('have.callCount', 9);
369+
doubleClickResizer('@resizer1', 'name', 251);
370+
cy.get('@resize').should('have.callCount', 18);
380371
});
381372

382373
it('scrollTo', () => {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { Virtualizer } from '@tanstack/react-virtual';
22
import { clsx } from 'clsx';
3-
import type { MutableRefObject } from 'react';
3+
import type { MutableRefObject, RefObject } from 'react';
44
import { useEffect, useMemo, useRef } from 'react';
55
import type {
66
AnalyticalTablePropTypes,
77
ClassNames,
88
DivWithCustomScrollProp,
9-
ScrollToRefType,
9+
ReactVirtualScrollToMethods,
1010
TableInstance,
1111
TriggerScrollState,
1212
} from '../types/index.js';
@@ -35,7 +35,7 @@ interface VirtualTableBodyProps {
3535
subRowsKey: string;
3636
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
3737
triggerScroll?: TriggerScrollState;
38-
scrollToRef: MutableRefObject<ScrollToRefType>;
38+
scrollToRef: RefObject<ReactVirtualScrollToMethods>;
3939
rowVirtualizer: Virtualizer<DivWithCustomScrollProp, HTMLElement>;
4040
}
4141

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { RefCallback, RefObject } from 'react';
2+
import { useCallback, useRef } from 'react';
3+
import type { AnalyticalTableScrollMode } from '../../../enums/index.js';
4+
import type { AnalyticalTableDomRef, ScrollToRefType, TableInstance } from '../types/index.js';
5+
6+
export function useScrollToRef(
7+
componentRef: (node: AnalyticalTableDomRef) => void,
8+
dispatch: TableInstance['dispatch'],
9+
): [RefCallback<AnalyticalTableDomRef>, RefObject<ScrollToRefType | null>] {
10+
const scrollToRef = useRef<ScrollToRefType | null>(null);
11+
12+
const cbRef: RefCallback<AnalyticalTableDomRef> = useCallback(
13+
(node) => {
14+
if (!node) return;
15+
16+
const extendedNode = Object.assign(node, {
17+
scrollTo: (offset: number, align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode) => {
18+
if (typeof scrollToRef.current?.scrollToOffset === 'function') {
19+
scrollToRef.current.scrollToOffset(offset, { align });
20+
} else {
21+
dispatch({
22+
type: 'TRIGGER_PROG_SCROLL',
23+
payload: { direction: 'vertical', type: 'offset', args: [offset, { align }] },
24+
});
25+
}
26+
},
27+
scrollToItem: (index: number, align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode) => {
28+
if (typeof scrollToRef.current?.scrollToIndex === 'function') {
29+
scrollToRef.current.scrollToIndex(index, { align });
30+
} else {
31+
dispatch({
32+
type: 'TRIGGER_PROG_SCROLL',
33+
payload: { direction: 'vertical', type: 'item', args: [index, { align }] },
34+
});
35+
}
36+
},
37+
horizontalScrollTo: (
38+
offset: number,
39+
align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode,
40+
) => {
41+
if (typeof scrollToRef.current?.horizontalScrollToOffset === 'function') {
42+
scrollToRef.current.horizontalScrollToOffset(offset, { align });
43+
} else {
44+
dispatch({
45+
type: 'TRIGGER_PROG_SCROLL',
46+
payload: { direction: 'horizontal', type: 'offset', args: [offset, { align }] },
47+
});
48+
}
49+
},
50+
horizontalScrollToItem: (
51+
index: number,
52+
align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode,
53+
) => {
54+
if (typeof scrollToRef.current?.horizontalScrollToIndex === 'function') {
55+
scrollToRef.current.horizontalScrollToIndex(index, { align });
56+
} else {
57+
dispatch({
58+
type: 'TRIGGER_PROG_SCROLL',
59+
payload: { direction: 'horizontal', type: 'item', args: [index, { align }] },
60+
});
61+
}
62+
},
63+
});
64+
65+
componentRef(extendedNode);
66+
},
67+
[componentRef, dispatch],
68+
);
69+
70+
return [cbRef, scrollToRef];
71+
}

packages/main/src/components/AnalyticalTable/hooks/useTableScrollHandles.ts

Lines changed: 0 additions & 82 deletions
This file was deleted.

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ import { useResizeColumnsConfig } from './hooks/useResizeColumnsConfig.js';
7070
import { useRowHighlight } from './hooks/useRowHighlight.js';
7171
import { useRowNavigationIndicators } from './hooks/useRowNavigationIndicator.js';
7272
import { useRowSelectionColumn } from './hooks/useRowSelectionColumn.js';
73+
import { useScrollToRef } from './hooks/useScrollToRef.js';
7374
import { useSelectionChangeCallback } from './hooks/useSelectionChangeCallback.js';
7475
import { useSingleRowStateSelection } from './hooks/useSingleRowStateSelection.js';
7576
import { useStyling } from './hooks/useStyling.js';
76-
import { useTableScrollHandles } from './hooks/useTableScrollHandles.js';
7777
import { useToggleRowExpand } from './hooks/useToggleRowExpand.js';
7878
import { useVisibleColumnsWidth } from './hooks/useVisibleColumnsWidth.js';
7979
import { VerticalScrollbar } from './scrollbars/VerticalScrollbar.js';
@@ -307,9 +307,10 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
307307
const noDataTextLocal =
308308
noDataText ?? (tableState.filters?.length > 0 || tableState.globalFilter ? noDataTextFiltered : noDataTextI18n);
309309

310-
const [componentRef, updatedRef] = useSyncRef<AnalyticalTableDomRef>(ref);
311-
//@ts-expect-error: types are compatible
312-
const isRtl = useIsRTL(updatedRef);
310+
const [componentRef, analyticalTableRef] = useSyncRef<AnalyticalTableDomRef>(ref);
311+
const [cbRef, scrollToRef] = useScrollToRef(componentRef, dispatch);
312+
// @ts-expect-error: is HTMLElement
313+
const isRtl = useIsRTL(analyticalTableRef);
313314

314315
const columnVirtualizer = useVirtualizer({
315316
count: visibleColumnsWidth.length,
@@ -340,8 +341,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
340341
}
341342
}, [tableState.groupBy, tableState.columnOrder]);
342343

343-
const [analyticalTableRef, scrollToRef] = useTableScrollHandles(updatedRef, dispatch);
344-
345344
if (parentRef.current) {
346345
scrollToRef.current = {
347346
...scrollToRef.current,
@@ -357,7 +356,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
357356
columnVirtualizer.scrollToIndex(...triggerScroll.args);
358357
}
359358
}
360-
}, [triggerScroll]);
359+
}, [columnVirtualizer, triggerScroll]);
361360

362361
const includeSubCompRowHeight =
363362
!!renderRowSubComponent &&
@@ -412,7 +411,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
412411
},
413412
});
414413
}
415-
}, [tableRef.current, scaleXFactor]);
414+
}, [dispatch, scaleXFactor]);
416415

417416
const updateRowsCount = useCallback(() => {
418417
if (
@@ -737,7 +736,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
737736
className={className}
738737
style={inlineStyle}
739738
//@ts-expect-error: types are compatible
740-
ref={componentRef}
739+
ref={cbRef}
741740
{...rest}
742741
>
743742
{header && (

packages/main/src/components/AnalyticalTable/types/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ export interface TableInstance {
101101
disableGlobalFilter?: boolean;
102102
disableGroupBy?: boolean;
103103
disableSortBy?: boolean;
104-
dispatch?: (action: any) => void;
104+
dispatch?: (action: {
105+
type: string;
106+
payload?: Record<string, unknown> | AnalyticalTableState['popInColumns'] | boolean | string;
107+
clientX?: number;
108+
}) => void;
105109
expandedDepth?: number;
106110
expandedRows?: RowType[];
107111
filteredFlatRows?: RowType[];
@@ -280,6 +284,13 @@ export interface TriggerScrollState {
280284
args: [number, Omit<ScrollToOptions, 'behavior'>?];
281285
}
282286

287+
export interface ReactVirtualScrollToMethods {
288+
scrollToOffset?: (offset: number, options?: ScrollToOptions) => void;
289+
scrollToIndex?: (index: number, options?: ScrollToOptions) => void;
290+
horizontalScrollToOffset?: (offset: number, options?: ScrollToOptions) => void;
291+
horizontalScrollToIndex?: (index: number, options?: ScrollToOptions) => void;
292+
}
293+
283294
interface PopInColumnsState {
284295
id: string;
285296
column: ColumnType;

0 commit comments

Comments
 (0)