@@ -49,6 +49,7 @@ export default function RangeSlider(props: RangeSliderProps) {
4949 const [ showInputs , setShowInputs ] = useState < boolean > ( value . length === 2 ) ;
5050
5151 const sliderRef = useRef < HTMLDivElement > ( null ) ;
52+ const inputRef = useRef < HTMLInputElement > ( null ) ;
5253
5354 // Handle initial mount - equivalent to componentWillMount
5455 useEffect ( ( ) => {
@@ -192,9 +193,95 @@ export default function RangeSlider(props: RangeSliderProps) {
192193 minWidth ,
193194 Math . min ( maxWidth , charBasedWidth )
194195 ) ;
195- return `${ calculatedWidth } px` ;
196+
197+ // Add padding if box-sizing is border-box
198+ let inputPadding = 0 ;
199+ if ( inputRef . current ) {
200+ const computedStyle = window . getComputedStyle ( inputRef . current ) ;
201+ if ( computedStyle . boxSizing === 'border-box' ) {
202+ const paddingLeft = parseFloat ( computedStyle . paddingLeft ) || 0 ;
203+ const paddingRight =
204+ parseFloat ( computedStyle . paddingRight ) || 0 ;
205+ inputPadding = paddingLeft + paddingRight ;
206+ }
207+ }
208+
209+ const totalWidth = calculatedWidth + inputPadding ;
210+
211+ return `${ totalWidth } px` ;
196212 } , [ sliderWidth , minMaxValues . min_mark , minMaxValues . max_mark ] ) ;
197213
214+ const valueIsValid = ( val : number ) : boolean => {
215+ // Check if value is within min/max bounds
216+ if ( val < minMaxValues . min_mark || val > minMaxValues . max_mark ) {
217+ return false ;
218+ }
219+
220+ // If step is defined, check if value aligns with step
221+ if ( stepValue !== undefined ) {
222+ const min = minMaxValues . min_mark ;
223+ const offset = val - min ;
224+ const remainder = Math . abs ( offset % stepValue ) ;
225+ const epsilon = 0.0001 ; // tolerance for floating point comparison
226+ if ( remainder > epsilon && remainder < stepValue - epsilon ) {
227+ return false ;
228+ }
229+ }
230+
231+ // If step is null and marks exist, value must match a mark
232+ if (
233+ step === null &&
234+ processedMarks &&
235+ typeof processedMarks === 'object'
236+ ) {
237+ const markValues = Object . keys ( processedMarks ) . map ( Number ) ;
238+ const epsilon = 0.0001 ;
239+ return markValues . some ( mark => Math . abs ( val - mark ) < epsilon ) ;
240+ }
241+
242+ return true ;
243+ } ;
244+
245+ const constrainToValidValue = ( val : number ) : number => {
246+ // First constrain to min/max bounds
247+ let constrained = Math . max (
248+ minMaxValues . min_mark ,
249+ Math . min ( minMaxValues . max_mark , val )
250+ ) ;
251+
252+ // If step is null and marks exist, snap to nearest mark
253+ if (
254+ step === null &&
255+ processedMarks &&
256+ typeof processedMarks === 'object'
257+ ) {
258+ return snapToNearestMark ( constrained , processedMarks ) ;
259+ }
260+
261+ // If step is defined, round to nearest step
262+ if ( stepValue !== undefined ) {
263+ const min = minMaxValues . min_mark ;
264+ const steps = Math . round ( ( constrained - min ) / stepValue ) ;
265+ constrained = min + steps * stepValue ;
266+
267+ // Round to avoid floating point precision issues
268+ // Determine decimal places from step value
269+ const stepStr = stepValue . toString ( ) ;
270+ const decimalPlaces = stepStr . includes ( '.' )
271+ ? stepStr . split ( '.' ) [ 1 ] . length
272+ : 0 ;
273+ constrained = Number ( constrained . toFixed ( decimalPlaces ) ) ;
274+
275+ // Ensure we stay within bounds after rounding
276+ constrained = Math . max (
277+ minMaxValues . min_mark ,
278+ Math . min ( minMaxValues . max_mark , constrained )
279+ ) ;
280+ }
281+
282+ return constrained ;
283+ } ;
284+
198285 const handleValueChange = ( newValue : number [ ] ) => {
199286 let adjustedValue = newValue ;
200287
@@ -233,26 +320,25 @@ export default function RangeSlider(props: RangeSliderProps) {
233320 type = "number"
234321 className = "dash-input-container dash-range-slider-input dash-range-slider-min-input"
235322 style = { { width : inputWidth } }
236- value = { value [ 0 ] ?? '' }
323+ value = { isNaN ( value [ 0 ] ) ? '' : value [ 0 ] }
237324 onChange = { e => {
238325 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- }
326+
327+ // Parse the input value
328+ const newMin = parseFloat ( inputValue ) ;
329+ const newValue = [ newMin , value [ 1 ] ] ;
330+ setValue ( newValue ) ;
331+ // Only update props if value is valid
332+ if ( valueIsValid ( newMin ) ) {
333+ if ( updatemode === 'drag' ) {
334+ setProps ( {
335+ value : newValue ,
336+ drag_value : newValue ,
337+ } ) ;
338+ } else {
339+ setProps ( {
340+ drag_value : newValue ,
341+ } ) ;
256342 }
257343 }
258344 } }
@@ -262,21 +348,25 @@ export default function RangeSlider(props: RangeSliderProps) {
262348
263349 // If empty, default to current value or min_mark
264350 if ( inputValue === '' ) {
265- newMin = value [ 0 ] ?? minMaxValues . min_mark ;
351+ newMin = isNaN ( value [ 0 ] )
352+ ? minMaxValues . min_mark
353+ : value [ 0 ] ;
266354 } else {
267355 newMin = parseFloat ( inputValue ) ;
268356 newMin = isNaN ( newMin )
269357 ? minMaxValues . min_mark
270358 : newMin ;
271359 }
272360
273- const constrainedMin = Math . max (
274- minMaxValues . min_mark ,
275- Math . min (
276- value [ 1 ] ?? minMaxValues . max_mark ,
277- newMin
278- )
361+ // Constrain to not exceed the max value
362+ newMin = Math . min (
363+ value [ 1 ] ?? minMaxValues . max_mark ,
364+ newMin
279365 ) ;
366+
367+ // Snap to valid value (respecting step and marks)
368+ const constrainedMin =
369+ constrainToValidValue ( newMin ) ;
280370 const newValue = [ constrainedMin , value [ 1 ] ] ;
281371 setValue ( newValue ) ;
282372 if ( updatemode === 'mouseup' ) {
@@ -285,39 +375,41 @@ export default function RangeSlider(props: RangeSliderProps) {
285375 } }
286376 pattern = "^\\d*\\.?\\d*$"
287377 min = { minMaxValues . min_mark }
288- max = { value [ 1 ] }
378+ max = { isNaN ( value [ 1 ] ) ? max : value [ 1 ] }
289379 step = { step || undefined }
290380 disabled = { disabled }
291381 />
292382 ) }
293383 { showInputs && ! vertical && (
294384 < input
385+ ref = { inputRef }
295386 type = "number"
296387 className = "dash-input-container dash-range-slider-input dash-range-slider-max-input"
297388 style = { { width : inputWidth } }
298- value = { value [ value . length - 1 ] ?? '' }
389+ value = {
390+ isNaN ( value [ value . length - 1 ] )
391+ ? ''
392+ : value [ value . length - 1 ]
393+ }
299394 onChange = { e => {
300395 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 ;
396+
397+ // Parse the input value
398+ const newMax = parseFloat ( inputValue ) ;
399+ const newValue = [ ...value ] ;
400+ newValue [ newValue . length - 1 ] = newMax ;
401+ setValue ( newValue ) ;
402+ // Only update props if value is valid
403+ if ( valueIsValid ( newMax ) ) {
404+ if ( updatemode === 'drag' ) {
317405 setProps ( {
318406 value : newValue ,
319407 drag_value : newValue ,
320408 } ) ;
409+ } else {
410+ setProps ( {
411+ drag_value : newValue ,
412+ } ) ;
321413 }
322414 }
323415 } }
@@ -327,23 +419,24 @@ export default function RangeSlider(props: RangeSliderProps) {
327419
328420 // If empty, default to current value or max_mark
329421 if ( inputValue === '' ) {
330- newMax =
331- value [ value . length - 1 ] ??
332- minMaxValues . max_mark ;
422+ newMax = isNaN ( value [ value . length - 1 ] )
423+ ? minMaxValues . max_mark
424+ : value [ value . length - 1 ] ;
333425 } else {
334426 newMax = parseFloat ( inputValue ) ;
335427 newMax = isNaN ( newMax )
336428 ? minMaxValues . max_mark
337429 : newMax ;
338430 }
339-
340- const constrainedMax = Math . min (
341- minMaxValues . max_mark ,
342- Math . max (
343- value [ 0 ] ?? minMaxValues . min_mark ,
344- newMax
345- )
431+ // Constrain to not be less than the min value
432+ newMax = Math . max (
433+ value [ 0 ] ?? minMaxValues . min_mark ,
434+ newMax
346435 ) ;
436+
437+ // Snap to valid value (respecting step and marks)
438+ const constrainedMax =
439+ constrainToValidValue ( newMax ) ;
347440 const newValue = [ ...value ] ;
348441 newValue [ newValue . length - 1 ] = constrainedMax ;
349442 setValue ( newValue ) ;
@@ -357,7 +450,11 @@ export default function RangeSlider(props: RangeSliderProps) {
357450 ? minMaxValues . min_mark
358451 : value [ 0 ]
359452 }
360- max = { minMaxValues . max_mark }
453+ max = {
454+ isNaN ( minMaxValues . max_mark )
455+ ? max
456+ : minMaxValues . max_mark
457+ }
361458 step = { step || undefined }
362459 disabled = { disabled }
363460 />
0 commit comments