diff --git a/src/components/Slider.react.js b/src/components/Slider.react.js
index 116180dd..30372213 100644
--- a/src/components/Slider.react.js
+++ b/src/components/Slider.react.js
@@ -115,10 +115,40 @@ Slider.propTypes = {
}),
/**
- * Value by which increments or decrements are made
+ * Value by which increments or decrements are made.
*/
step: PropTypes.number,
+ /**
+ * If true, display an Input component whose value is synced with the Slider's value.
+ */
+ synced_input: PropTypes.bool,
+
+ /**
+ * The classname to be given to the synced Input component.
+ */
+ synced_input_class_name: PropTypes.string,
+
+ /**
+ * The CSS to be applied to the class of the input (div).
+ */
+ synced_input_style: PropTypes.object,
+
+ /**
+ * The id to be applied to the input (div). Default is "syncedInput".
+ */
+ synced_input_id: PropTypes.string,
+
+ /**
+ * The CSS to be applied to the class of the slider (div).
+ */
+ style: PropTypes.object,
+
+ /**
+ * The amount of time the synced Input should wait before passing along state changes without a change of focus or the user pressing Enter. In milliseconds.
+ */
+ synced_input_debounce_time: PropTypes.number,
+
/**
* If true, the slider will be vertical
*/
@@ -199,6 +229,8 @@ Slider.defaultProps = {
persisted_props: ['value'],
persistence_type: 'local',
verticalHeight: 400,
+ synced_input_debounce_time: 450,
+ synced_input_id: 'syncedInput',
};
export const propTypes = Slider.propTypes;
diff --git a/src/fragments/Slider.react.js b/src/fragments/Slider.react.js
index 178014e3..221f662c 100644
--- a/src/fragments/Slider.react.js
+++ b/src/fragments/Slider.react.js
@@ -7,6 +7,10 @@ import 'rc-slider/assets/index.css';
import {propTypes, defaultProps} from '../components/Slider.react';
+function round(number, increment, offset) {
+ return Math.round((number - offset) / increment ) * increment + offset;
+}
+
/**
* A slider component with a single handle.
*/
@@ -18,6 +22,42 @@ 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(trigger) {
+
+ if (trigger == "onChange"){
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ return
+ }
+
+ if (this.input.current.value === ""){
+ this.input.current.value = this.props.value
+ }
+
+ if (Number(this.input.current.value) > this.props.max) {
+ this.input.current.value = this.props.max;
+ }
+
+ if (Number(this.input.current.value) < this.props.min) {
+ this.input.current.value = this.props.min;
+ }
+
+ if (this.props.step){
+ if ((this.input.current.value - this.props.step) % 1 !== 0){
+ this.input.current.value = round(this.input.current.value, this.props.step, 0)
+ }
+ }
+
+ this.setState({value: Number(this.input.current.value)});
+ this.props.setProps({
+ value: Number(this.input.current.value),
+ drag_value: Number(this.input.current.value),
+ });
}
UNSAFE_componentWillReceiveProps(newProps) {
@@ -47,6 +87,13 @@ export default class Slider extends Component {
setProps,
tooltip,
updatemode,
+ synced_input,
+ synced_input_debounce_time,
+ synced_input_class_name,
+ synced_input_style,
+ synced_input_id,
+ style,
+ step,
vertical,
verticalHeight,
} = this.props;
@@ -72,6 +119,18 @@ export default class Slider extends Component {
)
: this.props.marks;
+ const computedStyle = this._computeStyle(
+ vertical,
+ verticalHeight,
+ tooltip
+ );
+
+ const defaultInputStyle = {
+ width: '60px',
+ marginRight: vertical && synced_input ? '' : '25px',
+ marginBottom: vertical && synced_input ? '25px' : '',
+ };
+
return (
+ {synced_input ? (
+ {
+ this.timeout = setTimeout(
+ function() {
+ this.syncInputWithSlider("onChange");
+ }.bind(this),
+ synced_input_debounce_time
+ );
+ }}
+ onBlur={() => {
+ this.syncInputWithSlider("onBlur");
+ }}
+ onKeyPress={event => {
+ if (event.key === 'Enter') {
+ this.syncInputWithSlider("onKeyPress");
+ }
+ }}
+ type="number"
+ defaultValue={value}
+ step={step}
+ className={synced_input_class_name}
+ id={synced_input_id}
+ style={{...defaultInputStyle, ...synced_input_style}}
+ ref={this.input}
+ />
+ ) : null}
{
if (updatemode === 'drag') {
@@ -89,11 +175,17 @@ export default class Slider extends Component {
this.setState({value: value});
setProps({drag_value: value});
}
+ if (synced_input) {
+ this.input.current.value = value;
+ }
}}
onAfterChange={value => {
if (updatemode === 'mouseup') {
setProps({value});
}
+ if (synced_input) {
+ this.input.current.value = value;
+ }
}}
/*
if/when rc-slider or rc-tooltip are updated to latest versions,
diff --git a/src/utils/computeSliderStyle.js b/src/utils/computeSliderStyle.js
index 5d653131..eec3c72e 100644
--- a/src/utils/computeSliderStyle.js
+++ b/src/utils/computeSliderStyle.js
@@ -4,10 +4,12 @@ export default () => {
return memoizeWith(identity, (vertical, verticalHeight, tooltip) => {
const style = {
padding: '25px',
+ display: 'flex',
};
if (vertical) {
style.height = verticalHeight + 'px';
+ style.flexDirection = 'column';
if (
!tooltip ||
@@ -28,6 +30,7 @@ export default () => {
) {
style.paddingTop = '0px';
}
+ style.alignItems = 'center';
}
return style;
diff --git a/tests/integration/sliders/test_sliders.py b/tests/integration/sliders/test_sliders.py
index d28c0429..19987b35 100644
--- a/tests/integration/sliders/test_sliders.py
+++ b/tests/integration/sliders/test_sliders.py
@@ -306,3 +306,102 @@ def update_output(value):
dash_dcc.wait_for_text_to_equal("#out-value", "You have selected 5-15")
dash_dcc.release()
dash_dcc.wait_for_text_to_equal("#out-value", "You have selected 10-15")
+
+
+def test_slsl008_horizontal_slider_with_input(dash_dcc):
+ app = dash.Dash(__name__)
+ app.layout = html.Div(
+ [
+ dcc.Slider(
+ id="slider",
+ min=0,
+ max=20,
+ step=1,
+ value=5,
+ syncedInput=True,
+ syncedInputID="syncedInput",
+ tooltip={"always_visible": True},
+ ),
+ html.Div(id="out"),
+ ]
+ )
+
+ @app.callback(Output("out", "children"), [Input("slider", "value")])
+ def update_output(value):
+ return "You have selected {}".format(value)
+
+ dash_dcc.start_server(app)
+ dash_dcc.wait_for_text_to_equal("#out", "You have selected 5")
+
+ input_ = dash_dcc.find_element("#syncedInput")
+
+ input_.clear()
+ input_.send_keys("8")
+
+ dash_dcc.wait_for_text_to_equal("#out", "You have selected 8")
+
+
+def test_slsl009_vertical_slider_with_input(dash_dcc):
+ app = dash.Dash(__name__)
+ app.layout = html.Div(
+ [
+ dcc.Slider(
+ id="slider",
+ min=0,
+ max=20,
+ step=1,
+ value=5,
+ vertical=True,
+ syncedInput=True,
+ syncedInputID="arbitraryID",
+ tooltip={"always_visible": True},
+ ),
+ html.Div(id="out"),
+ ]
+ )
+
+ @app.callback(Output("out", "children"), [Input("slider", "value")])
+ def update_output(value):
+ return "You have selected {}".format(value)
+
+ dash_dcc.start_server(app)
+ dash_dcc.wait_for_text_to_equal("#out", "You have selected 5")
+
+ input_ = dash_dcc.find_element("#arbitraryID")
+
+ input_.clear()
+ input_.send_keys("8")
+
+ dash_dcc.wait_for_text_to_equal("#out", "You have selected 8")
+
+
+def test_slsl010_horizontal_slider_with_input(dash_dcc):
+ app = dash.Dash(__name__)
+ app.layout = html.Div(
+ [
+ dcc.Slider(
+ id="slider",
+ min=0,
+ max=20,
+ step=1,
+ value=5,
+ vertical=True,
+ syncedInput=True,
+ syncedInputClassName="slider-input",
+ syncedInputID="arbitraryID",
+ tooltip={"always_visible": True},
+ ),
+ html.Div(id="out"),
+ ]
+ )
+
+ @app.callback(Output("out", "children"), [Input("slider", "value")])
+ def update_output(value):
+ return "You have selected {}".format(value)
+
+ dash_dcc.start_server(app)
+ dash_dcc.wait_for_text_to_equal("#out", "You have selected 5")
+
+ input_ = dash_dcc.find_element("#arbitraryID")
+
+ assert input_.get_attribute("class") == "slider-input"
diff --git a/tests/test_integration_1.py b/tests/test_integration_1.py
index b202f9d0..aca05067 100644
--- a/tests/test_integration_1.py
+++ b/tests/test_integration_1.py
@@ -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):
+ 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()