1
- import { RefObject , useCallback , useEffect , useState } from "react" ;
1
+ import { RefObject , useCallback , useEffect , useState , useRef } from "react" ;
2
2
import { useWindowSize } from "react-use" ;
3
3
import { FixedSizeList } from "react-window" ;
4
4
import { useTableDensity } from "@common/features/data/lib/useTableDensity" ;
5
5
6
- const MIN_SCROLLBAR_SIZE = 12 ;
6
+ const MIN_SCROLLBAR_SIZE = 64 ;
7
7
8
8
// The handlers returned by this hook were mostly copied/inspired by:
9
9
// https://www.thisdot.co/blog/creating-custom-scrollbars-with-react
@@ -31,6 +31,8 @@ function useScrollbar(
31
31
) ;
32
32
const [ initialScrollTop , setInitialScrollTop ] = useState < number > ( 0 ) ;
33
33
const [ isDragging , setIsDragging ] = useState ( false ) ;
34
+ const rafIdRef = useRef < number > ( 0 ) ;
35
+ const lastMouseYRef = useRef < number | null > ( null ) ;
34
36
35
37
const handleTrackClick = useCallback (
36
38
( e : React . MouseEvent ) => {
@@ -68,39 +70,75 @@ function useScrollbar(
68
70
const handleThumbMouseup = useCallback ( ( ) => {
69
71
if ( isDragging ) {
70
72
setIsDragging ( false ) ;
73
+
74
+ // Cancel any pending animation frame
75
+ if ( rafIdRef . current ) {
76
+ cancelAnimationFrame ( rafIdRef . current ) ;
77
+ rafIdRef . current = 0 ;
78
+ }
71
79
}
72
80
} , [ isDragging ] ) ;
73
81
82
+ const updateScrollPosition = useCallback ( ( ) => {
83
+ if (
84
+ isDragging &&
85
+ outerEl &&
86
+ listEl &&
87
+ scrollStartPosition &&
88
+ lastMouseYRef . current !== null
89
+ ) {
90
+ const {
91
+ scrollHeight : contentScrollHeight ,
92
+ offsetHeight : contentOffsetHeight ,
93
+ } = outerEl ;
94
+
95
+ // Subtract the current mouse y position from where you started to get the pixel difference
96
+ const deltaY =
97
+ ( lastMouseYRef . current - scrollStartPosition ) *
98
+ ( contentOffsetHeight / scrollbarHeight ) ;
99
+
100
+ const newScrollTop = Math . max (
101
+ 0 ,
102
+ Math . min (
103
+ initialScrollTop + deltaY ,
104
+ contentScrollHeight - contentOffsetHeight ,
105
+ ) ,
106
+ ) ;
107
+
108
+ // Apply the scroll
109
+ listEl ?. scrollTo ( newScrollTop ) ;
110
+
111
+ // Continue animation loop
112
+ rafIdRef . current = requestAnimationFrame ( updateScrollPosition ) ;
113
+ }
114
+ } , [
115
+ isDragging ,
116
+ scrollStartPosition ,
117
+ scrollbarHeight ,
118
+ outerEl ,
119
+ listEl ,
120
+ initialScrollTop ,
121
+ ] ) ;
122
+
74
123
const handleThumbMousemove = useCallback (
75
124
( e : MouseEvent ) => {
76
- if ( isDragging && outerEl && listEl && scrollStartPosition ) {
125
+ if ( isDragging ) {
77
126
e . preventDefault ( ) ;
78
127
e . stopPropagation ( ) ;
79
- const {
80
- scrollHeight : contentScrollHeight ,
81
- offsetHeight : contentOffsetHeight ,
82
- } = outerEl ;
83
-
84
- // Subtract the current mouse y position from where you started to get the pixel difference in mouse position. Multiply by ratio of visible content height to thumb height to scale up the difference for content scrolling.
85
- const deltaY =
86
- ( e . clientY - scrollStartPosition ) *
87
- ( contentOffsetHeight / scrollbarHeight ) ;
88
- const newScrollTop = Math . min (
89
- initialScrollTop + deltaY ,
90
- contentScrollHeight - contentOffsetHeight ,
91
- ) ;
92
128
93
- listEl ?. scrollTo ( newScrollTop ) ;
129
+ // Store mouse position for animation frame
130
+ lastMouseYRef . current = e . clientY ;
131
+
132
+ // Start animation frame if not already running
133
+ if ( ! rafIdRef . current ) {
134
+ rafIdRef . current = requestAnimationFrame ( updateScrollPosition ) ;
135
+ }
136
+
137
+ // Add user-select: none to body during drag
138
+ document . body . style . userSelect = "none" ;
94
139
}
95
140
} ,
96
- [
97
- isDragging ,
98
- scrollStartPosition ,
99
- scrollbarHeight ,
100
- outerEl ,
101
- listEl ,
102
- initialScrollTop ,
103
- ] ,
141
+ [ isDragging , updateScrollPosition ] ,
104
142
) ;
105
143
106
144
// Listen for mouse events to handle scrolling by dragging the thumb
@@ -112,6 +150,14 @@ function useScrollbar(
112
150
document . removeEventListener ( "mousemove" , handleThumbMousemove ) ;
113
151
document . removeEventListener ( "mouseup" , handleThumbMouseup ) ;
114
152
document . removeEventListener ( "mouseleave" , handleThumbMouseup ) ;
153
+
154
+ // Reset user-select
155
+ document . body . style . userSelect = "" ;
156
+
157
+ // Clean up any ongoing animation frame
158
+ if ( rafIdRef . current ) {
159
+ cancelAnimationFrame ( rafIdRef . current ) ;
160
+ }
115
161
} ;
116
162
} , [ handleThumbMousemove , handleThumbMouseup ] ) ;
117
163
@@ -126,11 +172,13 @@ function useScrollbar(
126
172
( outerEl . scrollTop / totalRowHeight ) * outerEl . offsetHeight ,
127
173
0 ,
128
174
) ,
129
- outerEl . offsetHeight - scrollbarHeight ,
175
+ // Subtract an extra pixel to prevent scrollbar from going too far down
176
+ outerEl . offsetHeight - scrollbarHeight - 6 ,
130
177
)
131
178
: 0 ,
132
179
handleTrackClick,
133
180
handleThumbMousedown,
181
+ isDragging,
134
182
} ;
135
183
}
136
184
@@ -148,6 +196,7 @@ export function TableScrollbar({
148
196
scrollbarTop,
149
197
handleTrackClick,
150
198
handleThumbMousedown,
199
+ isDragging,
151
200
} = useScrollbar ( totalRowCount || 0 , outerRef , listRef ) ;
152
201
153
202
// Create a React handler from a native event handler, just for the types.
@@ -157,9 +206,9 @@ export function TableScrollbar({
157
206
) ;
158
207
159
208
const { densityValues } = useTableDensity ( ) ;
160
- return scrollbarHeight >= 0 ? (
209
+ return scrollbarHeight > 0 ? (
161
210
< div
162
- className = "absolute right-0 w-2 "
211
+ className = "absolute - right-px -mt-0.5 w-3 border-l border-t bg-macosScrollbar-track/75 py-0.5 "
163
212
role = "scrollbar"
164
213
aria-controls = "dataTable"
165
214
aria-valuenow = { scrollbarTop }
@@ -171,7 +220,7 @@ export function TableScrollbar({
171
220
{ /* eslint-disable */ }
172
221
{ /* I have no clue how to properly do a11y for this scrollbar,
173
222
but it seems to work well for scrollbars */ }
174
- < div onClick = { handleTrackClick } className = "fixed h-full w-2" />
223
+ < div onClick = { handleTrackClick } className = "fixed h-full w-2.5 " />
175
224
< div
176
225
style = { {
177
226
height :
@@ -185,11 +234,12 @@ export function TableScrollbar({
185
234
scrollbarHeight < MIN_SCROLLBAR_SIZE
186
235
? // If the scrollbar is using the minimum size, add some margin
187
236
// to the top so it snaps to the top and bottom of the table.
188
- Math . max ( scrollbarTop - ( MIN_SCROLLBAR_SIZE + 1 ) , 0 )
237
+ Math . max ( scrollbarTop - MIN_SCROLLBAR_SIZE , 0 )
189
238
: scrollbarTop ,
239
+ cursor : isDragging ? "grabbing" : "grab" ,
190
240
} }
191
241
onMouseDown = { handleReactThumbMousedown }
192
- className = { ` fixed w-2 bg-neutral-1 dark :bg-neutral-8` }
242
+ className = " fixed w-1.5 rounded-full transition-colors bg-macosScrollbar-thumb hover :bg-macosScrollbar-thumbHover ml-0.5"
193
243
/>
194
244
{ /* eslint-enable */ }
195
245
</ div >
0 commit comments