Skip to content

Commit 40c4a16

Browse files
authored
feat(ScrollBar): ✨ support dark mode (#312)
1 parent bf79bff commit 40c4a16

File tree

4 files changed

+137
-2
lines changed

4 files changed

+137
-2
lines changed

jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module.exports = {
2+
setupFiles: ['./tests/setup.js'],
3+
snapshotSerializers: [require.resolve('enzyme-to-json/serializer')],
4+
};

src/ScrollBar.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
4545
const [dragging, setDragging] = React.useState(false);
4646
const [pageXY, setPageXY] = React.useState<number | null>(null);
4747
const [startTop, setStartTop] = React.useState<number | null>(null);
48+
const [dark, setDark] = React.useState(false);
4849

4950
const isLTR = !rtl;
5051

@@ -187,6 +188,21 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
187188
}
188189
}, [dragging]);
189190

191+
React.useEffect(() => {
192+
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
193+
setDark(media.matches);
194+
195+
const listener = (e: MediaQueryListEvent) => {
196+
setDark(e.matches);
197+
};
198+
199+
media?.addEventListener('change', listener);
200+
201+
return () => {
202+
media?.removeEventListener('change', listener);
203+
};
204+
}, []);
205+
190206
React.useEffect(() => {
191207
delayHidden();
192208
return () => {
@@ -209,10 +225,11 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
209225

210226
const thumbStyle: React.CSSProperties = {
211227
position: 'absolute',
212-
background: 'rgba(0, 0, 0, 0.5)',
228+
background: dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.5)',
213229
borderRadius: 99,
214230
cursor: 'pointer',
215231
userSelect: 'none',
232+
...propsThumbStyle,
216233
};
217234

218235
if (horizontal) {
@@ -266,7 +283,7 @@ const ScrollBar = React.forwardRef<ScrollBarRef, ScrollBarProps>((props, ref) =>
266283
className={classNames(`${scrollbarPrefixCls}-thumb`, {
267284
[`${scrollbarPrefixCls}-thumb-moving`]: dragging,
268285
})}
269-
style={{ ...thumbStyle, ...propsThumbStyle }}
286+
style={{ ...thumbStyle }}
270287
onMouseDown={onThumbMouseDown}
271288
/>
272289
</div>

tests/dark.test.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React from 'react';
2+
import { mount } from 'enzyme';
3+
import { act } from 'react-dom/test-utils';
4+
import List from '../src';
5+
6+
describe('List.dark', () => {
7+
const originalMatchMedia = window.matchMedia;
8+
9+
afterEach(() => {
10+
window.matchMedia = originalMatchMedia;
11+
});
12+
13+
const mockMatchMedia = (matches) => {
14+
Object.defineProperty(window, 'matchMedia', {
15+
writable: true,
16+
value: jest.fn().mockImplementation(query => ({
17+
matches,
18+
media: query,
19+
onchange: null,
20+
addListener: jest.fn(), // deprecated
21+
removeListener: jest.fn(), // deprecated
22+
addEventListener: jest.fn(),
23+
removeEventListener: jest.fn(),
24+
dispatchEvent: jest.fn(),
25+
})),
26+
});
27+
};
28+
29+
it('should render dark scrollbar', () => {
30+
mockMatchMedia(true);
31+
32+
const wrapper = mount(
33+
<List data={[1, 2, 3]} height={10} itemHeight={5} itemKey={(item) => item}>
34+
{(item) => <div>{item}</div>}
35+
</List>
36+
);
37+
38+
const thumb = wrapper.find('.rc-virtual-list-scrollbar-thumb');
39+
expect(thumb.props().style.background).toBe('rgba(255, 255, 255, 0.5)');
40+
});
41+
42+
it('should render light scrollbar', () => {
43+
mockMatchMedia(false);
44+
45+
const wrapper = mount(
46+
<List data={[1, 2, 3]} height={10} itemHeight={5} itemKey={(item) => item}>
47+
{(item) => <div>{item}</div>}
48+
</List>
49+
);
50+
51+
const thumb = wrapper.find('.rc-virtual-list-scrollbar-thumb');
52+
expect(thumb.props().style.background).toBe('rgba(0, 0, 0, 0.5)');
53+
});
54+
55+
it('should update on theme change', () => {
56+
let listener;
57+
Object.defineProperty(window, 'matchMedia', {
58+
writable: true,
59+
value: jest.fn().mockImplementation(() => ({
60+
matches: false,
61+
addEventListener: (type, cb) => {
62+
if (type === 'change') {
63+
listener = cb;
64+
}
65+
},
66+
removeEventListener: () => {},
67+
})),
68+
});
69+
70+
const wrapper = mount(
71+
<List data={[1, 2, 3]} height={10} itemHeight={5} itemKey={(item) => item}>
72+
{(item) => <div>{item}</div>}
73+
</List>
74+
);
75+
76+
// Initial: light
77+
expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.background).toBe(
78+
'rgba(0, 0, 0, 0.5)',
79+
);
80+
81+
// Change to dark
82+
act(() => {
83+
listener({ matches: true });
84+
});
85+
wrapper.update();
86+
87+
expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.background).toBe(
88+
'rgba(255, 255, 255, 0.5)',
89+
);
90+
91+
// Change back to light
92+
act(() => {
93+
listener({ matches: false });
94+
});
95+
wrapper.update();
96+
97+
expect(wrapper.find('.rc-virtual-list-scrollbar-thumb').props().style.background).toBe(
98+
'rgba(0, 0, 0, 0.5)',
99+
);
100+
});
101+
});

tests/setup.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Object.defineProperty(window, 'matchMedia', {
2+
writable: true,
3+
value: jest.fn().mockImplementation(query => ({
4+
matches: false,
5+
media: query,
6+
onchange: null,
7+
addListener: jest.fn(), // deprecated
8+
removeListener: jest.fn(), // deprecated
9+
addEventListener: jest.fn(),
10+
removeEventListener: jest.fn(),
11+
dispatchEvent: jest.fn(),
12+
})),
13+
});

0 commit comments

Comments
 (0)