Skip to content

Commit 619d18e

Browse files
committed
fix(AnalyticalTable): fix vertical scrollbar styles for Chrome and Firefox
1 parent 701166d commit 619d18e

File tree

6 files changed

+86
-34
lines changed

6 files changed

+86
-34
lines changed

packages/main/src/components/AnalyticalTable/AnalyticalTable.module.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,17 @@
607607
box-sizing: border-box;
608608
border-inline-end: var(--_ui5wcr-AnalyticalTable-OuterBorderInline);
609609
}
610+
611+
/* ==========================================================================
612+
Firefox scrollbar styles
613+
========================================================================== */
614+
615+
.firefoxCell {
616+
&:last-child {
617+
padding-inline-end: 18px;
618+
}
619+
}
620+
621+
.firefoxNativeScrollbar {
622+
scrollbar-width: inherit;
623+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base';
2+
import { clsx } from 'clsx';
23
import type { MutableRefObject } from 'react';
34
import { useCallback, useEffect, useRef, useState } from 'react';
45
import type { AnalyticalTablePropTypes, TableInstance } from '../types/index.js';
@@ -20,6 +21,7 @@ interface VirtualTableBodyContainerProps {
2021
rowCollapsedFlag?: boolean;
2122
dispatch: (e: { type: string; payload?: any }) => void;
2223
isGrouped: boolean;
24+
isFirefox: boolean;
2325
}
2426

2527
export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps) => {
@@ -39,6 +41,7 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps)
3941
popInRowHeight,
4042
rowCollapsedFlag,
4143
isGrouped,
44+
isFirefox,
4245
dispatch,
4346
} = props;
4447
const [isMounted, setIsMounted] = useState(false);
@@ -114,7 +117,7 @@ export const VirtualTableBodyContainer = (props: VirtualTableBodyContainerProps)
114117

