Skip to content

fix(AnalyticalTable): improve programmatic scroll behavior & scroll types #5334

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 7 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ThemingParameters } from '@ui5/webcomponents-react-base';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { AnalyticalTablePropTypes } from '../..';
import type { AnalyticalTableDomRef, AnalyticalTablePropTypes } from '../..';
import {
AnalyticalTable,
AnalyticalTableHooks,
Expand Down Expand Up @@ -2577,6 +2577,66 @@ describe('AnalyticalTable', () => {
cy.get('[data-component-name="AnalyticalTableBody"]').should('have.css', 'height', '800px');
});

it('initial scroll-to', () => {
const ScrollTo = () => {
const tableRef = useRef<AnalyticalTableDomRef>(null);
useEffect(() => {
tableRef.current.scrollTo(520);
}, []);
return <AnalyticalTable data={generateMoreData(500)} columns={columns} ref={tableRef} />;
};
cy.mount(<ScrollTo />);
cy.findByText('Name-12').should('be.visible');
cy.findByText('Name-11').should('not.be.visible');

const ScrollToItem = () => {
const tableRef = useRef(null);
useEffect(() => {
tableRef.current.scrollToItem(12, { align: 'start' });
}, []);
return <AnalyticalTable data={generateMoreData(500)} columns={columns} ref={tableRef} />;
};
cy.mount(<ScrollToItem />);
cy.findByText('Name-12').should('be.visible');
cy.findByText('Name-11').should('not.be.visible');

const ScrollToHorizontal = () => {
const tableRef = useRef(null);
useEffect(() => {
tableRef.current.horizontalScrollTo(1020);
}, []);
return (
<AnalyticalTable
data={generateMoreData(500)}
columns={[
...columns,
...new Array(100).fill('').map((_, index) => ({ id: `${index}`, Header: () => index }))
]}
ref={tableRef}
/>
);
};
cy.mount(<ScrollToHorizontal />);
cy.findByText('13').should('be.visible');
cy.findByText('12').should('not.be.visible');
const ScrollToItemHorizontal = () => {
const tableRef = useRef(null);
useEffect(() => {
tableRef.current.horizontalScrollToItem(13, { align: 'start' });
}, []);
return (
<AnalyticalTable
data={generateMoreData(500)}
columns={new Array(100).fill('').map((_, index) => ({ id: `${index}`, Header: () => index }))}
ref={tableRef}
/>
);
};
cy.mount(<ScrollToItemHorizontal />);
cy.findByText('13').should('be.visible');
cy.findByText('12').should('not.be.visible');
});

cypressPassThroughTestsFactory(AnalyticalTable, { data, columns });
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ 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, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { AnalyticalTableSubComponentsBehavior } from '../../../enums/index.js';
import type { ScrollToRefType } from '../interfaces.js';
import type { AnalyticalTablePropTypes, DivWithCustomScrollProp } from '../types/index.js';
import type { AnalyticalTablePropTypes, DivWithCustomScrollProp, TriggerScrollState } from '../types/index.js';
import { getSubRowsByString } from '../util/index.js';
import { EmptyRow } from './EmptyRow.js';
import { RowSubComponent as SubComponent } from './RowSubComponent.js';
Expand Down Expand Up @@ -35,6 +35,7 @@ interface VirtualTableBodyProps {
subRowsKey: string;
scrollContainerRef?: MutableRefObject<HTMLDivElement>;
subComponentsBehavior: AnalyticalTablePropTypes['subComponentsBehavior'];
triggerScroll?: TriggerScrollState;
}

const measureElement = (el: HTMLElement) => {
Expand Down Expand Up @@ -66,7 +67,8 @@ export const VirtualTableBody = (props: VirtualTableBodyProps) => {
manualGroupBy,
subRowsKey,
scrollContainerRef,
subComponentsBehavior
subComponentsBehavior,
triggerScroll
} = props;

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

useEffect(() => {
if (triggerScroll && triggerScroll.direction === 'vertical') {
if (triggerScroll.type === 'offset') {
rowVirtualizer.scrollToOffset(...triggerScroll.args);
} else {
rowVirtualizer.scrollToIndex(...triggerScroll.args);
}
}
}, [triggerScroll]);

const popInColumn = useMemo(
() =>
visibleColumns.filter(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,77 @@
import type { ScrollToOptions } from '@tanstack/react-virtual';
import type { MutableRefObject } from 'react';
import { useEffect, useRef } from 'react';
import type { AnalyticalTableScrollMode } from '../../../enums/index.js';
import type { AnalyticalTableDomRef } from '../types/index.js';

export const useTableScrollHandles = (ref) => {
interface ScrollToMethods {
scrollTo: (offset: number, align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode) => void;
scrollToItem: (index: number, align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode) => void;
horizontalScrollTo: (
offset: number,
align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode
) => void;
horizontalScrollToItem: (
index: number,
align?: AnalyticalTableScrollMode | keyof typeof AnalyticalTableScrollMode
) => void;
}

interface ReactVirtualScrollToMethods {
scrollToOffset?: (offset: number, options?: ScrollToOptions) => void;
scrollToIndex?: (index: number, options?: ScrollToOptions) => void;
horizontalScrollToOffset?: (offset: number, options?: ScrollToOptions) => void;
horizontalScrollToIndex?: (index: number, options?: ScrollToOptions) => void;
}

export const useTableScrollHandles = (ref, dispatch) => {
let analyticalTableRef = useRef(null);
if (ref) {
analyticalTableRef = ref;
}
const scrollToRef = useRef<any>({});
const scrollToRef = useRef<ReactVirtualScrollToMethods>({});

useEffect(() => {
if (analyticalTableRef.current) {
Object.assign(analyticalTableRef.current, {
Object.assign<MutableRefObject<AnalyticalTableDomRef>, ScrollToMethods>(analyticalTableRef.current, {
scrollTo: (offset, align) => {
if (typeof scrollToRef.current?.scrollToOffset === 'function') {
scrollToRef.current.scrollToOffset(offset, { align });
} else {
dispatch({
type: 'TRIGGER_PROG_SCROLL',
payload: { direction: 'vertical', type: 'offset', args: [offset, { align }] }
});
}
},
scrollToItem: (index, align) => {
if (typeof scrollToRef.current?.scrollToIndex === 'function') {
scrollToRef.current.scrollToIndex(index, { align });
} else {
dispatch({
type: 'TRIGGER_PROG_SCROLL',
payload: { direction: 'vertical', type: 'item', args: [index, { align }] }
});
}
},
horizontalScrollTo: (offset, align) => {
if (typeof scrollToRef.current?.horizontalScrollToOffset === 'function') {
scrollToRef.current.horizontalScrollToOffset(offset, { align });
} else {
dispatch({
type: 'TRIGGER_PROG_SCROLL',
payload: { direction: 'horizontal', type: 'offset', args: [offset, { align }] }
});
}
},
horizontalScrollToItem: (index, align) => {
if (typeof scrollToRef.current?.horizontalScrollToIndex === 'function') {
scrollToRef.current.horizontalScrollToIndex(index, { align });
} else {
dispatch({
type: 'TRIGGER_PROG_SCROLL',
payload: { direction: 'horizontal', type: 'item', args: [index, { align }] }
});
}
}
});
Expand Down
92 changes: 57 additions & 35 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
useI18nBundle,
useIsomorphicId,
useIsomorphicLayoutEffect,
useIsRTL
useIsRTL,
useSyncRef
} from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import type { CSSProperties, MutableRefObject } from 'react';
Expand Down Expand Up @@ -175,10 +176,9 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp

const classes = useStyles();

const [analyticalTableRef, scrollToRef] = useTableScrollHandles(ref);
const tableRef = useRef<DivWithCustomScrollProp>(null);

const isRtl = useIsRTL(analyticalTableRef);
const parentRef = useRef<DivWithCustomScrollProp>(null);
const verticalScrollBarRef = useRef<DivWithCustomScrollProp>(null);

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

Expand Down Expand Up @@ -228,7 +228,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
markNavigatedRow,
renderRowSubComponent,
alwaysShowSubComponent,
scrollToRef,
showOverlay,
uniqueId,
subRowsKey,
Expand Down Expand Up @@ -275,6 +274,40 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
setGlobalFilter
} = tableInstanceRef.current;
const tableState: AnalyticalTableState = tableInstanceRef.current.state;
const { triggerScroll } = tableState;

const [componentRef, updatedRef] = useSyncRef<AnalyticalTableDomRef>(ref);
//@ts-expect-error: types are compatible
const isRtl = useIsRTL(updatedRef);

const columnVirtualizer = useVirtualizer({
count: visibleColumnsWidth.length,
getScrollElement: () => tableRef.current,
estimateSize: useCallback((index) => visibleColumnsWidth[index], [visibleColumnsWidth]),
horizontal: true,
overscan: isRtl ? Infinity : overscanCountHorizontal,
indexAttribute: 'data-column-index',
// necessary as otherwise values are rounded which leads to wrong total width calculation leading to unnecessary scrollbar
measureElement: !scaleXFactor || scaleXFactor === 1 ? (el) => el.getBoundingClientRect().width : undefined
});
const [analyticalTableRef, scrollToRef] = useTableScrollHandles(updatedRef, dispatch);

if (parentRef.current) {
scrollToRef.current = {
...scrollToRef.current,
horizontalScrollToOffset: columnVirtualizer.scrollToOffset,
horizontalScrollToIndex: columnVirtualizer.scrollToIndex
};
}
useEffect(() => {
if (triggerScroll && triggerScroll.direction === 'horizontal') {
if (triggerScroll.type === 'offset') {
columnVirtualizer.scrollToOffset(...triggerScroll.args);
} else {
columnVirtualizer.scrollToIndex(...triggerScroll.args);
}
}
}, [triggerScroll]);

const includeSubCompRowHeight =
!!renderRowSubComponent &&
Expand Down Expand Up @@ -543,40 +576,27 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
}
}, [tableState.columnResizing, retainColumnWidth, tableState.tableColResized]);

const parentRef = useRef<DivWithCustomScrollProp>(null);
const verticalScrollBarRef = useRef<DivWithCustomScrollProp>(null);

const handleBodyScroll = (e) => {
if (typeof onTableScroll === 'function') {
onTableScroll(e);
}
if (verticalScrollBarRef.current && verticalScrollBarRef.current.scrollTop !== parentRef.current.scrollTop) {
if (!parentRef.current.isExternalVerticalScroll) {
verticalScrollBarRef.current.scrollTop = parentRef.current.scrollTop;
const targetScrollTop = e.currentTarget.scrollTop;
if (verticalScrollBarRef.current && verticalScrollBarRef.current.scrollTop !== targetScrollTop) {
if (!e.currentTarget.isExternalVerticalScroll) {
verticalScrollBarRef.current.scrollTop = targetScrollTop;
verticalScrollBarRef.current.isExternalVerticalScroll = true;
}
parentRef.current.isExternalVerticalScroll = false;
e.currentTarget.isExternalVerticalScroll = false;
}
};

const handleVerticalScrollBarScroll = () => {
if (parentRef.current && !verticalScrollBarRef.current.isExternalVerticalScroll) {
parentRef.current.scrollTop = verticalScrollBarRef.current.scrollTop;
const handleVerticalScrollBarScroll = useCallback((e) => {
if (parentRef.current && !e.currentTarget.isExternalVerticalScroll) {
parentRef.current.scrollTop = e.currentTarget.scrollTop;
parentRef.current.isExternalVerticalScroll = true;
}
verticalScrollBarRef.current.isExternalVerticalScroll = false;
};

const columnVirtualizer = useVirtualizer({
count: visibleColumnsWidth.length,
getScrollElement: () => tableRef.current,
estimateSize: useCallback((index) => visibleColumnsWidth[index], [visibleColumnsWidth]),
horizontal: true,
overscan: isRtl ? Infinity : overscanCountHorizontal,
indexAttribute: 'data-column-index',
// necessary as otherwise values are rounded which leads to wrong total width calculation leading to unnecessary scrollbar
measureElement: !scaleXFactor || scaleXFactor === 1 ? (el) => el.getBoundingClientRect().width : undefined
});
e.currentTarget.isExternalVerticalScroll = false;
}, []);

useEffect(() => {
columnVirtualizer.measure();
Expand All @@ -592,12 +612,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
showVerticalEndBorder && classes.showVerticalEndBorder
);

scrollToRef.current = {
...scrollToRef.current,
horizontalScrollToOffset: columnVirtualizer.scrollToOffset,
horizontalScrollToIndex: columnVirtualizer.scrollToIndex
};

const handleOnLoadMore = (e) => {
const rootNodes = rows.filter((row) => row.depth === 0);
onLoadMore(
Expand All @@ -610,7 +624,13 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp

return (
<>
<div className={className} style={inlineStyle} ref={analyticalTableRef} {...rest}>
<div
className={className}
style={inlineStyle}
//@ts-expect-error: types are compatible
ref={componentRef}
{...rest}
>
{header && (
<TitleBar ref={titleBarRef} titleBarId={titleBarId}>
{header}
Expand Down Expand Up @@ -723,6 +743,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
manualGroupBy={reactTableOptions?.manualGroupBy as boolean | undefined}
subRowsKey={subRowsKey}
subComponentsBehavior={subComponentsBehavior}
triggerScroll={tableState.triggerScroll}
/>
</VirtualTableBodyContainer>
)}
Expand All @@ -736,6 +757,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
ref={verticalScrollBarRef}
data-native-scrollbar={props['data-native-scrollbar']}
scrollContainerRef={scrollContainerRef}
parentRef={parentRef}
/>
)}
</FlexBox>
Expand Down
Loading