From 39770efde25e143557ad672df22442a5d839a1f6 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Thu, 20 Jun 2019 16:34:05 +0200 Subject: [PATCH 01/13] Fix findDOMNode deprecation warning This PR fixes React `findDOMNode` deprecation warning. https://reactjs.org/docs/strict-mode.html#warning-about-deprecated-finddomnode-usage This makes sure the component is compatible with upcoming concurrent mode. It's a breaking change and requires a major release because it adds a node to the DOM, we could use `display: contents;` as suggested by React. --- visibility-sensor.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/visibility-sensor.js b/visibility-sensor.js index 067b3c4..8f97729 100644 --- a/visibility-sensor.js +++ b/visibility-sensor.js @@ -76,6 +76,8 @@ export default class VisibilitySensor extends React.Component { constructor(props) { super(props); + this.node = React.createRef(); + this.state = { isVisible: null, visibilityRect: {} @@ -83,7 +85,6 @@ export default class VisibilitySensor extends React.Component { } componentDidMount() { - this.node = ReactDOM.findDOMNode(this); if (this.props.active) { this.startWatching(); } @@ -94,9 +95,6 @@ export default class VisibilitySensor extends React.Component { } componentDidUpdate(prevProps) { - // re-register node in componentDidUpdate if children diffs [#103] - this.node = ReactDOM.findDOMNode(this); - if (this.props.active && !prevProps.active) { this.setState({ isVisible: null, @@ -219,7 +217,7 @@ export default class VisibilitySensor extends React.Component { * Check if the element is within the visible viewport */ check = () => { - const el = this.node; + const el = this.node && this.node.current; let rect; let containmentRect; @@ -324,13 +322,17 @@ export default class VisibilitySensor extends React.Component { return state; }; - render() { + renderChildren = () => { if (this.props.children instanceof Function) { return this.props.children({ isVisible: this.state.isVisible, visibilityRect: this.state.visibilityRect }); } - return React.Children.only(this.props.children); + return React.Children.only(this.props.children) + } + + render() { + return
{this.renderChildren()}
; } } From febe43fc012a8f3dc6b2ac9c9c0e21e732be8cc1 Mon Sep 17 00:00:00 2001 From: Josh Johnston Date: Thu, 11 Jul 2019 09:39:37 +0100 Subject: [PATCH 02/13] Fix case of absolutely positioned elements in a container - added getRef so consumers can specify which element is used for calculations - necessary for absolutely positioned elements in a container - added deprecation notice for passing elements as children - added some new test cases - updated README and examples --- README.md | 57 +++++++++------- example/main.js | 2 +- tests/visibility-sensor-spec.jsx | 109 +++++++++++++++++++++++++++---- visibility-sensor.js | 35 +++++++--- 4 files changed, 161 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 8d3ec6b..dfba7d4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -React Visibility Sensor -==== +# React Visibility Sensor [![Build Status](https://secure.travis-ci.org/joshwnj/react-visibility-sensor.png)](http://travis-ci.org/joshwnj/react-visibility-sensor) @@ -7,8 +6,7 @@ Sensor component for React that notifies you when it goes in or out of the windo Sponsored by [X-Team](https://x-team.com) -Install ----- +## Install `npm install react-visibility-sensor` @@ -23,8 +21,7 @@ In this case, make sure that `React` and `ReactDOM` are already loaded and globa Take a look at the [umd example](./example-umd/) to see this in action -Example ----- +## Example [View an example on codesandbox](https://codesandbox.io/s/p73kyx9zpm) @@ -40,13 +37,13 @@ To run the example locally: General usage goes something like: ```js -const VisibilitySensor = require('react-visibility-sensor'); +const VisibilitySensor = require("react-visibility-sensor"); -function onChange (isVisible) { - console.log('Element is now %s', isVisible ? 'visible' : 'hidden'); +function onChange(isVisible) { + console.log("Element is now %s", isVisible ? "visible" : "hidden"); } -function MyComponent (props) { +function MyComponent(props) { return (
...content goes here...
@@ -55,25 +52,43 @@ function MyComponent (props) { } ``` +### Child Function syntax + You can also pass a child function, which can be convenient if you don't need to store the visibility anywhere: ```js -function MyComponent (props) { +function MyComponent(props) { return ( - {({isVisible}) => -
I am {isVisible ? 'visible' : 'invisible'}
- } + {({ isVisible, visibilityRect, getRef }) => ( +
I am {isVisible ? "visible" : "invisible"}
+ )}
); } ``` -Props ----- +The child function must return an element. The 3 arguments that you can access from the child function are: + +- `isVisible`: Boolean +- `getRef`: a Function allowing you to specify which element should be used for calculating visibility. This is useful when you have something absolutely-positioned. Using `getRef` is optional, and if it is not called a wrapper `
` will be created with the ref. +- `visibilityRect`: an Object indicating which sides of the element are visible, in the shape of: + +``` +{ + top: Boolean, + bottom: Boolean, + left: Boolean, + right: Boolean +} +``` + +--- + +## Props - `onChange`: callback for whenever the element changes from being within the window viewport or not. Function is called with 1 argument `(isVisible: boolean)` -- `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback. +- `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback. - `partialVisibility`: (default `false`) consider element visible if only part of it is visible. Also possible values are - 'top', 'right', 'bottom', 'left' - in case it's needed to detect when one of these become visible explicitly. - `offset`: (default `{}`) with offset you can define amount of px from one side when the visibility should already change. So in example setting `offset={{top:10}}` means that the visibility changes hidden when there is less than 10px to top of the viewport. Offset works along with `partialVisibility` - `minTopValue`: (default `0`) consider element visible if only part of it is visible and a minimum amount of pixels could be set, so if at least 100px are in viewport, we mark element as visible. @@ -87,16 +102,14 @@ Props - `resizeThrottle`: (default: `-1`) by specifying a value > -1, you are enabling throttle instead of the delay to trigger checks on resize event. Throttle supercedes delay. - `containment`: (optional) element to use as a viewport when checking visibility. Default behaviour is to use the browser window as viewport. - `delayedCall`: (default `false`) if is set to true, wont execute on page load ( prevents react apps triggering elements as visible before styles are loaded ) -- `children`: can be a React element or a function. If you provide a function, it will be called with 1 argument `{isVisible: ?boolean, visibilityRect: Object}` +- `children`: can be a React element or a function. If you provide a function, it will be called with 1 argument `{isVisible: ?boolean, visibilityRect: Object}` It's possible to use both `intervalCheck` and `scrollCheck` together. This means you can detect most visibility changes quickly with `scrollCheck`, and an `intervalCheck` with a higher `intervalDelay` will act as a fallback for other visibility events, such as resize of a container. -Thanks ----- +## Thanks Special thanks to [contributors](https://github.com/joshwnj/react-visibility-sensor/graphs/contributors) -License ----- +## License MIT diff --git a/example/main.js b/example/main.js index 7be0055..6405821 100644 --- a/example/main.js +++ b/example/main.js @@ -34,7 +34,7 @@ class Example extends React.Component { partialVisibility={this.props.partialVisibility} offset={this.props.offset} > -
+ {({ getRef }) =>
}
diff --git a/tests/visibility-sensor-spec.jsx b/tests/visibility-sensor-spec.jsx index 742dad1..6a1d9b0 100644 --- a/tests/visibility-sensor-spec.jsx +++ b/tests/visibility-sensor-spec.jsx @@ -42,7 +42,7 @@ describe("VisibilitySensor", function() { var element = ( -
+ { ({ getRef }) =>
} ); @@ -78,9 +78,9 @@ describe("VisibilitySensor", function() { scrollDelay={10} onChange={onChange} intervalCheck={false} - > + >{ () => (
- + )}
); @@ -119,7 +119,7 @@ describe("VisibilitySensor", function() { function getElement(style) { return ( -
+ { () =>
} ); } @@ -135,7 +135,7 @@ describe("VisibilitySensor", function() { var element = ( -
+ { () =>
} ); @@ -224,7 +224,7 @@ describe("VisibilitySensor", function() { offset={{ top: 50 }} intervalDelay={10} > -
+ { ({ getRef }) =>
} ); @@ -255,7 +255,7 @@ describe("VisibilitySensor", function() { offset={{ direction: "top", value: 50 }} intervalDelay={10} > -
+ { () =>
} ); @@ -288,7 +288,7 @@ describe("VisibilitySensor", function() { offset={{ top: 50 }} intervalDelay={10} > -
+ { () =>
} ); @@ -321,7 +321,7 @@ describe("VisibilitySensor", function() { offset={{ top: -50 }} intervalDelay={10} > -
+ { () =>
} ); @@ -340,6 +340,10 @@ describe("VisibilitySensor", function() { "visibilityRect" in props, "children should be called with visibilityRect prop" ); + assert( + "getRef" in props, + "children should be called with getRef prop" + ); return
; }; @@ -364,7 +368,7 @@ describe("VisibilitySensor", function() { var element = ( -
+ { () =>
} ); @@ -383,7 +387,7 @@ describe("VisibilitySensor", function() { var element = (
-
+ { () =>
}
); @@ -391,3 +395,86 @@ describe("VisibilitySensor", function() { ReactDOM.render(element, node); }); }); + + +describe("VisibilitySensor with Container", function() { + var node; + var container; + + beforeEach(function() { + node = document.createElement("div"); + document.body.appendChild(node); + + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(function() { + ReactDOM.unmountComponentAtNode(node); + document.body.removeChild(node); + document.body.removeChild(container); + }); + + + it("should detect an absolutely positioned element inside the visible part of a container", function(done) { + var firstTime = true; + var onChange = function(isVisible) { + if (firstTime) { + assert.equal(isVisible, true, "Component is visible"); + done(); + } + }; + + container.style.width = 300 + container.style.height = 300 + container.style.position = 'relative' + container.style.overflow = 'hidden' + + var element = {( { getRef } ) => ( +
+ )} + + ReactDOM.render(element, node); + }); + + it("should not detect an absolutely positioned element outside the visible part of a container", function(done) { + var firstTime = true; + var onChange = function(isVisible) { + if (firstTime) { + assert.equal(isVisible, false, "Component is not visible"); + done(); + } + }; + + container.style.width = 300 + container.style.height = 300 + container.style.position = 'relative' + container.style.overflow = 'hidden' + + var element = {( { getRef } ) => ( +
+ )} + + ReactDOM.render(element, node); + }); +}); diff --git a/visibility-sensor.js b/visibility-sensor.js index 8f97729..4a346ce 100644 --- a/visibility-sensor.js +++ b/visibility-sensor.js @@ -322,17 +322,36 @@ export default class VisibilitySensor extends React.Component { return state; }; - renderChildren = () => { - if (this.props.children instanceof Function) { - return this.props.children({ + render() { + const { children } = this.props; + const isFunction = children instanceof Function; + + if (isFunction) { + let didAskForRef = false; + const getRef = () => { + didAskForRef = true; + return this.node; + }; + + const output = children({ isVisible: this.state.isVisible, - visibilityRect: this.state.visibilityRect + visibilityRect: this.state.visibilityRect, + getRef: getRef }); + + // if the consumer doesn't use our getRef function, we'll wrap + // it in a node and apply the ref ourselves. + return didAskForRef ? output :
{output}
; } - return React.Children.only(this.props.children) - } - render() { - return
{this.renderChildren()}
; + if (!React.Children.count(children)) { + return
; + } + + console.warn(`[notice] passing children directly into the VisibilitySensor has been deprecated, and will be removed in the next major version. + +Please upgrade to the Child Function syntax instead: https://github.com/joshwnj/react-visibility-sensor#child-function-syntax`); + + return
{children}
; } } From de5cf616a78dfe9653a6d79cd80b723ccaf9d5ab Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Thu, 11 Jul 2019 14:18:45 +0200 Subject: [PATCH 03/13] update dependencies and mark sideEffects free --- package-lock.json | 51 ++++++++++++++++++++--------------------------- package.json | 11 +++++----- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8591dbb..21db52d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6949,36 +6949,28 @@ "urix": "^0.1.0" } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "react": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", "dev": true, "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" } }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "react-dom": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", + "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", "dev": true, "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" } }, "spdx-license-ids": { @@ -7366,13 +7358,14 @@ "safe-regex": "^1.1.0" } }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "scheduler": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", + "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", "dev": true, "requires": { - "is-number": "^7.0.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" } }, "toidentifier": { diff --git a/package.json b/package.json index a876993..2c4c149 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "react-visibility-sensor", - "version": "5.1.1", + "version": "6.0.0", "description": "Sensor component for React that notifies you when it goes in or out of the window viewport.", "main": "dist/visibility-sensor.js", + "sideEffects": false, "scripts": { "clean": "rm -rf dist && mkdir dist", "prebuild": "npm run clean", @@ -21,8 +22,8 @@ "author": "joshwnj", "license": "MIT", "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" + "react": ">=16.8.0", + "react-dom": ">=16.8.0" }, "devDependencies": { "@babel/core": "^7.5.5", @@ -39,8 +40,8 @@ "lint-staged": "^9.2.1", "mocha": "^6.2.0", "prettier": "1.18.2", - "react": "^16.4.2", - "react-dom": "^16.4.2", + "react": "^16.8.6", + "react-dom": "^16.8.6", "uglify-js": "^3.6.0", "uglifyjs-webpack-plugin": "^2.1.3", "webpack": "^4.37.0", From 928f04880c4644504033acfb12cdacadddeb8ecf Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Thu, 11 Jul 2019 14:19:37 +0200 Subject: [PATCH 04/13] rewrite code into hooks --- lib/is-visible-with-offset.js | 35 --- lib/use-visibility-sensor.js | 265 +++++++++++++++++++++ visibility-sensor.js | 431 +++++++--------------------------- 3 files changed, 350 insertions(+), 381 deletions(-) delete mode 100644 lib/is-visible-with-offset.js create mode 100644 lib/use-visibility-sensor.js diff --git a/lib/is-visible-with-offset.js b/lib/is-visible-with-offset.js deleted file mode 100644 index 9882782..0000000 --- a/lib/is-visible-with-offset.js +++ /dev/null @@ -1,35 +0,0 @@ -// Tell whether the rect is visible, given an offset -// -// return: boolean -module.exports = function (offset, rect, containmentRect) { - var offsetDir = offset.direction; - var offsetVal = offset.value; - - // Rules for checking different kind of offsets. In example if the element is - // 90px below viewport and offsetTop is 100, it is considered visible. - switch (offsetDir) { - case 'top': - return ((containmentRect.top + offsetVal) < rect.top) && - (containmentRect.bottom > rect.bottom) && - (containmentRect.left < rect.left) && - (containmentRect.right > rect.right); - - case 'left': - return ((containmentRect.left + offsetVal) < rect.left) && - (containmentRect.bottom > rect.bottom) && - (containmentRect.top < rect.top) && - (containmentRect.right > rect.right); - - case 'bottom': - return ((containmentRect.bottom - offsetVal) > rect.bottom) && - (containmentRect.left < rect.left) && - (containmentRect.right > rect.right) && - (containmentRect.top < rect.top); - - case 'right': - return ((containmentRect.right - offsetVal) > rect.right) && - (containmentRect.left < rect.left) && - (containmentRect.top < rect.top) && - (containmentRect.bottom > rect.bottom); - } -} diff --git a/lib/use-visibility-sensor.js b/lib/use-visibility-sensor.js new file mode 100644 index 0000000..8cc0877 --- /dev/null +++ b/lib/use-visibility-sensor.js @@ -0,0 +1,265 @@ +import { useEffect, useState, useRef, useCallback } from "react"; + +function normalizeRect(rect) { + if (rect.width === undefined) { + rect.width = rect.right - rect.left; + } + + if (rect.height === undefined) { + rect.height = rect.bottom - rect.top; + } + + return rect; +} + +function roundRectDown(rect) { + return { + top: Math.floor(rect.top), + left: Math.floor(rect.left), + bottom: Math.floor(rect.bottom), + right: Math.floor(rect.right) + }; +} + +export default function useVisibilitySensor( + nodeRef, + { + active = true, + onChange, + partialVisibility = false, + minTopValue = 0, + scrollCheck = false, + scrollDelay = 250, + scrollThrottle = -1, + resizeCheck = false, + resizeDelay = 250, + resizeThrottle = -1, + intervalCheck = true, + intervalDelay = 100, + delayedCall = false, + offset = {}, + containment = null + } +) { + const debounceCheckRef = useRef(); + const intervalRef = useRef(); + const [isVisible, setIsVisible] = useState(false); + const [visibilityRect, setVisibilityRect] = useState({}); + + const getContainer = useCallback(() => containment || window, [containment]); + + // Check if the element is within the visible viewport + const visibilityCheck = useCallback( + () => { + const el = nodeRef && nodeRef.current; + let rect; + let containmentRect; + + // if the component has rendered to null, dont update visibility + if (!el) { + return; + } + + rect = normalizeRect(roundRectDown(el.getBoundingClientRect())); + + if (containment) { + const containmentDOMRect = containment.getBoundingClientRect(); + containmentRect = { + top: containmentDOMRect.top, + left: containmentDOMRect.left, + bottom: containmentDOMRect.bottom, + right: containmentDOMRect.right + }; + } else { + containmentRect = { + top: 0, + left: 0, + bottom: window.innerHeight || document.documentElement.clientHeight, + right: window.innerWidth || document.documentElement.clientWidth + }; + } + + // Check if visibility is wanted via offset? + const hasValidOffset = typeof offset === "object"; + if (hasValidOffset) { + containmentRect.top += offset.top || 0; + containmentRect.left += offset.left || 0; + containmentRect.bottom -= offset.bottom || 0; + containmentRect.right -= offset.right || 0; + } + + const nextVisibilityRect = { + top: rect.top >= containmentRect.top, + left: rect.left >= containmentRect.left, + bottom: rect.bottom <= containmentRect.bottom, + right: rect.right <= containmentRect.right + }; + + // https://github.com/joshwnj/react-visibility-sensor/pull/114 + const hasSize = rect.height > 0 && rect.width > 0; + + let nextIsVisible = + hasSize && + nextVisibilityRect.top && + nextVisibilityRect.left && + nextVisibilityRect.bottom && + nextVisibilityRect.right; + + // check for partial visibility + if (hasSize && partialVisibility) { + let partialVisible = + rect.top <= containmentRect.bottom && + rect.bottom >= containmentRect.top && + rect.left <= containmentRect.right && + rect.right >= containmentRect.left; + + // account for partial visibility on a single edge + if (typeof partialVisibility === "string") { + partialVisible = nextVisibilityRect[partialVisibility]; + } + + // if we have minimum top visibility set by props, lets check, if it meets the passed value + // so if for instance element is at least 200px in viewport, then show it. + nextIsVisible = minTopValue + ? partialVisible && rect.top <= containmentRect.bottom - minTopValue + : partialVisible; + } + + // notify the parent when the value changes + if (isVisible !== nextIsVisible) { + setIsVisible(nextIsVisible); + setVisibilityRect(nextVisibilityRect); + if (onChange) onChange(nextIsVisible); + } + }, + [ + isVisible, + offset, + containment, + partialVisibility, + minTopValue, + onChange, + setIsVisible, + setVisibilityRect + ] + ); + + const addEventListener = useCallback( + (target, event, delay, throttle) => { + if (!debounceCheckRef.current) { + debounceCheckRef.current = {}; + } + const debounceCheck = debounceCheckRef.current; + let timeout; + let func; + + const later = () => { + timeout = null; + visibilityCheck(); + }; + + if (throttle > -1) { + func = () => { + if (!timeout) { + timeout = setTimeout(later, throttle || 0); + } + }; + } else { + func = () => { + clearTimeout(timeout); + timeout = setTimeout(later, delay || 0); + }; + } + + const info = { + target: target, + fn: func, + getLastTimeout: () => { + return timeout; + } + }; + + target.addEventListener(event, info.fn); + debounceCheck[event] = info; + + return () => { + clearTimeout(timeout); + }; + }, + [visibilityCheck] + ); + + useEffect( + () => { + function watch() { + if (debounceCheckRef.current || intervalRef.current) { + return; + } + + if (intervalCheck) { + intervalRef.current = setInterval(visibilityCheck, intervalDelay); + } + + if (scrollCheck) { + addEventListener( + getContainer(), + "scroll", + scrollDelay, + scrollThrottle + ); + } + + if (resizeCheck) { + addEventListener(window, "resize", resizeDelay, resizeThrottle); + } + + // if dont need delayed call, check on load ( before the first interval fires ) + !delayedCall && visibilityCheck(); + } + + if (active) { + watch(); + } + + // stop any listeners and intervals on props change and re-registers + return () => { + if (debounceCheckRef.current) { + const debounceCheck = debounceCheckRef.current; + // clean up event listeners and their debounce callers + for (let debounceEvent in debounceCheck) { + if (debounceCheck.hasOwnProperty(debounceEvent)) { + const debounceInfo = debounceCheck[debounceEvent]; + + clearTimeout(debounceInfo.getLastTimeout()); + debounceInfo.target.removeEventListener( + debounceEvent, + debounceInfo.fn + ); + + debounceCheck[debounceEvent] = null; + } + } + } + debounceCheckRef.current = null; + + if (intervalRef.current) { + intervalRef.current = clearInterval(intervalRef.current); + } + }; + }, + [ + active, + scrollCheck, + scrollDelay, + scrollThrottle, + resizeCheck, + resizeDelay, + resizeThrottle, + intervalCheck, + intervalDelay, + visibilityCheck + ] + ); + + return { isVisible, visibilityRect }; +} diff --git a/visibility-sensor.js b/visibility-sensor.js index 4a346ce..598090b 100644 --- a/visibility-sensor.js +++ b/visibility-sensor.js @@ -1,357 +1,96 @@ "use strict"; -import React from "react"; +import React, { useRef } from "react"; import ReactDOM from "react-dom"; import PropTypes from "prop-types"; -import isVisibleWithOffset from "./lib/is-visible-with-offset"; - -function normalizeRect(rect) { - if (rect.width === undefined) { - rect.width = rect.right - rect.left; - } - - if (rect.height === undefined) { - rect.height = rect.bottom - rect.top; - } - - return rect; -} - -export default class VisibilitySensor extends React.Component { - static defaultProps = { - active: true, - partialVisibility: false, - minTopValue: 0, - scrollCheck: false, - scrollDelay: 250, - scrollThrottle: -1, - resizeCheck: false, - resizeDelay: 250, - resizeThrottle: -1, - intervalCheck: true, - intervalDelay: 100, - delayedCall: false, - offset: {}, - containment: null, - children: - }; - - static propTypes = { - onChange: PropTypes.func, - active: PropTypes.bool, - partialVisibility: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.oneOf(["top", "right", "bottom", "left"]) - ]), - delayedCall: PropTypes.bool, - offset: PropTypes.oneOfType([ - PropTypes.shape({ - top: PropTypes.number, - left: PropTypes.number, - bottom: PropTypes.number, - right: PropTypes.number - }), - // deprecated offset property - PropTypes.shape({ - direction: PropTypes.oneOf(["top", "right", "bottom", "left"]), - value: PropTypes.number - }) - ]), - scrollCheck: PropTypes.bool, - scrollDelay: PropTypes.number, - scrollThrottle: PropTypes.number, - resizeCheck: PropTypes.bool, - resizeDelay: PropTypes.number, - resizeThrottle: PropTypes.number, - intervalCheck: PropTypes.bool, - intervalDelay: PropTypes.number, - containment: - typeof window !== "undefined" - ? PropTypes.instanceOf(window.Element) - : PropTypes.any, - children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), - minTopValue: PropTypes.number - }; - - constructor(props) { - super(props); - - this.node = React.createRef(); - - this.state = { - isVisible: null, - visibilityRect: {} - }; - } - - componentDidMount() { - if (this.props.active) { - this.startWatching(); - } - } - - componentWillUnmount() { - this.stopWatching(); +import useVisibilitySensor from "./lib/use-visibility-sensor"; + +export { useVisibilitySensor }; + +export default function VisibilitySensor({ + children, + active, + onChange, + partialVisibility, + minTopValue, + scrollCheck, + scrollDelay, + scrollThrottle, + resizeCheck, + resizeDelay, + resizeThrottle, + intervalCheck, + intervalDelay, + delayedCall, + offset, + containment +}) { + const nodeRef = useRef(); + const { isVisible, visibilityRect } = useVisibilitySensor(nodeRef, { + active, + onChange, + partialVisibility, + minTopValue, + scrollCheck, + scrollDelay, + scrollThrottle, + resizeCheck, + resizeDelay, + resizeThrottle, + intervalCheck, + intervalDelay, + delayedCall, + offset, + containment + }); + const isFunction = children instanceof Function; + + if (isFunction) { + // if the consumer doesn't use our getRef function, we'll wrap + // it in a node and apply the ref ourselves. + return
{children({ isVisible, visibilityRect })}
; } - componentDidUpdate(prevProps) { - if (this.props.active && !prevProps.active) { - this.setState({ - isVisible: null, - visibilityRect: {} - }); - - this.startWatching(); - } else if (!this.props.active) { - this.stopWatching(); - } - } - - getContainer = () => { - return this.props.containment || window; - }; - - addEventListener = (target, event, delay, throttle) => { - if (!this.debounceCheck) { - this.debounceCheck = {}; - } - - let timeout; - let func; - - const later = () => { - timeout = null; - this.check(); - }; - - if (throttle > -1) { - func = () => { - if (!timeout) { - timeout = setTimeout(later, throttle || 0); - } - }; - } else { - func = () => { - clearTimeout(timeout); - timeout = setTimeout(later, delay || 0); - }; - } - - const info = { - target: target, - fn: func, - getLastTimeout: () => { - return timeout; - } - }; - - target.addEventListener(event, info.fn); - this.debounceCheck[event] = info; - }; - - startWatching = () => { - if (this.debounceCheck || this.interval) { - return; - } - - if (this.props.intervalCheck) { - this.interval = setInterval(this.check, this.props.intervalDelay); - } - - if (this.props.scrollCheck) { - this.addEventListener( - this.getContainer(), - "scroll", - this.props.scrollDelay, - this.props.scrollThrottle - ); - } - - if (this.props.resizeCheck) { - this.addEventListener( - window, - "resize", - this.props.resizeDelay, - this.props.resizeThrottle - ); - } - - // if dont need delayed call, check on load ( before the first interval fires ) - !this.props.delayedCall && this.check(); - }; - - stopWatching = () => { - if (this.debounceCheck) { - // clean up event listeners and their debounce callers - for (let debounceEvent in this.debounceCheck) { - if (this.debounceCheck.hasOwnProperty(debounceEvent)) { - const debounceInfo = this.debounceCheck[debounceEvent]; - - clearTimeout(debounceInfo.getLastTimeout()); - debounceInfo.target.removeEventListener( - debounceEvent, - debounceInfo.fn - ); - - this.debounceCheck[debounceEvent] = null; - } - } - } - this.debounceCheck = null; - - if (this.interval) { - this.interval = clearInterval(this.interval); - } - }; - - roundRectDown(rect) { - return { - top: Math.floor(rect.top), - left: Math.floor(rect.left), - bottom: Math.floor(rect.bottom), - right: Math.floor(rect.right) - }; + if (!React.Children.count(children)) { + return
; } - /** - * Check if the element is within the visible viewport - */ - check = () => { - const el = this.node && this.node.current; - let rect; - let containmentRect; - - // if the component has rendered to null, dont update visibility - if (!el) { - return this.state; - } - - rect = normalizeRect(this.roundRectDown(el.getBoundingClientRect())); - - if (this.props.containment) { - const containmentDOMRect = this.props.containment.getBoundingClientRect(); - containmentRect = { - top: containmentDOMRect.top, - left: containmentDOMRect.left, - bottom: containmentDOMRect.bottom, - right: containmentDOMRect.right - }; - } else { - containmentRect = { - top: 0, - left: 0, - bottom: window.innerHeight || document.documentElement.clientHeight, - right: window.innerWidth || document.documentElement.clientWidth - }; - } - - // Check if visibility is wanted via offset? - const offset = this.props.offset || {}; - const hasValidOffset = typeof offset === "object"; - - if (hasValidOffset) { - containmentRect.top += offset.top || 0; - containmentRect.left += offset.left || 0; - containmentRect.bottom -= offset.bottom || 0; - containmentRect.right -= offset.right || 0; - } - - const visibilityRect = { - top: rect.top >= containmentRect.top, - left: rect.left >= containmentRect.left, - bottom: rect.bottom <= containmentRect.bottom, - right: rect.right <= containmentRect.right - }; - - // https://github.com/joshwnj/react-visibility-sensor/pull/114 - const hasSize = rect.height > 0 && rect.width > 0; - - let isVisible = - hasSize && - visibilityRect.top && - visibilityRect.left && - visibilityRect.bottom && - visibilityRect.right; - - // check for partial visibility - if (hasSize && this.props.partialVisibility) { - let partialVisible = - rect.top <= containmentRect.bottom && - rect.bottom >= containmentRect.top && - rect.left <= containmentRect.right && - rect.right >= containmentRect.left; - - // account for partial visibility on a single edge - if (typeof this.props.partialVisibility === "string") { - partialVisible = visibilityRect[this.props.partialVisibility]; - } - - // if we have minimum top visibility set by props, lets check, if it meets the passed value - // so if for instance element is at least 200px in viewport, then show it. - isVisible = this.props.minTopValue - ? partialVisible && - rect.top <= containmentRect.bottom - this.props.minTopValue - : partialVisible; - } - - // Deprecated options for calculating offset. - if ( - typeof offset.direction === "string" && - typeof offset.value === "number" - ) { - console.warn( - "[notice] offset.direction and offset.value have been deprecated. They still work for now, but will be removed in next major version. Please upgrade to the new syntax: { %s: %d }", - offset.direction, - offset.value - ); - - isVisible = isVisibleWithOffset(offset, rect, containmentRect); - } - - let state = this.state; - // notify the parent when the value changes - if (this.state.isVisible !== isVisible) { - state = { - isVisible: isVisible, - visibilityRect: visibilityRect - }; - this.setState(state); - if (this.props.onChange) this.props.onChange(isVisible); - } - - return state; - }; - - render() { - const { children } = this.props; - const isFunction = children instanceof Function; - - if (isFunction) { - let didAskForRef = false; - const getRef = () => { - didAskForRef = true; - return this.node; - }; - - const output = children({ - isVisible: this.state.isVisible, - visibilityRect: this.state.visibilityRect, - getRef: getRef - }); - - // if the consumer doesn't use our getRef function, we'll wrap - // it in a node and apply the ref ourselves. - return didAskForRef ? output :
{output}
; - } - - if (!React.Children.count(children)) { - return
; - } - - console.warn(`[notice] passing children directly into the VisibilitySensor has been deprecated, and will be removed in the next major version. - + console.warn(`[notice] passing children directly into the VisibilitySensor has been deprecated, and will be removed in the next major version. Please upgrade to the Child Function syntax instead: https://github.com/joshwnj/react-visibility-sensor#child-function-syntax`); - return
{children}
; - } + return
{children}
; } + +VisibilitySensor.defaultProps = { + children: +}; + +VisibilitySensor.propTypes = { + onChange: PropTypes.func, + active: PropTypes.bool, + partialVisibility: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.oneOf(["top", "right", "bottom", "left"]) + ]), + delayedCall: PropTypes.bool, + offset: PropTypes.shape({ + top: PropTypes.number, + left: PropTypes.number, + bottom: PropTypes.number, + right: PropTypes.number + }), + scrollCheck: PropTypes.bool, + scrollDelay: PropTypes.number, + scrollThrottle: PropTypes.number, + resizeCheck: PropTypes.bool, + resizeDelay: PropTypes.number, + resizeThrottle: PropTypes.number, + intervalCheck: PropTypes.bool, + intervalDelay: PropTypes.number, + containment: + typeof window !== "undefined" + ? PropTypes.instanceOf(window.Element) + : PropTypes.any, + children: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + minTopValue: PropTypes.number +}; From 79f8fa00fd078c6e386db3b0a65a22070214a8cc Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Thu, 11 Jul 2019 14:20:23 +0200 Subject: [PATCH 05/13] update example to reflect API changes --- example/main.js | 98 +++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/example/main.js b/example/main.js index 6405821..88277a7 100644 --- a/example/main.js +++ b/example/main.js @@ -1,49 +1,61 @@ "use strict"; -import React from "react"; +import React, { useState, useCallback, useRef } from "react"; import ReactDOM from "react-dom"; -import VisibilitySensor from "../visibility-sensor"; - -class Example extends React.Component { - constructor(props) { - super(props); - - this.state = { - msg: "" - }; - } - - onChange = isVisible => { - this.setState({ - msg: "Element is now " + (isVisible ? "visible" : "hidden") - }); - }; - - render() { - return ( -
-

{this.state.msg}

-
- - {({ getRef }) =>
} - -
-
- ); - } +import VisibilitySensor, { useVisibilitySensor } from "../visibility-sensor"; + +function RegularExample({ containment, minTopValue, partialVisibility }) { + const [msg, setMsg] = useState(""); + const onChange = useCallback(isVisible => { + setMsg("Element is now " + (isVisible ? "visible" : "hidden")); + }, []); + + return ( +
+

{msg}

+
+ + {() =>
} + +
+
+ ); +} + +function HookExample({ containment, minTopValue, partialVisibility }) { + const sensorRef = useRef(); + const [msg, setMsg] = useState(""); + const onChange = useCallback(isVisible => { + setMsg("Element is now " + (isVisible ? "visible" : "hidden")); + }, []); + + useVisibilitySensor(sensorRef, { + scrollCheck: true, + scrollThrottle: 100, + intervalDelay: 8000, + containment, + minTopValue: 10, + partialVisibility: true, + onChange + }); + + return ( +
+

{msg}

+
+
+
+
+ ); } ReactDOM.render( - React.createElement(Example), + React.createElement(RegularExample), document.getElementById("example") ); @@ -54,10 +66,8 @@ container.scrollTop = 320; container.scrollLeft = 320; ReactDOM.render( - React.createElement(Example, { - containment: container, - minTopValue: 10, - partialVisibility: true + React.createElement(HookExample, { + containment: container }), elem ); From 1f18585ef197f9f9dff6d8bd5c3fb1ca69017228 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Fri, 12 Jul 2019 09:46:52 +0200 Subject: [PATCH 06/13] improve readability and maintainability --- lib/use-visibility-sensor.js | 154 ++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 67 deletions(-) diff --git a/lib/use-visibility-sensor.js b/lib/use-visibility-sensor.js index 8cc0877..dadd5f1 100644 --- a/lib/use-visibility-sensor.js +++ b/lib/use-visibility-sensor.js @@ -21,6 +21,83 @@ function roundRectDown(rect) { }; } +function getContainmentRect(containment, offset) { + let containmentRect; + if (containment) { + const containmentDOMRect = containment.getBoundingClientRect(); + containmentRect = { + top: containmentDOMRect.top, + left: containmentDOMRect.left, + bottom: containmentDOMRect.bottom, + right: containmentDOMRect.right + }; + } else { + containmentRect = { + top: 0, + left: 0, + bottom: window.innerHeight || document.documentElement.clientHeight, + right: window.innerWidth || document.documentElement.clientWidth + }; + } + // Check if visibility is wanted via offset? + const hasValidOffset = typeof offset === "object"; + if (hasValidOffset) { + containmentRect.top += offset.top || 0; + containmentRect.left += offset.left || 0; + containmentRect.bottom -= offset.bottom || 0; + containmentRect.right -= offset.right || 0; + } + + return containmentRect; +} + +function getVisibilityRect(rect, containmentRect) { + return { + top: rect.top >= containmentRect.top, + left: rect.left >= containmentRect.left, + bottom: rect.bottom <= containmentRect.bottom, + right: rect.right <= containmentRect.right + }; +} + +function checkIsVisible( + rect, + containmentRect, + visibilityRect, + partialVisibility, + minTopValue +) { + // https://github.com/joshwnj/react-visibility-sensor/pull/114 + const hasSize = rect.height > 0 && rect.width > 0; + const isVisible = + hasSize && + visibilityRect.top && + visibilityRect.left && + visibilityRect.bottom && + visibilityRect.right; + + // check for partial visibility + if (hasSize && partialVisibility) { + let partialVisible = + rect.top <= containmentRect.bottom && + rect.bottom >= containmentRect.top && + rect.left <= containmentRect.right && + rect.right >= containmentRect.left; + + // account for partial visibility on a single edge + if (typeof partialVisibility === "string") { + partialVisible = visibilityRect[partialVisibility]; + } + + // if we have minimum top visibility set by props, lets check, if it meets the passed value + // so if for instance element is at least 200px in viewport, then show it. + return minTopValue + ? partialVisible && rect.top <= containmentRect.bottom - minTopValue + : partialVisible; + } + return isVisible; +} + export default function useVisibilitySensor( nodeRef, { @@ -52,78 +129,21 @@ export default function useVisibilitySensor( const visibilityCheck = useCallback( () => { const el = nodeRef && nodeRef.current; - let rect; - let containmentRect; - // if the component has rendered to null, dont update visibility if (!el) { return; } - rect = normalizeRect(roundRectDown(el.getBoundingClientRect())); - - if (containment) { - const containmentDOMRect = containment.getBoundingClientRect(); - containmentRect = { - top: containmentDOMRect.top, - left: containmentDOMRect.left, - bottom: containmentDOMRect.bottom, - right: containmentDOMRect.right - }; - } else { - containmentRect = { - top: 0, - left: 0, - bottom: window.innerHeight || document.documentElement.clientHeight, - right: window.innerWidth || document.documentElement.clientWidth - }; - } - - // Check if visibility is wanted via offset? - const hasValidOffset = typeof offset === "object"; - if (hasValidOffset) { - containmentRect.top += offset.top || 0; - containmentRect.left += offset.left || 0; - containmentRect.bottom -= offset.bottom || 0; - containmentRect.right -= offset.right || 0; - } - - const nextVisibilityRect = { - top: rect.top >= containmentRect.top, - left: rect.left >= containmentRect.left, - bottom: rect.bottom <= containmentRect.bottom, - right: rect.right <= containmentRect.right - }; - - // https://github.com/joshwnj/react-visibility-sensor/pull/114 - const hasSize = rect.height > 0 && rect.width > 0; - - let nextIsVisible = - hasSize && - nextVisibilityRect.top && - nextVisibilityRect.left && - nextVisibilityRect.bottom && - nextVisibilityRect.right; - - // check for partial visibility - if (hasSize && partialVisibility) { - let partialVisible = - rect.top <= containmentRect.bottom && - rect.bottom >= containmentRect.top && - rect.left <= containmentRect.right && - rect.right >= containmentRect.left; - - // account for partial visibility on a single edge - if (typeof partialVisibility === "string") { - partialVisible = nextVisibilityRect[partialVisibility]; - } - - // if we have minimum top visibility set by props, lets check, if it meets the passed value - // so if for instance element is at least 200px in viewport, then show it. - nextIsVisible = minTopValue - ? partialVisible && rect.top <= containmentRect.bottom - minTopValue - : partialVisible; - } + const rect = normalizeRect(roundRectDown(el.getBoundingClientRect())); + const containmentRect = getContainmentRect(containment, offset); + const nextVisibilityRect = getVisibilityRect(rect, containmentRect); + const nextIsVisible = checkIsVisible( + rect, + containmentRect, + nextVisibilityRect, + partialVisibility, + minTopValue + ); // notify the parent when the value changes if (isVisible !== nextIsVisible) { From 56945041fed59334ecca7fe2a050f24005957c4b Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 12:40:15 +0200 Subject: [PATCH 07/13] bump umd-example react version --- example-umd/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example-umd/index.html b/example-umd/index.html index c47d41f..a4c83fa 100644 --- a/example-umd/index.html +++ b/example-umd/index.html @@ -77,8 +77,8 @@
- - + + From 3691668e68a56e8ee81cca0a4f2ed95e0d171747 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 12:42:18 +0200 Subject: [PATCH 08/13] change ref to be created inside useVisibilitySensor hook --- lib/use-visibility-sensor.js | 227 +++++++++++++++++------------------ visibility-sensor.js | 5 +- 2 files changed, 109 insertions(+), 123 deletions(-) diff --git a/lib/use-visibility-sensor.js b/lib/use-visibility-sensor.js index dadd5f1..593dbef 100644 --- a/lib/use-visibility-sensor.js +++ b/lib/use-visibility-sensor.js @@ -98,71 +98,66 @@ function checkIsVisible( return isVisible; } -export default function useVisibilitySensor( - nodeRef, - { - active = true, - onChange, - partialVisibility = false, - minTopValue = 0, - scrollCheck = false, - scrollDelay = 250, - scrollThrottle = -1, - resizeCheck = false, - resizeDelay = 250, - resizeThrottle = -1, - intervalCheck = true, - intervalDelay = 100, - delayedCall = false, - offset = {}, - containment = null - } -) { +export default function useVisibilitySensor({ + active = true, + onChange, + partialVisibility = false, + minTopValue = 0, + scrollCheck = false, + scrollDelay = 250, + scrollThrottle = -1, + resizeCheck = false, + resizeDelay = 250, + resizeThrottle = -1, + intervalCheck = true, + intervalDelay = 100, + delayedCall = false, + offset = {}, + containment = null +}) { + const nodeRef = useRef(); const debounceCheckRef = useRef(); const intervalRef = useRef(); - const [isVisible, setIsVisible] = useState(false); + const [isVisible, setIsVisible] = useState(null); const [visibilityRect, setVisibilityRect] = useState({}); const getContainer = useCallback(() => containment || window, [containment]); // Check if the element is within the visible viewport - const visibilityCheck = useCallback( - () => { - const el = nodeRef && nodeRef.current; - // if the component has rendered to null, dont update visibility - if (!el) { - return; - } + const visibilityCheck = useCallback(() => { + const el = nodeRef && nodeRef.current; + // if the component has rendered to null, dont update visibility + if (!el) { + return; + } - const rect = normalizeRect(roundRectDown(el.getBoundingClientRect())); - const containmentRect = getContainmentRect(containment, offset); - const nextVisibilityRect = getVisibilityRect(rect, containmentRect); - const nextIsVisible = checkIsVisible( - rect, - containmentRect, - nextVisibilityRect, - partialVisibility, - minTopValue - ); - - // notify the parent when the value changes - if (isVisible !== nextIsVisible) { - setIsVisible(nextIsVisible); - setVisibilityRect(nextVisibilityRect); - if (onChange) onChange(nextIsVisible); - } - }, - [ - isVisible, - offset, - containment, + const rect = normalizeRect(roundRectDown(el.getBoundingClientRect())); + const containmentRect = getContainmentRect(containment, offset); + const nextVisibilityRect = getVisibilityRect(rect, containmentRect); + const nextIsVisible = checkIsVisible( + rect, + containmentRect, + nextVisibilityRect, partialVisibility, - minTopValue, - onChange, - setIsVisible, - setVisibilityRect - ] - ); + minTopValue + ); + + // notify the parent when the value changes + if (isVisible !== nextIsVisible) { + setIsVisible(nextIsVisible); + setVisibilityRect(nextVisibilityRect); + if (onChange) onChange(nextIsVisible); + } + }, [ + isVisible, + offset, + containment, + partialVisibility, + minTopValue, + onChange, + setIsVisible, + setVisibilityRect + ]); const addEventListener = useCallback( (target, event, delay, throttle) => { @@ -209,77 +204,69 @@ export default function useVisibilitySensor( [visibilityCheck] ); - useEffect( - () => { - function watch() { - if (debounceCheckRef.current || intervalRef.current) { - return; - } - - if (intervalCheck) { - intervalRef.current = setInterval(visibilityCheck, intervalDelay); - } - - if (scrollCheck) { - addEventListener( - getContainer(), - "scroll", - scrollDelay, - scrollThrottle - ); - } + useEffect(() => { + function watch() { + if (debounceCheckRef.current || intervalRef.current) { + return; + } - if (resizeCheck) { - addEventListener(window, "resize", resizeDelay, resizeThrottle); - } + if (intervalCheck) { + intervalRef.current = setInterval(visibilityCheck, intervalDelay); + } - // if dont need delayed call, check on load ( before the first interval fires ) - !delayedCall && visibilityCheck(); + if (scrollCheck) { + addEventListener(getContainer(), "scroll", scrollDelay, scrollThrottle); } - if (active) { - watch(); + if (resizeCheck) { + addEventListener(window, "resize", resizeDelay, resizeThrottle); } - // stop any listeners and intervals on props change and re-registers - return () => { - if (debounceCheckRef.current) { - const debounceCheck = debounceCheckRef.current; - // clean up event listeners and their debounce callers - for (let debounceEvent in debounceCheck) { - if (debounceCheck.hasOwnProperty(debounceEvent)) { - const debounceInfo = debounceCheck[debounceEvent]; - - clearTimeout(debounceInfo.getLastTimeout()); - debounceInfo.target.removeEventListener( - debounceEvent, - debounceInfo.fn - ); - - debounceCheck[debounceEvent] = null; - } - } - } - debounceCheckRef.current = null; + // if dont need delayed call, check on load ( before the first interval fires ) + !delayedCall && visibilityCheck(); + } + + if (active) { + watch(); + } - if (intervalRef.current) { - intervalRef.current = clearInterval(intervalRef.current); + // stop any listeners and intervals on props change and re-registers + return () => { + if (debounceCheckRef.current) { + const debounceCheck = debounceCheckRef.current; + // clean up event listeners and their debounce callers + for (let debounceEvent in debounceCheck) { + if (debounceCheck.hasOwnProperty(debounceEvent)) { + const debounceInfo = debounceCheck[debounceEvent]; + + clearTimeout(debounceInfo.getLastTimeout()); + debounceInfo.target.removeEventListener( + debounceEvent, + debounceInfo.fn + ); + + debounceCheck[debounceEvent] = null; + } } - }; - }, - [ - active, - scrollCheck, - scrollDelay, - scrollThrottle, - resizeCheck, - resizeDelay, - resizeThrottle, - intervalCheck, - intervalDelay, - visibilityCheck - ] - ); + } + debounceCheckRef.current = null; - return { isVisible, visibilityRect }; + if (intervalRef.current) { + intervalRef.current = clearInterval(intervalRef.current); + } + }; + }, [ + active, + scrollCheck, + scrollDelay, + scrollThrottle, + resizeCheck, + resizeDelay, + resizeThrottle, + intervalCheck, + intervalDelay, + visibilityCheck + ]); + + return { nodeRef, isVisible, visibilityRect }; } diff --git a/visibility-sensor.js b/visibility-sensor.js index 598090b..7b04133 100644 --- a/visibility-sensor.js +++ b/visibility-sensor.js @@ -1,6 +1,6 @@ "use strict"; -import React, { useRef } from "react"; +import React from "react"; import ReactDOM from "react-dom"; import PropTypes from "prop-types"; import useVisibilitySensor from "./lib/use-visibility-sensor"; @@ -25,8 +25,7 @@ export default function VisibilitySensor({ offset, containment }) { - const nodeRef = useRef(); - const { isVisible, visibilityRect } = useVisibilitySensor(nodeRef, { + const { nodeRef, isVisible, visibilityRect } = useVisibilitySensor({ active, onChange, partialVisibility, From 22b2e57199ac73eb5ff4fca0334607f5c3161001 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 12:42:29 +0200 Subject: [PATCH 09/13] update lock file --- package-lock.json | 122 +++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21db52d..3740ea2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1834,7 +1834,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -1871,7 +1871,7 @@ }, "browserify-rsa": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", + "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { @@ -1905,7 +1905,7 @@ }, "buffer": { "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", "dev": true, "requires": { @@ -2074,7 +2074,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -2493,7 +2493,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -2506,7 +2506,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -2751,7 +2751,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -4391,7 +4391,7 @@ }, "is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, @@ -4671,7 +4671,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true } @@ -5254,7 +5254,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, @@ -5299,7 +5299,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "requires": { @@ -5978,7 +5978,7 @@ }, "jsonfile": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", + "resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", "integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=", "dev": true, "requires": { @@ -6289,7 +6289,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6681,7 +6681,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -6949,28 +6949,36 @@ "urix": "^0.1.0" } }, - "react": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", - "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.6" + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" } }, - "react-dom": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", - "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.6" + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, "spdx-license-ids": { @@ -7161,7 +7169,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { @@ -7358,14 +7366,13 @@ "safe-regex": "^1.1.0" } }, - "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "is-number": "^7.0.0" } }, "toidentifier": { @@ -7880,8 +7887,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -7902,14 +7908,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -7924,20 +7928,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -8054,8 +8055,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -8067,7 +8067,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -8082,7 +8081,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -8090,14 +8088,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -8116,7 +8112,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -8197,8 +8192,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -8210,7 +8204,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -8296,8 +8289,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -8333,7 +8325,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -8353,7 +8344,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -8397,14 +8387,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, From 8d0d3b9b1c21dffddf4bd5cd16ae963773d93b87 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 12:42:55 +0200 Subject: [PATCH 10/13] fix most of the tests by using act() --- tests/visibility-sensor-spec.jsx | 315 ++++++++++++++++--------------- 1 file changed, 168 insertions(+), 147 deletions(-) diff --git a/tests/visibility-sensor-spec.jsx b/tests/visibility-sensor-spec.jsx index 6a1d9b0..b8aabaa 100644 --- a/tests/visibility-sensor-spec.jsx +++ b/tests/visibility-sensor-spec.jsx @@ -1,7 +1,8 @@ import React from "react"; import ReactDOM from "react-dom"; +import { act } from 'react-dom/test-utils'; import assert from "assert"; -import VisibilitySensor from "../visibility-sensor"; +import VisibilitySensor, { useVisibilitySensor } from "../visibility-sensor"; describe("VisibilitySensor", function() { var node; @@ -25,10 +26,12 @@ describe("VisibilitySensor", function() { if (firstTime) { firstTime = false; assert.equal(isVisible, true, "Component starts out visible"); - node.setAttribute( - "style", - "position:absolute; width:100px; left:-101px" - ); + act(() => { + node.setAttribute( + "style", + "position:absolute; width:100px; left:-101px" + ); + }); } else { // after moving the sensor it should be not visible anymore assert.equal( @@ -40,11 +43,17 @@ describe("VisibilitySensor", function() { } }; - var element = ( - - { ({ getRef }) =>
} - - ); + var element; + act(() => { + element = ( + {() => ( +
+ )} + ); + }); ReactDOM.render(element, node); }); @@ -58,8 +67,9 @@ describe("VisibilitySensor", function() { if (firstTime) { firstTime = false; assert.equal(isVisible, true, "Component starts out visible"); - - window.scrollTo(0, 1000); + act(() => { + window.scrollTo(0, 1000); + }); } else { // after moving the sensor it should be not visible anymore assert.equal( @@ -71,18 +81,21 @@ describe("VisibilitySensor", function() { } }; - var element = ( -
- { () => ( -
- )} -
- ); + var element; + act(() => { + element = ( +
+ {() => ( +
+ )} +
+ ); + }); ReactDOM.render(element, node); }); @@ -163,23 +176,29 @@ describe("VisibilitySensor", function() { it("should clear interval and debounceCheck when deactivated", function() { var onChange = function() {}; - var element1 = ( - - ); + var element1; + var element2; - var element2 = ( - - ); + act(() => { + element1 = ( + + ); + }); + act(() => { + element2 = ( + + ); + }); var component1 = ReactDOM.render(element1, node); assert(component1.interval, "interval should be set"); @@ -192,8 +211,10 @@ describe("VisibilitySensor", function() { component1.debounceCheck.resize, "debounceCheck.scroll should be set" ); - - var component2 = ReactDOM.render(element2, node); + var component2; + act(() => { + component2 = ReactDOM.render(element2, node); + }); assert(!component2.interval, "interval should not be set"); assert(!component2.debounceCheck, "debounceCheck should not be set"); }); @@ -207,7 +228,9 @@ describe("VisibilitySensor", function() { if (firstTime) { firstTime = false; assert.equal(isVisible, true, "Component starts out visible"); - node.setAttribute("style", "position:absolute; top:49px"); + act(() => { + node.setAttribute("style", "position:absolute; top:49px"); + }); } else { assert.equal( isVisible, @@ -218,46 +241,19 @@ describe("VisibilitySensor", function() { } }; - var element = ( - - { ({ getRef }) =>
} - - ); - - ReactDOM.render(element, node); - }); - - it("should be backwards-compatible with old offset config", function(done) { - var firstTime = true; - node.setAttribute("style", "position:absolute; top:51px"); - var onChange = function(isVisible) { - if (firstTime) { - firstTime = false; - assert.equal(isVisible, true, "Component starts out visible"); - node.setAttribute("style", "position:absolute; top:49px"); - } else { - assert.equal( - isVisible, - false, - "Component has moved out of offset area" - ); - done(); - } - }; + var element; - var element = ( - - { () =>
} - - ); + act(() => { + element = ( + + {() =>
} + + ); + }); ReactDOM.render(element, node); }); @@ -271,7 +267,9 @@ describe("VisibilitySensor", function() { if (firstTime) { firstTime = false; assert.equal(isVisible, false, "Component starts out invisible"); - node.setAttribute("style", "position:absolute; top:51px"); + act(() => { + node.setAttribute("style", "position:absolute; top:51px"); + }); } else { assert.equal( isVisible, @@ -282,15 +280,18 @@ describe("VisibilitySensor", function() { } }; - var element = ( - - { () =>
} - - ); + var element; + act(() => { + element = ( + + {() =>
} + + ); + }); ReactDOM.render(element, node); }); @@ -304,7 +305,9 @@ describe("VisibilitySensor", function() { if (firstTime) { firstTime = false; assert.equal(isVisible, true, "Component starts out visible"); - node.setAttribute("style", "position:absolute; top:-51px"); + act(() => { + node.setAttribute("style", "position:absolute; top:-51px"); + }); } else { assert.equal( isVisible, @@ -315,15 +318,18 @@ describe("VisibilitySensor", function() { } }; - var element = ( - - { () =>
} - - ); + var element; + act(() => { + element = ( + + { () =>
} + + ); + }); ReactDOM.render(element, node); }); @@ -340,10 +346,6 @@ describe("VisibilitySensor", function() { "visibilityRect" in props, "children should be called with visibilityRect prop" ); - assert( - "getRef" in props, - "children should be called with getRef prop" - ); return
; }; @@ -365,12 +367,14 @@ describe("VisibilitySensor", function() { done(); } }; - - var element = ( - - { () =>
} - - ); + var element; + act(() => { + element = ( + + { () =>
} + + ); + }); ReactDOM.render(element, node); }); @@ -378,21 +382,28 @@ describe("VisibilitySensor", function() { it("should not return visible if the sensor is hidden", function(done) { var firstTime = true; var onChange = function(isVisible) { + // throw new Error(`LALALALALALALLA [${isVisible ? 'Bai' : 'Ez'}]`) if (firstTime) { assert.equal(isVisible, false, "Component is not visible"); done(); } }; + var element; + act(() => { + element = ( +
+ + {({ isVisible }) => { + // throw new Error(`MEMEMEMEMEMEMEMEMEMEM [${isVisible ? 'Bai' : 'Ez'}]`); + return
}} + +
+ ); + }); - var element = ( -
- - { () =>
} - -
- ); - - ReactDOM.render(element, node); + act(() => { + ReactDOM.render(element, node); + }); }); }); @@ -401,6 +412,12 @@ describe("VisibilitySensor with Container", function() { var node; var container; + function WithStyleVisibilitySensor({ style, ...restProps }) { + const { nodeRef } = useVisibilitySensor(restProps); + + return
+ } + beforeEach(function() { node = document.createElement("div"); document.body.appendChild(node); @@ -430,19 +447,21 @@ describe("VisibilitySensor with Container", function() { container.style.position = 'relative' container.style.overflow = 'hidden' - var element = {( { getRef } ) => ( -
- )} + var element; + act(() => { + element = ( + + ); + }); ReactDOM.render(element, node); }); @@ -461,19 +480,21 @@ describe("VisibilitySensor with Container", function() { container.style.position = 'relative' container.style.overflow = 'hidden' - var element = {( { getRef } ) => ( -
- )} + var element; + act(() => { + element = ( + + ); + }); ReactDOM.render(element, node); }); From 05229fd94934fd28585a9be081a11284a43c8436 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 13:06:33 +0200 Subject: [PATCH 11/13] improve docs with new hook API --- README.md | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dfba7d4..6a14ebf 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ To run the example locally: General usage goes something like: ```js -const VisibilitySensor = require("react-visibility-sensor"); +import VisibilitySensor from "react-visibility-sensor"; function onChange(isVisible) { console.log("Element is now %s", isVisible ? "visible" : "hidden"); @@ -57,11 +57,13 @@ function MyComponent(props) { You can also pass a child function, which can be convenient if you don't need to store the visibility anywhere: ```js +import VisibilitySensor from "react-visibility-sensor"; + function MyComponent(props) { return ( - {({ isVisible, visibilityRect, getRef }) => ( -
I am {isVisible ? "visible" : "invisible"}
+ {({ isVisible, visibilityRect }) => ( +
I am {isVisible ? "visible" : "invisible"}
)}
); @@ -71,7 +73,6 @@ function MyComponent(props) { The child function must return an element. The 3 arguments that you can access from the child function are: - `isVisible`: Boolean -- `getRef`: a Function allowing you to specify which element should be used for calculating visibility. This is useful when you have something absolutely-positioned. Using `getRef` is optional, and if it is not called a wrapper `
` will be created with the ref. - `visibilityRect`: an Object indicating which sides of the element are visible, in the shape of: ``` @@ -85,8 +86,43 @@ The child function must return an element. The 3 arguments that you can access f --- +### React hook syntax + +For more control on the nodeRef, you can use the React hook directly and attach it to the element: + +```js +import { useVisibilitySensor } from "react-visibility-sensor"; + +function MyComponent(props) { + const onChange = useCallback(isVisible => { + console.log(`Visibility changed! ${isVisible ? "visible" : "invisible"}`); + }, []); + const { nodeRef, isVisible } = useVisibilitySensor({ onChange }); + return
I am {isVisible ? "visible" : "invisible"}
; +} +``` + +The useVisibilitySensor hook returns an object containing: + +- `nodeRef`: Object, React ref instance to be attached to the element. +- `isVisible`: Boolean +- `visibilityRect`: Object, indicating which sides of the element are visible, in the shape of: + +``` +{ + top: Boolean, + bottom: Boolean, + left: Boolean, + right: Boolean +} +``` + +--- + ## Props +Most of the properties can be used for both `VisibilitySensor` Component and `useVisibilitySensor` hook. + - `onChange`: callback for whenever the element changes from being within the window viewport or not. Function is called with 1 argument `(isVisible: boolean)` - `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback. - `partialVisibility`: (default `false`) consider element visible if only part of it is visible. Also possible values are - 'top', 'right', 'bottom', 'left' - in case it's needed to detect when one of these become visible explicitly. @@ -102,7 +138,7 @@ The child function must return an element. The 3 arguments that you can access f - `resizeThrottle`: (default: `-1`) by specifying a value > -1, you are enabling throttle instead of the delay to trigger checks on resize event. Throttle supercedes delay. - `containment`: (optional) element to use as a viewport when checking visibility. Default behaviour is to use the browser window as viewport. - `delayedCall`: (default `false`) if is set to true, wont execute on page load ( prevents react apps triggering elements as visible before styles are loaded ) -- `children`: can be a React element or a function. If you provide a function, it will be called with 1 argument `{isVisible: ?boolean, visibilityRect: Object}` +- `children`: **Only for `VisibilitySensor` Component** can be a React element or a function. If you provide a function, it will be called with 1 argument `{isVisible: ?boolean, visibilityRect: Object}` It's possible to use both `intervalCheck` and `scrollCheck` together. This means you can detect most visibility changes quickly with `scrollCheck`, and an `intervalCheck` with a higher `intervalDelay` will act as a fallback for other visibility events, such as resize of a container. From fa0bdd482f898afbb7b3b7632e3aa495cbf733f4 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 13:08:00 +0200 Subject: [PATCH 12/13] fix example by using hook ref --- example/main.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/example/main.js b/example/main.js index 88277a7..8449d8f 100644 --- a/example/main.js +++ b/example/main.js @@ -1,6 +1,6 @@ "use strict"; -import React, { useState, useCallback, useRef } from "react"; +import React, { useState, useCallback } from "react"; import ReactDOM from "react-dom"; import VisibilitySensor, { useVisibilitySensor } from "../visibility-sensor"; @@ -28,13 +28,12 @@ function RegularExample({ containment, minTopValue, partialVisibility }) { } function HookExample({ containment, minTopValue, partialVisibility }) { - const sensorRef = useRef(); const [msg, setMsg] = useState(""); const onChange = useCallback(isVisible => { setMsg("Element is now " + (isVisible ? "visible" : "hidden")); }, []); - useVisibilitySensor(sensorRef, { + const { nodeRef } = useVisibilitySensor({ scrollCheck: true, scrollThrottle: 100, intervalDelay: 8000, @@ -48,7 +47,7 @@ function HookExample({ containment, minTopValue, partialVisibility }) {

{msg}

-
+
); From 3532d39f9f4c020453663010b350146883969862 Mon Sep 17 00:00:00 2001 From: Sergio Toro Date: Mon, 29 Jul 2019 13:10:50 +0200 Subject: [PATCH 13/13] add warning for passing onChange function --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a14ebf..938417f 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ The useVisibilitySensor hook returns an object containing: Most of the properties can be used for both `VisibilitySensor` Component and `useVisibilitySensor` hook. - `onChange`: callback for whenever the element changes from being within the window viewport or not. Function is called with 1 argument `(isVisible: boolean)` -- `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback. +- `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback. **When using hooks create the onChange function using useCallback to avoid performance issues.** - `partialVisibility`: (default `false`) consider element visible if only part of it is visible. Also possible values are - 'top', 'right', 'bottom', 'left' - in case it's needed to detect when one of these become visible explicitly. - `offset`: (default `{}`) with offset you can define amount of px from one side when the visibility should already change. So in example setting `offset={{top:10}}` means that the visibility changes hidden when there is less than 10px to top of the viewport. Offset works along with `partialVisibility` - `minTopValue`: (default `0`) consider element visible if only part of it is visible and a minimum amount of pixels could be set, so if at least 100px are in viewport, we mark element as visible.