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> | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's a version I made using Hooks. No TS, sorry.