Skip to content

Commit 0824e9b

Browse files
committed
Rework on Input (#580)
* 🐛 fixes both #455 and #513
1 parent 70b338d commit 0824e9b

File tree

11 files changed

+463
-227
lines changed

11 files changed

+463
-227
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,81 @@
11
version: 2
2-
32
jobs:
4-
'python-2.7': &test-template
5-
docker:
6-
- image: circleci/python:2.7-stretch-node-browsers
7-
environment:
8-
PYTHON_VERSION: py27
3+
"percy-finalize":
4+
docker:
5+
- image: percyio/agent
6+
steps:
7+
- run: percy finalize --all
8+
9+
'python-2.7': &test-template
10+
docker:
11+
- image: circleci/python:2.7-stretch-node-browsers
12+
environment:
13+
PYTHON_VERSION: py27
14+
PERCY_PARALLEL_TOTAL: '-1'
915

10-
steps:
11-
- checkout
16+
steps:
17+
- checkout
1218

13-
- run:
14-
name: Write job name
15-
command: echo $CIRCLE_JOB > circlejob.txt
19+
- run:
20+
name: Write job name
21+
command: echo $CIRCLE_JOB > circlejob.txt
1622

17-
- run:
18-
name: Install dependencies
19-
command: |
20-
sudo pip install virtualenv --upgrade
21-
python -m venv || virtualenv venv
22-
. venv/bin/activate
23-
pip install -r dev-requirements.txt
24-
npm install --ignore-scripts
23+
- run:
24+
name: Install dependencies
25+
command: |
26+
sudo pip install virtualenv --upgrade
27+
python -m venv || virtualenv venv
28+
. venv/bin/activate
29+
pip install -r dev-requirements.txt
30+
npm install --ignore-scripts
2531
26-
- run:
27-
name: Install dependencies (dash)
32+
- run:
33+
name: Install dependencies (dash)
34+
command: |
35+
git clone --depth 1 https://github.com/plotly/dash.git dash-main
36+
git clone --depth 1 https://github.com/plotly/dash-html-components.git
37+
. venv/bin/activate
38+
pip install -e ./dash-main[testing] --quiet
39+
cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
40+
cd dash-main/dash-renderer && npm install --ignore-scripts && npm run build && pip install -e . && cd ../..
41+
- run:
42+
name: Build
2843
command: |
29-
git clone --depth 1 https://github.com/plotly/dash.git dash-main
30-
git clone --depth 1 https://github.com/plotly/dash-html-components.git
3144
. venv/bin/activate
32-
pip install -e ./dash-main[testing] --quiet
33-
cd dash-html-components && npm install --ignore-scripts && npm run build && pip install -e . && cd ..
34-
cd dash-main/dash-renderer && npm install --ignore-scripts && npm run build && pip install -e . && cd ../..
35-
- run:
36-
name: Build
37-
command: |
38-
. venv/bin/activate
39-
npm run build
40-
pip install -e . --quiet
41-
pip list | grep dash
45+
npm run build
46+
pip install -e . --quiet
47+
pip list | grep dash
4248
43-
- run:
44-
name: Run tests
45-
command: |
46-
. venv/bin/activate
47-
python --version
48-
npm run test
49+
- run:
50+
name: Run tests
51+
command: |
52+
. venv/bin/activate
53+
python --version
54+
npm run test
4955
50-
'python-3.6':
51-
<<: *test-template
52-
docker:
53-
- image: circleci/python:3.6-stretch-node-browsers
54-
environment:
55-
PYTHON_VERSION: py36
56-
PERCY_ENABLE: 0
56+
'python-3.6':
57+
<<: *test-template
58+
docker:
59+
- image: circleci/python:3.6-stretch-node-browsers
60+
environment:
61+
PYTHON_VERSION: py36
62+
PERCY_ENABLE: 0
5763

58-
'python-3.7':
59-
<<: *test-template
60-
docker:
61-
- image: circleci/python:3.7-stretch-node-browsers
62-
environment:
63-
PYTHON_VERSION: py37
64-
PERCY_ENABLE: 0
64+
'python-3.7':
65+
<<: *test-template
66+
docker:
67+
- image: circleci/python:3.7-stretch-node-browsers
68+
environment:
69+
PYTHON_VERSION: py37
70+
PERCY_ENABLE: 0
6571

6672
workflows:
67-
version: 2
68-
build:
69-
jobs:
73+
version: 2
74+
build:
75+
jobs:
76+
- 'python-2.7'
77+
- "percy-finalize":
78+
requires:
7079
- 'python-2.7'
71-
- 'python-3.6'
72-
- 'python-3.7'
80+
- 'python-3.6'
81+
- 'python-3.7'

packages/dash-core-components/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.
33
This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [Unreleased] -
6+
7+
### Fixed
8+
- Fixed inconsistent behavior of `input` with `type=number` [#580](https://github.com/plotly/dash-core-components/pull/580)
9+
610
### Updated
711
- Upgraded plotly.js to 1.49.0 [#589](https://github.com/plotly/dash-core-components/pull/589)
812
- [Feature release 1.49.0](https://github.com/plotly/plotly.js/releases/tag/v1.49.0) which contains:

packages/dash-core-components/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"lint:py": "flake8 --ignore=E501,F401,F841,F811,W503 tests",
2020
"test": "run-s -c lint lint:py format:test test-unit test:py test:intg",
2121
"test:py": "pytest tests/test_integration.py",
22-
"test:intg": "pytest tests/integration",
22+
"test:intg": "pytest --nopercyfinalize --headless tests/integration",
2323
"test:pyimport": "pytest tests/test_dash_import.py",
2424
"test-unit": "jest",
2525
"format": "prettier --config .prettierrc --write src/**/*.js tests/unit/*.js",
@@ -45,7 +45,8 @@
4545
"react-markdown": "^4.0.6",
4646
"react-select-fast-filter-options": "^0.2.3",
4747
"react-virtualized-select": "^3.1.3",
48-
"uniqid": "^5.0.3"
48+
"uniqid": "^5.0.3",
49+
"fast-isnumeric": "^1.1.3"
4950
},
5051
"devDependencies": {
5152
"@babel/core": "^7.4.0",

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

+106-55
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import React, {Component} from 'react';
1+
import * as R from 'ramda';
2+
import React, {PureComponent} from 'react';
23
import PropTypes from 'prop-types';
3-
import {omit, isEmpty} from 'ramda';
4+
import isNumeric from 'fast-isnumeric';
5+
import './css/input.css';
6+
7+
// eslint-disable-next-line no-implicit-coercion
8+
const convert = val => (isNumeric(val) ? +val : NaN);
9+
10+
const isEquivalent = (v1, v2) => v1 === v2 || (isNaN(v1) && isNaN(v2));
411

512
/**
613
* A basic HTML input control for entering text, numbers, or passwords.
@@ -9,76 +16,60 @@ import {omit, isEmpty} from 'ramda';
916
* the Checklist and RadioItems component. Dates, times, and file uploads
1017
* are also supported through separate components.
1118
*/
12-
export default class Input extends Component {
19+
export default class Input extends PureComponent {
1320
constructor(props) {
1421
super(props);
15-
this.propsToState = this.propsToState.bind(this);
22+
23+
this.input = React.createRef();
24+
25+
this.onBlur = this.onBlur.bind(this);
26+
this.onChange = this.onChange.bind(this);
27+
this.onEvent = this.onEvent.bind(this);
28+
this.onKeyPress = this.onKeyPress.bind(this);
29+
this.setInputValue = this.setInputValue.bind(this);
30+
this.setPropValue = this.setPropValue.bind(this);
1631
}
1732

18-
propsToState(newProps) {
19-
this.setState({value: newProps.value});
33+
componentWillReceiveProps(nextProps) {
34+
const {value, valueAsNumber} = this.input.current;
35+
this.setInputValue(
36+
R.isNil(valueAsNumber) ? value : valueAsNumber,
37+
nextProps.value
38+
);
39+
if (this.props.type !== 'number') {
40+
this.setState({value: nextProps.value});
41+
}
2042
}
2143

22-
componentWillReceiveProps(newProps) {
23-
this.propsToState(newProps);
44+
componentDidMount() {
45+
const {value, valueAsNumber} = this.input.current;
46+
this.setInputValue(
47+
R.isNil(valueAsNumber) ? value : valueAsNumber,
48+
this.props.value
49+
);
2450
}
2551

2652
componentWillMount() {
27-
this.propsToState(this.props);
53+
if (this.props.type !== 'number') {
54+
this.setState({value: this.props.value});
55+
}
2856
}
2957

3058
render() {
31-
const {setProps, type, min, max, debounce, loading_state} = this.props;
32-
const value = this.state.value;
59+
const valprops =
60+
this.props.type === 'number' ? {} : {value: this.state.value};
61+
const {loading_state} = this.props;
3362
return (
3463
<input
3564
data-dash-is-loading={
3665
(loading_state && loading_state.is_loading) || undefined
3766
}
38-
onChange={e => {
39-
const newValue = e.target.value;
40-
if (
41-
(!isEmpty(min) && Number(newValue) < min) ||
42-
(!isEmpty(max) && Number(newValue) > max)
43-
) {
44-
return;
45-
}
46-
if (!debounce) {
47-
const castValue =
48-
type === 'number' ? Number(newValue) : newValue;
49-
setProps({
50-
value: castValue,
51-
});
52-
} else {
53-
this.setState({value: newValue});
54-
}
55-
}}
56-
onBlur={() => {
57-
const payload = {
58-
n_blur: this.props.n_blur + 1,
59-
n_blur_timestamp: Date.now(),
60-
};
61-
if (debounce) {
62-
payload.value =
63-
type === 'number' ? Number(value) : value;
64-
}
65-
setProps(payload);
66-
}}
67-
onKeyPress={e => {
68-
if (e.key === 'Enter') {
69-
const payload = {
70-
n_submit: this.props.n_submit + 1,
71-
n_submit_timestamp: Date.now(),
72-
};
73-
if (debounce) {
74-
payload.value =
75-
type === 'number' ? Number(value) : value;
76-
}
77-
setProps(payload);
78-
}
79-
}}
80-
value={value}
81-
{...omit(
67+
ref={this.input}
68+
onBlur={this.onBlur}
69+
onChange={this.onChange}
70+
onKeyPress={this.onKeyPress}
71+
{...valprops}
72+
{...R.omit(
8273
[
8374
'debounce',
8475
'value',
@@ -97,6 +88,65 @@ export default class Input extends Component {
9788
/>
9889
);
9990
}
91+
92+
setInputValue(base, value) {
93+
const __value = value;
94+
base = this.input.current.checkValidity() ? convert(base) : NaN;
95+
value = convert(value);
96+
97+
if (!isEquivalent(base, value)) {
98+
this.input.current.value = isNumeric(value) ? value : __value;
99+
}
100+
}
101+
102+
setPropValue(base, value) {
103+
base = convert(base);
104+
value = this.input.current.checkValidity() ? convert(value) : NaN;
105+
106+
if (!isEquivalent(base, value)) {
107+
this.props.setProps({value});
108+
}
109+
}
110+
111+
onEvent() {
112+
const {value, valueAsNumber} = this.input.current;
113+
if (this.props.type === 'number') {
114+
this.setPropValue(
115+
this.props.value,
116+
R.isNil(valueAsNumber) ? value : valueAsNumber
117+
);
118+
} else {
119+
this.props.setProps({value});
120+
}
121+
}
122+
123+
onBlur() {
124+
this.props.setProps({
125+
n_blur: this.props.n_blur + 1,
126+
n_blur_timestamp: Date.now(),
127+
});
128+
this.input.current.checkValidity();
129+
return this.props.debounce && this.onEvent();
130+
}
131+
132+
onKeyPress(e) {
133+
if (e.key === 'Enter') {
134+
this.props.setProps({
135+
n_submit: this.props.n_submit + 1,
136+
n_submit_timestamp: Date.now(),
137+
});
138+
this.input.current.checkValidity();
139+
}
140+
return this.props.debounce && e.key === 'Enter' && this.onEvent();
141+
}
142+
143+
onChange() {
144+
if (!this.props.debounce) {
145+
this.onEvent();
146+
} else if (this.props.type !== 'number') {
147+
this.setState({value: this.input.current.value});
148+
}
149+
}
100150
}
101151

102152
Input.defaultProps = {
@@ -106,6 +156,7 @@ Input.defaultProps = {
106156
n_submit: 0,
107157
n_submit_timestamp: -1,
108158
debounce: false,
159+
step: 'any',
109160
};
110161

111162
Input.propTypes = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
input:invalid {
2+
outline: solid red;
3+
}
4+
5+
input:valid {
6+
outline: none black;
7+
}

0 commit comments

Comments
 (0)