Skip to content

Commit 7909533

Browse files
committed
Fix breaking behaviour around marks/steps options
1 parent f4b2add commit 7909533

File tree

9 files changed

+331
-297
lines changed

9 files changed

+331
-297
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ export default function Slider({
2525
persistence_type = PersistenceTypes.local,
2626
// eslint-disable-next-line no-magic-numbers
2727
verticalHeight = 400,
28+
step = 1,
2829
...rest
2930
}: SliderProps) {
3031
const props = {
3132
updatemode,
3233
persisted_props,
3334
persistence_type,
3435
verticalHeight,
36+
step,
3537
...rest,
3638
};
3739

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

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
padding-bottom: 28px; /* Space for horizontal mark labels below */
1818
}
1919

20-
.dash-slider-root[data-orientation="vertical"].has-marks {
20+
.dash-slider-root[data-orientation='vertical'].has-marks {
2121
padding-bottom: 0px;
2222
}
2323

24-
.dash-slider-root[data-orientation="vertical"] {
24+
.dash-slider-root[data-orientation='vertical'] {
2525
flex-direction: column;
2626
width: 20px;
2727
height: 100%;
@@ -32,10 +32,10 @@
3232
flex-grow: 1;
3333
border-radius: 9999px;
3434
height: 4px;
35-
background-color: #d1d5db;
35+
background-color: var(--Dash-Fill-Disabled);
3636
}
3737

38-
.dash-slider-root[data-orientation="vertical"] .dash-slider-track {
38+
.dash-slider-root[data-orientation='vertical'] .dash-slider-track {
3939
width: 4px;
4040
height: auto;
4141
overflow-y: hidden;
@@ -53,7 +53,7 @@
5353
height: 4px;
5454
}
5555

56-
.dash-slider-root[data-orientation="vertical"] .dash-slider-range {
56+
.dash-slider-root[data-orientation='vertical'] .dash-slider-range {
5757
width: 100%;
5858
height: auto;
5959
}
@@ -91,24 +91,33 @@
9191
}
9292

9393
.dash-slider-mark:before {
94-
content: '';
95-
position: absolute;
96-
bottom: 100%;
97-
left: 50%;
98-
transform: translateX(-50%);
99-
background-color: var(--Dash-Fill-Disabled);
100-
width: 4px;
101-
height: 6px;
102-
border-radius: 2px;
103-
margin-bottom: 2px;
94+
content: '';
95+
position: absolute;
96+
bottom: 100%;
97+
left: 50%;
98+
transform: translateX(-50%);
99+
background-color: var(--Dash-Fill-Disabled);
100+
width: 4px;
101+
height: 6px;
102+
border-radius: 2px;
103+
margin-bottom: 2px;
104+
}
105+
.dash-slider-mark.with-dots:before {
106+
content: initial;
104107
}
105108

106-
.dash-slider-root[data-orientation="vertical"] .dash-slider-mark:before {
107-
content: initial;
109+
.dash-slider-root[data-orientation='vertical'] .dash-slider-mark:before {
110+
content: initial;
108111
}
109112

110113
.dash-slider-dot {
111114
pointer-events: none;
115+
position: absolute;
116+
width: 8px;
117+
height: 8px;
118+
border-radius: 50%;
119+
background-color: var(--Dash-Fill-Weak);
120+
background-color: red;
112121
}
113122

114123
.dash-slider-tooltip {
@@ -126,11 +135,11 @@
126135
}
127136

128137
/* Include property to mimic rc-slider behavior */
129-
.dash-slider-root:not([data-included="false"]) .dash-slider-range {
138+
.dash-slider-root:not([data-included='false']) .dash-slider-range {
130139
background-color: var(--Dash-Fill-Interactive-Strong);
131140
}
132141

133-
.dash-slider-root[data-included="false"] .dash-slider-range {
142+
.dash-slider-root[data-included='false'] .dash-slider-range {
134143
background-color: transparent;
135144
}
136145

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

Lines changed: 34 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -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';
1517
import LoadingElement from '../utils/_LoadingElement';
1618
import {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

Comments
 (0)