diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
index 14f5ae4c136..e76b3e7c21e 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.cy.tsx
@@ -4304,6 +4304,66 @@ describe('AnalyticalTable', () => {
cy.focused().should('have.text', 'Before');
});
+ it('vertical scroll sync', () => {
+ cy.mount();
+
+ cy.get('[data-component-name="AnalyticalTableBody"]').scrollTo(0, 2000).should('have.prop', 'scrollTop', 2000);
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').should('have.prop', 'scrollTop', 2000);
+
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]')
+ .scrollTo(0, 3000)
+ .should('have.prop', 'scrollTop', 3000);
+ cy.get('[data-component-name="AnalyticalTableBody"]').should('have.prop', 'scrollTop', 3000);
+
+ cy.get('[data-component-name="AnalyticalTableContainerWithScrollbar"]').realMouseWheel({ deltaY: 500 });
+ cy.get('[data-component-name="AnalyticalTableBody"]').should('have.prop', 'scrollTop', 3500);
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').should('have.prop', 'scrollTop', 3500);
+
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').realMouseWheel({ deltaY: -1000 });
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').should('have.prop', 'scrollTop', 2500);
+ cy.get('[data-component-name="AnalyticalTableBody"]').should('have.prop', 'scrollTop', 2500);
+
+ const TestComp = () => {
+ const [_data, setData] = useState([]);
+ useEffect(() => {
+ setTimeout(() => {
+ setData(generateMoreData(100));
+ }, 100);
+ }, []);
+
+ return (
+ <>
+
}
+ visibleRowCountMode="AutoWithEmptyRows"
+ />
+
+ >
+ );
+ };
+
+ cy.mount();
+
+ cy.get('[data-component-name="AnalyticalTableBody"]').scrollTo(0, 2000).should('have.prop', 'scrollTop', 2000);
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').should('have.prop', 'scrollTop', 2000);
+
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]')
+ .scrollTo(0, 3000)
+ .should('have.prop', 'scrollTop', 3000);
+ cy.get('[data-component-name="AnalyticalTableBody"]').should('have.prop', 'scrollTop', 3000);
+
+ cy.get('[data-component-name="AnalyticalTableContainerWithScrollbar"]').realMouseWheel({ deltaY: 500 });
+ cy.get('[data-component-name="AnalyticalTableBody"]').should('have.prop', 'scrollTop', 3500);
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').should('have.prop', 'scrollTop', 3500);
+
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').realMouseWheel({ deltaY: -1000 });
+ cy.get('[data-component-name="AnalyticalTableVerticalScrollbar"]').should('have.prop', 'scrollTop', 2500);
+ cy.get('[data-component-name="AnalyticalTableBody"]').should('have.prop', 'scrollTop', 2500);
+ });
+
cypressPassThroughTestsFactory(AnalyticalTable, { data, columns });
});
diff --git a/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css b/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css
index 835e90ed22c..4792fdff2f8 100644
--- a/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css
+++ b/packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css
@@ -607,3 +607,17 @@
box-sizing: border-box;
border-inline-end: var(--_ui5wcr-AnalyticalTable-OuterBorderInline);
}
+
+/* ==========================================================================
+ Firefox scrollbar styles
+ ========================================================================== */
+
+.firefoxCell {
+ &:last-child {
+ padding-inline-end: 18px;
+ }
+}
+
+.firefoxNativeScrollbar {
+ scrollbar-width: inherit;
+}
diff --git a/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx b/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx
index 19eb7075d19..0600da74c93 100644
--- a/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx
+++ b/packages/main/src/components/AnalyticalTable/TableBody/VirtualTableBodyContainer.tsx
@@ -1,4 +1,5 @@
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base';
+import { clsx } from 'clsx';
import type { MutableRefObject } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AnalyticalTablePropTypes, TableInstance } from '../types/index.js';
@@ -20,6 +21,7 @@ interface VirtualTableBodyContainerProps {
rowCollapsedFlag?: boolean;
dispatch: (e: { type: string; payload?: any }) => void;
isGrouped: boolean;
+ isFirefox: boolean;
}
export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps) => {
@@ -39,6 +41,7 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps)
popInRowHeight,
rowCollapsedFlag,
isGrouped,
+ isFirefox,
dispatch,
} = props;
const [isMounted, setIsMounted] = useState(false);
@@ -114,7 +117,7 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps)
return (
{
+ setIsFirefox(isFirefoxFn());
+ }, []);
+
+ return isFirefox;
+}
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts b/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts
index 495b2508d39..fc0ab819700 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useStyling.ts
@@ -1,3 +1,4 @@
+import { clsx } from 'clsx';
import type { CSSProperties } from 'react';
import { AnalyticalTableSelectionBehavior } from '../../../enums/AnalyticalTableSelectionBehavior.js';
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
@@ -85,14 +86,10 @@ const getRowProps = (
};
const getCellProps = (cellProps, { cell: { column }, instance }) => {
- const { classes } = instance.webComponentsReactProperties;
+ const { webComponentsReactProperties, state } = instance;
+ const { classes, isFirefox } = webComponentsReactProperties;
const style: CSSProperties = { width: `${column.totalWidth}px`, ...resolveCellAlignment(column) };
- let className = classes.tableCell;
- if (column.className) {
- className += ` ${column.className}`;
- }
-
if (
column.id === '__ui5wcr__internal_highlight_column' ||
column.id === '__ui5wcr__internal_selection_column' ||
@@ -104,7 +101,12 @@ const getCellProps = (cellProps, { cell: { column }, instance }) => {
return [
cellProps,
{
- className,
+ className: clsx(
+ cellProps.className,
+ classes.tableCell,
+ column.className,
+ isFirefox && state.isScrollable && classes.firefoxCell,
+ ),
style,
tabIndex: -1,
},
diff --git a/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts b/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts
index d6d39b3f902..2da0d705b75 100644
--- a/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts
+++ b/packages/main/src/components/AnalyticalTable/hooks/useSyncScroll.ts
@@ -1,12 +1,20 @@
import type { MutableRefObject } from 'react';
import { useEffect, useRef, useState } from 'react';
-export function useSyncScroll(refContent: MutableRefObject
, refScrollbar: MutableRefObject) {
- const ticking = useRef(false);
+export function useSyncScroll(
+ refContent: MutableRefObject,
+ refScrollbar: MutableRefObject,
+ isScrollable: boolean,
+ disabled = false,
+) {
const isProgrammatic = useRef(false);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
+ if (disabled || !isScrollable) {
+ return;
+ }
+
const content = refContent.current;
const scrollbar = refScrollbar.current;
@@ -18,24 +26,15 @@ export function useSyncScroll(refContent: MutableRefObject, refScro
scrollbar.scrollTop = content.scrollTop;
const sync = (source: 'content' | 'scrollbar') => {
- if (ticking.current) {
- return;
+ 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 = 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');
@@ -48,5 +47,5 @@ export function useSyncScroll(refContent: MutableRefObject, refScro
content.removeEventListener('scroll', onScrollContent);
scrollbar.removeEventListener('scroll', onScrollScrollbar);
};
- }, [isMounted, refContent, refScrollbar]);
+ }, [isMounted, refContent, refScrollbar, disabled, isScrollable]);
}
diff --git a/packages/main/src/components/AnalyticalTable/index.tsx b/packages/main/src/components/AnalyticalTable/index.tsx
index 1c9330600d6..2fc54505fcd 100644
--- a/packages/main/src/components/AnalyticalTable/index.tsx
+++ b/packages/main/src/components/AnalyticalTable/index.tsx
@@ -64,6 +64,7 @@ import { useColumnsDeps } from './hooks/useColumnsDeps.js';
import { useColumnDragAndDrop } from './hooks/useDragAndDrop.js';
import { useDynamicColumnWidths } from './hooks/useDynamicColumnWidths.js';
import { useFontsReady } from './hooks/useFontsReady.js';
+import { useIsFirefox } from './hooks/useIsFirefox.js';
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation.js';
import { usePopIn } from './hooks/usePopIn.js';
import { useResizeColumnsConfig } from './hooks/useResizeColumnsConfig.js';
@@ -190,6 +191,7 @@ const AnalyticalTable = forwardRef {
columnVirtualizer.measure();
@@ -844,6 +847,7 @@ const AnalyticalTable = forwardRef
)}
- {(additionalEmptyRowsCount || tableState.isScrollable) && (
+ {!isFirefox && (additionalEmptyRowsCount || tableState.isScrollable) && (
((props, ref) => {
const { internalRowHeight, tableRef, tableBodyHeight, scrollContainerRef, classNames } = props;
const hasHorizontalScrollbar = tableRef?.current?.offsetWidth !== tableRef?.current?.scrollWidth;
const horizontalScrollbarSectionStyles = clsx(hasHorizontalScrollbar && classNames.bottomSection);
+ const [componentRef, scrollbarRef] = useSyncRef(ref);
+ const contentRef = useRef(null);
+
+ // Force style recalculation to fix Chrome scrollbar-color bug (track height not updating correctly)
+ useEffect(() => {
+ if (!isChrome) {
+ return;
+ }
+
+ if (scrollbarRef.current && contentRef.current) {
+ const scrollbarElement = scrollbarRef.current;
+
+ const forceScrollbarUpdate = () => {
+ const originalHeight = scrollbarElement.style.height;
+ scrollbarElement.style.height = `${tableBodyHeight + 1}px`;
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ scrollbarElement.offsetHeight; // Force reflow
+ scrollbarElement.style.height = originalHeight ?? `${tableBodyHeight}px`;
+ };
+
+ requestAnimationFrame(forceScrollbarUpdate);
+ }
+ }, [tableBodyHeight, scrollContainerRef.current?.scrollHeight, scrollbarRef]);
return (
{
+ contentRef.current = node;
+ if (node && scrollContainerRef.current?.scrollHeight) {
+ node.style.height = `${scrollContainerRef.current?.scrollHeight}px`;
+ }
}}
+ className={classNames.verticalScroller}
/>