Skip to content

Commit 256ae10

Browse files
committed
Refactor Slider to use RangeSlider under the hood; remove now duplicated code
1 parent 2a66ac2 commit 256ae10

File tree

5 files changed

+108
-402
lines changed

5 files changed

+108
-402
lines changed

components/dash-core-components/src/components/Slider.tsx

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import React, {lazy, Suspense} from 'react';
2-
import {PersistedProps, PersistenceTypes, SliderProps} from '../types';
3-
import slider from '../utils/LazyLoader/slider';
1+
import {omit} from 'ramda';
2+
import React, {lazy, Suspense, useCallback, useMemo} from 'react';
3+
import {
4+
PersistedProps,
5+
PersistenceTypes,
6+
RangeSliderProps,
7+
SliderProps,
8+
} from '../types';
9+
import rangeSlider from '../utils/LazyLoader/rangeSlider';
410
import './css/sliders.css';
511

6-
const RealSlider = lazy(slider);
12+
const RealSlider = lazy(rangeSlider);
713

814
/**
915
* A slider component with a single handle.
@@ -17,14 +23,52 @@ export default function Slider({
1723
// eslint-disable-next-line no-magic-numbers
1824
verticalHeight = 400,
1925
step = 1,
26+
setProps,
27+
value,
28+
drag_value,
2029
...props
2130
}: SliderProps) {
31+
// This is actually a wrapper around a RangeSlider.
32+
// We'll modify key `Slider` props to be compatible with a Range Slider.
33+
34+
const mappedValue: RangeSliderProps['value'] = useMemo(() => {
35+
return typeof value === 'number' ? [value] : value;
36+
}, [value]);
37+
38+
const mappedDragValue: RangeSliderProps['drag_value'] = useMemo(() => {
39+
return typeof drag_value === 'number' ? [drag_value] : drag_value;
40+
}, [drag_value]);
41+
42+
const mappedSetProps: RangeSliderProps['setProps'] = useCallback(
43+
newProps => {
44+
const {value, drag_value} = newProps;
45+
const mappedProps: Partial<SliderProps> = omit(
46+
['value', 'drag_value', 'setProps'],
47+
newProps
48+
);
49+
if ('value' in newProps) {
50+
mappedProps.value = value ? value[0] : value;
51+
}
52+
if ('drag_value' in newProps) {
53+
mappedProps.drag_value = drag_value
54+
? drag_value[0]
55+
: drag_value;
56+
}
57+
58+
setProps(mappedProps);
59+
},
60+
[setProps]
61+
);
62+
2263
return (
2364
<Suspense fallback={null}>
2465
<RealSlider
2566
updatemode={updatemode}
2667
verticalHeight={verticalHeight}
2768
step={step}
69+
value={mappedValue}
70+
drag_value={mappedDragValue}
71+
setProps={mappedSetProps}
2872
{...props}
2973
/>
3074
</Suspense>

components/dash-core-components/src/components/css/sliders.css

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,12 @@
66
touch-action: none;
77
width: 100%;
88
height: 14px;
9-
padding: 8px 0;
9+
padding: 8px 0 28px 0;
1010
box-sizing: border-box;
1111
/* Override Radix's default margin/padding behavior */
1212
--radix-slider-thumb-width: 16px;
1313
}
1414

15-
/* Add space for marks when they exist */
16-
.dash-slider-root.has-marks {
17-
padding-bottom: 28px; /* Space for horizontal mark labels below */
18-
}
19-
2015
.dash-slider-root[data-orientation='vertical'].has-marks {
2116
padding-bottom: 0px;
2217
}
@@ -127,9 +122,6 @@
127122
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
128123
background-color: var(--Dash-Fill-Inverse-Strong);
129124
user-select: none;
130-
animation-duration: 400ms;
131-
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
132-
will-change: transform, opacity;
133125
z-index: 1000;
134126
fill: var(--Dash-Fill-Inverse-Strong);
135127
}
@@ -174,15 +166,6 @@
174166
min-width: 0;
175167
}
176168

177-
.dash-slider-input {
178-
width: 80px;
179-
padding: 4px 12px;
180-
color: var(--Dash-Text-Strong);
181-
border-radius: var(--Dash-Spacing);
182-
font-size: 14px;
183-
font-family: inherit;
184-
}
185-
186169
.dash-range-slider-inputs {
187170
display: flex;
188171
flex-direction: column;
@@ -197,4 +180,12 @@
197180
.dash-range-slider-input {
198181
width: 64px;
199182
margin-top: 8px;
183+
-moz-appearance: textfield;
184+
}
185+
186+
/* Hide the number input spinners */
187+
.dash-range-slider-input::-webkit-inner-spin-button,
188+
.dash-range-slider-input::-webkit-outer-spin-button {
189+
-webkit-appearance: none;
190+
margin: 0;
200191
}

components/dash-core-components/src/fragments/RangeSlider.tsx

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export default function RangeSlider(props: RangeSliderProps) {
3030
setProps,
3131
tooltip,
3232
updatemode,
33-
min = 0,
34-
max = 100,
33+
min,
34+
max,
3535
marks,
3636
step,
3737
vertical,
@@ -46,7 +46,7 @@ export default function RangeSlider(props: RangeSliderProps) {
4646
} = props;
4747

4848
// For range slider, we expect an array of values
49-
const [value, setValue] = useState<number[]>(propValue || [min, max]);
49+
const [value, setValue] = useState<number[]>(propValue || []);
5050

5151
// Track slider dimension (width for horizontal, height for vertical) for conditional input rendering
5252
const [sliderWidth, setSliderWidth] = useState<number | null>(null);
@@ -60,8 +60,7 @@ export default function RangeSlider(props: RangeSliderProps) {
6060
setValue(propValue);
6161
} else {
6262
// Default to range from min to max if no value provided
63-
const defaultValue = [min, max];
64-
setProps({drag_value: defaultValue});
63+
const defaultValue = [min ?? (propValue ? propValue[0] : 0)];
6564
setValue(defaultValue);
6665
}
6766
}, []);
@@ -80,7 +79,7 @@ export default function RangeSlider(props: RangeSliderProps) {
8079
return;
8180
}
8281

