Skip to content

Commit 610ffd2

Browse files
authored
fix(AnalyticalTable): improve programmatic scroll behavior & scroll types (#5334)
Fixes #5319
1 parent 73c17ea commit 610ffd2

File tree

7 files changed

+224
-48
lines changed

7 files changed

+224
-48
lines changed

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ThemingParameters } from '@ui5/webcomponents-react-base';
22
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3-
import type { AnalyticalTablePropTypes } from '../..';
3+
import type { AnalyticalTableDomRef, AnalyticalTablePropTypes } from '../..';
44
import {
55
AnalyticalTable,
66
AnalyticalTableHooks,
@@ -2577,6 +2577,66 @@ describe('AnalyticalTable', () => {
25772577
cy.get('[data-component-name="AnalyticalTableBody"]').should('have.css', 'height', '800px');
25782578
});
25792579

2580+
it('initial scroll-to', () => {
2581+
const ScrollTo = () => {
2582+
const tableRef = useRef<AnalyticalTableDomRef>(null);
2583+
useEffect(() => {
2584+
tableRef.current.scrollTo(520);
2585+
}, []);
2586+
return <AnalyticalTable data={generateMoreData(500)} columns={columns} ref={tableRef} />;
2587+
};
2588+
cy.mount(<ScrollTo />);
2589+
cy.findByText('Name-12').should('be.visible');
2590+
cy.findByText('Name-11').should('not.be.visible');
2591+
2592+
const ScrollToItem = () => {
2593+
const tableRef = useRef(null);
2594+
useEffect(() => {
2595+
tableRef.current.scrollToItem(12, { align: 'start' });
2596+
}, []);
2597+
return <AnalyticalTable data={generateMoreData(500)} columns={columns} ref={tableRef} />;
2598+
};
2599+
cy.mount(<ScrollToItem />);
2600+
cy.findByText('Name-12').should('be.visible');
2601+
cy.findByText('Name-11').should('not.be.visible');
2602+
2603+
const ScrollToHorizontal = () => {
2604+
const tableRef = useRef(null);
2605+
useEffect(() => {
2606+
tableRef.current.horizontalScrollTo(1020);
2607+
}, []);
2608+
return (
2609+
<AnalyticalTable
2610+
data={generateMoreData(500)}
2611+
columns={[
2612+
...columns,
2613+
...new Array(100).fill('').map((_, index) => ({ id: `${index}`, Header: () => index }))
2614+
]}
2615+
ref={tableRef}
2616+
/>
2617+
);
2618+
};
2619+
cy.mount(<ScrollToHorizontal />);
2620+
cy.findByText('13').should('be.visible');
2621+
cy.findByText('12').should('not.be.visible');
2622+
const ScrollToItemHorizontal = () => {
2623+
const tableRef = useRef(null);
2624+
useEffect(() => {
2625+
tableRef.current.horizontalScrollToItem(13, { align: 'start' });
2626+
}, []);
2627+
return (
2628+
<AnalyticalTable
2629+
data={generateMoreData(500)}
2630+
columns={new Array(100).fill('').map((_, index) => ({ id: `${index}`, Header: () => index }))}
2631+
ref={tableRef}
2632+
/>
2633+
);
2634+
};
2635+
cy.mount(<ScrollToItemHorizontal />);
2636+
cy.findByText('13').should('be.visible');
2637+
cy.findByText('12').should('not.be.visible');
2638+
});
2639+
25802640
cypressPassThroughTestsFactory(AnalyticalTable, { data, columns });
25812641
});
25822642

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { Virtualizer } from '@tanstack/react-virtual';
22
import { useVirtualizer } from '@tanstack/react-virtual';
33
import { clsx } from 'clsx';
44
import type { MutableRefObject, ReactNode } from 'react';
5-
import React, { useCallback, useMemo, useRef } from 'react';
5+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
66
import { AnalyticalTableSubComponentsBehavior } from '../../../enums/index.js';
77
import type { ScrollToRefType } from '../interfaces.js';
8-
import type { AnalyticalTablePropTypes, DivWithCustomScrollProp } from '../types/index.js';
8+
import type { AnalyticalTablePropTypes, DivWithCustomScrollProp, TriggerScrollState } from '../types/index.js';
99
import { getSubRowsByString } from '../util/index.js';
1010
import { EmptyRow } from './EmptyRow.js';
1111
import { RowSubComponent as SubComponent } from './RowSubComponent.js';
@@ -35,6 +35,7 @@ interface VirtualTableBodyProps {
3535
subRowsKey: string;
3636
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
3737
subComponentsBehavior: AnalyticalTablePropTypes['subComponentsBehavior'];
38+
triggerScroll?: TriggerScrollState;
3839
}
3940

