Skip to content
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
Expand Up @@ -158,6 +158,8 @@ const kitchenSinkArgs: AnalyticalTablePropTypes = {
visibleRowCountMode: AnalyticalTableVisibleRowCountMode.Interactive,
visibleRows: 5,
withRowHighlight: true,
// sb actions has a huge impact on performance here.
onTableScroll: undefined,
};

const meta = {
Expand Down Expand Up @@ -191,6 +193,8 @@ const meta = {
highlightField: 'status',
subRowsKey: 'subRows',
visibleRows: 5,
// sb actions has a huge impact on performance here.
onTableScroll: undefined,
},
argTypes: {
data: { control: { disable: true } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps)

const onScroll = useCallback(
(event) => {
handleExternalScroll(enrichEventWithDetails(event, { rows, rowElements: event.target.children[0].children }));
if (typeof handleExternalScroll === 'function') {
handleExternalScroll(enrichEventWithDetails(event, { rows, rowElements: event.target.children[0].children }));
}
const scrollOffset = event.target.scrollTop;
const isScrollingDown = lastScrollTop.current < scrollOffset;
const target = event.target;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { MutableRefObject } from 'react';
import { useEffect, useRef, useState } from 'react';

export function useSyncScroll(refContent: MutableRefObject<HTMLElement>, refScrollbar: MutableRefObject<HTMLElement>) {
const ticking = useRef(false);
const isProgrammatic = useRef(false);
const [isMounted, setIsMounted] = useState(false);

useEffect(() => {
const content = refContent.current;
const scrollbar = refScrollbar.current;

if (!content || !scrollbar || !isMounted) {
setIsMounted(true);
return;
}

scrollbar.scrollTop = content.scrollTop;

const sync = (source: 'content' | 'scrollbar') => {
if (ticking.current) {
return;
}
ticking.current = true;

requestAnimationFrame(() => {
const sourceEl = source === 'content' ? content : scrollbar;
const targetEl = source === 'content' ? scrollbar : content;

if (!isProgrammatic.current && targetEl.scrollTop !== sourceEl.scrollTop) {
isProgrammatic.current = true;
targetEl.scrollTop = sourceEl.scrollTop;
// Clear the flag on next frame
requestAnimationFrame(() => (isProgrammatic.current = false));
}

ticking.current = false;
});
};

const onScrollContent = () => sync('content');
const onScrollScrollbar = () => sync('scrollbar');

content.addEventListener('scroll', onScrollContent, { passive: true });
scrollbar.addEventListener('scroll', onScrollScrollbar, { passive: true });

return () => {
content.removeEventListener('scroll', onScrollContent);
scrollbar.removeEventListener('scroll', onScrollScrollbar);
};
}, [isMounted, refContent, refScrollbar]);
}
34 changes: 3 additions & 31 deletions packages/main/src/components/AnalyticalTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import { useScrollToRef } from './hooks/useScrollToRef.js';
import { useSelectionChangeCallback } from './hooks/useSelectionChangeCallback.js';
import { useSingleRowStateSelection } from './hooks/useSingleRowStateSelection.js';
import { useStyling } from './hooks/useStyling.js';
import { useSyncScroll } from './hooks/useSyncScroll.js';
import { useToggleRowExpand } from './hooks/useToggleRowExpand.js';
import { useVisibleColumnsWidth } from './hooks/useVisibleColumnsWidth.js';
import { VerticalScrollbar } from './scrollbars/VerticalScrollbar.js';
Expand Down Expand Up @@ -656,34 +657,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
}
}, [tableState.columnResizing, retainColumnWidth, tableState.tableColResized]);

const handleBodyScroll = (e) => {
if (typeof onTableScroll === 'function') {
onTableScroll(e);
}
const targetScrollTop = e.currentTarget.scrollTop;

if (verticalScrollBarRef.current) {
const vertScrollbarScrollElement = verticalScrollBarRef.current.firstElementChild as HTMLDivElement;
if (vertScrollbarScrollElement.offsetHeight !== scrollContainerRef.current?.offsetHeight) {
vertScrollbarScrollElement.style.height = `${scrollContainerRef.current.offsetHeight}px`;
}
if (verticalScrollBarRef.current.scrollTop !== targetScrollTop) {
if (!e.currentTarget.isExternalVerticalScroll) {
verticalScrollBarRef.current.scrollTop = targetScrollTop;
verticalScrollBarRef.current.isExternalVerticalScroll = true;
}
e.currentTarget.isExternalVerticalScroll = false;
}
}
};

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

useEffect(() => {
columnVirtualizer.measure();
Expand Down Expand Up @@ -870,7 +844,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
internalRowHeight={internalRowHeight}
popInRowHeight={popInRowHeight}
rows={rows}
handleExternalScroll={handleBodyScroll}
handleExternalScroll={onTableScroll}
visibleRows={internalVisibleRowCount}
isGrouped={isGrouped}
>
Expand Down Expand Up @@ -905,10 +879,8 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
tableBodyHeight={tableBodyHeight}
internalRowHeight={internalHeaderRowHeight}
tableRef={tableRef}
handleVerticalScrollBarScroll={handleVerticalScrollBarScroll}
ref={verticalScrollBarRef}
scrollContainerRef={scrollContainerRef}
parentRef={parentRef}
nativeScrollbar={nativeScrollbar}
classNames={classNames}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,22 @@
import { useSyncRef } from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import type { MutableRefObject, RefObject } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import type { MutableRefObject } from 'react';
import { forwardRef } from 'react';
import { FlexBoxDirection } from '../../../enums/FlexBoxDirection.js';
import { FlexBox } from '../../FlexBox/index.js';
import type { ClassNames } from '../types/index.js';

interface VerticalScrollbarProps {
internalRowHeight: number;
tableRef: RefObject<any>;
handleVerticalScrollBarScroll: any;
tableRef: MutableRefObject<HTMLDivElement>;
tableBodyHeight: number;
scrollContainerRef: MutableRefObject<HTMLDivElement>;
parentRef: MutableRefObject<HTMLDivElement>;
nativeScrollbar: boolean;
classNames: ClassNames;
}

export const VerticalScrollbar = forwardRef<HTMLDivElement, VerticalScrollbarProps>((props, ref) => {
const {
internalRowHeight,
tableRef,
handleVerticalScrollBarScroll,
tableBodyHeight,
scrollContainerRef,
nativeScrollbar,
parentRef,
classNames,
} = props;
const [componentRef, containerRef] = useSyncRef(ref);
const scrollElementRef = useRef(null);

const { internalRowHeight, tableRef, tableBodyHeight, scrollContainerRef, nativeScrollbar, classNames } = props;
const hasHorizontalScrollbar = tableRef?.current?.offsetWidth !== tableRef?.current?.scrollWidth;

useEffect(() => {
const observer = new ResizeObserver(([entry]) => {
if (containerRef.current && parentRef.current && entry.target.getBoundingClientRect().height > 0) {
containerRef.current.scrollTop = parentRef.current.scrollTop;
}
});
if (scrollElementRef.current) {
observer.observe(scrollElementRef.current);
}
return () => {
observer.disconnect();
};
}, []);
const horizontalScrollbarSectionStyles = clsx(hasHorizontalScrollbar && classNames.bottomSection);

return (
Expand All @@ -61,11 +32,10 @@ export const VerticalScrollbar = forwardRef<HTMLDivElement, VerticalScrollbarPro
className={classNames.headerSection}
/>
<div
ref={componentRef}
ref={ref}
style={{
height: tableRef.current ? `${tableBodyHeight}px` : '0',
}}
onScroll={handleVerticalScrollBarScroll}
className={clsx(
classNames.scrollbar,
nativeScrollbar
Expand All @@ -76,7 +46,6 @@ export const VerticalScrollbar = forwardRef<HTMLDivElement, VerticalScrollbarPro
tabIndex={-1}
>
<div
ref={scrollElementRef}
className={classNames.verticalScroller}
style={{
height: `${scrollContainerRef.current?.scrollHeight}px`,
Expand Down
10 changes: 9 additions & 1 deletion packages/main/src/components/AnalyticalTable/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,11 @@ export interface AnalyticalTableColumnDefinition {
*/
headerTooltip?: string;
/**
* Custom cell renderer. If set, the table will call that component for every cell and pass all required information as props, e.g. the cell value as `props.cell.value`
* Custom cell renderer. If set, the table will use this component or render the provided string for every cell,
* passing all necessary information as props, e.g., the cell value as `props.cell.value`.
*
* __Note:__ Using a custom component __can impact performance__!
* If you pass a component, __memoizing it is strongly recommended__, especially for complex components or large datasets.
*/
Cell?: string | ComponentType<CellInstance> | ((props?: CellInstance) => ReactNode);
/**
Expand Down Expand Up @@ -1019,6 +1023,10 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
onLoadMore?: (e?: CustomEvent<{ rowCount: number; totalRowCount: number }>) => void;
/**
* Fired when the body of the table is scrolled.
*
* __Note:__ This callback __must be memoized__! Since it is triggered on __every scroll event__,
* non-memoized or expensive calculations can have a __huge impact on performance__ and cause visible lag.
* Throttling or debouncing is always recommended to reduce performance overhead.
*/
onTableScroll?: (e?: CustomEvent<{ rows: Record<string, any>[]; rowElements: HTMLCollection }>) => void;
/**
Expand Down
Loading