Skip to content

Commit 57c5e53

Browse files
committed
Improve input controls for slider
1 parent 7909533 commit 57c5e53

File tree

5 files changed

+142
-60
lines changed

5 files changed

+142
-60
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
--Dash-Spacing: 4px;
33
--Dash-Stroke-Strong: rgba(0, 18, 77, 0.45);
44
--Dash-Stroke-Weak: rgba(0, 24, 102, 0.1);
5-
--Dash-Fill-Interactive-Strong: #7F4BC4;
5+
--Dash-Fill-Interactive-Strong: #7f4bc4;
66
--Dash-Fill-Weak: rgba(0, 30, 128, 0.04);
7-
--Dash-Fill-Inverse-strong: #fff;
7+
--Dash-Fill-Inverse-Strong: #fff;
88
--Dash-Text-Primary: rgba(0, 18, 77, 0.87);
9+
--Dash-Text-Strong: rgba(0, 9, 38, 0.9);
910
--Dash-Fill-Primary-Hover: rgba(0, 18, 77, 0.04);
1011
--Dash-Fill-Primary-Active: rgba(0, 18, 77, 0.08);
11-
--Dash-Fill-Disabled: rgba(0, 24, 102, 0.10);
12+
--Dash-Fill-Disabled: rgba(0, 24, 102, 0.1);
1213
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
.dash-input-container {
33
position: relative;
44
display: inline-flex;
5-
align-items: center;
5+
align-items: center;
66
width: 170px; /* default input width */
77
height: 32px;
88
border-radius: var(--Dash-Spacing);
@@ -15,7 +15,7 @@ align-items: center;
1515
/* Input field styles */
1616
.dash-input-element {
1717
padding: var(--Dash-Spacing) calc(2 * var(--Dash-Spacing));
18-
background: var(--Dash-Fill-Inverse-strong);
18+
background: var(--Dash-Fill-Inverse-Strong);
1919
border: none;
2020
border-radius: var(--Dash-Spacing);
2121
flex: 1 1 0;
@@ -58,7 +58,7 @@ align-items: center;
5858
width: 32px;
5959
height: 100%;
6060
border: none;
61-
background: var(--Dash-Fill-Inverse-strong);
61+
background: var(--Dash-Fill-Inverse-Strong);
6262
cursor: pointer;
6363
font-size: 16px;
6464
font-weight: bold;
@@ -74,7 +74,7 @@ align-items: center;
7474
}
7575

7676
.dash-input-stepper:disabled {
77-
background: var(--Dash-Fill-Inverse-strong);
77+
background: var(--Dash-Fill-Inverse-Strong);
7878
opacity: 0.5;
7979
cursor: default;
8080
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@
116116
width: 8px;
117117
height: 8px;
118118
border-radius: 50%;
119-
background-color: var(--Dash-Fill-Weak);
120-
background-color: red;
119+
background-color: var(--Dash-Fill-Disabled);
121120
}
122121

123122
.dash-slider-tooltip {
@@ -126,12 +125,13 @@
126125
font-size: 12px;
127126
line-height: 1;
128127
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
129-
background-color: var(--Dash-Fill-Inverse-strong);
128+
background-color: var(--Dash-Fill-Inverse-Strong);
130129
user-select: none;
131130
animation-duration: 400ms;
132131
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
133132
will-change: transform, opacity;
134133
z-index: 1000;
134+
fill: var(--Dash-Fill-Inverse-Strong);
135135
}
136136

137137
/* Include property to mimic rc-slider behavior */
@@ -176,9 +176,9 @@
176176

177177
.dash-slider-input {
178178
width: 80px;
179-
padding: 4px 8px;
180-
border: 1px solid #d1d5db;
181-
border-radius: 4px;
179+
padding: 4px 12px;
180+
color: var(--Dash-Text-Strong);
181+
border-radius: var(--Dash-Spacing);
182182
font-size: 14px;
183183
font-family: inherit;
184184
}

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

Lines changed: 89 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default function RangeSlider(props: RangeSliderProps) {
5050

5151
// Track slider dimension (width for horizontal, height for vertical) for conditional input rendering
5252
const [sliderWidth, setSliderWidth] = useState<number | null>(null);
53-
const [showInputs, setShowInputs] = useState<boolean>(true);
53+
const [showInputs, setShowInputs] = useState<boolean>(value.length === 2);
5454
const sliderRef = useRef<HTMLDivElement>(null);
5555

5656
// Handle initial mount - equivalent to componentWillMount
@@ -72,6 +72,19 @@ export default function RangeSlider(props: RangeSliderProps) {
7272
return;
7373
}
7474

75+
if (step === null) {
76+
// If the user has explicitly disabled stepping (step=None), then
77+
// the slider values are constrained to the given marks and user
78+
// cannot enter arbitrary values via the input element
79+
setShowInputs(false);
80+
return;
81+
}
82+
83+
if (value.length !== 2) {
84+
setShowInputs(false);
85+
return;
86+
}
87+
7588
const measureWidth = () => {
7689
if (sliderRef.current) {
7790
const rect = sliderRef.current.getBoundingClientRect();
@@ -106,7 +119,7 @@ export default function RangeSlider(props: RangeSliderProps) {
106119
return () => {
107120
resizeObserver.disconnect();
108121
};
109-
}, [showInputs, vertical]);
122+
}, [showInputs, vertical, step, value]);
110123

111124
// Handle prop value changes - equivalent to componentWillReceiveProps
112125
useEffect(() => {
@@ -209,32 +222,49 @@ export default function RangeSlider(props: RangeSliderProps) {
209222
<input
210223
type="number"
211224
className="dash-input-container dash-range-slider-input dash-range-slider-min-input"
212-
value={value[0] || ''}
225+
value={value[0] ?? ''}
213226
onChange={e => {
214-
const newMin = parseFloat(e.target.value);
215-
if (!isNaN(newMin)) {
216-
const newValue = [newMin, value[1]];
217-
setValue(newValue);
218-
if (updatemode === 'drag') {
219-
setProps({
220-
value: newValue,
221-
drag_value: newValue,
222-
});
223-
} else {
224-
setProps({drag_value: newValue});
227+
const inputValue = e.target.value;
228+
// Allow empty string (user is clearing the field)
229+
if (inputValue === '') {
230+
// Don't update props while user is typing, just update local state
231+
setValue([null as any, value[1]]);
232+
} else {
233+
const newMin = parseFloat(inputValue);
234+
if (!isNaN(newMin)) {
235+
const newValue = [newMin, value[1]];
236+
setValue(newValue);
237+
if (updatemode === 'drag') {
238+
setProps({
239+
value: newValue,
240+
drag_value: newValue,
241+
});
242+
} else {
243+
setProps({drag_value: newValue});
244+
}
225245
}
226246
}
227247
}}
228248
onBlur={e => {
229-
let newMin =
230-
parseFloat(e.target.value) ||
231-
minMaxValues.min_mark;
232-
newMin = isNaN(newMin)
233-
? minMaxValues.min_mark
234-
: newMin;
249+
const inputValue = e.target.value;
250+
let newMin: number;
251+
252+
// If empty, default to current value or min_mark
253+
if (inputValue === '') {
254+
newMin = value[0] ?? minMaxValues.min_mark;
255+
} else {
256+
newMin = parseFloat(inputValue);
257+
newMin = isNaN(newMin)
258+
? minMaxValues.min_mark
259+
: newMin;
260+
}
261+
235262
const constrainedMin = Math.max(
236263
minMaxValues.min_mark,
237-
Math.min(value[1], newMin)
264+
Math.min(
265+
value[1] ?? minMaxValues.max_mark,
266+
newMin
267+
)
238268
);
239269
const newValue = [constrainedMin, value[1]];
240270
setValue(newValue);
@@ -244,6 +274,7 @@ export default function RangeSlider(props: RangeSliderProps) {
244274
}}
245275
min={minMaxValues.min_mark}
246276
max={value[1]}
277+
step={step || undefined}
247278
disabled={disabled}
248279
/>
249280
)}
@@ -347,32 +378,49 @@ export default function RangeSlider(props: RangeSliderProps) {
347378
<input
348379
type="number"
349380
className="dash-input-container dash-range-slider-input"
350-
value={value[1] || ''}
381+
value={value[1] ?? ''}
351382
onChange={e => {
352-
const newMax = parseFloat(e.target.value);
353-
if (!isNaN(newMax)) {
354-
const newValue = [value[0], newMax];
355-
setValue(newValue);
356-
if (updatemode === 'drag') {
357-
setProps({
358-
value: newValue,
359-
drag_value: newValue,
360-
});
361-
} else {
362-
setProps({drag_value: newValue});
383+
const inputValue = e.target.value;
384+
// Allow empty string (user is clearing the field)
385+
if (inputValue === '') {
386+
// Don't update props while user is typing, just update local state
387+
setValue([value[0], null as any]);
388+
} else {
389+
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+
}
363401
}
364402
}
365403
}}
366404
onBlur={e => {
367-
let newMax =
368-
parseFloat(e.target.value) ||
369-
minMaxValues.max_mark;
370-
newMax = isNaN(newMax)
371-
? minMaxValues.max_mark
372-
: newMax;
405+
const inputValue = e.target.value;
406+
let newMax: number;
407+
408+
// If empty, default to current value or max_mark
409+
if (inputValue === '') {
410+
newMax = value[1] ?? minMaxValues.max_mark;
411+
} else {
412+
newMax = parseFloat(inputValue);
413+
newMax = isNaN(newMax)
414+
? minMaxValues.max_mark
415+
: newMax;
416+
}
417+
373418
const constrainedMax = Math.min(
374419
minMaxValues.max_mark,
375-
Math.max(value[0], newMax)
420+
Math.max(
421+
value[0] ?? minMaxValues.min_mark,
422+
newMax
423+
)
376424
);
377425
const newValue = [value[0], constrainedMax];
378426
setValue(newValue);
@@ -382,6 +430,7 @@ export default function RangeSlider(props: RangeSliderProps) {
382430
}}
383431
min={value[0]}
384432
max={minMaxValues.max_mark}
433+
step={step || undefined}
385434
disabled={disabled}
386435
/>
387436
)}

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

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ export default function Slider(props: SliderProps) {
6767
return;
6868
}
6969

70+
if (step === null) {
71+
// If the user has explicitly disabled stepping (step=None), then
72+
// the slider values are constrained to the given marks and user
73+
// cannot enter arbitrary values via the input element
74+
setShowInput(false);
75+
return;
76+
}
77+
7078
const measureWidth = () => {
7179
if (sliderRef.current) {
7280
const rect = sliderRef.current.getBoundingClientRect();
@@ -101,7 +109,7 @@ export default function Slider(props: SliderProps) {
101109
return () => {
102110
resizeObserver.disconnect();
103111
};
104-
}, [showInput, vertical]);
112+
}, [showInput, vertical, step]);
105113

106114
useEffect(() => {
107115
if (propValue !== value) {
@@ -288,23 +296,47 @@ export default function Slider(props: SliderProps) {
288296
<input
289297
type="number"
290298
className="dash-input-container dash-slider-input"
291-
value={value || ''}
299+
value={value ?? ''}
292300
onChange={e => {
293-
setValue(parseFloat(e.target.value));
301+
const inputValue = e.target.value;
302+
// Allow empty string (user is clearing the field)
303+
if (inputValue === '') {
304+
setValue(null);
305+
} else {
306+
const numericValue = parseFloat(inputValue);
307+
if (!isNaN(numericValue)) {
308+
setValue(numericValue);
309+
}
310+
}
294311
}}
295312
onBlur={e => {
296-
// Constrain value to the given min and max
297-
let value = parseFloat(e.target.value) || 0;
298-
value = isNaN(value) ? 0 : value;
313+
const inputValue = e.target.value;
314+
// If empty, use the current slider value or default to min
315+
if (inputValue === '') {
316+
const defaultValue =
317+
value ?? minMaxValues.min_mark;
318+
setValue(defaultValue);
319+
return;
320+
}
321+
322+
// Otherwise, constrain value to the given min and max
323+
let numericValue = parseFloat(inputValue);
324+
numericValue = isNaN(numericValue)
325+
? minMaxValues.min_mark
326+
: numericValue;
299327
const constrainedValue = Math.max(
300328
minMaxValues.min_mark,
301-
Math.min(minMaxValues.max_mark, value || 0)
329+
Math.min(
330+
minMaxValues.max_mark,
331+
numericValue
332+
)
302333
);
303334
setValue(constrainedValue);
304335
}}
305336
pattern="^\\d*\\.?\\d*$"
306337
min={minMaxValues.min_mark}
307338
max={minMaxValues.max_mark}
339+
step={step || undefined}
308340
disabled={disabled}
309341
/>
310342
)}

0 commit comments

Comments
 (0)