Skip to content

Instantly share code, notes, and snippets.

@flushentitypacket
Last active April 4, 2024 22:29
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save flushentitypacket/7717cb30d1b172e633cea864eeb4d2e7 to your computer and use it in GitHub Desktop.
Save flushentitypacket/7717cb30d1b172e633cea864eeb4d2e7 to your computer and use it in GitHub Desktop.
Typescript React component to provide click-and-drag scrolling
import * as React from 'react'
type DragScrollProvisions = {
onMouseDown: React.MouseEventHandler<HTMLElement>,
ref: React.Ref<HTMLElement>,
}
export type Props = {
children: (provisions: DragScrollProvisions) => React.ReactNode,
}
export type PrivateState = {
isMouseDown: boolean,
lastMousePosition: number | null,
}
// TODO: Right now only supports x-direction scrolling, but can easily be expanded someday to also support y-direction
export class DragScrollProvider extends React.Component<Props, {}> {
// Not using React state since we don't want to rerender on these state changes
private privateState: PrivateState = {
isMouseDown: false,
lastMousePosition: null,
}
private clearListeners: Array<() => void> = []
private refElement: HTMLElement | null = null
public render() {
return this.props.children({
onMouseDown: this.provisionOnMouseDown,
ref: this.provisionRef,
})
}
public componentDidMount() {
const onMouseUp = () => {
this.setPrivateState({
isMouseDown: false,
lastMousePosition: null,
})
}
document.documentElement.addEventListener('mouseup', onMouseUp)
const clearMouseUpListener = () => removeEventListener('mouseup', onMouseUp)
this.clearListeners.push(clearMouseUpListener)
const onMouseMove = (event: MouseEvent) => {
const {isMouseDown, lastMousePosition} = this.privateState
if (!isMouseDown) return
if (this.refElement === null) return
// The mousedown handler should have set the lastMousePosition, so this case should only happen if setPrivateState
// hasn't finished yet. In that case, let's just ignore this first movement and wait for that initial
// setPrivateState to complete.
if (lastMousePosition === null) return
this.refElement.scrollLeft += lastMousePosition - event.clientX
this.setPrivateState({lastMousePosition: event.clientX})
}
document.documentElement.addEventListener('mousemove', onMouseMove)
const clearMouseMoveListener = () => removeEventListener('mousemove', onMouseMove)
this.clearListeners.push(clearMouseMoveListener)
}
public componentWillUnmount() {
this.clearListeners.forEach((clear) => clear())
}
private provisionOnMouseDown: React.MouseEventHandler<HTMLElement> = (event) => {
this.setPrivateState({
isMouseDown: true,
lastMousePosition: event.clientX,
})
}
private provisionRef: React.Ref<HTMLElement> = (element: HTMLElement) => this.refElement = element
private setPrivateState = (state: Partial<PrivateState>) => {
this.privateState = {...this.privateState, ...state}
}
}
import * as React from 'react'
import {DragScrollProvider} from './DragScrollProvider'
const MyComponent: React.SFC = () => (
<DragScrollProvider>
{({onMouseDown, ref}) => (
<div className='scrollableDiv' onMouseDown={onMouseDown} ref={ref}>
<div className='overflowsTheParent' />
</div>
)}
</DragScrollProvider>
)
@davidsa
Copy link

davidsa commented Aug 2, 2018

how do you avoid conflicts when child elements implement onClick

@mreishus
Copy link

mreishus commented Aug 3, 2018

👍 And same question

@mreishus
Copy link

mreishus commented Aug 3, 2018

Still debugging it, but I'm able to occasionally get it stuck in the mouseDown: true position, even though I'm not holding down the mouse button.

How to recreate it, I'm not exactly sure.. but it's something like "click the mouse as quickly as possible while moving it at the same time."

@mreishus
Copy link

@davidsa - on a child onClick handler, add e.stopPropagation() to stop the event from making to your DragScrollProvider

@mreishus
Copy link

I had to convert this to flow for my usage. I also made a few small changes. I've posted my version here: https://gist.github.com/mreishus/9c368cb01d7dbb367202425384e19891

@flushentitypacket
Copy link
Author

flushentitypacket commented Aug 15, 2018

Sorry everyone, I don't get notifications for Gists so I didn't know there were folks asking questions here! (EDIT: Apparently the notifications thing is a known issue)

@mreishus Thanks for the flux conversion and advice provided. I've been able to reproduce the issue you're talking about too, but haven't figured out what to do about it. Did you get a fix working? I have a suspicion that the mouseup event isn't always firing. I believe the browser is supposed to implement it such that the event fires even when occurring outside the window, but it seems like not all browsers have done so.

@madisonbullard
Copy link

Here's a version I made using Hooks. No TS, sorry.

import { useState, useEffect, useRef } from 'react';

export default function useDragScroll() {
  const [isMouseDown, setIsMouseDown] = useState(false);
  const [lastMousePosition, setLastMousePosition] = useState(null);
  const ref = useRef(null);

  function onMouseDown(e) {
    setIsMouseDown(true);
    setLastMousePosition(e.clientX);
  }

  useEffect(() => {
    function onMouseUp() {
      setIsMouseDown(false);
      setLastMousePosition(null);
    }
    function onMouseMove(e) {
      if (!isMouseDown) return;
      if (ref.current === null) return;
      if (lastMousePosition === null) return;

      ref.current.scrollLeft += lastMousePosition - e.clientX;
      setLastMousePosition(e.clientX);
    }

    window.addEventListener('mouseup', onMouseUp);
    window.addEventListener('mousemove', onMouseMove);
    return () => {
      window.removeEventListener('mouseup', onMouseUp);
      window.removeEventListener('mousemove', onMouseMove);
    };
  }, [isMouseDown, lastMousePosition]);

  return {
    ref,
    onMouseDown,
  };
}
function Example() {
  const dragProps = useDragScroll();
  return (
    <div {...dragProps}>drag me bb</div>
  )
}

@rogeriocsilva
Copy link

@madisonbullard perfect, thanks 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment