Skip to content

Commit c4ca03b

Browse files
committed
Merge branch 'dev' into bugfix/dash-embedded-83
2 parents b156dc4 + ce0aabe commit c4ca03b

File tree

7 files changed

+170
-10
lines changed

7 files changed

+170
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/).
88

99
- [#2589](https://github.com/plotly/dash/pull/2589) CSS for input elements not scoped to Dash application
1010

11+
## Changed
12+
13+
- [#2593](https://github.com/plotly/dash/pull/2593) dcc.Input accepts a number for its debounce argument
14+
1115
## [2.11.1] - 2023-06-29
1216

1317
## Fixed

components/dash-core-components/.eslintrc

Lines changed: 1 addition & 1 deletion
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"]

components/dash-core-components/src/components/Input.react.js

Lines changed: 42 additions & 7 deletions
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,
@@ -125,6 +135,21 @@ export default class Input extends PureComponent {
125135
} else {
126136
this.props.setProps({value});
127137
}
138+
this.setState({pendingEvent: undefined});
139+
}
140+
141+
debounceEvent(seconds = 0.5) {
142+
const {value} = this.input.current;
143+
144+
window.clearTimeout(this.state?.pendingEvent);
145+
const pendingEvent = window.setTimeout(() => {
146+
this.onEvent();
147+
}, seconds * 1000);
148+
149+
this.setState({
150+
value,
151+
pendingEvent,
152+
});
128153
}
129154

130155
onBlur() {
@@ -133,7 +158,7 @@ export default class Input extends PureComponent {
133158
n_blur_timestamp: Date.now(),
134159
});
135160
this.input.current.checkValidity();
136-
return this.props.debounce && this.onEvent();
161+
return this.props.debounce === true && this.onEvent();
137162
}
138163

139164
onKeyPress(e) {
@@ -144,14 +169,22 @@ export default class Input extends PureComponent {
144169
});
145170
this.input.current.checkValidity();
146171
}
147-
return this.props.debounce && e.key === 'Enter' && this.onEvent();
172+
return (
173+
this.props.debounce === true && e.key === 'Enter' && this.onEvent()
174+
);
148175
}
149176

150177
onChange() {
151-
if (!this.props.debounce) {
178+
const {debounce} = this.props;
179+
if (debounce) {
180+
if (Number.isFinite(debounce)) {
181+
this.debounceEvent(debounce);
182+
}
183+
if (this.props.type !== 'number') {
184+
this.setState({value: this.input.current.value});
185+
}
186+
} else {
152187
this.onEvent();
153-
} else if (this.props.type !== 'number') {
154-
this.setState({value: this.input.current.value});
155188
}
156189
}
157190
}
@@ -192,9 +225,11 @@ Input.propTypes = {
192225

193226
/**
194227
* If true, changes to input will be sent back to the Dash server only on enter or when losing focus.
195-
* If it's false, it will sent the value back on every change.
228+
* If it's false, it will send the value back on every change.
229+
* If a number, it will not send anything back to the Dash server until the user has stopped
230+
* typing for that number of seconds.
196231
*/
197-
debounce: PropTypes.bool,
232+
debounce: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]),
198233

199234
/**
200235
* 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.

components/dash-core-components/tests/integration/input/conftest.py

Lines changed: 65 additions & 0 deletions
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
Lines changed: 56 additions & 0 deletions
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() == []

requires-all.txt

Lines changed: 1 addition & 1 deletion
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

tests/integration/devtools/test_props_check.py

Lines changed: 1 addition & 1 deletion
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)