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>
)
@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