@@ -12,6 +12,7 @@ import type { AppTheme } from "../../themes";
1212
1313const HIDE_DELAY_MS = 2000 ;
1414const SCROLLBAR_WIDTH = 1 ;
15+ const MIN_THUMB_HEIGHT = 2 ;
1516
1617export interface VerticalScrollbarHandle {
1718 show : ( ) => void ;
@@ -32,9 +33,10 @@ interface VerticalScrollbarProps {
3233export const VerticalScrollbar = forwardRef < VerticalScrollbarHandle , VerticalScrollbarProps > (
3334 function VerticalScrollbar ( { scrollRef, contentHeight, theme, height, onActivity } , ref ) {
3435 const [ isVisible , setIsVisible ] = useState ( false ) ;
35- const [ isDragging , setIsDragging ] = useState ( false ) ;
36- const [ dragStartY , setDragStartY ] = useState ( 0 ) ;
37- const [ dragStartScroll , setDragStartScroll ] = useState ( 0 ) ;
36+ const [ isDraggingState , setIsDraggingState ] = useState ( false ) ;
37+ const isDraggingRef = useRef ( false ) ;
38+ const dragStartYRef = useRef ( 0 ) ;
39+ const dragStartScrollRef = useRef ( 0 ) ;
3840 const hideTimeoutRef = useRef < ReturnType < typeof setTimeout > | null > ( null ) ;
3941
4042 const show = useCallback ( ( ) => {
@@ -43,12 +45,12 @@ export const VerticalScrollbar = forwardRef<VerticalScrollbarHandle, VerticalScr
4345 clearTimeout ( hideTimeoutRef . current ) ;
4446 }
4547 hideTimeoutRef . current = setTimeout ( ( ) => {
46- if ( ! isDragging ) {
48+ if ( ! isDraggingRef . current ) {
4749 setIsVisible ( false ) ;
4850 }
4951 } , HIDE_DELAY_MS ) ;
5052 onActivity ?.( ) ;
51- } , [ isDragging , onActivity ] ) ;
53+ } , [ onActivity ] ) ;
5254
5355 useImperativeHandle ( ref , ( ) => ( { show } ) , [ show ] ) ;
5456
@@ -67,7 +69,7 @@ export const VerticalScrollbar = forwardRef<VerticalScrollbarHandle, VerticalScr
6769 // Calculate thumb metrics
6870 const trackHeight = viewportHeight ;
6971 const scrollRatio = viewportHeight / contentHeight ;
70- const thumbHeight = Math . max ( SCROLLBAR_WIDTH , Math . floor ( trackHeight * scrollRatio ) ) ;
72+ const thumbHeight = Math . max ( MIN_THUMB_HEIGHT , Math . floor ( trackHeight * scrollRatio ) ) ;
7173 const maxThumbY = trackHeight - thumbHeight ;
7274
7375 const scrollTop = scrollRef . current ?. scrollTop ?? 0 ;
@@ -79,31 +81,59 @@ export const VerticalScrollbar = forwardRef<VerticalScrollbarHandle, VerticalScr
7981 if ( event . button !== 0 ) return ;
8082
8183 const currentScrollTop = scrollRef . current ?. scrollTop ?? 0 ;
82- setIsDragging ( true ) ;
83- setDragStartY ( event . y ) ;
84- setDragStartScroll ( currentScrollTop ) ;
84+ isDraggingRef . current = true ;
85+ setIsDraggingState ( true ) ;
86+ dragStartYRef . current = event . y ;
87+ dragStartScrollRef . current = currentScrollTop ;
8588 show ( ) ;
8689 event . preventDefault ( ) ;
8790 event . stopPropagation ( ) ;
8891 } ;
8992
9093 const handleMouseDrag = ( event : TuiMouseEvent ) => {
91- if ( ! isDragging ) return ;
94+ if ( ! isDraggingRef . current ) {
95+ return ;
96+ }
9297
93- const deltaY = event . y - dragStartY ;
98+ const deltaY = event . y - dragStartYRef . current ;
9499 const pixelsPerRow = maxThumbY / maxScroll ;
95100 const scrollDelta = deltaY / pixelsPerRow ;
96- const newScrollTop = Math . max ( 0 , Math . min ( maxScroll , dragStartScroll + scrollDelta ) ) ;
101+ const newScrollTop = Math . max (
102+ 0 ,
103+ Math . min ( maxScroll , dragStartScrollRef . current + scrollDelta ) ,
104+ ) ;
97105
98106 scrollRef . current ?. scrollTo ( newScrollTop ) ;
99107 show ( ) ;
100108 event . preventDefault ( ) ;
101109 event . stopPropagation ( ) ;
102110 } ;
103111
112+ const handleTrackClick = ( event : TuiMouseEvent ) => {
113+ if ( event . button !== 0 ) return ;
114+
115+ // Calculate where on the track was clicked
116+ const clickY = event . y ;
117+
118+ // If clicked above thumb, scroll up one viewport
119+ // If clicked below thumb, scroll down one viewport
120+ if ( clickY < thumbY ) {
121+ const newScrollTop = Math . max ( 0 , scrollTop - viewportHeight ) ;
122+ scrollRef . current ?. scrollTo ( newScrollTop ) ;
123+ } else if ( clickY >= thumbY + thumbHeight ) {
124+ const newScrollTop = Math . min ( maxScroll , scrollTop + viewportHeight ) ;
125+ scrollRef . current ?. scrollTo ( newScrollTop ) ;
126+ }
127+
128+ show ( ) ;
129+ event . preventDefault ( ) ;
130+ event . stopPropagation ( ) ;
131+ } ;
132+
104133 const handleMouseUp = ( event ?: TuiMouseEvent ) => {
105- if ( ! isDragging ) return ;
106- setIsDragging ( false ) ;
134+ if ( ! isDraggingRef . current ) return ;
135+ isDraggingRef . current = false ;
136+ setIsDraggingState ( false ) ;
107137 // Restart hide timer
108138 if ( hideTimeoutRef . current ) {
109139 clearTimeout ( hideTimeoutRef . current ) ;
@@ -128,7 +158,6 @@ export const VerticalScrollbar = forwardRef<VerticalScrollbarHandle, VerticalScr
128158 width : SCROLLBAR_WIDTH ,
129159 height : trackHeight ,
130160 backgroundColor : theme . panel ,
131- zIndex : 10 ,
132161 } }
133162 >
134163 { /* Track background */ }
@@ -141,6 +170,7 @@ export const VerticalScrollbar = forwardRef<VerticalScrollbarHandle, VerticalScr
141170 height : trackHeight ,
142171 backgroundColor : theme . border ,
143172 } }
173+ onMouseDown = { handleTrackClick }
144174 />
145175 { /* Thumb */ }
146176 < box
@@ -150,7 +180,7 @@ export const VerticalScrollbar = forwardRef<VerticalScrollbarHandle, VerticalScr
150180 left : 0 ,
151181 width : SCROLLBAR_WIDTH ,
152182 height : thumbHeight ,
153- backgroundColor : isDragging ? theme . accent : theme . accentMuted ,
183+ backgroundColor : isDraggingState ? theme . accent : theme . accentMuted ,
154184 } }
155185 onMouseDown = { handleMouseDown }
156186 onMouseDrag = { handleMouseDrag }
0 commit comments