83-
if (value.length !== 2) {
82+
if (!value || value.length > 2) {
8483
setShowInputs(false);
8584
return;
8685
}
@@ -93,8 +92,10 @@ export default function RangeSlider(props: RangeSliderProps) {
9392
if (dimension > 0) {
9493
setSliderWidth(dimension);
9594

96-
const HIDE_AT_WIDTH = 250;
97-
const SHOW_AT_WIDTH = 450;
95+
// eslint-disable-next-line no-magic-numbers
96+
const HIDE_AT_WIDTH = value.length === 1 ? 200 : 250;
97+
// eslint-disable-next-line no-magic-numbers
98+
const SHOW_AT_WIDTH = value.length === 1 ? 300 : 450;
9899
if (showInputs && dimension < HIDE_AT_WIDTH) {
99100
setShowInputs(false);
100101
} else if (!showInputs && dimension >= SHOW_AT_WIDTH) {
@@ -131,12 +132,17 @@ export default function RangeSlider(props: RangeSliderProps) {
131132

132133
// Check if marks exceed 500 limit for performance
133134
let processedMarks = marks;
134-
if (marks && Object.keys(marks).length > MAX_MARKS) {
135-
/* eslint-disable no-console */
136-
console.warn(
137-
`RangeSlider marks exceed ${MAX_MARKS} limit for performance. Marks have been disabled.`
138-
);
139-
processedMarks = undefined;
135+
if (marks && typeof marks === 'object' && marks !== null) {
136+
const marksCount = Object.keys(marks).length;
137+
if (marksCount > MAX_MARKS) {
138+
/* eslint-disable no-console */
139+
console.error(
140+
`Slider: Too many marks (${marksCount}) provided. ` +
141+
`For performance reasons, marks are limited to 500. ` +
142+
`Using auto-generated marks instead.`
143+
);
144+
processedMarks = undefined;
145+
}
140146
}
141147

142148
const minMaxValues = useMemo(() => {
@@ -218,7 +224,7 @@ export default function RangeSlider(props: RangeSliderProps) {
218224
className="dash-slider-container"
219225
{...loadingProps}
220226
>
221-
{showInputs && !vertical && (
227+
{showInputs && value.length === 2 && !vertical && (
222228
<input
223229
type="number"
224230
className="dash-input-container dash-range-slider-input dash-range-slider-min-input"
@@ -272,6 +278,7 @@ export default function RangeSlider(props: RangeSliderProps) {
272278
setProps({value: newValue});
273279
}
274280
}}
281+
pattern="^\\d*\\.?\\d*$"
275282
min={minMaxValues.min_mark}
276283
max={value[1]}
277284
step={step || undefined}
@@ -378,26 +385,29 @@ export default function RangeSlider(props: RangeSliderProps) {
378385
<input
379386
type="number"
380387
className="dash-input-container dash-range-slider-input"
381-
value={value[1] ?? ''}
388+
value={value[value.length - 1] ?? ''}
382389
onChange={e => {
383390
const inputValue = e.target.value;
384391
// Allow empty string (user is clearing the field)
385392
if (inputValue === '') {
386393
// Don't update props while user is typing, just update local state
387-
setValue([value[0], null as any]);
394+
const newValue = [...value];
395+
newValue[newValue.length - 1] = '' as any;
396+
setValue(newValue);
388397
} else {
389398
const newMax = parseFloat(inputValue);
390-
if (!isNaN(newMax)) {
391-
const newValue = [value[0], newMax];
392-
setValue(newValue);
393-
if (updatemode === 'drag') {
394-
setProps({
395-
value: newValue,
396-
drag_value: newValue,
397-
});
398-
} else {
399-
setProps({drag_value: newValue});
400-
}
399+
const constrainedMax = Math.max(
400+
minMaxValues.min_mark,
401+
Math.min(minMaxValues.max_mark, newMax)
402+
);
403+
404+
if (newMax === constrainedMax) {
405+
const newValue = [...value];
406+
newValue[newValue.length - 1] = newMax;
407+
setProps({
408+
value: newValue,
409+
drag_value: newValue,
410+
});
401411
}
402412
}
403413
}}
@@ -407,7 +417,9 @@ export default function RangeSlider(props: RangeSliderProps) {
407417

408418
// If empty, default to current value or max_mark
409419
if (inputValue === '') {
410-
newMax = value[1] ?? minMaxValues.max_mark;
420+
newMax =
421+
value[value.length - 1] ??
422+
minMaxValues.max_mark;
411423
} else {
412424
newMax = parseFloat(inputValue);
413425
newMax = isNaN(newMax)
@@ -422,13 +434,19 @@ export default function RangeSlider(props: RangeSliderProps) {
422434
newMax
423435
)
424436
);
425-
const newValue = [value[0], constrainedMax];
437+
const newValue = [...value];
438+
newValue[newValue.length - 1] = constrainedMax;
426439
setValue(newValue);
427440
if (updatemode === 'mouseup') {
428441
setProps({value: newValue});
429442
}
430443
}}
431-
min={value[0]}
444+
pattern="^\\d*\\.?\\d*$"
445+
min={
446+
value.length === 1
447+
? minMaxValues.min_mark
448+
: value[0]
449+
}
432450
max={minMaxValues.max_mark}
433451
step={step || undefined}
434452
disabled={disabled}

0 commit comments

Comments
 (0)