Skip to content

Commit

Permalink
fix: scrollbar measure should consider scrollbar-color `::webkit-sc…
Browse files Browse the repository at this point in the history
…rollbar` mixing (#507)

* docs: update demo

* chore: fix style shaking

* test: update testcase

* chore: comment
  • Loading branch information
zombieJ authored Mar 6, 2024
1 parent 871c2c1 commit e96b0c6
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 89 deletions.
80 changes: 53 additions & 27 deletions docs/examples/getScrollBarSize.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,83 @@
import React from 'react';
import getScrollBarSize, {
getTargetScrollBarSize,
} from 'rc-util/es/getScrollBarSize';
import React from 'react';

const cssText = `
#customizeContainer::-webkit-scrollbar {
width: 2em;
height: 23px;
background: blue;
}
#customizeContainer::-webkit-scrollbar-thumb {
background: red;
height: 30px;
}
#scrollContainer {
scrollbar-color: red orange;
scrollbar-width: thin;
}
`;

export default () => {
const divRef = React.useRef<HTMLDivElement>();
const webkitRef = React.useRef<HTMLDivElement>();
const scrollRef = React.useRef<HTMLDivElement>();
const [sizeData, setSizeData] = React.useState('');

React.useEffect(() => {
const originSize = getScrollBarSize();
const targetSize = getTargetScrollBarSize(divRef.current);
const webkitSize = getTargetScrollBarSize(webkitRef.current);
const scrollSize = getTargetScrollBarSize(scrollRef.current);

setSizeData(
`Origin: ${originSize}, Target: ${targetSize.width}/${targetSize.height}`,
[
`Origin: ${originSize}`,
`Webkit: ${webkitSize.width}/${webkitSize.height}`,
`Webkit: ${scrollSize.width}/${scrollSize.height}`,
].join(', '),
);
}, []);

return (
<div>
<style
dangerouslySetInnerHTML={{
__html: `
#customizeContainer::-webkit-scrollbar {
width: 2em;
height: 23px;
background: blue;
}
#customizeContainer::-webkit-scrollbar-thumb {
background: red;
height: 30px;
}
`,
__html: cssText,
}}
/>
<div
style={{ width: 100, height: 100, overflow: 'auto' }}
id="customizeContainer"
ref={divRef}
>
<div style={{ width: '100vw', height: '100vh', background: 'green' }}>
Hello World!
</div>
</div>

<div
style={{
width: 100,
width: 300,
height: 100,
overflow: 'scroll',
background: 'yellow',
}}
/>
>
Origin
</div>

<div
style={{ width: 300, height: 100, overflow: 'auto' }}
id="customizeContainer"
ref={webkitRef}
>
<div style={{ width: '200vw', height: '200vh', background: 'yellow' }}>
Customize `-webkit-scrollbar`
</div>
</div>

<div
style={{ width: 300, height: 100, overflow: 'auto' }}
id="scrollContainer"
ref={scrollRef}
>
<div style={{ width: '200vw', height: '200vh', background: 'yellow' }}>
scrollbar-style
</div>
</div>

<pre>{sizeData}</pre>
</div>
Expand Down
122 changes: 82 additions & 40 deletions src/getScrollBarSize.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,104 @@
/* eslint-disable no-param-reassign */
import { removeCSS, updateCSS } from './Dom/dynamicCSS';

let cached: number;
type ScrollBarSize = { width: number; height: number };

export default function getScrollBarSize(fresh?: boolean) {
if (typeof document === 'undefined') {
return 0;
}
type ExtendCSSStyleDeclaration = CSSStyleDeclaration & {
scrollbarColor?: string;
scrollbarWidth?: string;
};

if (fresh || cached === undefined) {
const inner = document.createElement('div');
inner.style.width = '100%';
inner.style.height = '200px';
let cached: ScrollBarSize;

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

outerStyle.position = 'absolute';
outerStyle.top = '0';
outerStyle.left = '0';
outerStyle.pointerEvents = 'none';
outerStyle.visibility = 'hidden';
outerStyle.width = '200px';
outerStyle.height = '150px';
outerStyle.overflow = 'hidden';
// Create Style
const measureStyle: ExtendCSSStyleDeclaration = measureEle.style;
measureStyle.position = 'absolute';
measureStyle.left = '0';
measureStyle.top = '0';
measureStyle.width = '100px';
measureStyle.height = '100px';
measureStyle.overflow = 'scroll';

outer.appendChild(inner);
// Clone Style if needed
let fallbackWidth: number;
let fallbackHeight: number;
if (ele) {
const targetStyle: ExtendCSSStyleDeclaration = getComputedStyle(ele);
measureStyle.scrollbarColor = targetStyle.scrollbarColor;
measureStyle.scrollbarWidth = targetStyle.scrollbarWidth;

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

const widthContained = inner.offsetWidth;
outer.style.overflow = 'scroll';
let widthScroll = inner.offsetWidth;
// Try wrap to handle CSP case
try {
updateCSS(
`
#${randomId}::-webkit-scrollbar {
width: ${webkitScrollbarStyle.width};
height: ${webkitScrollbarStyle.height};
}
`,
randomId,
);
} catch (e) {
// Can't wrap, just log error
console.error(e);

if (widthContained === widthScroll) {
widthScroll = outer.clientWidth;
// Get from style directly
fallbackWidth = parseInt(webkitScrollbarStyle.width, 10);
fallbackHeight = parseInt(webkitScrollbarStyle.height, 10);
}
}

document.body.removeChild(outer);
document.body.appendChild(measureEle);

cached = widthContained - widthScroll;
}
return cached;
// Measure. Get fallback style if provided
const scrollWidth =
ele && fallbackWidth && !isNaN(fallbackWidth)
? fallbackWidth
: measureEle.offsetWidth - measureEle.clientWidth;
const scrollHeight =
ele && fallbackHeight && !isNaN(fallbackHeight)
? fallbackHeight
: measureEle.offsetHeight - measureEle.clientHeight;

// Clean up
document.body.removeChild(measureEle);
removeCSS(randomId);

return {
width: scrollWidth,
height: scrollHeight,
};
}

function ensureSize(str: string) {
const match = str.match(/^(.*)px$/);
const value = Number(match?.[1]);
return Number.isNaN(value) ? getScrollBarSize() : value;
export default function getScrollBarSize(fresh?: boolean): number {
if (typeof document === 'undefined') {
return 0;
}

if (fresh || cached === undefined) {
cached = measureScrollbarSize();
}
return cached.width;
}

export function getTargetScrollBarSize(target: HTMLElement) {
if (typeof document === 'undefined' || !target || !(target instanceof Element)) {
if (
typeof document === 'undefined' ||
!target ||
!(target instanceof Element)
) {
return { width: 0, height: 0 };
}

const { width, height } = getComputedStyle(target, '::-webkit-scrollbar');
return {
width: ensureSize(width),
height: ensureSize(height),
};
return measureScrollbarSize(target);
}
29 changes: 7 additions & 22 deletions tests/getScrollBarSize.test.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { spyElementPrototypes } from '../src/test/domHook';
import getScrollBarSize, {
getTargetScrollBarSize,
} from '../src/getScrollBarSize';
import { spyElementPrototypes } from '../src/test/domHook';

const DEFAULT_SIZE = 16;

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

beforeAll(() => {
let i = 0;

spyElementPrototypes(HTMLElement, {
offsetWidth: {
get: () => {
i += 1;
return i % 2 ? 100 : 100 - defaultSize;
return 100;
},
},
clientWidth: {
get: () => {
return 100 - defaultSize;
},
},
});
Expand All @@ -37,23 +39,6 @@ describe('getScrollBarSize', () => {
});

describe('getTargetScrollBarSize', () => {
it('validate', () => {
const getSpy = jest.spyOn(window, 'getComputedStyle').mockImplementation(
() =>
({
width: '23px',
height: '93px',
} as any),
);

expect(getTargetScrollBarSize(document.createElement('div'))).toEqual({
width: 23,
height: 93,
});

getSpy.mockRestore();
});

it('invalidate', () => {
expect(
getTargetScrollBarSize({ notValidateObject: true } as any),
Expand Down

0 comments on commit e96b0c6

Please sign in to comment.