Skip to content

Instantly share code, notes, and snippets.

@flushentitypacket
Last active April 4, 2024 22:29
Show Gist options
  • 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>
)
@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