1
1
'use client' ;
2
2
import * as React from 'react' ;
3
- import type { BaseUIComponentProps } from '../../utils/types' ;
4
- import { useComponentRenderer } from '../../utils/useComponentRenderer' ;
3
+ import type { BaseUIComponentProps , GenericHTMLProps } from '../../utils/types' ;
5
4
import { ScrollAreaRootContext } from './ScrollAreaRootContext' ;
6
- import { useScrollAreaRoot } from './useScrollAreaRoot' ;
5
+ import { useRenderElement } from '../../utils/useRenderElement' ;
6
+ import { ScrollAreaRootCssVars } from './ScrollAreaRootCssVars' ;
7
+ import { useEventCallback } from '../../utils/useEventCallback' ;
8
+ import { SCROLL_TIMEOUT } from '../constants' ;
9
+ import { getOffset } from '../utils/getOffset' ;
10
+ import { ScrollAreaScrollbarDataAttributes } from '../scrollbar/ScrollAreaScrollbarDataAttributes' ;
11
+ import { useBaseUiId } from '../../utils/useBaseUiId' ;
12
+ import { useTimeout } from '../../utils/useTimeout' ;
7
13
8
- const state = { } ;
14
+ interface Size {
15
+ width : number ;
16
+ height : number ;
17
+ }
9
18
10
19
/**
11
20
* Groups all parts of the scroll area.
@@ -14,25 +23,239 @@ const state = {};
14
23
* Documentation: [Base UI Scroll Area](https://base-ui.com/react/components/scroll-area)
15
24
*/
16
25
export const ScrollAreaRoot = React . forwardRef ( function ScrollAreaRoot (
17
- props : ScrollAreaRoot . Props ,
26
+ componentProps : ScrollAreaRoot . Props ,
18
27
forwardedRef : React . ForwardedRef < HTMLDivElement > ,
19
28
) {
20
- const { render, className, ...otherProps } = props ;
29
+ const { render, className, ...elementProps } = componentProps ;
30
+
31
+ const [ hovering , setHovering ] = React . useState ( false ) ;
32
+ const [ scrollingX , setScrollingX ] = React . useState ( false ) ;
33
+ const [ scrollingY , setScrollingY ] = React . useState ( false ) ;
34
+ const [ cornerSize , setCornerSize ] = React . useState < Size > ( { width : 0 , height : 0 } ) ;
35
+ const [ thumbSize , setThumbSize ] = React . useState < Size > ( { width : 0 , height : 0 } ) ;
36
+ const [ touchModality , setTouchModality ] = React . useState ( false ) ;
37
+
38
+ const rootId = useBaseUiId ( ) ;
39
+
40
+ const viewportRef = React . useRef < HTMLDivElement | null > ( null ) ;
41
+ const scrollbarYRef = React . useRef < HTMLDivElement | null > ( null ) ;
42
+ const scrollbarXRef = React . useRef < HTMLDivElement | null > ( null ) ;
43
+ const thumbYRef = React . useRef < HTMLDivElement | null > ( null ) ;
44
+ const thumbXRef = React . useRef < HTMLDivElement | null > ( null ) ;
45
+ const cornerRef = React . useRef < HTMLDivElement | null > ( null ) ;
46
+
47
+ const thumbDraggingRef = React . useRef ( false ) ;
48
+ const startYRef = React . useRef ( 0 ) ;
49
+ const startXRef = React . useRef ( 0 ) ;
50
+ const startScrollTopRef = React . useRef ( 0 ) ;
51
+ const startScrollLeftRef = React . useRef ( 0 ) ;
52
+ const currentOrientationRef = React . useRef < 'vertical' | 'horizontal' > ( 'vertical' ) ;
53
+ const scrollYTimeout = useTimeout ( ) ;
54
+ const scrollXTimeout = useTimeout ( ) ;
55
+ const scrollPositionRef = React . useRef ( { x : 0 , y : 0 } ) ;
56
+
57
+ const [ hiddenState , setHiddenState ] = React . useState ( {
58
+ scrollbarYHidden : false ,
59
+ scrollbarXHidden : false ,
60
+ cornerHidden : false ,
61
+ } ) ;
62
+
63
+ const handleScroll = useEventCallback ( ( scrollPosition : { x : number ; y : number } ) => {
64
+ const offsetX = scrollPosition . x - scrollPositionRef . current . x ;
65
+ const offsetY = scrollPosition . y - scrollPositionRef . current . y ;
66
+ scrollPositionRef . current = scrollPosition ;
67
+
68
+ if ( offsetY !== 0 ) {
69
+ setScrollingY ( true ) ;
70
+
71
+ scrollYTimeout . start ( SCROLL_TIMEOUT , ( ) => {
72
+ setScrollingY ( false ) ;
73
+ } ) ;
74
+ }
75
+
76
+ if ( offsetX !== 0 ) {
77
+ setScrollingX ( true ) ;
78
+
79
+ scrollXTimeout . start ( SCROLL_TIMEOUT , ( ) => {
80
+ setScrollingX ( false ) ;
81
+ } ) ;
82
+ }
83
+ } ) ;
84
+
85
+ const handlePointerDown = useEventCallback ( ( event : React . PointerEvent ) => {
86
+ thumbDraggingRef . current = true ;
87
+ startYRef . current = event . clientY ;
88
+ startXRef . current = event . clientX ;
89
+ currentOrientationRef . current = event . currentTarget . getAttribute (
90
+ ScrollAreaScrollbarDataAttributes . orientation ,
91
+ ) as 'vertical' | 'horizontal' ;
92
+
93
+ if ( viewportRef . current ) {
94
+ startScrollTopRef . current = viewportRef . current . scrollTop ;
95
+ startScrollLeftRef . current = viewportRef . current . scrollLeft ;
96
+ }
97
+ if ( thumbYRef . current && currentOrientationRef . current === 'vertical' ) {
98
+ thumbYRef . current . setPointerCapture ( event . pointerId ) ;
99
+ }
100
+ if ( thumbXRef . current && currentOrientationRef . current === 'horizontal' ) {
101
+ thumbXRef . current . setPointerCapture ( event . pointerId ) ;
102
+ }
103
+ } ) ;
104
+
105
+ const handlePointerMove = useEventCallback ( ( event : React . PointerEvent ) => {
106
+ if ( ! thumbDraggingRef . current ) {
107
+ return ;
108
+ }
21
109
22
- const scrollAreaRoot = useScrollAreaRoot ( ) ;
110
+ const deltaY = event . clientY - startYRef . current ;
111
+ const deltaX = event . clientX - startXRef . current ;
23
112
24
- const { rootId } = scrollAreaRoot ;
113
+ if ( viewportRef . current ) {
114
+ const scrollableContentHeight = viewportRef . current . scrollHeight ;
115
+ const viewportHeight = viewportRef . current . clientHeight ;
116
+ const scrollableContentWidth = viewportRef . current . scrollWidth ;
117
+ const viewportWidth = viewportRef . current . clientWidth ;
25
118
26
- const { renderElement } = useComponentRenderer ( {
27
- propGetter : scrollAreaRoot . getRootProps ,
28
- render : render ?? 'div' ,
119
+ if (
120
+ thumbYRef . current &&
121
+ scrollbarYRef . current &&
122
+ currentOrientationRef . current === 'vertical'
123
+ ) {
124
+ const scrollbarYOffset = getOffset ( scrollbarYRef . current , 'padding' , 'y' ) ;
125
+ const thumbYOffset = getOffset ( thumbYRef . current , 'margin' , 'y' ) ;
126
+ const thumbHeight = thumbYRef . current . offsetHeight ;
127
+ const maxThumbOffsetY =
128
+ scrollbarYRef . current . offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset ;
129
+ const scrollRatioY = deltaY / maxThumbOffsetY ;
130
+ viewportRef . current . scrollTop =
131
+ startScrollTopRef . current + scrollRatioY * ( scrollableContentHeight - viewportHeight ) ;
132
+ event . preventDefault ( ) ;
133
+
134
+ setScrollingY ( true ) ;
135
+
136
+ scrollYTimeout . start ( SCROLL_TIMEOUT , ( ) => {
137
+ setScrollingY ( false ) ;
138
+ } ) ;
139
+ }
140
+
141
+ if (
142
+ thumbXRef . current &&
143
+ scrollbarXRef . current &&
144
+ currentOrientationRef . current === 'horizontal'
145
+ ) {
146
+ const scrollbarXOffset = getOffset ( scrollbarXRef . current , 'padding' , 'x' ) ;
147
+ const thumbXOffset = getOffset ( thumbXRef . current , 'margin' , 'x' ) ;
148
+ const thumbWidth = thumbXRef . current . offsetWidth ;
149
+ const maxThumbOffsetX =
150
+ scrollbarXRef . current . offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset ;
151
+ const scrollRatioX = deltaX / maxThumbOffsetX ;
152
+ viewportRef . current . scrollLeft =
153
+ startScrollLeftRef . current + scrollRatioX * ( scrollableContentWidth - viewportWidth ) ;
154
+ event . preventDefault ( ) ;
155
+
156
+ setScrollingX ( true ) ;
157
+
158
+ scrollXTimeout . start ( SCROLL_TIMEOUT , ( ) => {
159
+ setScrollingX ( false ) ;
160
+ } ) ;
161
+ }
162
+ }
163
+ } ) ;
164
+
165
+ const handlePointerUp = useEventCallback ( ( event : React . PointerEvent ) => {
166
+ thumbDraggingRef . current = false ;
167
+
168
+ if ( thumbYRef . current && currentOrientationRef . current === 'vertical' ) {
169
+ thumbYRef . current . releasePointerCapture ( event . pointerId ) ;
170
+ }
171
+ if ( thumbXRef . current && currentOrientationRef . current === 'horizontal' ) {
172
+ thumbXRef . current . releasePointerCapture ( event . pointerId ) ;
173
+ }
174
+ } ) ;
175
+
176
+ function handlePointerEnterOrMove ( { pointerType } : React . PointerEvent ) {
177
+ const isTouch = pointerType === 'touch' ;
178
+
179
+ setTouchModality ( isTouch ) ;
180
+
181
+ if ( ! isTouch ) {
182
+ setHovering ( true ) ;
183
+ }
184
+ }
185
+
186
+ const props : GenericHTMLProps = {
187
+ role : 'presentation' ,
188
+ onPointerEnter : handlePointerEnterOrMove ,
189
+ onPointerMove : handlePointerEnterOrMove ,
190
+ onPointerDown ( { pointerType } ) {
191
+ setTouchModality ( pointerType === 'touch' ) ;
192
+ } ,
193
+ onPointerLeave ( ) {
194
+ setHovering ( false ) ;
195
+ } ,
196
+ style : {
197
+ position : 'relative' ,
198
+ [ ScrollAreaRootCssVars . scrollAreaCornerHeight as string ] : `${ cornerSize . height } px` ,
199
+ [ ScrollAreaRootCssVars . scrollAreaCornerWidth as string ] : `${ cornerSize . width } px` ,
200
+ } ,
201
+ } ;
202
+
203
+ const renderElement = useRenderElement ( 'div' , componentProps , {
29
204
ref : forwardedRef ,
30
- className,
31
- state,
32
- extraProps : otherProps ,
205
+ props : [ props , elementProps ] ,
33
206
} ) ;
34
207
35
- const contextValue = React . useMemo ( ( ) => scrollAreaRoot , [ scrollAreaRoot ] ) ;
208
+ const contextValue = React . useMemo (
209
+ ( ) => ( {
210
+ handlePointerDown,
211
+ handlePointerMove,
212
+ handlePointerUp,
213
+ handleScroll,
214
+ cornerSize,
215
+ setCornerSize,
216
+ thumbSize,
217
+ setThumbSize,
218
+ touchModality,
219
+ cornerRef,
220
+ scrollingX,
221
+ setScrollingX,
222
+ scrollingY,
223
+ setScrollingY,
224
+ hovering,
225
+ setHovering,
226
+ viewportRef,
227
+ scrollbarYRef,
228
+ scrollbarXRef,
229
+ thumbYRef,
230
+ thumbXRef,
231
+ rootId,
232
+ hiddenState,
233
+ setHiddenState,
234
+ } ) ,
235
+ [
236
+ handlePointerDown ,
237
+ handlePointerMove ,
238
+ handlePointerUp ,
239
+ handleScroll ,
240
+ cornerSize ,
241
+ thumbSize ,
242
+ touchModality ,
243
+ cornerRef ,
244
+ scrollingX ,
245
+ setScrollingX ,
246
+ scrollingY ,
247
+ setScrollingY ,
248
+ hovering ,
249
+ setHovering ,
250
+ viewportRef ,
251
+ scrollbarYRef ,
252
+ scrollbarXRef ,
253
+ thumbYRef ,
254
+ thumbXRef ,
255
+ rootId ,
256
+ hiddenState ,
257
+ ] ,
258
+ ) ;
36
259
37
260
const viewportId = `[data-id="${ rootId } -viewport"]` ;
38
261
0 commit comments