Skip to content
144 changes: 143 additions & 1 deletion polyfill/intersection-observer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,15 @@ describe('IntersectionObserver', function() {
io = new IntersectionObserver(noop);
expect(io.root).to.be(null);

io = new IntersectionObserver(noop, {root: document});
expect(io.root).to.be(document);

io = new IntersectionObserver(noop, {root: rootEl});
expect(io.root).to.be(rootEl);
});


it('throws when root is not an Element', function() {
it('throws when root is not a Document or Element', function() {
expect(function() {
io = new IntersectionObserver(noop, {root: 'foo'});
}).to.throwException();
Expand Down Expand Up @@ -1398,6 +1401,89 @@ describe('IntersectionObserver', function() {
io.observe(iframeTargetEl2);
});

it('handles tracking iframe viewport', function(done) {
iframe.style.height = '100px';
iframe.style.top = '100px';
iframeWin.scrollTo(0, 110);
// {root:iframeDoc} means to track the iframe viewport.
var io = new IntersectionObserver(
function (records) {
io.unobserve(iframeTargetEl1);

var intersectionRect = rect({
top: 0, // if root=null, then this would be 100.
left: 0,
height: 90,
width: bodyWidth
});
expect(records.length).to.be(1);
expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
done();
},
{ root: iframeDoc }
);

io.observe(iframeTargetEl1);
});

it('handles tracking iframe viewport with rootMargin', function(done) {
iframe.style.height = '100px';

var io = new IntersectionObserver(
function (records) {
io.unobserve(iframeTargetEl1);
var intersectionRect = rect({
top: 0, // if root=null, then this would be 100.
left: 0,
height: 200,
width: bodyWidth
});

// rootMargin: 100% --> 3x width + 3x height.
var expectedRootBounds = rect({
top: -100,
left: -bodyWidth,
width: bodyWidth * 3,
height: 100 * 3
});
expect(records.length).to.be(1);
expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
done();
},
{ root: iframeDoc, rootMargin: '100%' }
);

io.observe(iframeTargetEl1);
});

// Current spec indicates that cross-document tracking yields
// an essentially empty IntersectionObserverEntry.
// See: https://github.com/w3c/IntersectionObserver/issues/87
it('does not track cross-document elements', function(done) {
var io = new IntersectionObserver(
function (records) {
io.unobserve(iframeTargetEl1)

expect(records.length).to.be(1);
const zeroesRect = rect({
top: 0,
left: 0,
width: 0,
height: 0
});
expect(rect(records[0].rootBounds)).to.eql(zeroesRect);
expect(rect(records[0].intersectionRect)).to.eql(zeroesRect);
expect(records.isIntersecting).false;
done();
},
{ root: document }
);

io.observe(iframeTargetEl1);
});

it('handles style changes', function(done) {
var spy = sinon.spy();

Expand Down Expand Up @@ -3022,6 +3108,62 @@ describe('IntersectionObserver', function() {
}
], done);
});

it('handles tracking iframe viewport', function(done) {
iframe.style.height = '100px';
iframe.style.top = '100px';
iframeWin.scrollTo(0, 110);
// {root:iframeDoc} means to track the iframe viewport.
var io = createObserver(
function (records) {
io.unobserve(iframeTargetEl1);
var intersectionRect = rect({
top: 0, // if root=null, then this would be 100.
left: 0,
height: 90,
width: bodyWidth
});
expect(records.length).to.be(1);
expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
done();
},
{ root: iframeDoc }
);

io.observe(iframeTargetEl1);
});

it('handles tracking iframe viewport with rootMargin', function(done) {
iframe.style.height = '100px';

var io = createObserver(
function (records) {
io.unobserve(iframeTargetEl1);
var intersectionRect = rect({
top: 0, // if root=null, then this would be 100.
left: 0,
height: 200,
width: bodyWidth
});

// rootMargin: 100% --> 3x width + 3x height.
var expectedRootBounds = rect({
top: -100,
left: -bodyWidth,
width: bodyWidth * 3,
height: 100 * 3
});
expect(records.length).to.be(1);
expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
done();
},
{ root: iframeDoc, rootMargin: '100%' }
);

io.observe(iframeTargetEl1);
});
});
});
});
Expand Down
48 changes: 38 additions & 10 deletions polyfill/intersection-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,12 @@ function IntersectionObserver(callback, opt_options) {
throw new Error('callback must be a function');
}

