@@ -39,6 +39,7 @@ export default function RangeSlider(props: RangeSliderProps) {
3939 allowCross,
4040 pushable,
4141 count,
42+ reverse,
4243 } = props ;
4344
4445 // For range slider, we expect an array of values
@@ -49,6 +50,7 @@ export default function RangeSlider(props: RangeSliderProps) {
4950 const [ showInputs , setShowInputs ] = useState < boolean > ( value . length === 2 ) ;
5051
5152 const sliderRef = useRef < HTMLDivElement > ( null ) ;
53+ const inputRef = useRef < HTMLInputElement > ( null ) ;
5254
5355 // Handle initial mount - equivalent to componentWillMount
5456 useEffect ( ( ) => {
@@ -192,9 +194,95 @@ export default function RangeSlider(props: RangeSliderProps) {
192194 minWidth ,
193195 Math . min ( maxWidth , charBasedWidth )
194196 ) ;
195- return `${ calculatedWidth } px` ;
197+
198+ // Add padding if box-sizing is border-box
199+ let inputPadding = 0 ;
200+ if ( inputRef . current ) {
201+ const computedStyle = window . getComputedStyle ( inputRef . current ) ;
202+ if ( computedStyle . boxSizing === 'border-box' ) {
203+ const paddingLeft = parseFloat ( computedStyle . paddingLeft ) || 0 ;
204+ const paddingRight =
205+ parseFloat ( computedStyle . paddingRight ) || 0 ;
206+ inputPadding = paddingLeft + paddingRight ;
207+ }
208+ }
209+
210+ const totalWidth = calculatedWidth + inputPadding ;
211+
212+ return `${ totalWidth } px` ;
196213 } , [ sliderWidth , minMaxValues . min_mark , minMaxValues . max_mark ] ) ;
197214
215+ const valueIsValid = ( val : number ) : boolean => {
216+ // Check if value is within min/max bounds
217+ if ( val < minMaxValues . min_mark || val > minMaxValues . max_mark ) {
218+ return false ;
219+ }
220+
221+ // If step is defined, check if value aligns with step
222+ if ( stepValue !== undefined ) {
223+ const min = minMaxValues . min_mark ;
224+ const offset = val - min ;
225+ const remainder = Math . abs ( offset % stepValue ) ;
226+ const epsilon = 0.0001 ; // tolerance for floating point comparison
227+ if ( remainder > epsilon && remainder < stepValue - epsilon ) {
228+ return false ;
229+ }
230+ }
231+
232+ // If step is null and marks exist, value must match a mark
233+ if (
234+ step === null &&
235+ processedMarks &&
236+ typeof processedMarks === 'object'
237+ ) {
238+ const markValues = Object . keys ( processedMarks ) . map ( Number ) ;
239+ const epsilon = 0.0001 ;
240+ return markValues . some ( mark => Math . abs ( val - mark ) < epsilon ) ;
241+ }
242+
243+ return true ;
244+ } ;
245+
246+ const constrainToValidValue = ( val : number ) : number => {
247+ // First constrain to min/max bounds
248+ let constrained = Math . max (
249+ minMaxValues . min_mark ,
250+ Math . min ( minMaxValues . max_mark , val )
251+ ) ;
252+
253+ // If step is null and marks exist, snap to nearest mark
254+ if (
255+ step === null &&
256+ processedMarks &&
257+ typeof processedMarks === 'object'
258+ ) {
259+ return snapToNearestMark ( constrained , processedMarks ) ;
260+ }
261+
262+ // If step is defined, round to nearest step
263+ if ( stepValue !== undefined ) {
264+ const min = minMaxValues . min_mark ;
265+ const steps = Math . round ( ( constrained - min ) / stepValue ) ;
266+ constrained = min + steps * stepValue ;
267+
268+ // Round to avoid floating point precision issues
269+ // Determine decimal places from step value
270+ const stepStr = stepValue . toString ( ) ;
271+ const decimalPlaces = stepStr . includes ( '.' )
272+ ? stepStr . split ( '.' ) [ 1 ] . length
273+ : 0 ;
274+ constrained = Number ( constrained . toFixed ( decimalPlaces ) ) ;
275+
276+ // Ensure we stay within bounds after rounding
277+ constrained = Math . max (
278+ minMaxValues . min_mark ,
279+ Math . min ( minMaxValues . max_mark , constrained )
280+ ) ;
281+ }
282+
283+ return constrained ;
284+ } ;
285+
198286 const handleValueChange = ( newValue : number [ ] ) => {
199287 let adjustedValue = newValue ;
200288
@@ -233,26 +321,25 @@ export default function RangeSlider(props: RangeSliderProps) {
233321 type = "number"
234322 className = "dash-input-container dash-range-slider-input dash-range-slider-min-input"
235323 style = { { width : inputWidth } }
236- value = { value [ 0 ] ?? '' }
324+ value = { isNaN ( value [ 0 ] ) ? '' : value [ 0 ] }
237325 onChange = { e => {
238326 const inputValue = e . target . value ;
239- // Allow empty string (user is clearing the field)
240- if ( inputValue === '' ) {
241- // Don't update props while user is typing, just update local state
242- setValue ( [ null as any , value [ 1 ] ] ) ;
243- } else {
244- const newMin = parseFloat ( inputValue ) ;
245- if ( ! isNaN ( newMin ) ) {
246- const newValue = [ newMin , value [ 1 ] ] ;
247- setValue ( newValue ) ;
248- if ( updatemode === 'drag' ) {
249- setProps ( {
250- value : newValue ,
251- drag_value : newValue ,
252- } ) ;
253- } else {
254- setProps ( { drag_value : newValue } ) ;
255- }
327+
328+ // Parse the input value
329+ const newMin = parseFloat ( inputValue ) ;
330+ const newValue = [ newMin , value [ 1 ] ] ;
331+ setValue ( newValue ) ;
332+ // Only update props if value is valid
333+ if ( valueIsValid ( newMin ) ) {
334+ if ( updatemode === 'drag' ) {
335+ setProps ( {
336+ value : newValue ,
337+ drag_value : newValue ,
338+ } ) ;
339+ } else {
340+ setProps ( {
341+ drag_value : newValue ,
342+ } ) ;
256343 }
257344 }
258345 } }
@@ -262,21 +349,25 @@ export default function RangeSlider(props: RangeSliderProps) {
262349
263350 // If empty, default to current value or min_mark
264351 if ( inputValue === '' ) {
265- newMin = value [ 0 ] ?? minMaxValues . min_mark ;
352+ newMin = isNaN ( value [ 0 ] )
353+ ? minMaxValues . min_mark
354+ : value [ 0 ] ;
266355 } else {
267356 newMin = parseFloat ( inputValue ) ;
268357 newMin = isNaN ( newMin )
269358 ? minMaxValues . min_mark
270359 : newMin ;
271360 }
272361
273- const constrainedMin = Math . max (
274- minMaxValues . min_mark ,
275- Math . min (
276- value [ 1 ] ?? minMaxValues . max_mark ,
277- newMin
278- )
362+ // Constrain to not exceed the max value
363+ newMin = Math . min (
364+ value [ 1 ] ?? minMaxValues . max_mark ,
365+ newMin
279366 ) ;
367+
368+ // Snap to valid value (respecting step and marks)
369+ const constrainedMin =
370+ constrainToValidValue ( newMin ) ;
280371 const newValue = [ constrainedMin , value [ 1 ] ] ;
281372 setValue ( newValue ) ;
282373 if ( updatemode === 'mouseup' ) {
@@ -285,39 +376,41 @@ export default function RangeSlider(props: RangeSliderProps) {
285376 } }
286377 pattern = "^\\d*\\.?\\d*$"
287378 min = { minMaxValues . min_mark }
288- max = { value [ 1 ] }
379+ max = { isNaN ( value [ 1 ] ) ? max : value [ 1 ] }
289380 step = { step || undefined }
290381 disabled = { disabled }
291382 />
292383 ) }
293384 { showInputs && ! vertical && (
294385 < input
386+ ref = { inputRef }
295387 type = "number"
296388 className = "dash-input-container dash-range-slider-input dash-range-slider-max-input"
297389 style = { { width : inputWidth } }
298- value = { value [ value . length - 1 ] ?? '' }
390+ value = {
391+ isNaN ( value [ value . length - 1 ] )
392+ ? ''
393+ : value [ value . length - 1 ]
394+ }
299395 onChange = { e => {
300396 const inputValue = e . target . value ;
301- // Allow empty string (user is clearing the field)
302- if ( inputValue === '' ) {
303- // Don't update props while user is typing, just update local state
304- const newValue = [ ...value ] ;
305- newValue [ newValue . length - 1 ] = '' as any ;
306- setValue ( newValue ) ;
307- } else {
308- const newMax = parseFloat ( inputValue ) ;
309- const constrainedMax = Math . max (
310- minMaxValues . min_mark ,
311- Math . min ( minMaxValues . max_mark , newMax )
312- ) ;
313-
314- if ( newMax === constrainedMax ) {
315- const newValue = [ ...value ] ;
316- newValue [ newValue . length - 1 ] = newMax ;
397+
398+ // Parse the input value
399+ const newMax = parseFloat ( inputValue ) ;
400+ const newValue = [ ...value ] ;
401+ newValue [ newValue . length - 1 ] = newMax ;
402+ setValue ( newValue ) ;
403+ // Only update props if value is valid
404+ if ( valueIsValid ( newMax ) ) {
405+ if ( updatemode === 'drag' ) {
317406 setProps ( {
318407 value : newValue ,
319408 drag_value : newValue ,
320409 } ) ;
410+ } else {
411+ setProps ( {
412+ drag_value : newValue ,
413+ } ) ;
321414 }
322415 }
323416 } }
@@ -327,23 +420,24 @@ export default function RangeSlider(props: RangeSliderProps) {
327420
328421 // If empty, default to current value or max_mark
329422 if ( inputValue === '' ) {
330- newMax =
331- value [ value . length - 1 ] ??
332- minMaxValues . max_mark ;
423+ newMax = isNaN ( value [ value . length - 1 ] )
424+ ? minMaxValues . max_mark
425+ : value [ value . length - 1 ] ;
333426 } else {
334427 newMax = parseFloat ( inputValue ) ;
335428 newMax = isNaN ( newMax )
336429 ? minMaxValues . max_mark
337430 : newMax ;
338431 }
339-
340- const constrainedMax = Math . min (
341- minMaxValues . max_mark ,
342- Math . max (
343- value [ 0 ] ?? minMaxValues . min_mark ,
344- newMax
345- )
432+ // Constrain to not be less than the min value
433+ newMax = Math . max (
434+ value [ 0 ] ?? minMaxValues . min_mark ,
435+ newMax
346436 ) ;
437+
438+ // Snap to valid value (respecting step and marks)
439+ const constrainedMax =
440+ constrainToValidValue ( newMax ) ;
347441 const newValue = [ ...value ] ;
348442 newValue [ newValue . length - 1 ] = constrainedMax ;
349443 setValue ( newValue ) ;
@@ -357,7 +451,11 @@ export default function RangeSlider(props: RangeSliderProps) {
357451 ? minMaxValues . min_mark
358452 : value [ 0 ]
359453 }
360- max = { minMaxValues . max_mark }
454+ max = {
455+ isNaN ( minMaxValues . max_mark )
456+ ? max
457+ : minMaxValues . max_mark
458+ }
361459 step = { step || undefined }
362460 disabled = { disabled }
363461 />
@@ -384,6 +482,7 @@ export default function RangeSlider(props: RangeSliderProps) {
384482 step = { stepValue }
385483 disabled = { disabled }
386484 orientation = { vertical ? 'vertical' : 'horizontal' }
485+ inverted = { reverse }
387486 data-included = { included !== false }
388487 minStepsBetweenThumbs = {
389488 typeof pushable === 'number'
@@ -401,14 +500,16 @@ export default function RangeSlider(props: RangeSliderProps) {
401500 renderedMarks ,
402501 ! ! vertical ,
403502 minMaxValues ,
404- ! ! dots
503+ ! ! dots ,
504+ ! ! reverse
405505 ) }
406506 { dots &&
407507 stepValue &&
408508 renderSliderDots (
409509 stepValue ,
410510 minMaxValues ,
411- ! ! vertical
511+ ! ! vertical ,
512+ ! ! reverse
412513 ) }
413514 { /* Render thumbs with tooltips for each value */ }
414515 { value . map ( ( val , index ) => {
0 commit comments