Skip to content

Commit e96b0c6

Browse files
authored
fix: scrollbar measure should consider scrollbar-color ::webkit-scrollbar mixing (#507)
* docs: update demo * chore: fix style shaking * test: update testcase * chore: comment
1 parent 871c2c1 commit e96b0c6

File tree

3 files changed

+142
-89
lines changed

3 files changed

+142
-89
lines changed

docs/examples/getScrollBarSize.tsx

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,83 @@
1-
import React from 'react';
21
import getScrollBarSize, {
32
getTargetScrollBarSize,
43
} from 'rc-util/es/getScrollBarSize';
4+
import React from 'react';
5+
6+
const cssText = `
7+
#customizeContainer::-webkit-scrollbar {
8+
width: 2em;
9+
height: 23px;
10+
background: blue;
11+
}
12+
13+
#customizeContainer::-webkit-scrollbar-thumb {
14+
background: red;
15+
height: 30px;
16+
}
17+
18+
#scrollContainer {
19+
scrollbar-color: red orange;
20+
scrollbar-width: thin;
21+
}
22+
`;
523

624
export default () => {
7-
const divRef = React.useRef<HTMLDivElement>();
25+
const webkitRef = React.useRef<HTMLDivElement>();
26+
const scrollRef = React.useRef<HTMLDivElement>();
827
const [sizeData, setSizeData] = React.useState('');
928

1029
React.useEffect(() => {
1130
const originSize = getScrollBarSize();
12-
const targetSize = getTargetScrollBarSize(divRef.current);
31+
const webkitSize = getTargetScrollBarSize(webkitRef.current);
32+
const scrollSize = getTargetScrollBarSize(scrollRef.current);
1333

1434
setSizeData(
15-
`Origin: ${originSize}, Target: ${targetSize.width}/${targetSize.height}`,
35+
[
36+
`Origin: ${originSize}`,
37+
`Webkit: ${webkitSize.width}/${webkitSize.height}`,
38+
`Webkit: ${scrollSize.width}/${scrollSize.height}`,
39+
].join(', '),
1640
);
1741
}, []);
1842

1943
return (
2044
<div>
2145
<style
2246
dangerouslySetInnerHTML={{
23-
__html: `
24-
#customizeContainer::-webkit-scrollbar {
25-
width: 2em;
26-
height: 23px;
27-
background: blue;
28-
}
29-
30-
#customizeContainer::-webkit-scrollbar-thumb {
31-
background: red;
32-
height: 30px;
33-
}
34-
`,
47+
__html: cssText,
3548
}}
3649
/>
37-
<div
38-
style={{ width: 100, height: 100, overflow: 'auto' }}
39-
id="customizeContainer"
40-
ref={divRef}
41-
>
42-
<div style={{ width: '100vw', height: '100vh', background: 'green' }}>
43-
Hello World!
44-
</div>
45-
</div>
4650

4751
<div
4852
style={{
49-
width: 100,
53+
width: 300,
5054
height: 100,
5155
overflow: 'scroll',
5256
background: 'yellow',
5357
}}
54-
/>
58+
>
59+
Origin
60+
</div>
61+
62+
<div
63+
style={{ width: 300, height: 100, overflow: 'auto' }}
64+
id="customizeContainer"
65+
ref={webkitRef}
66+
>
67+
<div style={{ width: '200vw', height: '200vh', background: 'yellow' }}>
68+
Customize `-webkit-scrollbar`
69+
</div>
70+
</div>
71+
72+
<div
73+
style={{ width: 300, height: 100, overflow: 'auto' }}
74+
id="scrollContainer"
75+
ref={scrollRef}
76+
>
77+
<div style={{ width: '200vw', height: '200vh', background: 'yellow' }}>
78+
scrollbar-style
79+
</div>
80+
</div>
5581

5682
<pre>{sizeData}</pre>
5783
</div>

src/getScrollBarSize.tsx

Lines changed: 82 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,104 @@
11
/* eslint-disable no-param-reassign */
2+
import { removeCSS, updateCSS } from './Dom/dynamicCSS';
23

3-
let cached: number;
4+
type ScrollBarSize = { width: number; height: number };
45

5-
export default function getScrollBarSize(fresh?: boolean) {
6-
if (typeof document === 'undefined') {
7-
return 0;
8-
}
6+
type ExtendCSSStyleDeclaration = CSSStyleDeclaration & {
7+
scrollbarColor?: string;
8+
scrollbarWidth?: string;
9+
};
910

10-
if (fresh || cached === undefined) {
11-
const inner = document.createElement('div');
12-
inner.style.width = '100%';
13-
inner.style.height = '200px';
11+
let cached: ScrollBarSize;
1412

15-
const outer = document.createElement('div');
16-
const outerStyle = outer.style;
13+
function measureScrollbarSize(ele?: HTMLElement): ScrollBarSize {
14+
const randomId = `rc-scrollbar-measure-${Math.random()
15+
.toString(36)
16+
.substring(7)}`;
17+
const measureEle = document.createElement('div');
18+
measureEle.id = randomId;
1719

18-
outerStyle.position = 'absolute';
19-
outerStyle.top = '0';
20-
outerStyle.left = '0';
21-
outerStyle.pointerEvents = 'none';
22-
outerStyle.visibility = 'hidden';
23-
outerStyle.width = '200px';
24-
outerStyle.height = '150px';
25-
outerStyle.overflow = 'hidden';
20+
// Create Style
21+
const measureStyle: ExtendCSSStyleDeclaration = measureEle.style;
22+
measureStyle.position = 'absolute';
23+
measureStyle.left = '0';
24+
measureStyle.top = '0';
25+
measureStyle.width = '100px';
26+
measureStyle.height = '100px';
27+
measureStyle.overflow = 'scroll';
2628

27-
outer.appendChild(inner);
29+
// Clone Style if needed
30+
let fallbackWidth: number;
31+
let fallbackHeight: number;
32+
if (ele) {
33+
const targetStyle: ExtendCSSStyleDeclaration = getComputedStyle(ele);
34+
measureStyle.scrollbarColor = targetStyle.scrollbarColor;
35+
measureStyle.scrollbarWidth = targetStyle.scrollbarWidth;
2836

29-
document.body.appendChild(outer);
37+
// Set Webkit style
38+
const webkitScrollbarStyle = getComputedStyle(ele, '::-webkit-scrollbar');
3039

31-
const widthContained = inner.offsetWidth;
32-
outer.style.overflow = 'scroll';
33-
let widthScroll = inner.offsetWidth;
40+
// Try wrap to handle CSP case
41+
try {
42+
updateCSS(
43+
`
44+
#${randomId}::-webkit-scrollbar {
45+
width: ${webkitScrollbarStyle.width};
46+
height: ${webkitScrollbarStyle.height};
47+
}
48+
`,
49+
randomId,
50+
);
51+
} catch (e) {
52+
// Can't wrap, just log error
53+
console.error(e);
3454

35-
if (widthContained === widthScroll) {
36-
widthScroll = outer.clientWidth;
55+
// Get from style directly
56+
fallbackWidth = parseInt(webkitScrollbarStyle.width, 10);
57+
fallbackHeight = parseInt(webkitScrollbarStyle.height, 10);
3758
}
59+
}
3860

39-
document.body.removeChild(outer);
61+
document.body.appendChild(measureEle);
4062

41-
cached = widthContained - widthScroll;
42-
}
43-
return cached;
63+
// Measure. Get fallback style if provided
64+
const scrollWidth =
65+
ele && fallbackWidth && !isNaN(fallbackWidth)
66+
? fallbackWidth
67+
: measureEle.offsetWidth - measureEle.clientWidth;
68+
const scrollHeight =
69+
ele && fallbackHeight && !isNaN(fallbackHeight)
70+
? fallbackHeight
71+
: measureEle.offsetHeight - measureEle.clientHeight;
72+
73+
// Clean up
74+
document.body.removeChild(measureEle);
75+
removeCSS(randomId);
76+
77+
return {
78+
width: scrollWidth,
79+
height: scrollHeight,
80+
};
4481
}
4582

46-
function ensureSize(str: string) {
47-
const match = str.match(/^(.*)px$/);
48-
const value = Number(match?.[1]);
49-
return Number.isNaN(value) ? getScrollBarSize() : value;
83+
export default function getScrollBarSize(fresh?: boolean): number {
84+
if (typeof document === 'undefined') {
85+
return 0;
86+
}
87+
88+
if (fresh || cached === undefined) {
89+
cached = measureScrollbarSize();
90+
}
91+
return cached.width;
5092
}
5193

5294
export function getTargetScrollBarSize(target: HTMLElement) {
53-
if (typeof document === 'undefined' || !target || !(target instanceof Element)) {
95+
if (
96+
typeof document === 'undefined' ||
97+
!target ||
98+
!(target instanceof Element)
99+
) {
54100
return { width: 0, height: 0 };
55101
}
56102

57-
const { width, height } = getComputedStyle(target, '::-webkit-scrollbar');
58-
return {
59-
width: ensureSize(width),
60-
height: ensureSize(height),
61-
};
103+
return measureScrollbarSize(target);
62104
}

tests/getScrollBarSize.test.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
1-
import { spyElementPrototypes } from '../src/test/domHook';
21
import getScrollBarSize, {
32
getTargetScrollBarSize,
43
} from '../src/getScrollBarSize';
4+
import { spyElementPrototypes } from '../src/test/domHook';
55

66
const DEFAULT_SIZE = 16;
77

88
describe('getScrollBarSize', () => {
99
let defaultSize = DEFAULT_SIZE;
1010

1111
beforeAll(() => {
12-
let i = 0;
13-
1412
spyElementPrototypes(HTMLElement, {
1513
offsetWidth: {
1614
get: () => {
17-
i += 1;
18-
return i % 2 ? 100 : 100 - defaultSize;
15+
return 100;
16+
},
17+
},
18+
clientWidth: {
19+
get: () => {
20+
return 100 - defaultSize;
1921
},
2022
},
2123
});
@@ -37,23 +39,6 @@ describe('getScrollBarSize', () => {
3739
});
3840

3941
describe('getTargetScrollBarSize', () => {
40-
it('validate', () => {
41-
const getSpy = jest.spyOn(window, 'getComputedStyle').mockImplementation(
42-
() =>
43-
({
44-
width: '23px',
45-
height: '93px',
46-
} as any),
47-
);
48-
49-
expect(getTargetScrollBarSize(document.createElement('div'))).toEqual({
50-
width: 23,
51-
height: 93,
52-
});
53-
54-
getSpy.mockRestore();
55-
});
56-
5742
it('invalidate', () => {
5843
expect(
5944
getTargetScrollBarSize({ notValidateObject: true } as any),

0 commit comments

Comments
 (0)