@@ -30,6 +30,19 @@ function createTouches(touches) {
3030describe . skipIf ( ! supportsTouch ( ) ) ( '<Slider />' , ( ) => {
3131 const { render } = createRenderer ( ) ;
3232
33+ beforeEach ( ( ) => {
34+ // jsdom doesn't implement Pointer Capture API
35+ if ( ! Element . prototype . setPointerCapture ) {
36+ Element . prototype . setPointerCapture = stub ( ) ;
37+ }
38+ if ( ! Element . prototype . releasePointerCapture ) {
39+ Element . prototype . releasePointerCapture = stub ( ) ;
40+ }
41+ if ( ! Element . prototype . hasPointerCapture ) {
42+ Element . prototype . hasPointerCapture = stub ( ) . returns ( false ) ;
43+ }
44+ } ) ;
45+
3346 describeConformance (
3447 < Slider value = { 0 } marks = { [ { value : 0 , label : '0' } ] } valueLabelDisplay = "on" /> ,
3548 ( ) => ( {
@@ -65,13 +78,14 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
6578 } ,
6679 } ,
6780 skip : [
81+ 'componentsProp' ,
6882 'slotPropsCallback' , // not supported yet
6983 'slotPropsCallbackWithPropsAsOwnerState' , // not supported yet
7084 ] ,
7185 } ) ,
7286 ) ;
7387
74- it ( 'should call handlers' , ( ) => {
88+ it . skipIf ( isJsdom ( ) ) ( 'should call handlers' , ( ) => {
7589 const handleChange = spy ( ) ;
7690 const handleChangeCommitted = spy ( ) ;
7791
@@ -84,13 +98,15 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
8498 } ) ) ;
8599 const slider = screen . getByRole ( 'slider' ) ;
86100
87- fireEvent . mouseDown ( container . firstChild , {
101+ fireEvent . pointerDown ( container . firstChild , {
88102 buttons : 1 ,
89103 clientX : 10 ,
104+ pointerId : 1 ,
90105 } ) ;
91- fireEvent . mouseUp ( container . firstChild , {
106+ fireEvent . pointerUp ( container . firstChild , {
92107 buttons : 1 ,
93108 clientX : 10 ,
109+ pointerId : 1 ,
94110 } ) ;
95111
96112 expect ( handleChange . callCount ) . to . equal ( 1 ) ;
@@ -140,57 +156,63 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
140156 expect ( handleChangeCommitted . callCount ) . to . equal ( 1 ) ;
141157 } ) ;
142158
143- it ( 'should hedge against a dropped mouseup event' , ( ) => {
159+ it . skipIf ( isJsdom ( ) ) ( 'should hedge against a dropped pointerup event' , ( ) => {
144160 const handleChange = spy ( ) ;
145161 const { container } = render ( < Slider onChange = { handleChange } value = { 0 } /> ) ;
146162 stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
147163 width : 100 ,
148164 left : 0 ,
149165 } ) ) ;
150166
151- fireEvent . mouseDown ( container . firstChild , {
167+ fireEvent . pointerDown ( container . firstChild , {
152168 buttons : 1 ,
153169 clientX : 1 ,
170+ pointerId : 1 ,
154171 } ) ;
155172 expect ( handleChange . callCount ) . to . equal ( 1 ) ;
156173 expect ( handleChange . args [ 0 ] [ 1 ] ) . to . equal ( 1 ) ;
157174
158- fireEvent . mouseMove ( document . body , {
175+ fireEvent . pointerMove ( document . body , {
159176 buttons : 1 ,
160177 clientX : 10 ,
178+ pointerId : 1 ,
161179 } ) ;
162180 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
163181 expect ( handleChange . args [ 1 ] [ 1 ] ) . to . equal ( 10 ) ;
164182
165- fireEvent . mouseMove ( document . body , {
183+ fireEvent . pointerMove ( document . body , {
166184 buttons : 0 ,
167185 clientX : 11 ,
186+ pointerId : 1 ,
168187 } ) ;
169- // The mouse 's button was released, stop the dragging session.
188+ // The pointer 's button was released, stop the dragging session.
170189 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
171190 } ) ;
172191
173- it ( 'should only fire onChange when the value changes' , ( ) => {
192+ it . skipIf ( isJsdom ( ) ) ( 'should only fire onChange when the value changes' , ( ) => {
174193 const handleChange = spy ( ) ;
175194 const { container } = render ( < Slider defaultValue = { 20 } onChange = { handleChange } /> ) ;
176195 stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
177196 width : 100 ,
178197 left : 0 ,
179198 } ) ) ;
180199
181- fireEvent . mouseDown ( container . firstChild , {
200+ fireEvent . pointerDown ( container . firstChild , {
182201 buttons : 1 ,
183202 clientX : 21 ,
203+ pointerId : 1 ,
184204 } ) ;
185205
186- fireEvent . mouseMove ( document . body , {
206+ fireEvent . pointerMove ( document . body , {
187207 buttons : 1 ,
188208 clientX : 22 ,
209+ pointerId : 1 ,
189210 } ) ;
190211 // Sometimes another event with the same position is fired by the browser.
191- fireEvent . mouseMove ( document . body , {
212+ fireEvent . pointerMove ( document . body , {
192213 buttons : 1 ,
193214 clientX : 22 ,
215+ pointerId : 1 ,
194216 } ) ;
195217
196218 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
@@ -324,7 +346,7 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
324346 expect ( document . activeElement ) . to . have . attribute ( 'data-index' , '0' ) ;
325347 } ) ;
326348
327- it ( 'should focus the slider when dragging' , async ( ) => {
349+ it . skipIf ( isJsdom ( ) ) ( 'should focus the slider when dragging' , async ( ) => {
328350 const { container } = render (
329351 < Slider
330352 slotProps = { { thumb : { 'data-testid' : 'thumb' } } }
@@ -341,9 +363,10 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
341363 left : 0 ,
342364 } ) ) ;
343365
344- fireEvent . mouseDown ( thumb , {
366+ fireEvent . pointerDown ( thumb , {
345367 buttons : 1 ,
346368 clientX : 1 ,
369+ pointerId : 1 ,
347370 } ) ;
348371
349372 await waitFor ( ( ) => {
@@ -384,13 +407,13 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
384407 expect ( handleChange . args [ 1 ] [ 1 ] ) . to . deep . equal ( [ 22 , 30 ] ) ;
385408 } ) ;
386409
387- it ( 'should not react to right clicks' , ( ) => {
410+ it . skipIf ( isJsdom ( ) ) ( 'should not react to right clicks' , ( ) => {
388411 const handleChange = spy ( ) ;
389412
390413 render ( < Slider onChange = { handleChange } defaultValue = { 30 } step = { 10 } marks /> ) ;
391414
392415 const thumb = screen . getByRole ( 'slider' ) ;
393- fireEvent . mouseDown ( thumb , { button : 2 } ) ;
416+ fireEvent . pointerDown ( thumb , { button : 2 , pointerId : 1 } ) ;
394417 expect ( handleChange . callCount ) . to . equal ( 0 ) ;
395418 } ) ;
396419 } ) ;
@@ -1692,37 +1715,165 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
16921715 } ) ;
16931716 } ) ;
16941717
1695- describe ( 'When the onMouseUp event occurs at a different location than the last onChange event' , ( ) => {
1696- it ( 'should pass onChangeCommitted the same value that was passed to the last onChange event' , ( ) => {
1697- const handleChange = spy ( ) ;
1698- const handleChangeCommitted = spy ( ) ;
1718+ describe ( 'When the pointer up event occurs at a different location than the last onChange event' , ( ) => {
1719+ it . skipIf ( isJsdom ( ) ) (
1720+ 'should pass onChangeCommitted the same value that was passed to the last onChange event' ,
1721+ ( ) => {
1722+ const handleChange = spy ( ) ;
1723+ const handleChangeCommitted = spy ( ) ;
1724+
1725+ const { container } = render (
1726+ < Slider onChange = { handleChange } onChangeCommitted = { handleChangeCommitted } value = { 0 } /> ,
1727+ ) ;
1728+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1729+ width : 100 ,
1730+ left : 0 ,
1731+ } ) ) ;
1732+
1733+ fireEvent . pointerDown ( container . firstChild , {
1734+ buttons : 1 ,
1735+ clientX : 10 ,
1736+ pointerId : 1 ,
1737+ } ) ;
1738+ fireEvent . pointerMove ( container . firstChild , {
1739+ buttons : 1 ,
1740+ clientX : 15 ,
1741+ pointerId : 1 ,
1742+ } ) ;
1743+ fireEvent . pointerUp ( container . firstChild , {
1744+ buttons : 1 ,
1745+ clientX : 20 ,
1746+ pointerId : 1 ,
1747+ } ) ;
1748+
1749+ expect ( handleChange . callCount ) . to . equal ( 2 ) ;
1750+ expect ( handleChange . args [ 0 ] [ 1 ] ) . to . equal ( 10 ) ;
1751+ expect ( handleChange . args [ 1 ] [ 1 ] ) . to . equal ( 15 ) ;
1752+ expect ( handleChangeCommitted . callCount ) . to . equal ( 1 ) ;
1753+ expect ( handleChangeCommitted . args [ 0 ] [ 1 ] ) . to . equal ( 15 ) ;
1754+ } ,
1755+ ) ;
1756+ } ) ;
1757+
1758+ it . skipIf ( isJsdom ( ) ) ( 'should not crash when unmounted during a pointer drag (#26754)' , ( ) => {
1759+ const { container, unmount } = render ( < Slider defaultValue = { 50 } /> ) ;
1760+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1761+ width : 100 ,
1762+ left : 0 ,
1763+ } ) ) ;
1764+
1765+ fireEvent . pointerDown ( container . firstChild , { clientX : 100 , pointerId : 1 } ) ;
1766+ unmount ( ) ;
1767+ fireEvent . pointerMove ( document , { clientX : 150 , pointerId : 1 } ) ;
1768+ fireEvent . pointerUp ( document , { pointerId : 1 } ) ;
1769+ } ) ;
1770+
1771+ it ( 'should not crash when unmounted during a touch drag (#26754)' , ( ) => {
1772+ const { container, unmount } = render ( < Slider defaultValue = { 50 } /> ) ;
1773+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1774+ width : 100 ,
1775+ height : 10 ,
1776+ bottom : 10 ,
1777+ left : 0 ,
1778+ } ) ) ;
1779+
1780+ fireEvent . touchStart (
1781+ container . firstChild ,
1782+ createTouches ( [ { identifier : 0 , clientX : 100 , clientY : 5 } ] ) ,
1783+ ) ;
1784+ unmount ( ) ;
1785+ fireEvent . touchMove ( document , createTouches ( [ { identifier : 0 , clientX : 150 , clientY : 5 } ] ) ) ;
1786+ fireEvent . touchEnd ( document , createTouches ( [ { identifier : 0 , clientX : 150 , clientY : 5 } ] ) ) ;
1787+ } ) ;
1788+
1789+ it . skipIf ( isJsdom ( ) ) ( 'should end drag when pointermove fires with buttons === 0' , ( ) => {
1790+ const onChangeCommitted = spy ( ) ;
1791+ const { container } = render (
1792+ < Slider defaultValue = { 50 } onChangeCommitted = { onChangeCommitted } /> ,
1793+ ) ;
1794+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1795+ width : 100 ,
1796+ left : 0 ,
1797+ } ) ) ;
1798+
1799+ fireEvent . pointerDown ( container . firstChild , { clientX : 100 , pointerId : 1 } ) ;
1800+ fireEvent . pointerMove ( document , { clientX : 150 , pointerId : 1 , buttons : 0 } ) ;
1801+ expect ( onChangeCommitted . callCount ) . to . equal ( 1 ) ;
1802+ } ) ;
16991803
1804+ it . skipIf ( isJsdom ( ) ) (
1805+ 'should allow consumers to prevent drag via onPointerDown + preventDefault()' ,
1806+ ( ) => {
1807+ const handleChange = spy ( ) ;
17001808 const { container } = render (
1701- < Slider onChange = { handleChange } onChangeCommitted = { handleChangeCommitted } value = { 0 } /> ,
1809+ < Slider
1810+ defaultValue = { 50 }
1811+ onChange = { handleChange }
1812+ slotProps = { {
1813+ root : {
1814+ onPointerDown : ( pointerDownEvent ) => pointerDownEvent . preventDefault ( ) ,
1815+ } ,
1816+ } }
1817+ /> ,
17021818 ) ;
17031819 stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
17041820 width : 100 ,
17051821 left : 0 ,
17061822 } ) ) ;
17071823
1708- fireEvent . mouseDown ( container . firstChild , {
1709- buttons : 1 ,
1710- clientX : 10 ,
1711- } ) ;
1712- fireEvent . mouseMove ( container . firstChild , {
1713- buttons : 1 ,
1714- clientX : 15 ,
1715- } ) ;
1716- fireEvent . mouseUp ( container . firstChild , {
1717- buttons : 1 ,
1718- clientX : 20 ,
1719- } ) ;
1824+ fireEvent . pointerDown ( container . firstChild , { clientX : 20 , pointerId : 1 } ) ;
1825+ expect ( handleChange . callCount ) . to . equal ( 0 ) ;
1826+ } ,
1827+ ) ;
1828+
1829+ it . skipIf ( isJsdom ( ) ) (
1830+ 'should not fire onChange twice on touch devices (pointer+touch dual fire)' ,
1831+ ( ) => {
1832+ const handleChange = spy ( ) ;
1833+ const { container } = render ( < Slider defaultValue = { 50 } onChange = { handleChange } /> ) ;
1834+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1835+ width : 100 ,
1836+ height : 10 ,
1837+ bottom : 10 ,
1838+ left : 0 ,
1839+ } ) ) ;
1840+
1841+ // Touch devices fire both pointer and touch events for the same physical touch
1842+ fireEvent . pointerDown ( container . firstChild , { clientX : 20 , pointerId : 1 } ) ;
1843+ fireEvent . touchStart ( container . firstChild , createTouches ( [ { identifier : 0 , clientX : 20 } ] ) ) ;
17201844
1845+ // Move — only the pointer path listener should be on document
1846+ fireEvent . pointerMove ( document , { clientX : 40 , pointerId : 1 , buttons : 1 } ) ;
1847+
1848+ // onChange: once from pointerDown (value change) + once from pointerMove = 2, not 3
17211849 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
1722- expect ( handleChange . args [ 0 ] [ 1 ] ) . to . equal ( 10 ) ;
1723- expect ( handleChange . args [ 1 ] [ 1 ] ) . to . equal ( 15 ) ;
1724- expect ( handleChangeCommitted . callCount ) . to . equal ( 1 ) ;
1725- expect ( handleChangeCommitted . args [ 0 ] [ 1 ] ) . to . equal ( 15 ) ;
1726- } ) ;
1727- } ) ;
1850+ } ,
1851+ ) ;
1852+
1853+ it . skipIf ( isJsdom ( ) ) (
1854+ 'should ignore pointerup from a different pointer than the one that started the drag' ,
1855+ ( ) => {
1856+ const handleChange = spy ( ) ;
1857+ const { container } = render ( < Slider defaultValue = { 50 } onChange = { handleChange } /> ) ;
1858+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1859+ width : 100 ,
1860+ height : 10 ,
1861+ bottom : 10 ,
1862+ left : 0 ,
1863+ } ) ) ;
1864+
1865+ // Start drag with pointer 1
1866+ fireEvent . pointerDown ( container . firstChild , { clientX : 50 , pointerId : 1 } ) ;
1867+ const changesAfterDown = handleChange . callCount ;
1868+
1869+ // A second pointer fires pointerup — should be ignored
1870+ fireEvent . pointerUp ( document , { clientX : 60 , pointerId : 2 } ) ;
1871+
1872+ // The drag should still be active — a move from the original pointer
1873+ // must still produce onChange. Without pointerId filtering, the stray
1874+ // pointerup tears down listeners and this move is silently dropped.
1875+ fireEvent . pointerMove ( document , { clientX : 70 , pointerId : 1 , buttons : 1 } ) ;
1876+ expect ( handleChange . callCount ) . to . be . greaterThan ( changesAfterDown ) ;
1877+ } ,
1878+ ) ;
17281879} ) ;
0 commit comments