Skip to content

Commit 1b27c89

Browse files
committed
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
1 parent f3c22d1 commit 1b27c89

File tree

4 files changed

+161
-42
lines changed

4 files changed

+161
-42
lines changed

README.md

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
React Visibility Sensor
2-
====
1+
# React Visibility Sensor
32

43
[![Build Status](https://secure.travis-ci.org/joshwnj/react-visibility-sensor.png)](http://travis-ci.org/joshwnj/react-visibility-sensor)
54

65
Sensor component for React that notifies you when it goes in or out of the window viewport.
76

87
Sponsored by [X-Team](https://x-team.com)
98

10-
Install
11-
----
9+
## Install
1210

1311
`npm install react-visibility-sensor`
1412

@@ -23,8 +21,7 @@ In this case, make sure that `React` and `ReactDOM` are already loaded and globa
2321

2422
Take a look at the [umd example](./example-umd/) to see this in action
2523

26-
Example
27-
----
24+
## Example
2825

2926
[View an example on codesandbox](https://codesandbox.io/s/p73kyx9zpm)
3027

@@ -40,13 +37,13 @@ To run the example locally:
4037
General usage goes something like:
4138

4239
```js
43-
const VisibilitySensor = require('react-visibility-sensor');
40+
const VisibilitySensor = require("react-visibility-sensor");
4441

45-
function onChange (isVisible) {
46-
console.log('Element is now %s', isVisible ? 'visible' : 'hidden');
42+
function onChange(isVisible) {
43+
console.log("Element is now %s", isVisible ? "visible" : "hidden");
4744
}
4845

49-
function MyComponent (props) {
46+
function MyComponent(props) {
5047
return (
5148
<VisibilitySensor onChange={onChange}>
5249
<div>...content goes here...</div>
@@ -55,25 +52,43 @@ function MyComponent (props) {
5552
}
5653
```
5754

55+
### Child Function syntax
56+
5857
You can also pass a child function, which can be convenient if you don't need to store the visibility anywhere:
5958

6059
```js
61-
function MyComponent (props) {
60+
function MyComponent(props) {
6261
return (
6362
<VisibilitySensor>
64-
{({isVisible}) =>
65-
<div>I am {isVisible ? 'visible' : 'invisible'}</div>
66-
}
63+
{({ isVisible, visibilityRect, getRef }) => (
64+
<div ref={getRef()}>I am {isVisible ? "visible" : "invisible"}</div>
65+
)}
6766
</VisibilitySensor>
6867
);
6968
}
7069
```
7170

72-
Props
73-
----
71+
The child function must return an element. The 3 arguments that you can access from the child function are:
72+
73+
- `isVisible`: Boolean
74+
- `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 `<div>` will be created with the ref.
75+
- `visibilityRect`: an Object indicating which sides of the element are visible, in the shape of:
76+
77+
```
78+
{
79+
top: Boolean,
80+
bottom: Boolean,
81+
left: Boolean,
82+
right: Boolean
83+
}
84+
```
85+
86+
---
87+
88+
## Props
7489

7590
- `onChange`: callback for whenever the element changes from being within the window viewport or not. Function is called with 1 argument `(isVisible: boolean)`
76-
- `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback.
91+
- `active`: (default `true`) boolean flag for enabling / disabling the sensor. When `active !== true` the sensor will not fire the `onChange` callback.
7792
- `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.
7893
- `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`
7994
- `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
87102
- `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.
88103
- `containment`: (optional) element to use as a viewport when checking visibility. Default behaviour is to use the browser window as viewport.
89104
- `delayedCall`: (default `false`) if is set to true, wont execute on page load ( prevents react apps triggering elements as visible before styles are loaded )
90-
- `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}`
105+
- `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}`
91106

92107
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.
93108

94-
Thanks
95-
----
109+
## Thanks
96110

97111
Special thanks to [contributors](https://github.com/joshwnj/react-visibility-sensor/graphs/contributors)
98112

99-
License
100-
----
113+
## License
101114

102115
MIT

example/main.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class Example extends React.Component {
3434
partialVisibility={this.props.partialVisibility}
3535
offset={this.props.offset}
3636
>
37-
<div className="sensor" />
37+
{({ getRef }) => <div ref={getRef()} className="sensor" />}
3838
</VisibilitySensor>
3939
<div className="after" />
4040
</div>

tests/visibility-sensor-spec.jsx

Lines changed: 98 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe("VisibilitySensor", function() {
4242

4343
var element = (
4444
<VisibilitySensor onChange={onChange} intervalDelay={10}>
45-
<div style={{ height: "10px", width: "10px" }} />
45+
{ ({ getRef }) => <div ref={getRef()} style={{ height: "10px", width: "10px" }} /> }
4646
</VisibilitySensor>
4747
);
4848

@@ -78,9 +78,9 @@ describe("VisibilitySensor", function() {
7878
scrollDelay={10}
7979
onChange={onChange}
8080
intervalCheck={false}
81-
>
81+
>{ () => (
8282
<div style={{ height: "10px", width: "10px" }} />
83-
</VisibilitySensor>
83+
)}</VisibilitySensor>
8484
</div>
8585
);
8686

@@ -119,7 +119,7 @@ describe("VisibilitySensor", function() {
119119
function getElement(style) {
120120
return (
121121
<VisibilitySensor onChange={onChange} intervalDelay={10}>
122-
<div style={style} />
122+
{ () => <div style={style} /> }
123123
</VisibilitySensor>
124124
);
125125
}
@@ -135,7 +135,7 @@ describe("VisibilitySensor", function() {
135135

136136
var element = (
137137
<VisibilitySensor onChange={onChange}>
138-
<div style={{ height: "10px", width: "10px" }} />
138+
{ () => <div style={{ height: "10px", width: "10px" }} /> }
139139
</VisibilitySensor>
140140
);
141141

@@ -224,7 +224,7 @@ describe("VisibilitySensor", function() {
224224
offset={{ top: 50 }}
225225
intervalDelay={10}
226226
>
227-
<div style={{ height: "10px", width: "10px" }} />
227+
{ ({ getRef }) => <div ref={getRef()} style={{ height: "10px", width: "10px" }} /> }
228228
</VisibilitySensor>
229229
);
230230

@@ -255,7 +255,7 @@ describe("VisibilitySensor", function() {
255255
offset={{ direction: "top", value: 50 }}
256256
intervalDelay={10}
257257
>
258-
<div style={{ height: "10px", width: "10px" }} />
258+
{ () => <div style={{ height: "10px", width: "10px" }} /> }
259259
</VisibilitySensor>
260260
);
261261

@@ -288,7 +288,7 @@ describe("VisibilitySensor", function() {
288288
offset={{ top: 50 }}
289289
intervalDelay={10}
290290
>
291-
<div style={{ height: "10px", width: "10px" }} />
291+
{ () => <div style={{ height: "10px", width: "10px" }} /> }
292292
</VisibilitySensor>
293293
);
294294

@@ -321,7 +321,7 @@ describe("VisibilitySensor", function() {
321321
offset={{ top: -50 }}
322322
intervalDelay={10}
323323
>
324-
<div style={{ height: "10px", width: "10px" }} />
324+
{ () => <div style={{ height: "10px", width: "10px" }} /> }
325325
</VisibilitySensor>
326326
);
327327

@@ -340,6 +340,10 @@ describe("VisibilitySensor", function() {
340340
"visibilityRect" in props,
341341
"children should be called with visibilityRect prop"
342342
);
343+
assert(
344+
"getRef" in props,
345+
"children should be called with getRef prop"
346+
);
343347
return <div />;
344348
};
345349

@@ -364,7 +368,7 @@ describe("VisibilitySensor", function() {
364368

365369
var element = (
366370
<VisibilitySensor onChange={onChange}>
367-
<div style={{ height: "0px", width: "0px" }} />
371+
{ () => <div style={{ height: "0px", width: "0px" }} /> }
368372
</VisibilitySensor>
369373
);
370374

@@ -383,11 +387,94 @@ describe("VisibilitySensor", function() {
383387
var element = (
384388
<div style={{ display: "none" }}>
385389
<VisibilitySensor onChange={onChange}>
386-
<div style={{ height: "10px", width: "10px" }} />
390+
{ () => <div style={{ height: "10px", width: "10px" }} /> }
387391
</VisibilitySensor>
388392
</div>
389393
);
390394

391395
ReactDOM.render(element, node);
392396
});
393397
});
398+
399+
400+
describe("VisibilitySensor with Container", function() {
401+
var node;
402+
var container;
403+
404+
beforeEach(function() {
405+
node = document.createElement("div");
406+
document.body.appendChild(node);
407+
408+
container = document.createElement('div');
409+
document.body.appendChild(container);
410+
});
411+
412+
afterEach(function() {
413+
ReactDOM.unmountComponentAtNode(node);
414+
document.body.removeChild(node);
415+
document.body.removeChild(container);
416+
});
417+
418+
419+
it("should detect an absolutely positioned element inside the visible part of a container", function(done) {
420+
var firstTime = true;
421+
var onChange = function(isVisible) {
422+
if (firstTime) {
423+
assert.equal(isVisible, true, "Component is visible");
424+
done();
425+
}
426+
};
427+
428+
container.style.width = 300
429+
container.style.height = 300
430+
container.style.position = 'relative'
431+
container.style.overflow = 'hidden'
432+
433+
var element = <VisibilitySensor
434+
onChange={onChange}
435+
containment={container}
436+
partialVisibility={true}
437+
>{( { getRef } ) => (
438+
<div ref={getRef()} style={{
439+
position: "absolute",
440+
left: "1px",
441+
top: "1px",
442+
height: "10px",
443+
width: "10px",
444+
}} />
445+
)}</VisibilitySensor>
446+
447+
ReactDOM.render(element, node);
448+
});
449+
450+
it("should not detect an absolutely positioned element outside the visible part of a container", function(done) {
451+
var firstTime = true;
452+
var onChange = function(isVisible) {
453+
if (firstTime) {
454+
assert.equal(isVisible, false, "Component is not visible");
455+
done();
456+
}
457+
};
458+
459+
container.style.width = 300
460+
container.style.height = 300
461+
container.style.position = 'relative'
462+
container.style.overflow = 'hidden'
463+
464+
var element = <VisibilitySensor
465+
onChange={onChange}
466+
containment={container}
467+
partialVisibility={true}
468+
>{( { getRef } ) => (
469+
<div ref={getRef()} style={{
470+
position: "absolute",
471+
left: "400px",
472+
top: "400px",
473+
height: "10px",
474+
width: "10px",
475+
}} />
476+
)}</VisibilitySensor>
477+
478+
ReactDOM.render(element, node);
479+
});
480+
});

visibility-sensor.js

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -322,17 +322,36 @@ export default class VisibilitySensor extends React.Component {
322322
return state;
323323
};
324324

325-
renderChildren = () => {
326-
if (this.props.children instanceof Function) {
327-
return this.props.children({
325+
render() {
326+
const { children } = this.props;
327+
const isFunction = children instanceof Function;
328+
329+
if (isFunction) {
330+
let didAskForRef = false;
331+
const getRef = () => {
332+
didAskForRef = true;
333+
return this.node;
334+
};
335+
336+
const output = children({
328337
isVisible: this.state.isVisible,
329-
visibilityRect: this.state.visibilityRect
338+
visibilityRect: this.state.visibilityRect,
339+
getRef: getRef
330340
});
341+
342+
// if the consumer doesn't use our getRef function, we'll wrap
343+
// it in a node and apply the ref ourselves.
344+
return didAskForRef ? output : <div ref={this.node}>{output}</div>;
331345
}
332-
return React.Children.only(this.props.children)
333-
}
334346

335-
render() {
336-
return <div ref={this.node}>{this.renderChildren()}</div>;
347+
if (!React.Children.count(children)) {
348+
return <div ref={this.node} />;
349+
}
350+
351+
console.warn(`[notice] passing children directly into the VisibilitySensor has been deprecated, and will be removed in the next major version.
352+
353+
Please upgrade to the Child Function syntax instead: https://github.com/joshwnj/react-visibility-sensor#child-function-syntax`);
354+
355+
return <div ref={this.node}>{children}</div>;
337356
}
338357
}

0 commit comments

Comments
 (0)