@@ -12,6 +12,8 @@ import {
1212 formatSliderTooltip ,
1313 transformSliderTooltip ,
1414} from '../utils/formatSliderTooltip' ;
15+ import { snapToNearestMark } from '../utils/sliderSnapToMark' ;
16+ import { renderSliderMarks , renderSliderDots } from '../utils/sliderRendering' ;
1517import LoadingElement from '../utils/_LoadingElement' ;
1618import { RangeSliderProps } from '../types' ;
1719
@@ -129,13 +131,16 @@ export default function RangeSlider(props: RangeSliderProps) {
129131 } , [ min , max , processedMarks ] ) ;
130132
131133 const stepValue = useMemo ( ( ) => {
132- return step === null && ! isNil ( processedMarks )
134+ return step === null && isNil ( processedMarks )
133135 ? undefined
134136 : calcStep ( min , max , step ) ;
135137 } , [ min , max , processedMarks , step ] ) ;
136138
137139 // Sanitize marks for rendering
138140 const renderedMarks = useMemo ( ( ) => {
141+ if ( processedMarks === null ) {
142+ return null ;
143+ }
139144 return sanitizeMarks ( {
140145 min,
141146 max,
@@ -146,11 +151,24 @@ export default function RangeSlider(props: RangeSliderProps) {
146151 } , [ min , max , processedMarks , step , sliderWidth ] ) ;
147152
148153 const handleValueChange = ( newValue : number [ ] ) => {
149- setValue ( newValue ) ;
154+ let adjustedValue = newValue ;
155+
156+ // Snap to nearest marks if step is null and marks exist
157+ if (
158+ step === null &&
159+ processedMarks &&
160+ typeof processedMarks === 'object'
161+ ) {
162+ adjustedValue = newValue . map ( val =>
163+ snapToNearestMark ( val , processedMarks )
164+ ) ;
165+ }
166+
167+ setValue ( adjustedValue ) ;
150168 if ( updatemode === 'drag' ) {
151- setProps ( { value : newValue , drag_value : newValue } ) ;
169+ setProps ( { value : adjustedValue , drag_value : adjustedValue } ) ;
152170 } else {
153- setProps ( { drag_value : newValue } ) ;
171+ setProps ( { drag_value : adjustedValue } ) ;
154172 }
155173 } ;
156174
@@ -179,59 +197,6 @@ export default function RangeSlider(props: RangeSliderProps) {
179197 ) ;
180198 } ;
181199
182- // Replicate Radix UI's exact positioning logic including pixel offsets
183- const convertValueToPercentage = (
184- value : number ,
185- min : number ,
186- max : number
187- ) => {
188- const maxSteps = max - min ;
189- const percentPerStep = 100 / maxSteps ;
190- const percentage = percentPerStep * ( value - min ) ;
191- // Clamp to 0-100 range like Radix does
192- return Math . max ( 0 , Math . min ( 100 , percentage ) ) ;
193- } ;
194-
195- const linearScale = (
196- input : readonly [ number , number ] ,
197- output : readonly [ number , number ]
198- ) => {
199- return ( value : number ) => {
200- if ( input [ 0 ] === input [ 1 ] || output [ 0 ] === output [ 1 ] ) {
201- return output [ 0 ] ;
202- }
203- const ratio = ( output [ 1 ] - output [ 0 ] ) / ( input [ 1 ] - input [ 0 ] ) ;
204- return output [ 0 ] + ratio * ( value - input [ 0 ] ) ;
205- } ;
206- } ;
207-
208- const getThumbInBoundsOffset = (
209- width : number ,
210- left : number ,
211- direction : number
212- ) => {
213- const halfWidth = width / 2 ;
214- const halfPercent = 50 ;
215- const offset = linearScale ( [ 0 , halfPercent ] , [ 0 , halfWidth ] ) ;
216- return ( halfWidth - offset ( left ) * direction ) * direction ;
217- } ;
218-
219- // Calculate the exact position including pixel offset as Radix does
220- const getRadixThumbPosition = ( value : number , thumbWidth = 16 ) => {
221- const percentage = convertValueToPercentage (
222- value ,
223- minMaxValues . min_mark ,
224- minMaxValues . max_mark
225- ) ;
226- const direction = 1 ; // LTR direction
227- const thumbInBoundsOffset = getThumbInBoundsOffset (
228- thumbWidth ,
229- percentage ,
230- direction
231- ) ;
232- return { percentage, offset : thumbInBoundsOffset } ;
233- } ;
234-
235200 return (
236201 < LoadingElement >
237202 { loadingProps => (
@@ -317,58 +282,19 @@ export default function RangeSlider(props: RangeSliderProps) {
317282 < RadixSlider . Range className = "dash-slider-range" />
318283 ) }
319284 </ RadixSlider . Track >
320- { /* Render marks if they exist */ }
321285 { renderedMarks &&
322- Object . entries ( renderedMarks ) . map (
323- ( [ position , mark ] ) => {
324- const pos = parseFloat ( position ) ;
325- // Use the exact same positioning logic as Radix UI thumbs
326- const thumbPosition =
327- getRadixThumbPosition ( pos ) ;
328- const markStyle : React . CSSProperties =
329- vertical
330- ? {
331- bottom : `calc(${ thumbPosition . percentage } % + ${ thumbPosition . offset } px - 13px)` ,
332- left : 'calc(100% + 8px)' ,
333- transform :
334- 'translateY(-50%)' ,
335- }
336- : {
337- left : `calc(${ thumbPosition . percentage } % + ${ thumbPosition . offset } px)` ,
338- bottom : 0 , // Position at the bottom edge of container
339- transform :
340- 'translateX(-50%)' ,
341- } ;
342-
343- const markContent =
344- typeof mark === 'string'
345- ? mark
346- : mark . label ;
347- const markLabelStyle =
348- typeof mark === 'object'
349- ? mark . style
350- : undefined ;
351-
352- return (
353- < div
354- key = { position }
355- className = "dash-slider-mark"
356- style = { markStyle }
357- >
358- < div className = "dash-slider-mark-dot" />
359- { markContent && (
360- < div
361- className = "dash-slider-mark-label"
362- style = {
363- markLabelStyle
364- }
365- >
366- { markContent }
367- </ div >
368- ) }
369- </ div >
370- ) ;
371- }
286+ renderSliderMarks (
287+ renderedMarks ,
288+ ! ! vertical ,
289+ minMaxValues ,
290+ ! ! dots
291+ ) }
292+ { dots &&
293+ stepValue &&
294+ renderSliderDots (
295+ stepValue ,
296+ minMaxValues ,
297+ ! ! vertical
372298 ) }
373299 { /* Render thumbs with tooltips for each value */ }
374300 { value . map ( ( val , index ) => {
0 commit comments