├── .gitignore
├── demo.gif
├── .github
└── workflows
│ ├── size.yml
│ └── main.yml
├── README.md
├── LICENSE
├── tsconfig.json
├── package.json
└── src
└── index.tsx
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/g-delmo/use-draggable-scroll/HEAD/demo.gif
--------------------------------------------------------------------------------
/.github/workflows/size.yml:
--------------------------------------------------------------------------------
1 | name: size
2 | on: [pull_request]
3 | jobs:
4 | size:
5 | runs-on: ubuntu-latest
6 | env:
7 | CI_JOB_NUMBER: 1
8 | steps:
9 | - uses: actions/checkout@v1
10 | - uses: andresz1/size-limit-action@v1
11 | with:
12 | github_token: ${{ secrets.GITHUB_TOKEN }}
13 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push]
3 | jobs:
4 | build:
5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}
6 |
7 | runs-on: ${{ matrix.os }}
8 | strategy:
9 | matrix:
10 | node: ['10.x', '12.x', '14.x']
11 | os: [ubuntu-latest, windows-latest, macOS-latest]
12 |
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v2
16 |
17 | - name: Use Node ${{ matrix.node }}
18 | uses: actions/setup-node@v1
19 | with:
20 | node-version: ${{ matrix.node }}
21 |
22 | - name: Install deps and build (with cache)
23 | uses: bahmutov/npm-install@v1
24 |
25 | - name: Lint
26 | run: yarn lint
27 |
28 | - name: Build
29 | run: yarn build
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # use-draggable-scroll
2 |
3 | React hook to add draggability to scrollable content easily.
4 |
5 | 
6 |
7 | ### Installation
8 |
9 | ```
10 | yarn add use-draggable-scroll
11 | ```
12 | or
13 | ```
14 | npm install use-draggable-scroll
15 | ```
16 |
17 | ### Usage
18 |
19 | ```tsx
20 | import useDraggableScroll from 'use-draggable-scroll';
21 |
22 | const Component = () => {
23 | const ref = useRef(null);
24 |
25 | const { onMouseDown } = useDraggableScroll(ref);
26 |
27 | return (
28 |
29 |
child 1
30 |
child 2
31 |
child 3
32 |
33 | );
34 | };
35 | ```
36 |
37 | ### Optional parameters
38 |
39 | You can specify the drag direction that is allowed (`vertical`, `horizontal` or `both`(default))
40 |
41 | ```tsx
42 | const { onMouseDown } = useDraggableScroll(ref, { direction: 'vertical' });
43 | ```
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Guillermo del Molino
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | // output .d.ts declaration files for consumers
9 | "declaration": true,
10 | // output .js.map sourcemap files for consumers
11 | "sourceMap": true,
12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index
13 | "rootDir": "./src",
14 | // stricter type-checking for stronger correctness. Recommended by TS
15 | "strict": true,
16 | // linter checks for common issues
17 | "noImplicitReturns": true,
18 | "noFallthroughCasesInSwitch": true,
19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | // use Node's module resolution algorithm, instead of the legacy TS one
23 | "moduleResolution": "node",
24 | // transpile JSX to React.createElement
25 | "jsx": "react",
26 | // interop between ESM and CJS modules. Recommended by TS
27 | "esModuleInterop": true,
28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS
29 | "skipLibCheck": true,
30 | // error out if import and file system have a casing mismatch. Recommended by TS
31 | "forceConsistentCasingInFileNames": true,
32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc`
33 | "noEmit": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "license": "MIT",
4 | "main": "dist/index.js",
5 | "typings": "dist/index.d.ts",
6 | "files": [
7 | "dist",
8 | "src"
9 | ],
10 | "engines": {
11 | "node": ">=10"
12 | },
13 | "scripts": {
14 | "start": "tsdx watch",
15 | "build": "tsdx build",
16 | "lint": "tsdx lint",
17 | "prepare": "tsdx build",
18 | "size": "size-limit",
19 | "analyze": "size-limit --why"
20 | },
21 | "peerDependencies": {
22 | "react": ">=16"
23 | },
24 | "husky": {
25 | "hooks": {
26 | "pre-commit": "tsdx lint"
27 | }
28 | },
29 | "prettier": {
30 | "printWidth": 80,
31 | "semi": true,
32 | "singleQuote": true,
33 | "trailingComma": "es5"
34 | },
35 | "name": "use-draggable-scroll",
36 | "author": "Guillermo del Molino",
37 | "module": "dist/use-draggable-scroll.esm.js",
38 | "size-limit": [
39 | {
40 | "path": "dist/use-draggable-scroll.cjs.production.min.js",
41 | "limit": "10 KB"
42 | },
43 | {
44 | "path": "dist/use-draggable-scroll.esm.js",
45 | "limit": "10 KB"
46 | }
47 | ],
48 | "devDependencies": {
49 | "@size-limit/preset-small-lib": "^4.12.0",
50 | "@types/react": "^17.0.11",
51 | "@types/react-dom": "^17.0.8",
52 | "husky": "^6.0.0",
53 | "react": "^17.0.2",
54 | "react-dom": "^17.0.2",
55 | "size-limit": "^4.12.0",
56 | "tsdx": "^0.14.1",
57 | "tslib": "^2.3.0",
58 | "typescript": "^4.3.4"
59 | },
60 | "description": "React hook to add draggability to scrollable content easily",
61 | "repository": "https://github.com/g-delmo/use-draggable-scroll",
62 | "keywords": [
63 | "draggable",
64 | "drag",
65 | "scroll",
66 | "react",
67 | "draggability"
68 | ]
69 | }
70 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react';
2 |
3 | export default function useDraggableScroll(
4 | ref: RefObject,
5 | options: {
6 | direction?: 'vertical' | 'horizontal' | 'both';
7 | } = { direction: 'both' }
8 | ) {
9 | if (process.env.NODE_ENV === 'development') {
10 | if (typeof ref !== 'object' || typeof ref.current === 'undefined') {
11 | console.error('`useDraggableScroll` expects a single ref argument.');
12 | }
13 | }
14 |
15 | const { direction } = options;
16 |
17 | // The initial position (scroll progress and mouse location) when the mouse is pressed down on the element
18 | let initialPosition = { scrollTop: 0, scrollLeft: 0, mouseX: 0, mouseY: 0 };
19 |
20 | const mouseMoveHandler = (event: { clientX: number; clientY: number }) => {
21 | if (ref.current) {
22 | // Calculate differences to see how far the user has moved
23 | const dx = event.clientX - initialPosition.mouseX;
24 | const dy = event.clientY - initialPosition.mouseY;
25 |
26 | // Scroll the element according to those differences
27 | if (direction !== 'horizontal')
28 | ref.current.scrollTop = initialPosition.scrollTop - dy;
29 | if (direction !== 'vertical')
30 | ref.current.scrollLeft = initialPosition.scrollLeft - dx;
31 | }
32 | };
33 |
34 | const mouseUpHandler = () => {
35 | // Return to cursor: grab after the user is no longer pressing
36 | if (ref.current) ref.current.style.cursor = 'grab';
37 |
38 | // Remove the event listeners since it is not necessary to track the mouse position anymore
39 | document.removeEventListener('mousemove', mouseMoveHandler);
40 | document.removeEventListener('mouseup', mouseUpHandler);
41 | };
42 |
43 | const onMouseDown = (event: { clientX: number; clientY: number }) => {
44 | if (ref.current) {
45 | // Save the position at the moment the user presses down
46 | initialPosition = {
47 | scrollLeft: ref.current.scrollLeft,
48 | scrollTop: ref.current.scrollTop,
49 | mouseX: event.clientX,
50 | mouseY: event.clientY,
51 | };
52 |
53 | // Show a cursor: grabbing style and set user-select: none to avoid highlighting text while dragging
54 | ref.current.style.cursor = 'grabbing';
55 | ref.current.style.userSelect = 'none';
56 |
57 | // Add the event listeners that will track the mouse position for the rest of the interaction
58 | document.addEventListener('mousemove', mouseMoveHandler);
59 | document.addEventListener('mouseup', mouseUpHandler);
60 | }
61 | };
62 |
63 | return { onMouseDown };
64 | }
65 |
--------------------------------------------------------------------------------