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 067b3c4..4a346ce 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; @@ -325,12 +323,35 @@ export default class VisibilitySensor extends React.Component { }; render() { - if (this.props.children instanceof Function) { - return this.props.children({ + 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); + + 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}
; } }