@@ -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 ( ) => ( {
@@ -71,7 +84,7 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
7184 } ) ,
7285 ) ;
7386
74- it ( 'should call handlers' , ( ) => {
87+ it . skipIf ( isJsdom ( ) ) ( 'should call handlers' , ( ) => {
7588 const handleChange = spy ( ) ;
7689 const handleChangeCommitted = spy ( ) ;
7790
@@ -84,13 +97,15 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
8497 } ) ) ;
8598 const slider = screen . getByRole ( 'slider' ) ;
8699
87- fireEvent . mouseDown ( container . firstChild , {
100+ fireEvent . pointerDown ( container . firstChild , {
88101 buttons : 1 ,
89102 clientX : 10 ,
103+ pointerId : 1 ,
90104 } ) ;
91- fireEvent . mouseUp ( container . firstChild , {
105+ fireEvent . pointerUp ( container . firstChild , {
92106 buttons : 1 ,
93107 clientX : 10 ,
108+ pointerId : 1 ,
94109 } ) ;
95110
96111 expect ( handleChange . callCount ) . to . equal ( 1 ) ;
@@ -140,57 +155,63 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
140155 expect ( handleChangeCommitted . callCount ) . to . equal ( 1 ) ;
141156 } ) ;
142157
143- it ( 'should hedge against a dropped mouseup event' , ( ) => {
158+ it . skipIf ( isJsdom ( ) ) ( 'should hedge against a dropped pointerup event' , ( ) => {
144159 const handleChange = spy ( ) ;
145160 const { container } = render ( < Slider onChange = { handleChange } value = { 0 } /> ) ;
146161 stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
147162 width : 100 ,
148163 left : 0 ,
149164 } ) ) ;
150165
151- fireEvent . mouseDown ( container . firstChild , {
166+ fireEvent . pointerDown ( container . firstChild , {
152167 buttons : 1 ,
153168 clientX : 1 ,
169+ pointerId : 1 ,
154170 } ) ;
155171 expect ( handleChange . callCount ) . to . equal ( 1 ) ;
156172 expect ( handleChange . args [ 0 ] [ 1 ] ) . to . equal ( 1 ) ;
157173
158- fireEvent . mouseMove ( document . body , {
174+ fireEvent . pointerMove ( document . body , {
159175 buttons : 1 ,
160176 clientX : 10 ,
177+ pointerId : 1 ,
161178 } ) ;
162179 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
163180 expect ( handleChange . args [ 1 ] [ 1 ] ) . to . equal ( 10 ) ;
164181
165- fireEvent . mouseMove ( document . body , {
182+ fireEvent . pointerMove ( document . body , {
166183 buttons : 0 ,
167184 clientX : 11 ,
185+ pointerId : 1 ,
168186 } ) ;
169- // The mouse 's button was released, stop the dragging session.
187+ // The pointer 's button was released, stop the dragging session.
170188 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
171189 } ) ;
172190
173- it ( 'should only fire onChange when the value changes' , ( ) => {
191+ it . skipIf ( isJsdom ( ) ) ( 'should only fire onChange when the value changes' , ( ) => {
174192 const handleChange = spy ( ) ;
175193 const { container } = render ( < Slider defaultValue = { 20 } onChange = { handleChange } /> ) ;
176194 stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
177195 width : 100 ,
178196 left : 0 ,
179197 } ) ) ;
180198
181- fireEvent . mouseDown ( container . firstChild , {
199+ fireEvent . pointerDown ( container . firstChild , {
182200 buttons : 1 ,
183201 clientX : 21 ,
202+ pointerId : 1 ,
184203 } ) ;
185204
186- fireEvent . mouseMove ( document . body , {
205+ fireEvent . pointerMove ( document . body , {
187206 buttons : 1 ,
188207 clientX : 22 ,
208+ pointerId : 1 ,
189209 } ) ;
190210 // Sometimes another event with the same position is fired by the browser.
191- fireEvent . mouseMove ( document . body , {
211+ fireEvent . pointerMove ( document . body , {
192212 buttons : 1 ,
193213 clientX : 22 ,
214+ pointerId : 1 ,
194215 } ) ;
195216
196217 expect ( handleChange . callCount ) . to . equal ( 2 ) ;
@@ -324,7 +345,7 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
324345 expect ( document . activeElement ) . to . have . attribute ( 'data-index' , '0' ) ;
325346 } ) ;
326347
327- it ( 'should focus the slider when dragging' , async ( ) => {
348+ it . skipIf ( isJsdom ( ) ) ( 'should focus the slider when dragging' , async ( ) => {
328349 const { container } = render (
329350 < Slider
330351 slotProps = { { thumb : { 'data-testid' : 'thumb' } } }
@@ -341,9 +362,10 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
341362 left : 0 ,
342363 } ) ) ;
343364
344- fireEvent . mouseDown ( thumb , {
365+ fireEvent . pointerDown ( thumb , {
345366 buttons : 1 ,
346367 clientX : 1 ,
368+ pointerId : 1 ,
347369 } ) ;
348370
349371 await waitFor ( ( ) => {
@@ -384,13 +406,13 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
384406 expect ( handleChange . args [ 1 ] [ 1 ] ) . to . deep . equal ( [ 22 , 30 ] ) ;
385407 } ) ;
386408
387- it ( 'should not react to right clicks' , ( ) => {
409+ it . skipIf ( isJsdom ( ) ) ( 'should not react to right clicks' , ( ) => {
388410 const handleChange = spy ( ) ;
389411
390412 render ( < Slider onChange = { handleChange } defaultValue = { 30 } step = { 10 } marks /> ) ;
391413
392414 const thumb = screen . getByRole ( 'slider' ) ;
393- fireEvent . mouseDown ( thumb , { button : 2 } ) ;
415+ fireEvent . pointerDown ( thumb , { button : 2 , pointerId : 1 } ) ;
394416 expect ( handleChange . callCount ) . to . equal ( 0 ) ;
395417 } ) ;
396418 } ) ;
@@ -1692,37 +1714,165 @@ describe.skipIf(!supportsTouch())('<Slider />', () => {
16921714 } ) ;
16931715 } ) ;
16941716
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 ( ) ;
1717+ describe ( 'When the pointer up event occurs at a different location than the last onChange event' , ( ) => {
1718+ it . skipIf ( isJsdom ( ) ) (
1719+ 'should pass onChangeCommitted the same value that was passed to the last onChange event' ,
1720+ ( ) => {
1721+ const handleChange = spy ( ) ;
1722+ const handleChangeCommitted = spy ( ) ;
1723+
1724+ const { container } = render (
1725+ < Slider onChange = { handleChange } onChangeCommitted = { handleChangeCommitted } value = { 0 } /> ,
1726+ ) ;
1727+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1728+ width : 100 ,
1729+ left : 0 ,
1730+ } ) ) ;
1731+
1732+ fireEvent . pointerDown ( container . firstChild , {
1733+ buttons : 1 ,
1734+ clientX : 10 ,
1735+ pointerId : 1 ,
1736+ } ) ;
1737+ fireEvent . pointerMove ( container . firstChild , {
1738+ buttons : 1 ,
1739+ clientX : 15 ,
1740+ pointerId : 1 ,
1741+ } ) ;
1742+ fireEvent . pointerUp ( container . firstChild , {
1743+ buttons : 1 ,
1744+ clientX : 20 ,
1745+ pointerId : 1 ,
1746+ } ) ;
1747+
1748+ expect ( handleChange . callCount ) . to . equal ( 2 ) ;
1749+ expect ( handleChange . args [ 0 ] [ 1 ] ) . to . equal ( 10 ) ;
1750+ expect ( handleChange . args [ 1 ] [ 1 ] ) . to . equal ( 15 ) ;
1751+ expect ( handleChangeCommitted . callCount ) . to . equal ( 1 ) ;
1752+ expect ( handleChangeCommitted . args [ 0 ] [ 1 ] ) . to . equal ( 15 ) ;
1753+ } ,
1754+ ) ;
1755+ } ) ;
1756+
1757+ it . skipIf ( isJsdom ( ) ) ( 'should not crash when unmounted during a pointer drag (#26754)' , ( ) => {
1758+ const { container, unmount } = render ( < Slider defaultValue = { 50 } /> ) ;
1759+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1760+ width : 100 ,
1761+ left : 0 ,
1762+ } ) ) ;
1763+
1764+ fireEvent . pointerDown ( container . firstChild , { clientX : 100 , pointerId : 1 } ) ;
1765+ unmount ( ) ;
1766+ fireEvent . pointerMove ( document , { clientX : 150 , pointerId : 1 } ) ;
1767+ fireEvent . pointerUp ( document , { pointerId : 1 } ) ;
1768+ } ) ;
1769+
1770+ it ( 'should not crash when unmounted during a touch drag (#26754)' , ( ) => {
1771+ const { container, unmount } = render ( < Slider defaultValue = { 50 } /> ) ;
1772+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1773+ width : 100 ,
1774+ height : 10 ,
1775+ bottom : 10 ,
1776+ left : 0 ,
1777+ } ) ) ;
1778+
1779+ fireEvent . touchStart (
1780+ container . firstChild ,
1781+ createTouches ( [ { identifier : 0 , clientX : 100 , clientY : 5 } ] ) ,
1782+ ) ;
1783+ unmount ( ) ;
1784+ fireEvent . touchMove ( document , createTouches ( [ { identifier : 0 , clientX : 150 , clientY : 5 } ] ) ) ;
1785+ fireEvent . touchEnd ( document , createTouches ( [ { identifier : 0 , clientX : 150 , clientY : 5 } ] ) ) ;
1786+ } ) ;
1787+
1788+ it . skipIf ( isJsdom ( ) ) ( 'should end drag when pointermove fires with buttons === 0' , ( ) => {
1789+ const onChangeCommitted = spy ( ) ;
1790+ const { container } = render (
1791+ < Slider defaultValue = { 50 } onChangeCommitted = { onChangeCommitted } /> ,
1792+ ) ;
1793+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1794+ width : 100 ,
1795+ left : 0 ,
1796+ } ) ) ;
1797+
1798+ fireEvent . pointerDown ( container . firstChild , { clientX : 100 , pointerId : 1 } ) ;
1799+ fireEvent . pointerMove ( document , { clientX : 150 , pointerId : 1 , buttons : 0 } ) ;
1800+ expect ( onChangeCommitted . callCount ) . to . equal ( 1 ) ;
1801+ } ) ;
16991802
1803+ it . skipIf ( isJsdom ( ) ) (
1804+ 'should allow consumers to prevent drag via onPointerDown + preventDefault()' ,
1805+ ( ) => {
1806+ const handleChange = spy ( ) ;
17001807 const { container } = render (
1701- < Slider onChange = { handleChange } onChangeCommitted = { handleChangeCommitted } value = { 0 } /> ,
1808+ < Slider
1809+ defaultValue = { 50 }
1810+ onChange = { handleChange }
1811+ slotProps = { {
1812+ root : {
1813+ onPointerDown : ( pointerDownEvent ) => pointerDownEvent . preventDefault ( ) ,
1814+ } ,
1815+ } }
1816+ /> ,
17021817 ) ;
17031818 stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
17041819 width : 100 ,
17051820 left : 0 ,
17061821 } ) ) ;
17071822
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- } ) ;
1823+ fireEvent . pointerDown ( container . firstChild , { clientX : 20 , pointerId : 1 } ) ;
1824+ expect ( handleChange . callCount ) . to . equal ( 0 ) ;
1825+ } ,
1826+ ) ;
1827+
1828+ it . skipIf ( isJsdom ( ) ) (
1829+ 'should not fire onChange twice on touch devices (pointer+touch dual fire)' ,
1830+ ( ) => {
1831+ const handleChange = spy ( ) ;
1832+ const { container } = render ( < Slider defaultValue = { 50 } onChange = { handleChange } /> ) ;
1833+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1834+ width : 100 ,
1835+ height : 10 ,
1836+ bottom : 10 ,
1837+ left : 0 ,
1838+ } ) ) ;
1839+
1840+ // Touch devices fire both pointer and touch events for the same physical touch
1841+ fireEvent . pointerDown ( container . firstChild , { clientX : 20 , pointerId : 1 } ) ;
1842+ fireEvent . touchStart ( container . firstChild , createTouches ( [ { identifier : 0 , clientX : 20 } ] ) ) ;
17201843
1844+ // Move — only the pointer path listener should be on document
1845+ fireEvent . pointerMove ( document , { clientX : 40 , pointerId : 1 , buttons : 1 } ) ;
1846+
1847+ // onChange: once from pointerDown (value change) + once from pointerMove = 2, not 3
17211848 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- } ) ;
1849+ } ,
1850+ ) ;
1851+
1852+ it . skipIf ( isJsdom ( ) ) (
1853+ 'should ignore pointerup from a different pointer than the one that started the drag' ,
1854+ ( ) => {
1855+ const handleChange = spy ( ) ;
1856+ const { container } = render ( < Slider defaultValue = { 50 } onChange = { handleChange } /> ) ;
1857+ stub ( container . firstChild , 'getBoundingClientRect' ) . callsFake ( ( ) => ( {
1858+ width : 100 ,
1859+ height : 10 ,
1860+ bottom : 10 ,
1861+ left : 0 ,
1862+ } ) ) ;
1863+
1864+ // Start drag with pointer 1
1865+ fireEvent . pointerDown ( container . firstChild , { clientX : 50 , pointerId : 1 } ) ;
1866+ const changesAfterDown = handleChange . callCount ;
1867+
1868+ // A second pointer fires pointerup — should be ignored
1869+ fireEvent . pointerUp ( document , { clientX : 60 , pointerId : 2 } ) ;
1870+
1871+ // The drag should still be active — a move from the original pointer
1872+ // must still produce onChange. Without pointerId filtering, the stray
1873+ // pointerup tears down listeners and this move is silently dropped.
1874+ fireEvent . pointerMove ( document , { clientX : 70 , pointerId : 1 , buttons : 1 } ) ;
1875+ expect ( handleChange . callCount ) . to . be . greaterThan ( changesAfterDown ) ;
1876+ } ,
1877+ ) ;
17281878} ) ;
0 commit comments