@@ -239,6 +239,80 @@ describe('<ScrollArea.Thumb />', () => {
239239 await waitFor ( ( ) => expect ( scrollbar ) . not . toHaveAttribute ( 'data-scrolling' ) ) ;
240240 } ) ;
241241
242+ describe ( 'scroll snap' , ( ) => {
243+ function defineThumbPointerCapture ( thumb : HTMLElement ) {
244+ Object . defineProperties ( thumb , {
245+ setPointerCapture : {
246+ configurable : true ,
247+ value : ( ) => { } ,
248+ } ,
249+ hasPointerCapture : {
250+ configurable : true ,
251+ value : ( ) => false ,
252+ } ,
253+ } ) ;
254+ }
255+
256+ function renderWithSnap ( ) {
257+ return render (
258+ < ScrollArea . Root style = { { width : 200 , height : 200 } } >
259+ < ScrollArea . Viewport
260+ data-testid = "viewport"
261+ style = { { width : '100%' , height : '100%' , scrollSnapType : 'y mandatory' } }
262+ >
263+ < div style = { { width : 200 , height : 1000 } } />
264+ </ ScrollArea . Viewport >
265+ < ScrollArea . Scrollbar orientation = "vertical" keepMounted >
266+ < ScrollArea . Thumb data-testid = "thumb" />
267+ </ ScrollArea . Scrollbar >
268+ </ ScrollArea . Root > ,
269+ ) ;
270+ }
271+
272+ it ( 'disables viewport scroll snap while dragging and restores it on release' , async ( ) => {
273+ await renderWithSnap ( ) ;
274+
275+ const viewport = screen . getByTestId ( 'viewport' ) ;
276+ const thumb = screen . getByTestId ( 'thumb' ) ;
277+ defineThumbPointerCapture ( thumb ) ;
278+
279+ fireEvent . pointerDown ( thumb , { button : 0 , clientY : 0 , pointerId : 1 } ) ;
280+ expect ( viewport . style . scrollSnapType ) . toBe ( 'none' ) ;
281+
282+ fireEvent . pointerUp ( thumb , { pointerId : 1 } ) ;
283+ expect ( viewport . style . scrollSnapType ) . toBe ( 'y mandatory' ) ;
284+ } ) ;
285+
286+ it ( 'restores viewport scroll snap on pointer cancel' , async ( ) => {
287+ await renderWithSnap ( ) ;
288+
289+ const viewport = screen . getByTestId ( 'viewport' ) ;
290+ const thumb = screen . getByTestId ( 'thumb' ) ;
291+ defineThumbPointerCapture ( thumb ) ;
292+
293+ fireEvent . pointerDown ( thumb , { button : 0 , clientY : 0 , pointerId : 1 } ) ;
294+ expect ( viewport . style . scrollSnapType ) . toBe ( 'none' ) ;
295+
296+ fireEvent . pointerCancel ( thumb , { pointerId : 1 } ) ;
297+ expect ( viewport . style . scrollSnapType ) . toBe ( 'y mandatory' ) ;
298+ } ) ;
299+
300+ it ( 'keeps the saved scroll snap value when a second pointer starts mid-drag' , async ( ) => {
301+ await renderWithSnap ( ) ;
302+
303+ const viewport = screen . getByTestId ( 'viewport' ) ;
304+ const thumb = screen . getByTestId ( 'thumb' ) ;
305+ defineThumbPointerCapture ( thumb ) ;
306+
307+ fireEvent . pointerDown ( thumb , { button : 0 , clientY : 0 , pointerId : 1 } ) ;
308+ fireEvent . pointerDown ( thumb , { button : 0 , clientY : 0 , pointerId : 2 } ) ;
309+ expect ( viewport . style . scrollSnapType ) . toBe ( 'none' ) ;
310+
311+ fireEvent . pointerUp ( thumb , { pointerId : 1 } ) ;
312+ expect ( viewport . style . scrollSnapType ) . toBe ( 'y mandatory' ) ;
313+ } ) ;
314+ } ) ;
315+
242316 describe ( 'data-scrolling attribute' , ( ) => {
243317 const { render : renderWithClock , clock } = createRenderer ( ) ;
244318
0 commit comments