Skip to content

Commit 504c65d

Browse files
authored
Merge pull request #3472 from plotly/bugfix/dcc-redesign-bugfixes
Bugfix/dcc redesign bugfixes
2 parents 4b66809 + bfe970b commit 504c65d

File tree

6 files changed

+445
-62
lines changed

6 files changed

+445
-62
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.dash-dropdown {
22
display: block;
3+
flex: 1;
34
box-sizing: border-box;
45
margin: calc(var(--Dash-Spacing) * 2) 0;
56
padding: 0;
@@ -44,6 +45,10 @@
4445
height: 100%;
4546
}
4647

48+
.dash-dropdown:focus {
49+
outline: 1px solid var(--Dash-Fill-Interactive-Strong);
50+
}
51+
4752
.dash-dropdown:disabled {
4853
opacity: 0.6;
4954
cursor: not-allowed;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,12 @@ const Dropdown = (props: DropdownProps) => {
420420
</button>
421421
</Popover.Trigger>
422422

423-
<Popover.Portal>
423+
<Popover.Portal
424+
// container is required otherwise popover will be rendered
425+
// at document root, which may be outside of the Dash app (i.e.
426+
// an embedded app)
427+
container={dropdownContainerRef.current?.parentElement}
428+
>
424429
<Popover.Content
425430
ref={dropdownContentRef}
426431
className="dash-dropdown-content"

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

Lines changed: 158 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -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) => {

components/dash-core-components/src/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ export interface SliderProps extends BaseComponentProps<SliderProps> {
176176
*/
177177
included?: boolean;
178178

179+
/**
180+
* If the value is true, the slider is rendered in reverse.
181+
*/
182+
reverse?: boolean;
183+
179184
/**
180185
* Configuration for tooltips describing the current slider value
181186
*/
@@ -277,6 +282,11 @@ export interface RangeSliderProps extends BaseComponentProps<RangeSliderProps> {
277282
*/
278283
included?: boolean;
279284

285+
/**
286+
* If the value is true, the slider is rendered in reverse.
287+
*/
288+
reverse?: boolean;
289+
280290
/**
281291
* Configuration for tooltips describing the current slider values
282292
*/

0 commit comments

Comments
 (0)