115118
return (
116119
<div
117-
className={classes.tbody}
120+
className={clsx(classes.tbody, isFirefox && classes.firefoxNativeScrollbar)}
118121
ref={parentRef}
119122
onScroll={onScroll}
120123
style={{

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { clsx } from 'clsx';
12
import type { CSSProperties } from 'react';
23
import { AnalyticalTableSelectionBehavior } from '../../../enums/AnalyticalTableSelectionBehavior.js';
34
import { AnalyticalTableSelectionMode } from '../../../enums/AnalyticalTableSelectionMode.js';
@@ -85,14 +86,10 @@ const getRowProps = (
8586
};
8687

8788
const getCellProps = (cellProps, { cell: { column }, instance }) => {
88-
const { classes } = instance.webComponentsReactProperties;
89+
const { webComponentsReactProperties, state } = instance;
90+
const { classes, isFirefox } = webComponentsReactProperties;
8991
const style: CSSProperties = { width: `${column.totalWidth}px`, ...resolveCellAlignment(column) };
9092

91-
let className = classes.tableCell;
92-
if (column.className) {
93-
className += ` ${column.className}`;
94-
}
95-
9693
if (
9794
column.id === '__ui5wcr__internal_highlight_column' ||
9895
column.id === '__ui5wcr__internal_selection_column' ||
@@ -104,7 +101,12 @@ const getCellProps = (cellProps, { cell: { column }, instance }) => {
104101
return [
105102
cellProps,
106103
{
107-
className,
104+
className: clsx(
105+
cellProps.className,
106+
classes.tableCell,
107+
column.className,
108+
isFirefox && state.isScrollable && classes.firefoxCell,
109+
),
108110
style,
109111
tabIndex: -1,
110112
},

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

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import type { MutableRefObject } from 'react';
22
import { useEffect, useRef, useState } from 'react';
33

4-
export function useSyncScroll(refContent: MutableRefObject<HTMLElement>, refScrollbar: MutableRefObject<HTMLElement>) {
5-
const ticking = useRef(false);
4+
export function useSyncScroll(
5+
refContent: MutableRefObject<HTMLElement>,
6+
refScrollbar: MutableRefObject<HTMLElement>,
7+
disabled = false,
8+
) {
69
const isProgrammatic = useRef(false);
710
const [isMounted, setIsMounted] = useState(false);
811

912
useEffect(() => {
1013
const content = refContent.current;
1114
const scrollbar = refScrollbar.current;
1215

16+
if (disabled) {
17+
return;
18+
}
19+
1320
if (!content || !scrollbar || !isMounted) {
1421
setIsMounted(true);
1522
return;
@@ -18,24 +25,15 @@ export function useSyncScroll(refContent: MutableRefObject<HTMLElement>, refScro
1825
scrollbar.scrollTop = content.scrollTop;
1926

2027
const sync = (source: 'content' | 'scrollbar') => {
21-
if (ticking.current) {
22-
return;
28+
const sourceEl = source === 'content' ? content : scrollbar;
29+
const targetEl = source === 'content' ? scrollbar : content;
30+
31+
if (!isProgrammatic.current && targetEl.scrollTop !== sourceEl.scrollTop) {
32+
isProgrammatic.current = true;
33+
targetEl.scrollTop = sourceEl.scrollTop;
34+
// Clear the flag on next frame
35+
requestAnimationFrame(() => (isProgrammatic.current = false));
2336
}
24-
ticking.current = true;
25-
26-
requestAnimationFrame(() => {
27-
const sourceEl = source === 'content' ? content : scrollbar;
28-
const targetEl = source === 'content' ? scrollbar : content;
29-
30-
if (!isProgrammatic.current && targetEl.scrollTop !== sourceEl.scrollTop) {
31-
isProgrammatic.current = true;
32-
targetEl.scrollTop = sourceEl.scrollTop;
33-
// Clear the flag on next frame
34-
requestAnimationFrame(() => (isProgrammatic.current = false));
35-
}
36-
37-
ticking.current = false;
38-
});
3937
};
4038

4139
const onScrollContent = () => sync('content');

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
useStylesheet,
1111
useSyncRef,
1212
} from '@ui5/webcomponents-react-base';
13+
import { isFirefox as isFireFoxFn } from '@ui5/webcomponents-react-base/Device';
1314
import { clsx } from 'clsx';
1415
import type { CSSProperties, MutableRefObject } from 'react';
1516
import { forwardRef, useCallback, useEffect, useId, useMemo, useRef } from 'react';
@@ -99,6 +100,8 @@ import {
99100
} from './util/index.js';
100101
import { VerticalResizer } from './VerticalResizer.js';
101102

103+
const isFirefox = isFireFoxFn();
104+
102105
// When a sorted column is removed from the visible columns array (e.g. when "popped-in"), it doesn't clean up the sorted columns leading to an undefined `sortType`.
103106
const sortTypesFallback = {
104107
undefined: () => undefined,
@@ -256,6 +259,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
256259
classes: classNames,
257260
fontsReady,
258261
highlightField,
262+
isFirefox,
259263
isTreeTable,
260264
loading,
261265
markNavigatedRow,
@@ -658,7 +662,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
658662
}
659663
}, [tableState.columnResizing, retainColumnWidth, tableState.tableColResized]);
660664

661-
useSyncScroll(parentRef, verticalScrollBarRef);
665+
useSyncScroll(parentRef, verticalScrollBarRef, isFirefox);
662666

663667
useEffect(() => {
664668
columnVirtualizer.measure();
@@ -844,6 +848,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
844848
handleExternalScroll={onTableScroll}
845849
visibleRows={internalVisibleRowCount}
846850
isGrouped={isGrouped}
851+
isFirefox={isFirefox}
847852
>
848853
<VirtualTableBody
849854
scrollContainerRef={scrollContainerRef}
@@ -871,7 +876,7 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
871876
</VirtualTableBodyContainer>
872877
)}
873878
</div>
874-
{(additionalEmptyRowsCount || tableState.isScrollable) && (
879+
{!isFirefox && (additionalEmptyRowsCount || tableState.isScrollable) && (
875880
<VerticalScrollbar
876881
tableBodyHeight={tableBodyHeight}
877882
internalRowHeight={internalHeaderRowHeight}

packages/main/src/components/AnalyticalTable/scrollbars/VerticalScrollbar.tsx

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import { isChrome as isChromeFn } from '@ui5/webcomponents-react-base/Device';
2+
import { useSyncRef } from '@ui5/webcomponents-react-base/internal/hooks';
13
import { clsx } from 'clsx';
24
import type { MutableRefObject } from 'react';
3-
import { forwardRef } from 'react';
5+
import { forwardRef, useEffect, useRef } from 'react';
46
import { FlexBoxDirection } from '../../../enums/FlexBoxDirection.js';
57
import { FlexBox } from '../../FlexBox/index.js';
68
import type { ClassNames } from '../types/index.js';
@@ -13,10 +15,35 @@ interface VerticalScrollbarProps {
1315
classNames: ClassNames;
1416
}
1517

18+
const isChrome = isChromeFn();
19+
1620
export const VerticalScrollbar = forwardRef<HTMLDivElement, VerticalScrollbarProps>((props, ref) => {
1721
const { internalRowHeight, tableRef, tableBodyHeight, scrollContainerRef, classNames } = props;
1822
const hasHorizontalScrollbar = tableRef?.current?.offsetWidth !== tableRef?.current?.scrollWidth;
1923
const horizontalScrollbarSectionStyles = clsx(hasHorizontalScrollbar && classNames.bottomSection);
24+
const [componentRef, scrollbarRef] = useSyncRef<HTMLDivElement>(ref);
25+
const contentRef = useRef<HTMLDivElement>(null);
26+
27+
// Force style recalculation to fix Chrome scrollbar-color bug (track height not updating correctly)
28+
useEffect(() => {
29+
if (!isChrome) {
30+
return;
31+
}
32+
33+
if (scrollbarRef.current && contentRef.current) {
34+
const scrollbarElement = scrollbarRef.current;
35+
36+
const forceScrollbarUpdate = () => {
37+
const originalHeight = scrollbarElement.style.height;
38+
scrollbarElement.style.height = `${tableBodyHeight + 1}px`;
39+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
40+
scrollbarElement.offsetHeight; // Force reflow
41+
scrollbarElement.style.height = originalHeight ?? `${tableBodyHeight}px`;
42+
};
43+
44+
requestAnimationFrame(forceScrollbarUpdate);
45+
}
46+
}, [tableBodyHeight, scrollContainerRef.current?.scrollHeight, scrollbarRef]);
2047

2148
return (
2249
<FlexBox
@@ -31,7 +58,7 @@ export const VerticalScrollbar = forwardRef<HTMLDivElement, VerticalScrollbarPro
3158
className={classNames.headerSection}
3259
/>
3360
<div
34-
ref={ref}
61+
ref={componentRef}
3562
style={{
3663
height: tableRef.current ? `${tableBodyHeight}px` : '0',
3764
}}
@@ -40,10 +67,13 @@ export const VerticalScrollbar = forwardRef<HTMLDivElement, VerticalScrollbarPro
4067
tabIndex={-1}
4168
>
4269
<div
43-
className={classNames.verticalScroller}
44-
style={{
45-
height: `${scrollContainerRef.current?.scrollHeight}px`,
70+
ref={(node) => {
71+
contentRef.current = node;
72+
if (node && scrollContainerRef.current?.scrollHeight) {
73+
node.style.height = `${scrollContainerRef.current?.scrollHeight}px`;
74+
}
4675
}}
76+
className={classNames.verticalScroller}
4777
/>
4878
</div>
4979
<div className={horizontalScrollbarSectionStyles} />

0 commit comments

Comments
 (0)