diff --git a/src/components/RangeSlider.react.js b/src/components/RangeSlider.react.js index 3b3e02bbc..2b293044b 100644 --- a/src/components/RangeSlider.react.js +++ b/src/components/RangeSlider.react.js @@ -49,6 +49,11 @@ RangeSlider.propTypes = { */ value: PropTypes.arrayOf(PropTypes.number), + /** + * The value of the input during a drag + */ + drag_value: PropTypes.arrayOf(PropTypes.number), + /** * allowCross could be set as true to allow those handles to cross. */ @@ -145,18 +150,18 @@ RangeSlider.propTypes = { verticalHeight: PropTypes.number, /** - * Determines when the component should update - * its value. If `mouseup`, then the slider - * will only trigger its value when the user has - * finished dragging the slider. If `drag`, then - * the slider will update its value continuously - * as it is being dragged. - * Only use `drag` if your updates are fast. + * Determines when the component should update its `value` + * property. If `mouseup` (the default) then the slider + * will only trigger its value when the user has finished + * dragging the slider. If `drag`, then the slider will + * update its value continuously as it is being dragged. + * Note that for the latter case, the `drag_value` + * property could be used instead. */ updatemode: PropTypes.oneOf(['mouseup', 'drag']), /** - * Dash-assigned callback that gets fired when the value changes. + * Dash-assigned callback that gets fired when the value or drag_value changes. */ setProps: PropTypes.func, diff --git a/src/components/Slider.react.js b/src/components/Slider.react.js index 37fcd4ecf..116180dd6 100644 --- a/src/components/Slider.react.js +++ b/src/components/Slider.react.js @@ -48,6 +48,11 @@ Slider.propTypes = { */ value: PropTypes.number, + /** + * The value of the input during a drag + */ + drag_value: PropTypes.number, + /** * Additional CSS class for the root DOM node */ @@ -125,18 +130,19 @@ Slider.propTypes = { verticalHeight: PropTypes.number, /** - * Determines when the component should update - * its value. If `mouseup`, then the slider - * will only trigger its value when the user has - * finished dragging the slider. If `drag`, then - * the slider will update its value continuously - * as it is being dragged. - * Only use `drag` if your updates are fast. + * Determines when the component should update its `value` + * property. If `mouseup` (the default) then the slider + * will only trigger its value when the user has finished + * dragging the slider. If `drag`, then the slider will + * update its value continuously as it is being dragged. + * If you want different actions during and after drag, + * leave `updatemode` as `mouseup` and use `drag_value` + * for the continuously updating value. */ updatemode: PropTypes.oneOf(['mouseup', 'drag']), /** - * Dash-assigned callback that gets fired when the value changes. + * Dash-assigned callback that gets fired when the value or drag_value changes. */ setProps: PropTypes.func, diff --git a/src/fragments/RangeSlider.react.js b/src/fragments/RangeSlider.react.js index 932c6b5d8..75130b94e 100644 --- a/src/fragments/RangeSlider.react.js +++ b/src/fragments/RangeSlider.react.js @@ -10,20 +10,11 @@ import {propTypes, defaultProps} from '../components/RangeSlider.react'; export default class RangeSlider extends Component { constructor(props) { super(props); - this.propsToState = this.propsToState.bind(this); this.DashSlider = props.tooltip ? createSliderWithTooltip(Range) : Range; this._computeStyle = computeSliderStyle(); - this.state = { - value: props.value, - }; - } - - propsToState(newProps) { - if (newProps.value !== this.props.value) { - this.setState({value: newProps.value}); - } + this.state = {value: props.value}; } UNSAFE_componentWillReceiveProps(newProps) { @@ -32,11 +23,17 @@ export default class RangeSlider extends Component { ? createSliderWithTooltip(Range) : Range; } - this.propsToState(newProps); + if (newProps.value !== this.props.value) { + this.props.setProps({drag_value: newProps.value}); + this.setState({value: newProps.value}); + } } UNSAFE_componentWillMount() { - this.propsToState(this.props); + if (this.props.value !== null) { + this.props.setProps({drag_value: this.props.value}); + this.setState({value: this.props.value}); + } } render() { @@ -84,9 +81,10 @@ export default class RangeSlider extends Component { { if (updatemode === 'drag') { - setProps({value}); + setProps({value: value, drag_value: value}); } else { - this.setState({value}); + this.setState({value: value}); + setProps({drag_value: value}); } }} onAfterChange={value => { @@ -101,6 +99,7 @@ export default class RangeSlider extends Component { [ 'className', 'value', + 'drag_value', 'setProps', 'marks', 'updatemode', diff --git a/src/fragments/Slider.react.js b/src/fragments/Slider.react.js index 63eda8bdc..178014e39 100644 --- a/src/fragments/Slider.react.js +++ b/src/fragments/Slider.react.js @@ -13,20 +13,11 @@ import {propTypes, defaultProps} from '../components/Slider.react'; export default class Slider extends Component { constructor(props) { super(props); - this.propsToState = this.propsToState.bind(this); this.DashSlider = props.tooltip ? createSliderWithTooltip(ReactSlider) : ReactSlider; this._computeStyle = computeSliderStyle(); - this.state = { - value: props.value, - }; - } - - propsToState(newProps) { - if (newProps.value !== this.props.value) { - this.setState({value: newProps.value}); - } + this.state = {value: props.value}; } UNSAFE_componentWillReceiveProps(newProps) { @@ -35,11 +26,17 @@ export default class Slider extends Component { ? createSliderWithTooltip(ReactSlider) : ReactSlider; } - this.propsToState(newProps); + if (newProps.value !== this.props.value) { + this.props.setProps({drag_value: newProps.value}); + this.setState({value: newProps.value}); + } } UNSAFE_componentWillMount() { - this.propsToState(this.props); + if (this.props.value !== null) { + this.props.setProps({drag_value: this.props.value}); + this.setState({value: this.props.value}); + } } render() { @@ -87,9 +84,10 @@ export default class Slider extends Component { { if (updatemode === 'drag') { - setProps({value}); + setProps({value: value, drag_value: value}); } else { - this.setState({value}); + this.setState({value: value}); + setProps({drag_value: value}); } }} onAfterChange={value => { @@ -114,6 +112,7 @@ export default class Slider extends Component { 'setProps', 'updatemode', 'value', + 'drag_value', 'marks', 'verticalHeight', ], diff --git a/tests/dash_core_components_page.py b/tests/dash_core_components_page.py index b7cc962f4..909de7a3f 100644 --- a/tests/dash_core_components_page.py +++ b/tests/dash_core_components_page.py @@ -2,6 +2,7 @@ from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.action_chains import ActionChains logger = logging.getLogger(__name__) @@ -100,3 +101,20 @@ def _wait_until_day_is_clickable(self, timeout=1): @property def date_picker_day_locator(self): return 'div[data-visible="true"] td.CalendarDay' + + def click_and_hold_at_coord_fractions(self, elem_or_selector, fx, fy): + elem = self._get_element(elem_or_selector) + + ActionChains(self.driver).move_to_element_with_offset( + elem, elem.size["width"] * fx, elem.size["height"] * fy + ).click_and_hold().perform() + + def move_to_coord_fractions(self, elem_or_selector, fx, fy): + elem = self._get_element(elem_or_selector) + + ActionChains(self.driver).move_to_element_with_offset( + elem, elem.size["width"] * fx, elem.size["height"] * fy + ).perform() + + def release(self): + ActionChains(self.driver).release().perform() diff --git a/tests/integration/sliders/test_sliders.py b/tests/integration/sliders/test_sliders.py index a5483e093..09ab15963 100644 --- a/tests/integration/sliders/test_sliders.py +++ b/tests/integration/sliders/test_sliders.py @@ -160,3 +160,84 @@ def test_slsl005_slider_tooltip(dash_dcc): dash_dcc.percy_snapshot( "slider-make sure tooltips are only visible if parent slider is visible" ) + + +def test_slsl006_drag_value_slider(dash_dcc): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + dcc.Slider( + id="slider", + min=0, + max=20, + step=1, + value=5, + tooltip={"always_visible": True}, + ), + html.Div(id="out-value"), + html.Div(id="out-drag-value"), + ] + ) + + @app.callback(Output("out-drag-value", "children"), [Input("slider", "drag_value")]) + def update_output(value): + return "You have dragged {}".format(value) + + @app.callback(Output("out-value", "children"), [Input("slider", "value")]) + def update_output(value): + return "You have selected {}".format(value) + + dash_dcc.start_server(app) + slider = dash_dcc.find_element("#slider") + + dash_dcc.wait_for_text_to_equal("#out-value", "You have selected 5") + dash_dcc.wait_for_text_to_equal("#out-drag-value", "You have dragged 5") + + dash_dcc.click_and_hold_at_coord_fractions(slider, 0.25, 0.25) + dash_dcc.move_to_coord_fractions(slider, 0.75, 0.25) + dash_dcc.wait_for_text_to_equal("#out-drag-value", "You have dragged 15") + dash_dcc.move_to_coord_fractions(slider, 0.5, 0.25) + dash_dcc.wait_for_text_to_equal("#out-drag-value", "You have dragged 10") + dash_dcc.wait_for_text_to_equal("#out-value", "You have selected 5") + dash_dcc.release() + dash_dcc.wait_for_text_to_equal("#out-value", "You have selected 10") + + +def test_slsl007_drag_value_rangeslider(dash_dcc): + app = dash.Dash(__name__) + app.layout = html.Div( + [ + dcc.RangeSlider( + id="slider", + min=0, + max=20, + step=1, + value=(5, 15), + tooltip={"always_visible": True}, + ), + html.Div(id="out-value"), + html.Div(id="out-drag-value"), + ] + ) + + @app.callback(Output("out-drag-value", "children"), [Input("slider", "drag_value")]) + def update_output(value): + value = value or (None, None) + return "You have dragged {}-{}".format(*value) + + @app.callback(Output("out-value", "children"), [Input("slider", "value")]) + def update_output(value): + return "You have selected {}-{}".format(*value) + + dash_dcc.start_server(app) + slider = dash_dcc.find_element("#slider") + + dash_dcc.wait_for_text_to_equal("#out-value", "You have selected 5-15") + dash_dcc.wait_for_text_to_equal("#out-drag-value", "You have dragged 5-15") + + dash_dcc.click_and_hold_at_coord_fractions(slider, 0.25, 0.25) + dash_dcc.move_to_coord_fractions(slider, 0.5, 0.25) + dash_dcc.wait_for_text_to_equal("#out-drag-value", "You have dragged 10-15") + 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")