Skip to content

Commit

Permalink
Merge branch 'dev' into fix/dash_table-missing-key-prop
Browse files Browse the repository at this point in the history
  • Loading branch information
T4rk1n authored Jul 20, 2023
2 parents bd51176 + 3c43d95 commit 3c202b5
Showing 14 changed files with 384 additions and 37 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,11 +2,18 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).


## [UNRELEASED]

## Fixed

- [#2596](https://github.com/plotly/dash/pull/2596) Fix react-dom throwing unique key prop error for markdown table, fix [#1433](https://github.com/plotly/dash/issues/1433)
- [#2589](https://github.com/plotly/dash/pull/2589) CSS for input elements not scoped to Dash application
- [#2599](https://github.com/plotly/dash/pull/2599) Fix background callback cancel inputs used in multiple callbacks and mixed cancel inputs across pages.

## Changed

- [#2593](https://github.com/plotly/dash/pull/2593) dcc.Input accepts a number for its debounce argument

## [2.11.1] - 2023-06-29

2 changes: 1 addition & 1 deletion components/dash-core-components/.eslintrc
Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@
}],
"no-magic-numbers": ["error", {
"ignoreArrayIndexes": true,
"ignore": [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, 10, 16, 0.5, 25]
"ignore": [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 100, 10, 16, 0.5, 25, 1000]
}],
"no-underscore-dangle": ["off"],
"no-useless-escape": ["off"]
53 changes: 46 additions & 7 deletions components/dash-core-components/src/components/Input.react.js
Original file line number Diff line number Diff line change
@@ -20,18 +20,28 @@ export default class Input extends PureComponent {
constructor(props) {
super(props);

this.state = {
pendingEvent: undefined,
value: '',
};

this.input = React.createRef();

this.onBlur = this.onBlur.bind(this);
this.onChange = this.onChange.bind(this);
this.onEvent = this.onEvent.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.debounceEvent = this.debounceEvent.bind(this);
this.setInputValue = this.setInputValue.bind(this);
this.setPropValue = this.setPropValue.bind(this);
}

UNSAFE_componentWillReceiveProps(nextProps) {
const {value} = this.input.current;
if (this.state.pendingEvent) {
// avoid updating the input while awaiting a debounced event
return;
}
const valueAsNumber = convert(value);
this.setInputValue(
isNil(valueAsNumber) ? value : valueAsNumber,
@@ -61,18 +71,22 @@ export default class Input extends PureComponent {
const valprops =
this.props.type === 'number' ? {} : {value: this.state.value};
const {loading_state} = this.props;
let {className} = this.props;
className = 'dash-input' + (className ? ` ${className}` : '');
return (
<input
data-dash-is-loading={
(loading_state && loading_state.is_loading) || undefined
}
className={className}
ref={this.input}
onBlur={this.onBlur}
onChange={this.onChange}
onKeyPress={this.onKeyPress}
{...valprops}
{...omit(
[
'className',
'debounce',
'value',
'n_blur',
@@ -121,6 +135,21 @@ export default class Input extends PureComponent {
} else {
this.props.setProps({value});
}
this.setState({pendingEvent: undefined});
}

debounceEvent(seconds = 0.5) {
const {value} = this.input.current;

window.clearTimeout(this.state?.pendingEvent);
const pendingEvent = window.setTimeout(() => {
this.onEvent();
}, seconds * 1000);

this.setState({
value,
pendingEvent,
});
}

onBlur() {
@@ -129,7 +158,7 @@ export default class Input extends PureComponent {
n_blur_timestamp: Date.now(),
});
this.input.current.checkValidity();
return this.props.debounce && this.onEvent();
return this.props.debounce === true && this.onEvent();
}

onKeyPress(e) {
@@ -140,14 +169,22 @@ export default class Input extends PureComponent {
});
this.input.current.checkValidity();
}
return this.props.debounce && e.key === 'Enter' && this.onEvent();
return (
this.props.debounce === true && e.key === 'Enter' && this.onEvent()
);
}

onChange() {
if (!this.props.debounce) {
const {debounce} = this.props;
if (debounce) {
if (Number.isFinite(debounce)) {
this.debounceEvent(debounce);
}
if (this.props.type !== 'number') {
this.setState({value: this.input.current.value});
}
} else {
this.onEvent();
} else if (this.props.type !== 'number') {
this.setState({value: this.input.current.value});
}
}
}
@@ -188,9 +225,11 @@ Input.propTypes = {

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

/**
* 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.
6 changes: 3 additions & 3 deletions components/dash-core-components/src/components/css/input.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
input:invalid {
input.dash-input:invalid {
outline: solid red;
}

input:valid {
input.dash-input:valid {
outline: none black;
}
}
Original file line number Diff line number Diff line change
@@ -57,3 +57,68 @@ def range_out(val):
return val

yield app


@pytest.fixture(scope="module")
def debounce_text_app():
app = Dash(__name__)
app.layout = html.Div(
[
dcc.Input(
id="input-slow",
debounce=3,
placeholder="long wait",
),
html.Div(id="div-slow"),
dcc.Input(
id="input-fast",
debounce=0.25,
placeholder="short wait",
),
html.Div(id="div-fast"),
]
)

@app.callback(
[Output("div-slow", "children"), Output("div-fast", "children")],
[Input("input-slow", "value"), Input("input-fast", "value")],
)
def render(slow_val, fast_val):
return [slow_val, fast_val]

yield app


@pytest.fixture(scope="module")
def debounce_number_app():
app = Dash(__name__)
app.layout = html.Div(
[
dcc.Input(
id="input-slow",
debounce=3,
type="number",
placeholder="long wait",
),
html.Div(id="div-slow"),
dcc.Input(
id="input-fast",
debounce=0.25,
type="number",
min=10,
max=10000,
step=3,
placeholder="short wait",
),
html.Div(id="div-fast"),
]
)

@app.callback(
[Output("div-slow", "children"), Output("div-fast", "children")],
[Input("input-slow", "value"), Input("input-fast", "value")],
)
def render(slow_val, fast_val):
return [slow_val, fast_val]

yield app
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.by import By
import pytest


def test_debounce_text_by_time(dash_dcc, debounce_text_app):
dash_dcc.start_server(debounce_text_app)

# expect that a long debounce does not call back in a short amount of time
elem = dash_dcc.find_element("#input-slow")
elem.send_keys("unit test slow")
with pytest.raises(TimeoutException):
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='unit test slow']")
)

# but do expect that it is eventually called
dash_dcc.wait_for_text_to_equal(
"#div-slow", "unit test slow"
), "long debounce is eventually called back"

# expect that a short debounce calls back within a short amount of time
elem = dash_dcc.find_element("#input-fast")
elem.send_keys("unit test fast")
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='unit test fast']")
)

assert dash_dcc.get_logs() == []


def test_debounce_number_by_time(dash_dcc, debounce_number_app):
dash_dcc.start_server(debounce_number_app)

# expect that a long debounce does not call back in a short amount of time
elem = dash_dcc.find_element("#input-slow")
elem.send_keys("12345")
with pytest.raises(TimeoutException):
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='12345']")
)

# but do expect that it is eventually called
dash_dcc.wait_for_text_to_equal(
"#div-slow", "12345"
), "long debounce is eventually called back"

# expect that a short debounce calls back within a short amount of time
elem = dash_dcc.find_element("#input-fast")
elem.send_keys("10000")
WebDriverWait(dash_dcc.driver, timeout=1).until(
lambda d: d.find_element(By.XPATH, "//*[text()='10000']")
)

assert dash_dcc.get_logs() == []
Original file line number Diff line number Diff line change
@@ -72,3 +72,34 @@ def test_inbs002_user_class(dash_dcc):
dash_dcc.wait_for_style_to_equal(".test-input-css input", "width", "420px")

assert dash_dcc.get_logs() == []


def test_inbs003_styles_are_scoped(dash_dcc):
app = Dash(__name__)

app.index_string = """
<html>
<body>
<input id="ExternalInput" required />
{%app_entry%}
{%config%}
{%scripts%}
{%renderer%}
</body>
</html>
"""

app.layout = html.Div(
className="test-input-css",
children=[dcc.Input(id="DashInput", required=True, className="unittest")],
)

dash_dcc.start_server(app)

external_input = dash_dcc.find_element("#ExternalInput")
dash_input = dash_dcc.find_element(".unittest")

external_outline_css = external_input.value_of_css_property("outline")
dash_outline_css = dash_input.value_of_css_property("outline")

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

0 comments on commit 3c202b5

Please # to comment.