-
-
Notifications
You must be signed in to change notification settings - Fork 145
Add optional numerical input box to dcc.Slider #944
base: dev
Are you sure you want to change the base?
Changes from 24 commits
c858dc9
6aeea78
d57bf70
7fc4de4
e24c3f5
796d30d
c5b7e51
168a99c
2e2dbe0
57936d4
205b9ed
3168a64
128017b
b555515
3e62912
3820ce4
884cccb
403ee9e
761103c
1709c21
2db424b
da7d116
4b45284
b7c3154
aa1ab08
0111a74
02c10c6
aebce8d
f445f58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,30 @@ export default class Slider extends Component { | |
: ReactSlider; | ||
this._computeStyle = computeSliderStyle(); | ||
this.state = {value: props.value}; | ||
this.syncInputWithSlider = this.syncInputWithSlider.bind(this); | ||
this.input = React.createRef(); | ||
} | ||
|
||
syncInputWithSlider() { | ||
if (this.input.current.value > this.props.max) { | ||
this.input.current.value = this.props.max; | ||
} | ||
|
||
if (this.input.current.value < this.props.min) { | ||
this.input.current.value = this.props.min; | ||
} | ||
|
||
if (this.timeout) { | ||
clearTimeout(this.timeout); | ||
} | ||
|
||
const valueAsNumber = Number(this.input.current.value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jdamiba looking good! Playing with this using or enter a value that's incompatible with the Also: if you enter an out-of-range number, after the timeout time it will be replaced by the closest limit value. This is a problem if the limit is bigger than zero, it can prevent you from typing in the number you want. For example set In all of these cases I think we should allow the value to stay in the input box until blur, and NOT update the slider or the prop. Then on blur my gut reaction is:
Finally, the reason I commented on this particular line: you're casting to number here, but you already used it above as though it was a number, when it was still a string. |
||
|
||
this.setState({value: valueAsNumber}); | ||
this.props.setProps({ | ||
value: valueAsNumber, | ||
drag_value: valueAsNumber, | ||
}); | ||
} | ||
|
||
UNSAFE_componentWillReceiveProps(newProps) { | ||
|
@@ -47,6 +71,13 @@ export default class Slider extends Component { | |
setProps, | ||
tooltip, | ||
updatemode, | ||
syncedInput, | ||
syncedInputDebounceTime, | ||
syncedInputClassName, | ||
syncedInputStyle, | ||
syncedInputID, | ||
style, | ||
step, | ||
vertical, | ||
verticalHeight, | ||
} = this.props; | ||
|
@@ -72,15 +103,54 @@ export default class Slider extends Component { | |
) | ||
: this.props.marks; | ||
|
||
const computedStyle = this._computeStyle( | ||
vertical, | ||
verticalHeight, | ||
tooltip | ||
); | ||
|
||
const defaultInputStyle = { | ||
width: '60px', | ||
marginRight: vertical && syncedInput ? '' : '25px', | ||
marginBottom: vertical && syncedInput ? '25px' : '', | ||
}; | ||
|
||
return ( | ||
<div | ||
id={id} | ||
data-dash-is-loading={ | ||
(loading_state && loading_state.is_loading) || undefined | ||
} | ||
className={className} | ||
style={this._computeStyle(vertical, verticalHeight, tooltip)} | ||
style={{...computedStyle, ...style}} | ||
> | ||
{syncedInput ? ( | ||
<input | ||
onChange={() => { | ||
this.timeout = setTimeout( | ||
function() { | ||
this.syncInputWithSlider(); | ||
}.bind(this), | ||
syncedInputDebounceTime | ||
); | ||
}} | ||
onBlur={() => { | ||
this.syncInputWithSlider(); | ||
}} | ||
onKeyPress={event => { | ||
if (event.key === 'Enter') { | ||
this.syncInputWithSlider(); | ||
} | ||
}} | ||
type="number" | ||
defaultValue={value} | ||
step={step} | ||
className={syncedInputClassName} | ||
id={syncedInputID} | ||
style={{...defaultInputStyle, ...syncedInputStyle}} | ||
ref={this.input} | ||
/> | ||
) : null} | ||
<this.DashSlider | ||
onChange={value => { | ||
if (updatemode === 'drag') { | ||
|
@@ -89,11 +159,17 @@ export default class Slider extends Component { | |
this.setState({value: value}); | ||
setProps({drag_value: value}); | ||
} | ||
if (syncedInput) { | ||
this.input.current.value = value; | ||
} | ||
}} | ||
onAfterChange={value => { | ||
if (updatemode === 'mouseup') { | ||
setProps({value}); | ||
} | ||
if (syncedInput) { | ||
this.input.current.value = value; | ||
} | ||
}} | ||
/* | ||
if/when rc-slider or rc-tooltip are updated to latest versions, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -156,6 +156,61 @@ def test_vertical_slider(self): | |
for entry in self.get_log(): | ||
raise Exception("browser error logged during test", entry) | ||
|
||
def test_horizontal_slider_with_input(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a great test, but let's not add any more tests to the top-level files |
||
app = dash.Dash(__name__) | ||
|
||
app.layout = html.Div( | ||
[ | ||
html.Label("Horizontal Slider with Input"), | ||
dcc.Slider( | ||
id="horizontal-slider-with-input", | ||
min=0, | ||
max=9, | ||
value=5, | ||
syncedInputClassName="arbitraryClassName", | ||
syncedInput=True, | ||
), | ||
], | ||
style={"height": "500px"}, | ||
) | ||
self.startServer(app) | ||
|
||
self.wait_for_element_by_css_selector("#horizontal-slider-with-input") | ||
self.wait_for_element_by_css_selector(".arbitraryClassName") | ||
|
||
self.snapshot("horizontal slider with input") | ||
|
||
for entry in self.get_log(): | ||
raise Exception("browser error logged during test", entry) | ||
|
||
def test_vertical_slider_with_input(self): | ||
app = dash.Dash(__name__) | ||
|
||
app.layout = html.Div( | ||
[ | ||
html.Label("Vertical Slider with Input"), | ||
dcc.Slider( | ||
id="vertical-slider-with-input", | ||
min=0, | ||
max=9, | ||
value=5, | ||
vertical=True, | ||
syncedInputClassName="arbitraryClassName", | ||
syncedInput=True, | ||
), | ||
], | ||
style={"height": "500px"}, | ||
) | ||
self.startServer(app) | ||
|
||
self.wait_for_element_by_css_selector("#vertical-slider-with-input") | ||
self.wait_for_element_by_css_selector(".arbitraryClassName") | ||
|
||
self.snapshot("vertical slider with input") | ||
|
||
for entry in self.get_log(): | ||
raise Exception("browser error logged during test", entry) | ||
|
||
def test_loading_range_slider(self): | ||
lock = Lock() | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per the discussion @chriddyp and I had just now and summarized on Slack: let's convert all these new props to
snake_case
. In the short term this will make slider a little funny since it already has somecamelCase
props. That's OK, we'll get to it soon, and adding the backward-compatible conversion of other props is going to take some more work, especially since we useomit
in this component.