Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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