Skip to content

Commit ce0aabe

Browse files
authored
Merge pull request #2593 from plotly/feature/debounce-time
Feature: dcc.Input accepts a number for its debounce argument
2 parents a7a12d1 + 1691007 commit ce0aabe

File tree

8 files changed

+173
-11
lines changed

8 files changed

+173
-11
lines changed

Diff for: CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## UNRELEASED
6+
7+
## Fixed
8+
9+
- [#2593](https://github.com/plotly/dash/pull/2593) dcc.Input accepts a number for its debounce argument
10+
511
## [2.11.1] - 2023-06-29
612

713
## Fixed

Diff for: components/dash-core-components/.eslintrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@
117117
}],
118118
"no-magic-numbers": ["error", {
119119
"ignoreArrayIndexes": true,
120-
"ignore": [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, 10, 16, 0.5, 25]
120+
"ignore": [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, 10, 16, 0.5, 25, 1000]
121121
}],
122122
"no-underscore-dangle": ["off"],
123123
"no-useless-escape": ["off"]

Diff for: components/dash-core-components/src/components/Input.react.js

+42-7
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,28 @@ export default class Input extends PureComponent {
2020
constructor(props) {
2121
super(props);
2222

23+
this.state = {
24+
pendingEvent: undefined,
25+
value: '',
26+
};
27+
2328
this.input = React.createRef();
2429

2530
this.onBlur = this.onBlur.bind(this);
2631
this.onChange = this.onChange.bind(this);
2732
this.onEvent = this.onEvent.bind(this);
2833
this.onKeyPress = this.onKeyPress.bind(this);
34+
this.debounceEvent = this.debounceEvent.bind(this);
2935
this.setInputValue = this.setInputValue.bind(this);
3036
this.setPropValue = this.setPropValue.bind(this);
3137
}
3238

3339
UNSAFE_componentWillReceiveProps(nextProps) {
3440
const {value} = this.input.current;
41+
if (this.state.pendingEvent) {
42+
// avoid updating the input while awaiting a debounced event
43+
return;
44+
}
3545
const valueAsNumber = convert(value);
3646
this.setInputValue(
3747
isNil(valueAsNumber) ? value : valueAsNumber,
@@ -121,6 +131,21 @@ export default class Input extends PureComponent {
121131
} else {
122132
this.props.setProps({value});
123133
}
134+
this.setState({pendingEvent: undefined});
135+
}
136+
137+
debounceEvent(seconds = 0.5) {
138+
const {value} = this.input.current;
139+
140+
window.clearTimeout(this.state?.pendingEvent);
141+
const pendingEvent = window.setTimeout(() => {
142+
this.onEvent();
143+
}, seconds * 1000);
144+
145+
this.setState({
146+
value,
147+
pendingEvent,
148+
});
124149
}
125150

126151
onBlur() {
@@ -129,7 +154,7 @@ export default class Input extends PureComponent {
129154
n_blur_timestamp: Date.now(),
130155
});
131156
this.input.current.checkValidity();
132-
return this.props.debounce && this.onEvent();
157+
return this.props.debounce === true && this.onEvent();
133158
}
134159

135160
onKeyPress(e) {
@@ -140,14 +165,22 @@ export default class Input extends PureComponent {
140165
});
141166
this.input.current.checkValidity();
142167
}
143-
return this.props.debounce && e.key === 'Enter' && this.onEvent();
168+
return (
169+
this.props.debounce === true && e.key === 'Enter' && this.onEvent()
170+
);
144171
}
145172

