Skip to content

Instantly share code, notes, and snippets.

@mreishus
Last active August 10, 2018 19:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mreishus/9c368cb01d7dbb367202425384e19891 to your computer and use it in GitHub Desktop.
Save mreishus/9c368cb01d7dbb367202425384e19891 to your computer and use it in GitHub Desktop.
React component to provide click-and-drag scrolling
// @flow
import * as React from "react";
// I got this from https://gist.github.com/flushentitypacket/7717cb30d1b172e633cea864eeb4d2e7
// Changes made
// 1. Added Y position
// 2. Converted from TypeScript to FlowType
// 3. Changed removeEventListeners to run off document.documentElement - they weren't working for me
// 4. Added event.preventDefault to mouse down handler - might prevent getting stuck in dragging state
// 5. Changed privateState updates to use a mutational style.
type DragScrollProvisions = {
onMouseDown: (event: SyntheticMouseEvent<*>) => void,
ref: (element: ?HTMLElement) => ?HTMLElement,
};
type Props = {
children: (provisions: DragScrollProvisions) => React.Node,
};
type PrivateState = {
isMouseDown: boolean,
lastMousePositionX: number | null,
lastMousePositionY: number | null,
};
export class DragScrollProvider extends React.Component<Props> {
// Not using React state since we don't want to rerender on these state changes
privateState: PrivateState = {
isMouseDown: false,
lastMousePositionX: null,
lastMousePositionY: null,
};
clearListeners: Array<() => any> = [];
refElement: ?HTMLElement = null;
render() {
return this.props.children({
onMouseDown: this.provisionOnMouseDown,
ref: this.provisionRef,
});
}
componentDidMount() {
const onMouseUp = () => {
this.privateState = {
isMouseDown: false,
lastMousePositionX: null,
lastMousePositionY: null,
};
};
if (document.documentElement == null) {
return;
}
document.documentElement.addEventListener("mouseup", onMouseUp);
const clearMouseUpListener = () =>
document.documentElement != null && document.documentElement.removeEventListener("mouseup", onMouseUp);
this.clearListeners.push(clearMouseUpListener);
const onMouseMove = (event: MouseEvent) => {
if (!this.privateState.isMouseDown || this.refElement == null) {
return;
}
const { lastMousePositionX, lastMousePositionY } = this.privateState;
// The mousedown handler should have set the lastMousePositionX, 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 (lastMousePositionX === null || lastMousePositionY === null) return;
this.refElement.scrollLeft += lastMousePositionX - event.clientX;
this.refElement.scrollTop += lastMousePositionY - event.clientY;
this.privateState.lastMousePositionX = event.clientX;
this.privateState.lastMousePositionY = event.clientY;
};
if (document.documentElement == null) {
return;
}
document.documentElement.addEventListener("mousemove", onMouseMove);
const clearMouseMoveListener = () =>
document.documentElement != null && document.documentElement.removeEventListener("mousemove", onMouseMove);
this.clearListeners.push(clearMouseMoveListener);
}
componentWillUnmount() {
this.clearListeners.forEach(clear => clear());
}
provisionOnMouseDown = (event: SyntheticMouseEvent<*>) => {
event.preventDefault(); // This may prevent it from getting stuck in the "down" position
this.privateState = {
isMouseDown: true,
lastMousePositionX: event.clientX,
lastMousePositionY: event.clientY,
};
};
provisionRef = (element: ?HTMLElement) => (this.refElement = element);
}
import * as React from 'react'
import {DragScrollProvider} from './DragScrollProvider'
const MyComponent = () => (
<DragScrollProvider>
{({onMouseDown, ref}) => (
<div className='scrollableDiv' onMouseDown={onMouseDown} ref={ref}>
<div className='overflowsTheParent' />
</div>
)}
</DragScrollProvider>
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment