Last active
April 4, 2024 22:29
-
-
Save flushentitypacket/7717cb30d1b172e633cea864eeb4d2e7 to your computer and use it in GitHub Desktop.
Typescript React component to provide click-and-drag scrolling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | |
) |
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.
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>
)
}
@madisonbullard perfect, thanks 😄
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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