if (options.root && options.root.nodeType != 1) {
throw new Error('root must be an Element');
if (
options.root &&
options.root.nodeType != 1 &&
options.root.nodeType != 9
) {
throw new Error('root must be a Document or Element');
}

// Binds and throttles `this._checkForIntersections`.
Expand Down Expand Up @@ -395,7 +399,9 @@ IntersectionObserver.prototype._monitorIntersections = function(doc) {
});

// Also monitor the parent.
if (doc != (this.root && this.root.ownerDocument || document)) {
var rootDoc =
(this.root && (this.root.ownerDocument || this.root)) || document;
if (doc != rootDoc) {
var frame = getFrameElement(doc);
if (frame) {
this._monitorIntersections(frame.ownerDocument);
Expand All @@ -415,7 +421,8 @@ IntersectionObserver.prototype._unmonitorIntersections = function(doc) {
return;
}

var rootDoc = (this.root && this.root.ownerDocument || document);
var rootDoc =
(this.root && (this.root.ownerDocument || this.root)) || document;

// Check if any dependent targets are still remaining.
var hasDependentTargets =
Expand Down Expand Up @@ -493,11 +500,18 @@ IntersectionObserver.prototype._checkForIntersections = function() {
var intersectionRect = rootIsInDom && rootContainsTarget &&
this._computeTargetAndRootIntersection(target, targetRect, rootRect);

var rootBounds = null;
if (!this._rootContainsTarget(target)) {
rootBounds = getEmptyRect();
} else if (!crossOriginUpdater || this.root) {
rootBounds = rootRect;
}

var newEntry = item.entry = new IntersectionObserverEntry({
time: now(),
target: target,
boundingClientRect: targetRect,
rootBounds: crossOriginUpdater && !this.root ? null : rootRect,
rootBounds: rootBounds,
intersectionRect: intersectionRect
});

Expand Down Expand Up @@ -618,12 +632,13 @@ IntersectionObserver.prototype._computeTargetAndRootIntersection =
*/
IntersectionObserver.prototype._getRootRect = function() {
var rootRect;
if (this.root) {
if (this.root && !isDoc(this.root)) {
rootRect = getBoundingClientRect(this.root);
} else {
// Use <html>/<body> instead of window since scroll bars affect size.
var html = document.documentElement;
var body = document.body;
var doc = isDoc(this.root) ? this.root : document;
var html = doc.documentElement;
var body = doc.body;
rootRect = {
top: 0,
left: 0,
Expand Down Expand Up @@ -714,8 +729,12 @@ IntersectionObserver.prototype._rootIsInDom = function() {
* @private
*/
IntersectionObserver.prototype._rootContainsTarget = function(target) {
return containsDeep(this.root || document, target) &&
(!this.root || this.root.ownerDocument == target.ownerDocument);
var rootDoc =
(this.root && (this.root.ownerDocument || this.root)) || document;
return (
containsDeep(rootDoc, target) &&
(!this.root || rootDoc == target.ownerDocument)
);
};


Expand Down Expand Up @@ -978,6 +997,15 @@ function getParentNode(node) {
return parent;
}

/**
* Returns true if `node` is a Document.
* @param {!Node} node
* @returns {boolean}
*/
function isDoc(node) {
return node && node.nodeType === 9;
}


// Exposes the constructors globally.
window.IntersectionObserver = IntersectionObserver;
Expand Down
2 changes: 1 addition & 1 deletion polyfill/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "intersection-observer",
"version": "0.11.0",
"version": "0.12.0",
"description": "A polyfill for IntersectionObserver",
"main": "intersection-observer",
"repository": {
Expand Down