Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions components/dash-core-components/src/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ function Input({
(direction: 'increment' | 'decrement') => {
const currentValue = parseFloat(input.current.value) || 0;
const stepAsNum = parseFloat(step as string) || 1;

// Count decimal places to avoid floating point precision issues
const decimalPlaces = (stepAsNum.toString().split('.')[1] || '')
.length;

const newValue =
direction === 'increment'
? currentValue + stepAsNum
Expand All @@ -196,8 +201,13 @@ function Input({
);
}

input.current.value = constrainedValue.toString();
setValue(constrainedValue.toString());
// Round to the step's decimal precision
const roundedValue = parseFloat(
constrainedValue.toFixed(decimalPlaces)
);

input.current.value = roundedValue.toString();
setValue(roundedValue.toString());
onEvent();
},
[step, props.min, props.max, onEvent]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@
z-index: 500;
box-shadow: 0px 10px 38px -10px var(--Dash-Shading-Strong),
0px 10px 20px -15px var(--Dash-Shading-Weak);
overscroll-behavior: contain;
}

.dash-datepicker-calendar-wrapper {
Expand Down Expand Up @@ -226,8 +227,3 @@
width: 20px;
height: 20px;
}

/* Override Radix's position: fixed to use position: absolute when using custom container */
div[data-radix-popper-content-wrapper]:has(.dash-datepicker-content) {
position: absolute !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
z-index: 500;
box-shadow: 0px 10px 38px -10px var(--Dash-Shading-Strong),
0px 10px 20px -15px var(--Dash-Shading-Weak);
overscroll-behavior: contain;
}

.dash-dropdown-value-count,
Expand Down Expand Up @@ -235,8 +236,3 @@
.dash-dropdown-wrapper {
position: relative;
}

/* Override Radix's position: fixed to use position: absolute when using custom container */
div[data-radix-popper-content-wrapper]:has(.dash-dropdown-content) {
position: absolute !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ const DatePickerRange = ({
id={start_date_id || accessibleId}
inputClassName="dash-datepicker-input dash-datepicker-start-date"
value={startInputValue}
onChange={e => setStartInputValue(e.target.value)}
onChange={e => setStartInputValue(e.target?.value)}
onKeyDown={handleStartInputKeyDown}
onFocus={() => {
if (isCalendarOpen) {
Expand All @@ -354,7 +354,7 @@ const DatePickerRange = ({
id={end_date_id || accessibleId + '-end-date'}
inputClassName="dash-datepicker-input dash-datepicker-end-date"
value={endInputValue}
onChange={e => setEndInputValue(e.target.value)}
onChange={e => setEndInputValue(e.target?.value)}
onKeyDown={handleEndInputKeyDown}
onFocus={() => {
if (isCalendarOpen) {
Expand Down Expand Up @@ -384,9 +384,6 @@ const DatePickerRange = ({
className="dash-datepicker-content"
align="start"
sideOffset={5}
collisionBoundary={containerRef.current?.closest(
'#_dash-app-content'
)}
onOpenAutoFocus={e => e.preventDefault()}
onCloseAutoFocus={e => {
e.preventDefault();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ const DatePickerSingle = ({
id={accessibleId}
inputClassName="dash-datepicker-input dash-datepicker-end-date"
value={inputValue}
onChange={e => setInputValue(e.target.value)}
onChange={e => setInputValue(e.target?.value)}
onKeyDown={handleInputKeyDown}
placeholder={placeholder}
disabled={disabled}
Expand All @@ -203,9 +203,6 @@ const DatePickerSingle = ({
className="dash-datepicker-content"
align="start"
sideOffset={5}
collisionBoundary={containerRef.current?.closest(
'#_dash-app-content'
)}
onOpenAutoFocus={e => e.preventDefault()}
onCloseAutoFocus={e => {
e.preventDefault();
Expand Down
3 changes: 0 additions & 3 deletions components/dash-core-components/src/fragments/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -458,9 +458,6 @@ const Dropdown = (props: DropdownProps) => {
className="dash-dropdown-content"
align="start"
sideOffset={5}
collisionBoundary={positioningContainerRef.current?.closest(
'#_dash-app-content'
)}
onOpenAutoFocus={e => e.preventDefault()}
onKeyDown={handleKeyDown}
style={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
import sys
import pytest
from dash import Dash, Input, Output, html, dcc
from selenium.webdriver.common.keys import Keys

Expand Down Expand Up @@ -148,6 +149,93 @@ def update_output(val):
assert dash_dcc.get_logs() == []


@pytest.mark.parametrize("step", [0.1, 0.01, 0.001, 0.0001])
def test_inni006_stepper_floating_point_precision(dash_dcc, step):
"""Test that stepper increments/decrements with decimal steps don't accumulate floating point errors."""

app = Dash(__name__)
app.layout = html.Div(
[
dcc.Input(id="decimal-input", value=0, type="number", step=step),
html.Div(id="output"),
]
)

@app.callback(Output("output", "children"), [Input("decimal-input", "value")])
def update_output(val):
return val

dash_dcc.start_server(app)
increment_btn = dash_dcc.find_element(".dash-stepper-increment")
decrement_btn = dash_dcc.find_element(".dash-stepper-decrement")

# Determine decimal places for formatting
decimal_places = len(str(step).split(".")[1]) if "." in str(step) else 0
num_clicks = 9

# Test increment: without precision fix, accumulates floating point errors (e.g., 0.30000000000000004)
for i in range(1, num_clicks + 1):
increment_btn.click()
expected = format(step * i, f".{decimal_places}f")
dash_dcc.wait_for_text_to_equal("#output", expected)

# Test decrement: should go back down through the same values
for i in range(num_clicks - 1, 0, -1):
decrement_btn.click()
expected = format(step * i, f".{decimal_places}f")
dash_dcc.wait_for_text_to_equal("#output", expected)

# One more decrement to get back to 0
decrement_btn.click()
dash_dcc.wait_for_text_to_equal("#output", "0")

assert dash_dcc.get_logs() == []


@pytest.mark.parametrize("step", [0.00001, 0.000001])
def test_inni007_stepper_very_small_steps(dash_dcc, step):
"""Test that stepper works correctly with very small decimal steps."""

app = Dash(__name__)
app.layout = html.Div(
[
dcc.Input(id="decimal-input", value=0, type="number", step=step),
html.Div(id="output"),
]
)

@app.callback(Output("output", "children"), [Input("decimal-input", "value")])
def update_output(val):
return val

dash_dcc.start_server(app)
increment_btn = dash_dcc.find_element(".dash-stepper-increment")
decrement_btn = dash_dcc.find_element(".dash-stepper-decrement")

# For very small steps, format with enough precision then strip trailing zeros
step_str = f"{step:.10f}".rstrip("0").rstrip(".")
decimal_places = len(step_str.split(".")[1]) if "." in step_str else 0
num_clicks = 5

# Test increment
for i in range(1, num_clicks + 1):
increment_btn.click()
expected = f"{step * i:.{decimal_places}f}".rstrip("0").rstrip(".")
dash_dcc.wait_for_text_to_equal("#output", expected)

# Test decrement
for i in range(num_clicks - 1, 0, -1):
decrement_btn.click()
expected = f"{step * i:.{decimal_places}f}".rstrip("0").rstrip(".")
dash_dcc.wait_for_text_to_equal("#output", expected)

# One more decrement to get back to 0
decrement_btn.click()
dash_dcc.wait_for_text_to_equal("#output", "0")

assert dash_dcc.get_logs() == []


def test_inni010_valid_numbers(dash_dcc, ninput_app):
dash_dcc.start_server(ninput_app)
for num, op in (
Expand Down
Loading