├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.setup.js ├── package.json ├── src ├── checkVisibility.js ├── checkVisibility.test.js ├── index.js └── index.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,json}] 10 | indent_size = 2 11 | indent_style = space 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "plugin:prettier/recommended"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.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 | 63 | # Build directory 64 | lib 65 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "proseWrap": "always", 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: 5 | - stable 6 | 7 | cache: yarn 8 | 9 | script: yarn test:coverage 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to 7 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 | 9 | ## [Unreleased] 10 | 11 | ## [0.3.0] - 2019-10-02 12 | 13 | ### Fixed 14 | 15 | - Peer dependencies have correct `react` and `react-dom` versions. 16 | 17 | ## [0.2.0] - 2018-12-09 18 | 19 | ### Added 20 | 21 | - `scrollableEl` option. 22 | 23 | ## 0.1.0 - 2018-11-01 24 | 25 | Initial version. 26 | 27 | [unreleased]: 28 | https://github.com/olistic/react-use-visibility/compare/v0.2.0...HEAD 29 | [0.2.0]: https://github.com/olistic/react-use-visibility/compare/v0.1.0...v0.2.0 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matías Olivera 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-use-visibility 2 | 3 | React hook for tracking components visibility. 4 | 5 | [![Travis][build-badge]][build] [![Codecov][codecov-badge]][codecov] 6 | 7 | ![Demo](https://user-images.githubusercontent.com/5600126/47879786-42fc5700-de00-11e8-8ffc-70ff651b4a8b.gif) 8 | 9 | > _Electrons becoming excited as they enter the screen._ 10 | 11 | ## Installation 12 | 13 | ```sh 14 | $ npm install --save react-use-visibility 15 | ``` 16 | 17 | Additionally, you'll need to install version `16.7.0-alpha.0` of `react` and 18 | `react-dom` since this package relies on 19 | [React Hooks](https://reactjs.org/hooks): 20 | 21 | ```sh 22 | $ npm install --save react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 23 | ``` 24 | 25 | > **DISCLAIMER:** React Hooks are an experimental proposal. The Hooks API, as 26 | > well as this library's, are unstable and subject to change. 27 | 28 | ## Usage 29 | 30 | ```js 31 | import React, { useRef } from 'react'; 32 | import useVisibility from 'react-use-visibility'; 33 | 34 | function Electron() { 35 | // Use a ref to attach to the element whose visibility you want to keep track of. 36 | const imgRef = useRef(); 37 | 38 | // `current` points to the mounted img element. 39 | const isVisible = useVisibility(imgRef.current); 40 | 41 | return ( 42 | an electron 48 | ); 49 | } 50 | ``` 51 | 52 | ## API Reference 53 | 54 | ### `useVisibility` 55 | 56 | ```js 57 | const isVisible = useVisibility(el, options); 58 | ``` 59 | 60 | Accepts a React element as the first argument (`el`) and returns whether it is 61 | on the screen or not. 62 | 63 | Optionally, you can pass a second argument to `useVisibility` that is an object 64 | with the following properties: 65 | 66 | - `partial` (_boolean_): Whether to consider the element visible when only a 67 | part of it is on the screen. Defaults to `false`. 68 | - `scrollableEl` (_Element_): The parent element triggering the scroll event. 69 | Defaults to `window`. 70 | 71 | [build-badge]: 72 | https://img.shields.io/travis/olistic/react-use-visibility/master.svg 73 | [build]: https://travis-ci.org/olistic/react-use-visibility 74 | [codecov-badge]: 75 | https://img.shields.io/codecov/c/github/olistic/react-use-visibility/master.svg 76 | [codecov]: https://codecov.io/gh/olistic/react-use-visibility 77 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-use-visibility", 3 | "version": "0.3.0", 4 | "description": "React hook for tracking components visibility", 5 | "keywords": [ 6 | "react", 7 | "hooks", 8 | "visibility", 9 | "screen" 10 | ], 11 | "main": "lib/index.js", 12 | "repository": "https://github.com/olistic/react-use-visibility.git", 13 | "author": "Matias Olivera ", 14 | "license": "MIT", 15 | "scripts": { 16 | "build": "yarn clean:build && babel src --out-dir lib", 17 | "clean": "yarn clean:build & yarn clean:coverage & yarn clean:modules", 18 | "clean:build": "rimraf lib", 19 | "clean:coverage": "rimraf coverage", 20 | "clean:modules": "rimraf node_modules", 21 | "lint": "eslint --cache src", 22 | "lint:fix": "yarn lint --fix", 23 | "pretest": "yarn lint", 24 | "test": "jest", 25 | "test:coverage": "yarn test --coverage", 26 | "test:watch": "yarn test --watch" 27 | }, 28 | "files": [ 29 | "lib" 30 | ], 31 | "dependencies": { 32 | "lodash.throttle": "^4.1.1" 33 | }, 34 | "peerDependencies": { 35 | "react": "^16.8.0", 36 | "react-dom": "^16.8.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "^7.6.2", 40 | "@babel/core": "^7.6.2", 41 | "@babel/preset-env": "^7.6.2", 42 | "@babel/preset-react": "^7.0.0", 43 | "@testing-library/jest-dom": "^4.1.0", 44 | "@testing-library/react": "^9.2.0", 45 | "babel-core": "^7.0.0-bridge.0", 46 | "babel-eslint": "10.0.3", 47 | "babel-jest": "^24.9.0", 48 | "eslint": "6.5.1", 49 | "eslint-config-prettier": "^6.3.0", 50 | "eslint-config-react-app": "^5.0.2", 51 | "eslint-plugin-flowtype": "4.3.0", 52 | "eslint-plugin-import": "2.18.2", 53 | "eslint-plugin-jsx-a11y": "6.2.3", 54 | "eslint-plugin-prettier": "^3.1.1", 55 | "eslint-plugin-react": "7.15.1", 56 | "eslint-plugin-react-hooks": "^2.1.1", 57 | "husky": "^3.0.8", 58 | "jest": "^24.9.0", 59 | "lint-staged": "^9.4.1", 60 | "prettier": "1.18.2", 61 | "react": "^16.10.1", 62 | "react-dom": "^16.10.1", 63 | "regenerator-runtime": "^0.13.3", 64 | "rimraf": "^3.0.0" 65 | }, 66 | "jest": { 67 | "clearMocks": true, 68 | "setupFilesAfterEnv": [ 69 | "./jest.setup.js" 70 | ] 71 | }, 72 | "husky": { 73 | "hooks": { 74 | "pre-commit": "lint-staged" 75 | } 76 | }, 77 | "lint-staged": { 78 | "*.{js,json,css,md}": [ 79 | "prettier --write", 80 | "git add" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/checkVisibility.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Checks if the given element is on the screen. 3 | * 4 | * @param {Element} el The element. 5 | * @param {boolean} partial Whether to consider the element visible when only a 6 | * part of it is on the screen. 7 | * 8 | * @returns {boolean} Whether the element is visible. 9 | */ 10 | function checkVisibility(el, partial) { 11 | if (!el) { 12 | return false; 13 | } 14 | 15 | const { 16 | top, 17 | right, 18 | bottom, 19 | left, 20 | width, 21 | height, 22 | } = el.getBoundingClientRect(); 23 | 24 | if (top + right + bottom + left === 0) { 25 | return false; 26 | } 27 | 28 | const topCheck = partial ? top + height : top; 29 | const bottomCheck = partial ? bottom - height : bottom; 30 | const rightCheck = partial ? right - width : right; 31 | const leftCheck = partial ? left + width : left; 32 | 33 | const windowWidth = window.innerWidth; 34 | const windowHeight = window.innerHeight; 35 | 36 | return ( 37 | topCheck >= 0 && 38 | leftCheck >= 0 && 39 | bottomCheck <= windowHeight && 40 | rightCheck <= windowWidth 41 | ); 42 | } 43 | 44 | export default checkVisibility; 45 | -------------------------------------------------------------------------------- /src/checkVisibility.test.js: -------------------------------------------------------------------------------- 1 | import checkVisibility from './checkVisibility'; 2 | 3 | describe('checkVisibility', () => { 4 | let el; 5 | 6 | beforeEach(() => { 7 | window.innerWidth = 1024; 8 | window.innerHeight = 768; 9 | }); 10 | 11 | afterEach(() => { 12 | window.innerWidth = null; 13 | window.innerHeight = null; 14 | }); 15 | 16 | describe('with no element', () => { 17 | beforeEach(() => { 18 | el = null; 19 | }); 20 | 21 | test('is not visible', () => { 22 | expect(checkVisibility(el, false)).toBe(false); 23 | }); 24 | 25 | test('is not partially visible', () => { 26 | expect(checkVisibility(el, true)).toBe(false); 27 | }); 28 | }); 29 | 30 | describe('when element has no size', () => { 31 | beforeEach(() => { 32 | el = { 33 | getBoundingClientRect: () => ({ 34 | top: 0, 35 | right: 0, 36 | bottom: 0, 37 | left: 0, 38 | width: 0, 39 | height: 0, 40 | }), 41 | }; 42 | }); 43 | 44 | test('is not visible', () => { 45 | expect(checkVisibility(el, false)).toBe(false); 46 | }); 47 | 48 | test('is not partially visible', () => { 49 | expect(checkVisibility(el, true)).toBe(false); 50 | }); 51 | }); 52 | 53 | describe('when element is on the screen', () => { 54 | beforeEach(() => { 55 | el = { 56 | getBoundingClientRect: () => ({ 57 | top: 0, 58 | right: 100, 59 | bottom: 100, 60 | left: 0, 61 | width: 100, 62 | height: 100, 63 | }), 64 | }; 65 | }); 66 | 67 | test('is visible', () => { 68 | expect(checkVisibility(el, false)).toBe(true); 69 | }); 70 | 71 | test('is partially visible', () => { 72 | expect(checkVisibility(el, true)).toBe(true); 73 | }); 74 | }); 75 | 76 | describe('when element is not fully on the screen', () => { 77 | describe('appearing from the top', () => { 78 | beforeEach(() => { 79 | el = { 80 | getBoundingClientRect: () => ({ 81 | top: -50, 82 | right: 100, 83 | bottom: 50, 84 | left: 0, 85 | width: 100, 86 | height: 100, 87 | }), 88 | }; 89 | }); 90 | 91 | test('is not visible', () => { 92 | expect(checkVisibility(el, false)).toBe(false); 93 | }); 94 | 95 | test('is partially visible', () => { 96 | expect(checkVisibility(el, true)).toBe(true); 97 | }); 98 | }); 99 | 100 | describe('appearing from the right', () => { 101 | beforeEach(() => { 102 | el = { 103 | getBoundingClientRect: () => ({ 104 | top: 0, 105 | right: 1074, 106 | bottom: 100, 107 | left: 974, 108 | width: 100, 109 | height: 100, 110 | }), 111 | }; 112 | }); 113 | 114 | test('is not visible', () => { 115 | expect(checkVisibility(el, false)).toBe(false); 116 | }); 117 | 118 | test('is partially visible', () => { 119 | expect(checkVisibility(el, true)).toBe(true); 120 | }); 121 | }); 122 | 123 | describe('appearing from the bottom', () => { 124 | beforeEach(() => { 125 | el = { 126 | getBoundingClientRect: () => ({ 127 | top: 758, 128 | right: 100, 129 | bottom: 858, 130 | left: 0, 131 | width: 100, 132 | height: 100, 133 | }), 134 | }; 135 | }); 136 | 137 | test('is not visible', () => { 138 | expect(checkVisibility(el, false)).toBe(false); 139 | }); 140 | 141 | test('is partially visible', () => { 142 | expect(checkVisibility(el, true)).toBe(true); 143 | }); 144 | }); 145 | 146 | describe('appearing from the left', () => { 147 | beforeEach(() => { 148 | el = { 149 | getBoundingClientRect: () => ({ 150 | top: 0, 151 | right: 50, 152 | bottom: 100, 153 | left: -50, 154 | width: 100, 155 | height: 100, 156 | }), 157 | }; 158 | }); 159 | 160 | test('is not visible', () => { 161 | expect(checkVisibility(el, false)).toBe(false); 162 | }); 163 | 164 | test('is partially visible', () => { 165 | expect(checkVisibility(el, true)).toBe(true); 166 | }); 167 | }); 168 | }); 169 | }); 170 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import throttle from 'lodash.throttle'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import checkVisibility from './checkVisibility'; 5 | 6 | const throttleInterval = 150; 7 | 8 | const useVisibility = (el, { partial = false, scrollableEl = window } = {}) => { 9 | const [isVisible, setIsVisible] = useState(false); 10 | 11 | useEffect(() => { 12 | const handleScrollOrResize = throttle( 13 | () => setIsVisible(checkVisibility(el, partial)), 14 | throttleInterval, 15 | ); 16 | 17 | scrollableEl.addEventListener('scroll', handleScrollOrResize); 18 | window.addEventListener('resize', handleScrollOrResize); 19 | 20 | setIsVisible(checkVisibility(el, partial)); 21 | 22 | return () => { 23 | scrollableEl.removeEventListener('scroll', handleScrollOrResize); 24 | window.removeEventListener('resize', handleScrollOrResize); 25 | }; 26 | }, [el, partial, scrollableEl]); 27 | 28 | return isVisible; 29 | }; 30 | 31 | export default useVisibility; 32 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import checkVisibility from './checkVisibility'; 5 | import useVisibility from '.'; 6 | 7 | jest.mock('./checkVisibility'); 8 | const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); 9 | 10 | const Foo = ({ partial, scrollableEl }) => { 11 | const elRef = useRef(null); 12 | const isVisible = useVisibility(elRef.current, { partial, scrollableEl }); 13 | return
{isVisible ? 'visible' : 'not visible'}
; 14 | }; 15 | 16 | describe('useVisibility', () => { 17 | test('is initially not visible', () => { 18 | const { container } = render(); 19 | const el = container.firstChild; 20 | expect(el).toHaveTextContent('not visible'); 21 | }); 22 | 23 | test('attaches scroll and resize event listeners to window', () => { 24 | const { rerender } = render(); 25 | // Force useEffect to run. 26 | rerender(); 27 | expect(addEventListenerSpy).toHaveBeenCalledWith( 28 | 'scroll', 29 | expect.any(Function), 30 | ); 31 | expect(addEventListenerSpy).toHaveBeenCalledWith( 32 | 'resize', 33 | expect.any(Function), 34 | ); 35 | }); 36 | 37 | test('updates visibility after component is rendered', () => { 38 | const { container, rerender } = render(); 39 | const el = container.firstChild; 40 | expect(el).toHaveTextContent('not visible'); 41 | checkVisibility.mockReturnValue(true); 42 | // Force useEffect to run. 43 | rerender(); 44 | expect(el).toHaveTextContent('visible'); 45 | expect(checkVisibility).toHaveBeenCalledWith(el, false); 46 | }); 47 | 48 | test.skip('updates visibility on window scroll', () => {}); 49 | 50 | test.skip('updates visibility on window resize', () => {}); 51 | 52 | describe('with partial option', () => { 53 | test('checks partial visibility', () => { 54 | const { container, rerender } = render(); 55 | const el = container.firstChild; 56 | // Force useEffect to run. 57 | rerender(); 58 | expect(checkVisibility).toHaveBeenCalledWith(el, true); 59 | }); 60 | }); 61 | 62 | describe('with scrollableEl option', () => { 63 | test('attaches scroll event listener to passed in scrollable element', () => { 64 | const scrollableEl = { 65 | addEventListener: jest.fn(), 66 | removeEventListener: jest.fn(), 67 | }; 68 | const { rerender } = render(); 69 | // Force useEffect to run. 70 | rerender(); 71 | expect(scrollableEl.addEventListener).toHaveBeenCalledWith( 72 | 'scroll', 73 | expect.any(Function), 74 | ); 75 | }); 76 | }); 77 | }); 78 | --------------------------------------------------------------------------------