146173
onChange() {
147-
if (!this.props.debounce) {
174+
const {debounce} = this.props;
175+
if (debounce) {
176+
if (Number.isFinite(debounce)) {
177+
this.debounceEvent(debounce);
178+
}
179+
if (this.props.type !== 'number') {
180+
this.setState({value: this.input.current.value});
181+
}
182+
} else {
148183
this.onEvent();
149-
} else if (this.props.type !== 'number') {
150-
this.setState({value: this.input.current.value});
151184
}
152185
}
153186
}
@@ -188,9 +221,11 @@ Input.propTypes = {
188221

189222
/**
190223
* If true, changes to input will be sent back to the Dash server only on enter or when losing focus.
191-
* If it's false, it will sent the value back on every change.
224+
* If it's false, it will send the value back on every change.
225+
* If a number, it will not send anything back to the Dash server until the user has stopped
226+
* typing for that number of seconds.
192227
*/
193-
debounce: PropTypes.bool,
228+
debounce: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
194229

195230
/**
196231
* A hint to the user of what can be entered in the control . The placeholder text must not contain carriage returns or line-feeds. Note: Do not use the placeholder attribute instead of a <label> element, their purposes are different. The <label> attribute describes the role of the form element (i.e. it indicates what kind of information is expected), and the placeholder attribute is a hint about the format that the content should take. There are cases in which the placeholder attribute is never displayed to the user, so the form must be understandable without it.

Diff for: components/dash-core-components/tests/integration/input/conftest.py

+65
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,68 @@ def range_out(val):
5757
return val
5858

5959
yield app
60+
61+
62+
@pytest.fixture(scope="module")
63+
def debounce_text_app():
64+
app = Dash(__name__)
65+
app.layout = html.Div(
66+
[
67+
dcc.Input(
68+
id="input-slow",
69+
debounce=3,
70+
placeholder="long wait",
71+
),
72+
html.Div(id="div-slow"),
73+
dcc.Input(
74+
id="input-fast",
75+
debounce=0.25,
76+
placeholder="short wait",
77+
),
78+
html.Div(id="div-fast"),
79+
]
80+
)
81+
82+
@app.callback(
83+
[Output("div-slow", "children"), Output("div-fast", "children")],
84+
[Input("input-slow", "value"), Input("input-fast", "value")],
85+
)
86+
def render(slow_val, fast_val):
87+
return [slow_val, fast_val]
88+
89+
yield app
90+
91+
92+
@pytest.fixture(scope="module")
93+
def debounce_number_app():
94+
app = Dash(__name__)
95+
app.layout = html.Div(
96+
[
97+
dcc.Input(
98+
id="input-slow",
99+
debounce=3,
100+
type="number",
101+
placeholder="long wait",
102+
),
103+
html.Div(id="div-slow"),
104+
dcc.Input(
105+
id="input-fast",
106+
debounce=0.25,
107+
type="number",
108+
min=10,
109+
max=10000,
110+
step=3,
111+
placeholder="short wait",
112+
),
113+
html.Div(id="div-fast"),
114+
]
115+
)
116+
117+
@app.callback(
118+
[Output("div-slow", "children"), Output("div-fast", "children")],
119+
[Input("input-slow", "value"), Input("input-fast", "value")],
120+
)
121+
def render(slow_val, fast_val):
122+
return [slow_val, fast_val]
123+
124+
yield app
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from selenium.common.exceptions import TimeoutException
2+
from selenium.webdriver.support.wait import WebDriverWait
3+
from selenium.webdriver.common.by import By
4+
import pytest
5+
6+
7+
def test_debounce_text_by_time(dash_dcc, debounce_text_app):
8+
dash_dcc.start_server(debounce_text_app)
9+
10+
# expect that a long debounce does not call back in a short amount of time
11+
elem = dash_dcc.find_element("#input-slow")
12+
elem.send_keys("unit test slow")
13+
with pytest.raises(TimeoutException):
14+
WebDriverWait(dash_dcc.driver, timeout=1).until(
15+
lambda d: d.find_element(By.XPATH, "//*[text()='unit test slow']")
16+
)
17+
18+
# but do expect that it is eventually called
19+
dash_dcc.wait_for_text_to_equal(
20+
"#div-slow", "unit test slow"
21+
), "long debounce is eventually called back"
22+
23+
# expect that a short debounce calls back within a short amount of time
24+
elem = dash_dcc.find_element("#input-fast")
25+
elem.send_keys("unit test fast")
26+
WebDriverWait(dash_dcc.driver, timeout=1).until(
27+
lambda d: d.find_element(By.XPATH, "//*[text()='unit test fast']")
28+
)
29+
30+
assert dash_dcc.get_logs() == []
31+
32+
33+
def test_debounce_number_by_time(dash_dcc, debounce_number_app):
34+
dash_dcc.start_server(debounce_number_app)
35+
36+
# expect that a long debounce does not call back in a short amount of time
37+
elem = dash_dcc.find_element("#input-slow")
38+
elem.send_keys("12345")
39+
with pytest.raises(TimeoutException):
40+
WebDriverWait(dash_dcc.driver, timeout=1).until(
41+
lambda d: d.find_element(By.XPATH, "//*[text()='12345']")
42+
)
43+
44+
# but do expect that it is eventually called
45+
dash_dcc.wait_for_text_to_equal(
46+
"#div-slow", "12345"
47+
), "long debounce is eventually called back"
48+
49+
# expect that a short debounce calls back within a short amount of time
50+
elem = dash_dcc.find_element("#input-fast")
51+
elem.send_keys("10000")
52+
WebDriverWait(dash_dcc.driver, timeout=1).until(
53+
lambda d: d.find_element(By.XPATH, "//*[text()='10000']")
54+
)
55+
56+
assert dash_dcc.get_logs() == []

Diff for: components/dash-html-components/scripts/extract-elements.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const expectedElCount = 125;
1313
*/
1414
function extractElements($) {
1515
const excludeElements = [
16-
'html', 'head', 'body', 'style', 'h1–h6', 'input',
16+
'html', 'head', 'body', 'style', 'h1–h6', 'input', 'search',
1717
// out of scope, different namespaces - but Mozilla added these to the
1818
// above reference page Jan 2021 so we need to exclude them now.
1919
// see https://github.com/mdn/content/pull/410

Diff for: requires-all.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
redis>=3.5.3
33
celery[redis]>=5.1.2
44
# Dependencies used by CI on github.com/plotly/dash
5-
black==21.6b0
5+
black==22.3.0
66
dash-flow-example==0.0.5
77
dash-dangerously-set-inner-html
88
flake8==3.9.2

Diff for: tests/integration/devtools/test_props_check.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"fail": True,
88
"name": 'simple "not a boolean" check',
99
"component": dcc.Input,
10-
"props": {"debounce": 0},
10+
"props": {"multiple": 0},
1111
},
1212
"missing-required-nested-prop": {
1313
"fail": True,

0 commit comments

Comments
 (0)