Scroll events unintentionally changing Material UI slider values

Viewed 944

I have a React application that is utilizing Material UI. The application has slider components implemented in a lot of places (https://material.io/components/sliders). When using a touch screen device, I am unintentionally impacting slider components (the value is getting changed) while trying to scroll up or down the page. This does not happen with other page components, I suspect this is because sliders do (and should) respond to swipe events.

Material UI has documentation that implies a way to discern between a "scroll" and a "swipe" (https://material.io/design/interaction/gestures.html#types-of-gestures). Is there a way for me to indicate to my slider components that a "scroll" should be ignored. Or, can I discern between a vertical or horizontal swipe, telling the slider to ignore vertical swipes?

2 Answers

I have come up with a fairly elegant solution, I believe, which allows the user to scroll if their scroll position begins on the track but not on the thumbs. This replicates the native HTML range input so I feel that this is the best solution.

There's two parts to this

Step 1, allow touch-action on the slider root element as it is disabled by default and prevents the user from starting a scroll on the slider

const useStyles = makeStyles({
  sliderRoot: {
    touchAction: "auto"
  }
});

return (
  <Slider
    classes={{
      root: classes.sliderRoot
    }}
    ...
  />

Step 2, stop propagation on the root element with a ref

const ref = useRef(null);

useEffect(() => {
  if (ref.current) {
    ref.current.addEventListener(
      "touchstart",
      (e) => {
        const isThumb = e.target?.dataset.index;

        if (!isThumb) {
          e.stopPropagation();
        }
      },
      { capture: true }
    );
  }
});

return (
  <Slider
    ref={ref}
    ...
  />

And here is a fully working demo on Codesandbox.

Edit Material demo (forked)

There is not going to be a completely clean solution to this other than telling your users to watch their finger placement when scrolling on mobile devices.

Using a controlled Slider, one approach would be to listen to the touchstart event and record the current pageY on the first changedTouches object. Then compare that coordinate to the pageY on the onChangeCommited event handler for the corresponding touchmove event. If the difference between the two coordinates is larger than some predefined range, then do not update the Slider value.

Inside your component using the Slider:

  const delta = 50
  const sliderRef = useRef(null)
  const [value, setValue] = useState(0) // Or from some prop
  const [touchStart, setTouchStart] = useState(0)
  const debouncedHandler = useMemo(() => {
    // Using lodash.debounce
    return debounce((evt, value) => {
      // If it is a mouse event then just update value as usual
      if (evt instanceof MouseEvent) {
        setValue(value)
      }
    }, 25)
  }, [])

  useLayoutEffect(() => {
    if (sliderRef.current) {
      sliderRef.current.addEventListener('touchstart', evt => {
        setTouchStart(evt.changedTouches[0].pageY)
      })
    }
  }, [])

  return (
    <Slider
      value={value}
      ref={sliderRef}
      onChange={debouncedHandler}
      onChangeCommitted={(evt, value) => {
        if (evt instanceof TouchEvent) {
          if (Math.abs(touchStart - evt.changedTouches[0].pageY) < delta) {
            setValue(value)
          }
        } else {
          setValue(value)
        }
      }}
    />
  )

This will prevent the Slider from changing value on TouchEvent when the difference between the starting y-coordinate and the ending y-coordinate is larger than delta. Adjust delta to whatever value you like. The tradeoff is that you will not get as smooth of a transition when adjusting the Slider with a normal MouseEvent (or TouchEvent within the predefined range).

See the jsFiddle.

Or npm i mui-scrollable-slider-hook and use it like

  import Slider from '@mui/material/Slider'
  import { useMuiScrollableSlider } from 'mui-scrollable-slider-hook'

  const { ref, value, onChange, onChangeCommitted } = useMuiScrollableSlider()

  return <Slider ref={ref} value={value} onChange={onChange} onChangeCommitted={onChangeCommitted} />

An example of using mui-scrollable-slider-hook on codesandbox.

Related