@@ -4,6 +4,7 @@ import { isElement } from '@floating-ui/utils/dom';
44import { ownerDocument } from '@base-ui-components/utils/owner' ;
55import { useAnimationFrame } from '@base-ui-components/utils/useAnimationFrame' ;
66import { useStableCallback } from '@base-ui-components/utils/useStableCallback' ;
7+ import { useValueAsRef } from '@base-ui-components/utils/useValueAsRef' ;
78import { activeElement , contains } from '../../floating-ui-react/utils' ;
89import type { Coords } from '../../floating-ui-react/types' ;
910import { clamp } from '../../utils/clamp' ;
@@ -19,9 +20,9 @@ import { useSliderRootContext } from '../root/SliderRootContext';
1920import { sliderStateAttributesMapping } from '../root/stateAttributesMapping' ;
2021import type { SliderRoot } from '../root/SliderRoot' ;
2122import { getMidpoint } from '../utils/getMidpoint' ;
22- import { replaceArrayItemAtIndex } from '../utils/replaceArrayItemAtIndex' ;
2323import { roundValueToStep } from '../utils/roundValueToStep' ;
2424import { validateMinimumDistance } from '../utils/validateMinimumDistance' ;
25+ import { resolveThumbCollision } from '../utils/resolveThumbCollision' ;
2526
2627const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2 ;
2728
@@ -101,13 +102,15 @@ export const SliderControl = React.forwardRef(function SliderControl(
101102 pressedInputRef,
102103 pressedThumbCenterOffsetRef,
103104 pressedThumbIndexRef,
105+ pressedValuesRef,
104106 registerFieldControlRef,
105107 renderBeforeHydration,
106108 setActive,
107109 setDragging,
108110 setValue,
109111 state,
110112 step,
113+ thumbCollisionBehavior,
111114 thumbRefs,
112115 values,
113116 } = useSliderRootContext ( ) ;
@@ -133,6 +136,23 @@ export const SliderControl = React.forwardRef(function SliderControl(
133136 // The offset amount to each side of the control for inset sliders.
134137 // This value should be equal to the radius or half the width/height of the thumb.
135138 const insetThumbOffsetRef = React . useRef ( 0 ) ;
139+ const latestValuesRef = useValueAsRef ( values ) ;
140+
141+ const updatePressedThumb = useStableCallback ( ( nextIndex : number ) => {
142+ if ( pressedThumbIndexRef . current !== nextIndex ) {
143+ pressedThumbIndexRef . current = nextIndex ;
144+ }
145+
146+ const thumbElement = thumbRefs . current [ nextIndex ] ;
147+
148+ if ( ! thumbElement ) {
149+ pressedThumbCenterOffsetRef . current = null ;
150+ pressedInputRef . current = null ;
151+ return ;
152+ }
153+
154+ pressedInputRef . current = thumbElement . querySelector < HTMLInputElement > ( 'input[type="range"]' ) ;
155+ } ) ;
136156
137157 const getFingerState = useStableCallback ( ( fingerCoords : Coords ) : FingerState | null => {
138158 const control = controlRef . current ;
@@ -165,25 +185,42 @@ export const SliderControl = React.forwardRef(function SliderControl(
165185 return {
166186 value : newValue ,
167187 thumbIndex : 0 ,
188+ didSwap : false ,
168189 } ;
169190 }
170191
171- const minValueDifference = minStepsBetweenValues * step ;
192+ const thumbIndex = pressedThumbIndexRef . current ;
193+
194+ if ( thumbIndex < 0 ) {
195+ return null ;
196+ }
197+
198+ const collisionResult = resolveThumbCollision ( {
199+ behavior : thumbCollisionBehavior ,
200+ values,
201+ currentValues : latestValuesRef . current ?? values ,
202+ initialValues : pressedValuesRef . current ,
203+ pressedIndex : thumbIndex ,
204+ nextValue : newValue ,
205+ min,
206+ max,
207+ step,
208+ minStepsBetweenValues,
209+ } ) ;
172210
173- // Bound the new value to the thumb's neighbours.
174- newValue = clamp (
175- newValue ,
176- values [ pressedThumbIndexRef . current - 1 ] + minValueDifference || - Infinity ,
177- values [ pressedThumbIndexRef . current + 1 ] - minValueDifference || Infinity ,
178- ) ;
211+ if ( thumbCollisionBehavior === 'swap' && collisionResult . didSwap ) {
212+ updatePressedThumb ( collisionResult . thumbIndex ) ;
213+ } else {
214+ pressedThumbIndexRef . current = collisionResult . thumbIndex ;
215+ }
179216
180- return {
181- value : replaceArrayItemAtIndex ( values , pressedThumbIndexRef . current , newValue ) ,
182- thumbIndex : pressedThumbIndexRef . current ,
183- } ;
217+ return collisionResult ;
184218 } ) ;
185219
186220 const startPressing = useStableCallback ( ( fingerCoords : Coords ) => {
221+ pressedValuesRef . current = range ? values . slice ( ) : null ;
222+ latestValuesRef . current = values ;
223+
187224 const pressedThumbIndex = pressedThumbIndexRef . current ;
188225 let closestThumbIndex = pressedThumbIndex ;
189226
@@ -219,7 +256,7 @@ export const SliderControl = React.forwardRef(function SliderControl(
219256 }
220257
221258 if ( closestThumbIndex > - 1 && closestThumbIndex !== pressedThumbIndex ) {
222- pressedThumbIndexRef . current = closestThumbIndex ;
259+ updatePressedThumb ( closestThumbIndex ) ;
223260 }
224261
225262 if ( inset ) {
@@ -270,6 +307,12 @@ export const SliderControl = React.forwardRef(function SliderControl(
270307 activeThumbIndex : finger . thumbIndex ,
271308 } ) ,
272309 ) ;
310+
311+ latestValuesRef . current = Array . isArray ( finger . value ) ? finger . value : [ finger . value ] ;
312+
313+ if ( finger . didSwap ) {
314+ focusThumb ( finger . thumbIndex ) ;
315+ }
273316 }
274317 } ) ;
275318
@@ -282,25 +325,17 @@ export const SliderControl = React.forwardRef(function SliderControl(
282325 pressedThumbIndexRef . current = - 1 ;
283326
284327 const fingerCoords = getFingerCoords ( nativeEvent , touchIdRef ) ;
285-
286- if ( fingerCoords == null ) {
287- return ;
288- }
289-
290- const finger = getFingerState ( fingerCoords ) ;
291-
292- if ( finger == null ) {
293- return ;
328+ const finger = fingerCoords != null ? getFingerState ( fingerCoords ) : null ;
329+
330+ if ( finger != null ) {
331+ const commitReason = lastChangeReasonRef . current ;
332+ validation . commit ( lastChangedValueRef . current ?? finger . value ) ;
333+ onValueCommitted (
334+ lastChangedValueRef . current ?? finger . value ,
335+ createGenericEventDetails ( commitReason , nativeEvent ) ,
336+ ) ;
294337 }
295338
296- const commitReason = lastChangeReasonRef . current ;
297-
298- validation . commit ( lastChangedValueRef . current ?? finger . value ) ;
299- onValueCommitted (
300- lastChangedValueRef . current ?? finger . value ,
301- createGenericEventDetails ( commitReason , nativeEvent ) ,
302- ) ;
303-
304339 if (
305340 'pointerType' in nativeEvent &&
306341 controlRef . current ?. hasPointerCapture ( nativeEvent . pointerId )
@@ -309,6 +344,7 @@ export const SliderControl = React.forwardRef(function SliderControl(
309344 }
310345
311346 touchIdRef . current = null ;
347+ pressedValuesRef . current = null ;
312348 // eslint-disable-next-line @typescript-eslint/no-use-before-define
313349 stopListening ( ) ;
314350 }
@@ -342,6 +378,12 @@ export const SliderControl = React.forwardRef(function SliderControl(
342378 activeThumbIndex : finger . thumbIndex ,
343379 } ) ,
344380 ) ;
381+
382+ latestValuesRef . current = Array . isArray ( finger . value ) ? finger . value : [ finger . value ] ;
383+
384+ if ( finger . didSwap ) {
385+ focusThumb ( finger . thumbIndex ) ;
386+ }
345387 }
346388
347389 moveCountRef . current = 0 ;
@@ -356,6 +398,7 @@ export const SliderControl = React.forwardRef(function SliderControl(
356398 doc . removeEventListener ( 'pointerup' , handleTouchEnd ) ;
357399 doc . removeEventListener ( 'touchmove' , handleTouchMove ) ;
358400 doc . removeEventListener ( 'touchend' , handleTouchEnd ) ;
401+ pressedValuesRef . current = null ;
359402 } ) ;
360403
361404 const focusFrame = useAnimationFrame ( ) ;
@@ -438,6 +481,12 @@ export const SliderControl = React.forwardRef(function SliderControl(
438481 activeThumbIndex : finger . thumbIndex ,
439482 } ) ,
440483 ) ;
484+
485+ latestValuesRef . current = Array . isArray ( finger . value ) ? finger . value : [ finger . value ] ;
486+
487+ if ( finger . didSwap ) {
488+ focusThumb ( finger . thumbIndex ) ;
489+ }
441490 }
442491 }
443492
@@ -463,10 +512,12 @@ export const SliderControl = React.forwardRef(function SliderControl(
463512interface FingerState {
464513 value : number | number [ ] ;
465514 thumbIndex : number ;
515+ didSwap : boolean ;
466516}
467517
468518export interface SliderControlProps extends BaseUIComponentProps < 'div' , SliderRoot . State > { }
469519
470520export namespace SliderControl {
521+ export type State = SliderRoot . State ;
471522 export type Props = SliderControlProps ;
472523}
0 commit comments