Repository: mui/material-ui (TypeScript)
Date: 2019-02-09 20:57:49+00:00
Task type: Bug Fix

Issue:
Slider's callbacks are executed in an unreliable order
The Slider's callbacks are executed in an unreliable order

## Expected Behavior

The `onDragStart`, `onChange`, `onDragEnd` callbacks should be called in a consistent order.

## Current Behavior

The callbacks are called in varying orders depending on the user interaction.

- when dragging: `onDragStart > onChange > ... > onChange > onDragEnd`
- when clicking: `onDragStart > onDragEnd > onChange`

From time to time, clicking also yields the sequence `onDragStart > onChange > onDragEnd` but I have trouble reproducing this consistenly (clicking outside of the browser windows and then on the slider _sometimes_ triggers it).

## Steps to Reproduce

Open this [codesandbox](https://codesandbox.io/s/mo6oxolv29), manipulate the slider, and check out the console.

The issue seems to happen less often (still happens) in a Browser environment (Chrome 69). Happens a lot in an Electron context.

## Context

I need reliable callbacks to detect when dragging ends and react accordingly using the current slider value (and not the previous one).

Basically, I just need to react when the final value is chosen, not while the slider is dragged.

## Your Environment

| Tech         | Version |
|--------------|---------|
| Material-UI  | 3.0.0  |
| React        |     16.5.2    |

## Idea

One simple fix would be to pass the Slider's value to the onDragEnd event.

That would **not** fix the order issue but at least the callbacks order would be made irrelevant for most use cases.



Issue discussion:
I fear that there is no easy built-in solution for this since browsers always fire mousedown -> mouseup -> click. That's what you are seeing in the second example.

I think what you want is to debounce changes during dragging only. I think an implementation with reactive programming would be the simplest solution. You could also use https://codesandbox.io/s/23jn91lo30 which makes the most out of "react-only".
Thanks for the code @eps1lon . I actually experimented with a similar solution but I would prefer to avoid micro-managing the slider's internal state in such a way.

What do you think of **providing the current slider's value through the onDragEnd callback**?

It would not fix the original issue (as you said, it's a browser issue) but it would solve my use case, which seems pretty common.

What do you think? I could create a pull request.
@merwaaan Sounds reasonable. This should be included in `onDragStart` too for symmetry. Feel free to open a PR.
Came across this issue myself. Are you working on it @merwaaan or can I help out here?

Solution patch:
diff --git a/packages/material-ui-lab/src/Slider/Slider.js b/packages/material-ui-lab/src/Slider/Slider.js
--- a/packages/material-ui-lab/src/Slider/Slider.js
+++ b/packages/material-ui-lab/src/Slider/Slider.js
@@ -323,15 +323,7 @@ class Slider extends React.Component {
   };
 
   handleClick = event => {
-    const { min, max, vertical } = this.props;
-    const percent = calculatePercent(
-      this.containerRef,
-      event,
-      vertical,
-      this.isReverted(),
-      this.touchId,
-    );
-    const value = percentToValue(percent, min, max);
+    const value = this.calculateValueFromPercent(event);
 
     this.emitChange(event, value, () => {
       this.playJumpAnimation();
@@ -360,10 +352,15 @@ class Slider extends React.Component {
     }
     this.setState({ currentState: 'activated' });
 
+    const { onDragStart, valueReducer } = this.props;
+
+    const value = this.calculateValueFromPercent(event);
+    const newValue = valueReducer(value, this.props, event);
+
     document.body.addEventListener('touchend', this.handleTouchEnd);
 
-    if (typeof this.props.onDragStart === 'function') {
-      this.props.onDragStart(event);
+    if (typeof onDragStart === 'function') {
+      onDragStart(event, newValue);
     }
   };
 
@@ -371,13 +368,18 @@ class Slider extends React.Component {
     event.preventDefault();
     this.setState({ currentState: 'activated' });
 
+    const { onDragStart, valueReducer } = this.props;
+
+    const value = this.calculateValueFromPercent(event);
+    const newValue = valueReducer(value, this.props, event);
+
     document.body.addEventListener('mouseenter', this.handleMouseEnter);
     document.body.addEventListener('mouseleave', this.handleMouseLeave);
     document.body.addEventListener('mousemove', this.handleMouseMove);
     document.body.addEventListener('mouseup', this.handleMouseUp);
 
-    if (typeof this.props.onDragStart === 'function') {
-      this.props.onDragStart(event);
+    if (typeof onDragStart === 'function') {
+      onDragStart(event, newValue);
     }
   };
 
@@ -414,20 +416,17 @@ class Slider extends React.Component {
   };
 
   handleMouseMove = event => {
-    const { min, max, vertical } = this.props;
-    const percent = calculatePercent(
-      this.containerRef,
-      event,
-      vertical,
-      this.isReverted(),
-      this.touchId,
-    );
-    const value = percentToValue(percent, min, max);
+    const value = this.calculateValueFromPercent(event);
 
     this.emitChange(event, value);
   };
 
   handleDragEnd(event) {
+    const { onDragEnd, valueReducer } = this.props;
+
+    const value = this.calculateValueFromPercent(event);
+    const newValue = valueReducer(value, this.props, event);
+
     this.setState({ currentState: 'normal' });
 
     document.body.removeEventListener('mouseenter', this.handleMouseEnter);
@@ -436,8 +435,8 @@ class Slider extends React.Component {
     document.body.removeEventListener('mouseup', this.handleMouseUp);
     document.body.removeEventListener('touchend', this.handleTouchEnd);
 
-    if (typeof this.props.onDragEnd === 'function') {
-      this.props.onDragEnd(event);
+    if (typeof onDragEnd === 'function') {
+      onDragEnd(event, newValue);
     }
   }
 
@@ -474,6 +473,18 @@ class Slider extends React.Component {
     }
   }
 
+  calculateValueFromPercent(event) {
+    const { min, max, vertical } = this.props;
+    const percent = calculatePercent(
+      this.containerRef,
+      event,
+      vertical,
+      this.isReverted(),
+      this.touchId,
+    );
+    return percentToValue(percent, min, max);
+  }
+
   playJumpAnimation() {
     this.setState({ currentState: 'jumped' }, () => {
       clearTimeout(this.jumpAnimationTimeoutId);


Test patch:
diff --git a/packages/material-ui-lab/src/Slider/Slider.test.js b/packages/material-ui-lab/src/Slider/Slider.test.js
--- a/packages/material-ui-lab/src/Slider/Slider.test.js
+++ b/packages/material-ui-lab/src/Slider/Slider.test.js
@@ -70,6 +70,22 @@ describe('<Slider />', () => {
     assert.strictEqual(handleChange.callCount, 1, 'should have called the handleChange cb');
     assert.strictEqual(handleDragStart.callCount, 1, 'should have called the handleDragStart cb');
     assert.strictEqual(handleDragEnd.callCount, 1, 'should have called the handleDragEnd cb');
+
+    assert.strictEqual(
+      handleChange.args[0].length,
+      2,
+      'should have called the handleDragEnd cb with 2 arguments',
+    );
+    assert.strictEqual(
+      handleDragStart.args[0].length,
+      2,
+      'should have called the handleDragEnd cb with 2 argument',
+    );
+    assert.strictEqual(
+      handleDragEnd.args[0].length,
+      2,
+      'should have called the handleDragEnd cb with 2 arguments',
+    );
   });
 
   it('should only listen to changes from the same touchpoint', () => {