4041
const measureElement = (el: HTMLElement) => {
@@ -66,7 +67,8 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
6667
manualGroupBy,
6768
subRowsKey,
6869
scrollContainerRef,
69-
subComponentsBehavior
70+
subComponentsBehavior,
71+
triggerScroll
7072
} = props;
7173

7274
const itemCount = Math.max(minRows, rows.length);
@@ -100,6 +102,16 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
100102
scrollToIndex: rowVirtualizer.scrollToIndex
101103
};
102104

105+
useEffect(() => {
106+
if (triggerScroll && triggerScroll.direction === 'vertical') {
107+
if (triggerScroll.type === 'offset') {
108+
rowVirtualizer.scrollToOffset(...triggerScroll.args);
109+
} else {
110+
rowVirtualizer.scrollToIndex(...triggerScroll.args);
111+
}
112+
}
113+
}, [triggerScroll]);
114+
103115
const popInColumn = useMemo(
104116
() =>
105117
visibleColumns.filter(

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,77 @@
1+
import type { ScrollToOptions } from '@tanstack/react-virtual';
2+
import type { MutableRefObject } from 'react';
13
import { useEffect, useRef } from 'react';
4+
import type { AnalyticalTableScrollMode } from '../../../enums/index.js';
5+
import type { AnalyticalTableDomRef } from '../types/index.js';
26

3-
export const useTableScrollHandles = (ref) => {
7+
interface ScrollToMethods {
8+
scrollTo: (offset: number, align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode) => void;
9+
scrollToItem: (index: number, align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode) => void;
10+
horizontalScrollTo: (
11+
offset: number,
12+
align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode
13+
) => void;
14+
horizontalScrollToItem: (
15+
index: number,
16+
align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode
17+
) => void;
18+
}
19+
20+
interface ReactVirtualScrollToMethods {
21+
scrollToOffset?: (offset: number, options?: ScrollToOptions) => void;
22+
scrollToIndex?: (index: number, options?: ScrollToOptions) => void;
23+
horizontalScrollToOffset?: (offset: number, options?: ScrollToOptions) => void;
24+
horizontalScrollToIndex?: (index: number, options?: ScrollToOptions) => void;
25+
}
26+
27+
export const useTableScrollHandles = (ref, dispatch) => {
428
let analyticalTableRef = useRef(null);
529
if (ref) {
630
analyticalTableRef = ref;
731
}
8-
const scrollToRef = useRef<any>({});
32+
const scrollToRef = useRef<ReactVirtualScrollToMethods>({});
933

1034
useEffect(() => {
1135
if (analyticalTableRef.current) {
12-
Object.assign(analyticalTableRef.current, {
36+
Object.assign<MutableRefObject<AnalyticalTableDomRef>, ScrollToMethods>(analyticalTableRef.current, {
1337
scrollTo: (offset, align) => {
1438
if (typeof scrollToRef.current?.scrollToOffset === 'function') {
1539
scrollToRef.current.scrollToOffset(offset, { align });
40+
} else {
41+
dispatch({
42+
type: 'TRIGGER_PROG_SCROLL',
43+
payload: { direction: 'vertical', type: 'offset', args: [offset, { align }] }
44+
});
1645
}
1746
},
1847
scrollToItem: (index, align) => {
1948
if (typeof scrollToRef.current?.scrollToIndex === 'function') {
2049
scrollToRef.current.scrollToIndex(index, { align });
50+
} else {
51+
dispatch({
52+
type: 'TRIGGER_PROG_SCROLL',
53+
payload: { direction: 'vertical', type: 'item', args: [index, { align }] }
54+
});
2155
}
2256
},
2357
horizontalScrollTo: (offset, align) => {
2458
if (typeof scrollToRef.current?.horizontalScrollToOffset === 'function') {
2559
scrollToRef.current.horizontalScrollToOffset(offset, { align });
60+
} else {
61+
dispatch({
62+
type: 'TRIGGER_PROG_SCROLL',
63+
payload: { direction: 'horizontal', type: 'offset', args: [offset, { align }] }
64+
});
2665
}
2766
},
2867
horizontalScrollToItem: (index, align) => {
2968
if (typeof scrollToRef.current?.horizontalScrollToIndex === 'function') {
3069
scrollToRef.current.horizontalScrollToIndex(index, { align });
70+
} else {
71+
dispatch({
72+
type: 'TRIGGER_PROG_SCROLL',
73+
payload: { direction: 'horizontal', type: 'item', args: [index, { align }] }
74+
});
3175
}
3276
}
3377
});

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

Lines changed: 57 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
useI18nBundle,
99
useIsomorphicId,
1010
useIsomorphicLayoutEffect,
11-
useIsRTL
11+
useIsRTL,
12+
useSyncRef
1213
} from '@ui5/webcomponents-react-base';
1314
import { clsx } from 'clsx';
1415
import type { CSSProperties, MutableRefObject } from 'react';
@@ -175,10 +176,9 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
175176

176177
const classes = useStyles();
177178

178-
const [analyticalTableRef, scrollToRef] = useTableScrollHandles(ref);
179179
const tableRef = useRef<DivWithCustomScrollProp>(null);
180-
181-
const isRtl = useIsRTL(analyticalTableRef);
180+
const parentRef = useRef<DivWithCustomScrollProp>(null);
181+
const verticalScrollBarRef = useRef<DivWithCustomScrollProp>(null);
182182

183183
const getSubRows = useCallback((row) => getSubRowsByString(subRowsKey, row) || [], [subRowsKey]);
184184

@@ -228,7 +228,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
228228
markNavigatedRow,
229229
renderRowSubComponent,
230230
alwaysShowSubComponent,
231-
scrollToRef,
232231
showOverlay,
233232
uniqueId,
234233
subRowsKey,
@@ -275,6 +274,40 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
275274
setGlobalFilter
276275
} = tableInstanceRef.current;
277276
const tableState: AnalyticalTableState = tableInstanceRef.current.state;
277+
const { triggerScroll } = tableState;
278+
279+
const [componentRef, updatedRef] = useSyncRef<AnalyticalTableDomRef>(ref);
280+
//@ts-expect-error: types are compatible
281+
const isRtl = useIsRTL(updatedRef);
282+
283+
const columnVirtualizer = useVirtualizer({
284+
count: visibleColumnsWidth.length,
285+
getScrollElement: () => tableRef.current,
286+
estimateSize: useCallback((index) => visibleColumnsWidth[index], [visibleColumnsWidth]),
287+
horizontal: true,
288+
overscan: isRtl ? Infinity : overscanCountHorizontal,
289+
indexAttribute: 'data-column-index',
290+
// necessary as otherwise values are rounded which leads to wrong total width calculation leading to unnecessary scrollbar
291+
measureElement: !scaleXFactor || scaleXFactor === 1 ? (el) => el.getBoundingClientRect().width : undefined
292+
});
293+
const [analyticalTableRef, scrollToRef] = useTableScrollHandles(updatedRef, dispatch);
294+
295+
if (parentRef.current) {
296+
scrollToRef.current = {
297+
...scrollToRef.current,
298+
horizontalScrollToOffset: columnVirtualizer.scrollToOffset,
299+
horizontalScrollToIndex: columnVirtualizer.scrollToIndex
300+
};
301+
}
302+
useEffect(() => {
303+
if (triggerScroll && triggerScroll.direction === 'horizontal') {
304+
if (triggerScroll.type === 'offset') {
305+
columnVirtualizer.scrollToOffset(...triggerScroll.args);
306+
} else {
307+
columnVirtualizer.scrollToIndex(...triggerScroll.args);
308+
}
309+
}
310+
}, [triggerScroll]);
278311

279312
const includeSubCompRowHeight =
280313
!!renderRowSubComponent &&
@@ -543,40 +576,27 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
543576
}
544577
}, [tableState.columnResizing, retainColumnWidth, tableState.tableColResized]);
545578

546-
const parentRef = useRef<DivWithCustomScrollProp>(null);
547-
const verticalScrollBarRef = useRef<DivWithCustomScrollProp>(null);
548-
549579
const handleBodyScroll = (e) => {
550580
if (typeof onTableScroll === 'function') {
551581
onTableScroll(e);
552582
}
553-
if (verticalScrollBarRef.current && verticalScrollBarRef.current.scrollTop !== parentRef.current.scrollTop) {
554-
if (!parentRef.current.isExternalVerticalScroll) {
555-
verticalScrollBarRef.current.scrollTop = parentRef.current.scrollTop;
583+
const targetScrollTop = e.currentTarget.scrollTop;
584+
if (verticalScrollBarRef.current && verticalScrollBarRef.current.scrollTop !== targetScrollTop) {
585+
if (!e.currentTarget.isExternalVerticalScroll) {
586+
verticalScrollBarRef.current.scrollTop = targetScrollTop;
556587
verticalScrollBarRef.current.isExternalVerticalScroll = true;
557588
}
558-
parentRef.current.isExternalVerticalScroll = false;
589+
e.currentTarget.isExternalVerticalScroll = false;
559590
}
560591
};
561592

562-
const handleVerticalScrollBarScroll = () => {
563-
if (parentRef.current && !verticalScrollBarRef.current.isExternalVerticalScroll) {
564-
parentRef.current.scrollTop = verticalScrollBarRef.current.scrollTop;
593+
const handleVerticalScrollBarScroll = useCallback((e) => {
594+
if (parentRef.current && !e.currentTarget.isExternalVerticalScroll) {
595+
parentRef.current.scrollTop = e.currentTarget.scrollTop;
565596
parentRef.current.isExternalVerticalScroll = true;
566597
}
567-
verticalScrollBarRef.current.isExternalVerticalScroll = false;
568-
};
569-
570-
const columnVirtualizer = useVirtualizer({
571-
count: visibleColumnsWidth.length,
572-
getScrollElement: () => tableRef.current,
573-
estimateSize: useCallback((index) => visibleColumnsWidth[index], [visibleColumnsWidth]),
574-
horizontal: true,
575-
overscan: isRtl ? Infinity : overscanCountHorizontal,
576-
indexAttribute: 'data-column-index',
577-
// necessary as otherwise values are rounded which leads to wrong total width calculation leading to unnecessary scrollbar
578-
measureElement: !scaleXFactor || scaleXFactor === 1 ? (el) => el.getBoundingClientRect().width : undefined
579-
});
598+
e.currentTarget.isExternalVerticalScroll = false;
599+
}, []);
580600

581601
useEffect(() => {
582602
columnVirtualizer.measure();
@@ -592,12 +612,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
592612
showVerticalEndBorder && classes.showVerticalEndBorder
593613
);
594614

595-
scrollToRef.current = {
596-
...scrollToRef.current,
597-
horizontalScrollToOffset: columnVirtualizer.scrollToOffset,
598-
horizontalScrollToIndex: columnVirtualizer.scrollToIndex
599-
};
600-
601615
const handleOnLoadMore = (e) => {
602616
const rootNodes = rows.filter((row) => row.depth === 0);
603617
onLoadMore(
@@ -610,7 +624,13 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
610624

611625
return (
612626
<>
613-
<div className={className} style={inlineStyle} ref={analyticalTableRef} {...rest}>
627+
<div
628+
className={className}
629+
style={inlineStyle}
630+
//@ts-expect-error: types are compatible
631+
ref={componentRef}
632+
{...rest}
633+
>
614634
{header && (
615635
<TitleBar ref={titleBarRef} titleBarId={titleBarId}>
616636
{header}
@@ -723,6 +743,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
723743
manualGroupBy={reactTableOptions?.manualGroupBy as boolean | undefined}
724744
subRowsKey={subRowsKey}
725745
subComponentsBehavior={subComponentsBehavior}
746+
triggerScroll={tableState.triggerScroll}
726747
/>
727748
</VirtualTableBodyContainer>
728749
)}
@@ -736,6 +757,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
736757
ref={verticalScrollBarRef}
737758
data-native-scrollbar={props['data-native-scrollbar']}
738759
scrollContainerRef={scrollContainerRef}
760+
parentRef={parentRef}
739761
/>
740762
)}
741763
</FlexBox>

0 commit comments

Comments
 (0)