├── .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 | --------------------------------------------------------------------------------