Skip to content

Commit 3d6bdf8

Browse files
Lucifergenechristoph-jerolimov
authored andcommitted
custom scrollbar implementation
Signed-off-by: Lucifergene <[email protected]>
1 parent 6be99c9 commit 3d6bdf8

File tree

2 files changed

+443
-0
lines changed

2 files changed

+443
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { forwardRef } from 'react';
2+
3+
import Box from '@mui/material/Box';
4+
import { styled } from '@mui/material/styles';
5+
import type { SxProps, Theme } from '@mui/material/styles';
6+
7+
import { useScrollbar } from '../../hooks/useScrollbar';
8+
9+
interface ScrollableContainerProps {
10+
children: React.ReactNode;
11+
className?: string;
12+
sx?: SxProps<Theme>;
13+
}
14+
15+
const ScrollContainer = styled(Box)(({ theme }) => ({
16+
position: 'relative',
17+
flex: 1,
18+
minHeight: 0,
19+
overflow: 'visible',
20+
'&:focus': {
21+
outline: `2px solid ${theme.palette.primary.main}`,
22+
outlineOffset: '-2px',
23+
},
24+
}));
25+
26+
const ScrollContent = styled(Box)(() => ({
27+
height: '100%',
28+
overflow: 'hidden auto',
29+
paddingRight: '10px',
30+
marginRight: '0px',
31+
32+
scrollbarWidth: 'none',
33+
msOverflowStyle: 'none',
34+
'&::-webkit-scrollbar': {
35+
display: 'none',
36+
},
37+
38+
scrollBehavior: 'smooth',
39+
transform: 'translateZ(0)',
40+
WebkitOverflowScrolling: 'touch',
41+
overscrollBehavior: 'contain',
42+
}));
43+
44+
const ScrollbarTrack = styled(Box)<{ visible: boolean }>(({ visible }) => ({
45+
position: 'absolute',
46+
top: 0,
47+
right: '2px',
48+
width: '6px',
49+
height: '100%',
50+
backgroundColor: 'transparent',
51+
opacity: visible ? 1 : 0,
52+
transition: 'opacity 0.2s ease-in-out',
53+
zIndex: 1,
54+
pointerEvents: visible ? 'auto' : 'none',
55+
56+
'@media (prefers-reduced-motion: reduce)': {
57+
transition: 'none',
58+
},
59+
}));
60+
61+
const ScrollbarThumb = styled(Box)<{
62+
height: number;
63+
top: number;
64+
isDragging: boolean;
65+
}>(({ theme, height, top, isDragging }) => ({
66+
position: 'absolute',
67+
left: 0,
68+
top: `${top}px`,
69+
width: '100%',
70+
height: `${height}px`,
71+
backgroundColor:
72+
theme.palette.mode === 'dark'
73+
? theme.palette.grey[600]
74+
: theme.palette.grey[400],
75+
borderRadius: '3px',
76+
cursor: isDragging ? 'grabbing' : 'grab',
77+
transition: isDragging
78+
? 'background-color 0.15s ease'
79+
: 'top 0.1s ease-out, height 0.1s ease-out, background-color 0.15s ease',
80+
transform: 'translateZ(0)',
81+
willChange: isDragging ? 'top, height' : 'auto',
82+
83+
'&:hover': {
84+
backgroundColor:
85+
theme.palette.mode === 'dark'
86+
? theme.palette.grey[500]
87+
: theme.palette.grey[600],
88+
},
89+
90+
'&:active': {
91+
backgroundColor:
92+
theme.palette.mode === 'dark'
93+
? theme.palette.grey[400]
94+
: theme.palette.grey[700],
95+
},
96+
97+
'@media (prefers-reduced-motion: reduce)': {
98+
transition: 'background-color 0.15s ease',
99+
},
100+
}));
101+
102+
export const ScrollableContainer = forwardRef<
103+
HTMLDivElement,
104+
ScrollableContainerProps
105+
>(({ children, className, sx }, ref) => {
106+
const {
107+
scrollbarState,
108+
containerRef,
109+
trackRef,
110+
thumbRef,
111+
handleMouseEnter,
112+
handleMouseLeave,
113+
handleTrackClick,
114+
handleThumbMouseDown,
115+
handleKeyDown,
116+
} = useScrollbar();
117+
118+
return (
119+
<ScrollContainer
120+
ref={ref}
121+
className={className}
122+
sx={sx}
123+
onMouseEnter={handleMouseEnter}
124+
onMouseLeave={handleMouseLeave}
125+
onKeyDown={handleKeyDown}
126+
tabIndex={0}
127+
role="region"
128+
aria-label="Scrollable sidebar content"
129+
>
130+
<ScrollContent ref={containerRef}>{children}</ScrollContent>
131+
132+
{scrollbarState.isScrollable && (
133+
<ScrollbarTrack
134+
ref={trackRef}
135+
visible={scrollbarState.isVisible}
136+
onMouseDown={handleTrackClick}
137+
role="scrollbar"
138+
aria-orientation="vertical"
139+
aria-valuenow={Math.round(
140+
(scrollbarState.scrollTop /
141+
(scrollbarState.scrollHeight - scrollbarState.clientHeight)) *
142+
100,
143+
)}
144+
aria-valuemin={0}
145+
aria-valuemax={100}
146+
aria-label="Vertical scrollbar"
147+
>
148+
<ScrollbarThumb
149+
ref={thumbRef}
150+
height={scrollbarState.thumbHeight}
151+
top={scrollbarState.thumbTop}
152+
isDragging={scrollbarState.isDragging}
153+
onMouseDown={handleThumbMouseDown}
154+
role="button"
155+
aria-label="Scrollbar thumb"
156+
tabIndex={-1}
157+
/>
158+
</ScrollbarTrack>
159+
)}
160+
</ScrollContainer>
161+
);
162+
});
163+
164+
ScrollableContainer.displayName = 'ScrollableContainer';

0 commit comments

Comments
 (0)