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 ( + <> +
+ Header
} + 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} />