├── .gitignore
├── LICENSE
├── README.md
├── index.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Tomás Tarragón
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-element-scroll-hook
2 |
3 | A react hook to use the scroll information of an element.
4 |
5 | ## Install
6 |
7 | `npm install react-element-scroll-hook`
8 |
9 | ## Usage
10 |
11 | ```
12 | // Import the hook
13 | import useScrollInfo from 'react-element-scroll-hook';
14 |
15 | function Mycomponent(props) {
16 | // Initialize the hook
17 | const [scrollInfo, setRef] = useScrollInfo();
18 |
19 | // Use the scrollInfo at will
20 | console.log(scrollInfo);
21 |
22 | // use setRef to indicate the element you want to monitor
23 | return (
24 |
25 | {props.children}
26 |
27 | );
28 | }
29 | ```
30 |
31 | ## scrollInfo object
32 |
33 | When using this hook, you'll get an object containing the scroll data. It has two keys, `x` and `y`, each of them contain the following keys:
34 |
35 | - value: amount of pixels scrolled
36 | - total: amount of pixels that can be scrolled
37 | - percentage: a value from 0 to 1 or `null` if there's no scroll
38 | - className: a string to identify the state of the scroll
39 | - direction: the direction of the last scroll: 1 for down/right, -1 for up/left, 0 for initial value
40 |
41 | #### className
42 |
43 | For the y axis, className can take 4 values:
44 | `scroll-top`, `scroll-middle-y`, `scroll-bottom`, and `no-scroll-y`.
45 |
46 | For the x axis, the values are:
47 | `scroll-left`, `scroll-middle-x`, `scroll-right`, and `no-scroll-x`.
48 |
49 | #### Example scrollInfo object
50 |
51 | ```
52 | {
53 | x: {
54 | percentage: 0.5,
55 | value: 120,
56 | total: 240,
57 | className: 'scroll-middle-x',
58 | direction: 1
59 | },
60 | y: {
61 | percentage: 1,
62 | value: 200,
63 | total: 200,
64 | className: 'scroll-bottom',
65 | direction: -1
66 | }
67 | }
68 | ```
69 |
70 | ## Basic Example
71 |
72 | In this basic example, we'll add the scroll Y className to a component, to later style it with CSS based on weather it's scrolle or not.
73 |
74 | ```
75 | // Import the hook
76 | import useScrollInfo from 'react-element-scroll-hook';
77 |
78 | function Mycomponent(props) {
79 | // Initialize the hook
80 | const [scrollInfo, setRef] = useScrollInfo();
81 |
82 | return (
83 |
84 | {props.children}
85 |
86 | );
87 | }
88 | ```
89 |
90 | ## Accessing the element ref
91 |
92 | If you also need to access the monitored element, you can use the third constant returned by `useScrollInfo`:
93 |
94 | ```
95 | function Mycomponent(props) {
96 | // Initialize the hook
97 | const [scrollInfo, setRef, ref] = useScrollInfo();
98 |
99 | // Do something with the element
100 | console.log(ref.current);
101 |
102 | return (
103 |
104 | {props.children}
105 |
106 | );
107 | }
108 | ```
109 |
110 | ## Browser compatibility
111 |
112 | This should work out of the box on all major browser, including Edge. However, it uses ResizeObserver to work completely flawless.
113 | If you want it to work on older browsers properly when the element is resized (but the viewport isn't), you'll need a ResizeObserver polyfill.
114 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState, useCallback } from 'react';
2 |
3 | // Edge has a bug where scrollHeight is 1px bigger than clientHeight when there's no scroll.
4 | const isEdge = typeof navigator !== 'undefined' && /Edge\/\d./i.test(window.navigator.userAgent);
5 |
6 | // Small hook to use ResizeOberver if available. This fixes some issues when the component is resized.
7 | // This needs a polyfill to work on all browsers. The polyfill is not included in order to keep the package light.
8 | function useResizeObserver(ref, callback) {
9 | useEffect(() => {
10 | if (typeof window !== 'undefined' && window.ResizeObserver) {
11 | const resizeObserver = new ResizeObserver((entries) => {
12 | // Wrap it in requestAnimationFrame to avoid this error - ResizeObserver loop limit exceeded
13 | window.requestAnimationFrame(() => {
14 | if (!Array.isArray(entries) || !entries.length) {
15 | return;
16 | }
17 | callback(entries[0].contentRect);
18 | });
19 | });
20 |
21 | resizeObserver.observe(ref.current);
22 |
23 | return () => {
24 | resizeObserver.disconnect();
25 | };
26 | }
27 | }, [ref]);
28 | };
29 |
30 | function throttle(func, wait) {
31 | let context, args, result;
32 | let timeout = null;
33 | let previous = 0;
34 | const later = function () {
35 | timeout = null;
36 | result = func.apply(context, args);
37 | if (!timeout) {
38 | context = args = null;
39 | }
40 | };
41 | return function () {
42 | const now = Date.now();
43 | const remaining = wait - (now - previous);
44 | context = this;
45 | args = arguments;
46 | if (remaining <= 0 || remaining > wait) {
47 | if (timeout) {
48 | clearTimeout(timeout);
49 | timeout = null;
50 | }
51 | previous = now;
52 | result = func.apply(context, args);
53 | if (!timeout) {
54 | context = args = null;
55 | }
56 | } else if (!timeout) {
57 | timeout = setTimeout(later, remaining);
58 | }
59 | return result;
60 | };
61 | }
62 |
63 | function useScrollInfo(throttleTime = 50) {
64 | const [scroll, setScroll] = useState({ x: {}, y: {} });
65 | const ref = useRef(null);
66 | const previousScroll = useRef(null);
67 |
68 | useResizeObserver(ref, () => {
69 | update();
70 | });
71 |
72 | function update() {
73 | const element = ref.current;
74 | let maxY = element.scrollHeight - element.clientHeight;
75 | const maxX = element.scrollWidth - element.clientWidth;
76 |
77 | // Edge has a bug where scrollHeight is 1px bigger than clientHeight when there's no scroll.
78 | if (isEdge && maxY === 1 && element.scrollTop === 0) {
79 | maxY = 0;
80 | }
81 |
82 | const percentageY = maxY !== 0 ? element.scrollTop / maxY : null;
83 | const percentageX = maxX !== 0 ? element.scrollLeft / maxX : null;
84 |
85 | let classNameY = 'no-scroll-y';
86 | if (percentageY === 0) {
87 | classNameY = 'scroll-top';
88 | } else if (percentageY === 1) {
89 | classNameY = 'scroll-bottom';
90 | } else if (percentageY) {
91 | classNameY = 'scroll-middle-y';
92 | }
93 |
94 | let classNameX = 'no-scroll-x';
95 | if (percentageX === 0) {
96 | classNameX = 'scroll-left';
97 | } else if (percentageX === 1) {
98 | classNameX = 'scroll-right';
99 | } else if (percentageX) {
100 | classNameX = 'scroll-middle-x';
101 | }
102 |
103 | const previous = previousScroll.current;
104 |
105 | const scrollInfo = {
106 | x: {
107 | percentage: percentageX,
108 | value: element.scrollLeft,
109 | total: maxX,
110 | className: classNameX,
111 | direction: previous ? Math.sign(element.scrollLeft - previous.x.value) : 0,
112 | },
113 | y: {
114 | percentage: percentageY,
115 | value: element.scrollTop,
116 | total: maxY,
117 | className: classNameY,
118 | direction: previous ? Math.sign(element.scrollTop - previous.y.value) : 0,
119 | }
120 | };
121 | previousScroll.current = scrollInfo;
122 | setScroll(scrollInfo);
123 | }
124 |
125 | const throttledUpdate = throttle(update, throttleTime);
126 |
127 | const setRef = useCallback(node => {
128 | if (node) {
129 | // When the ref is first set (after mounting)
130 | node.addEventListener('scroll', throttledUpdate);
131 | if (!window.ResizeObserver) {
132 | window.addEventListener('resize', throttledUpdate); // Fallback if ResizeObserver is not available
133 | }
134 | ref.current = node;
135 | throttledUpdate(); // initialization
136 | } else if (ref.current) {
137 | // When unmounting
138 | ref.current.removeEventListener('scroll', throttledUpdate);
139 | if (!window.ResizeObserver) {
140 | window.removeEventListener('resize', throttledUpdate);
141 | }
142 | }
143 | }, []);
144 |
145 | return [scroll, setRef, ref];
146 | }
147 |
148 | export default useScrollInfo;
149 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-element-scroll-hook",
3 | "version": "1.1.0",
4 | "description": "A react hook to use the scroll information of an element",
5 | "main": "index.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/tomisu/react-element-scroll-hook.git"
9 | },
10 | "keywords": [
11 | "scroll",
12 | "position",
13 | "react",
14 | "hook",
15 | "scroll position",
16 | "react hook",
17 | "element scroll",
18 | "scroll info"
19 | ],
20 | "author": "Tomás Tarragón ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/tomisu/react-element-scroll-hook/issues"
24 | },
25 | "homepage": "https://github.com/tomisu/react-element-scroll-hook#readme",
26 | "peerDependencies": {
27 | "react-dom": "^16.8.0",
28 | "react": "^16.8.6"
29 | },
30 | "devDependencies": {
31 | "react-dom": "^16.8.0",
32 | "react": "^16.8.6"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------