intersection-observer 0.10.0 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -61,16 +61,16 @@ There are, however, additional use cases that the default configuration will not
61
61
 
62
62
  If you need to handle any of these use-cases, you can configure the polyfill to poll the document by setting the `POLL_INTERVAL` property. This can be set either globally or on a per-instance basis.
63
63
 
64
- **Enabling polling for all instance:**
64
+ **Enabling polling for all instances:**
65
65
 
66
- To enable polling for all instance, set a value for `POLL_INTERVAL` on the `IntersectionObserver` prototype:
66
+ To enable polling for all instances, set a value for `POLL_INTERVAL` on the `IntersectionObserver` prototype:
67
67
 
68
68
 
69
69
  ```js
70
70
  IntersectionObserver.prototype.POLL_INTERVAL = 100; // Time in milliseconds.
71
71
  ```
72
72
 
73
- **Enabling polling for individual instance:**
73
+ **Enabling polling for individual instances:**
74
74
 
75
75
  To enable polling on only specific instances, set a `POLL_INTERVAL` value on the instance itself:
76
76
 
@@ -94,7 +94,7 @@ var io = new IntersectionObserver(callback);
94
94
  io.USE_MUTATION_OBSERVER = false;
95
95
  ```
96
96
 
97
- This is recommended in cases where the DOM will update frequently but you know those updates will have no affect on the position or your target elements.
97
+ This is recommended in cases where the DOM will update frequently but you know those updates will have no effect on the position or your target elements.
98
98
 
99
99
 
100
100
  ## iframe support
@@ -171,7 +171,7 @@ Legacy support is also possible in very old browsers by including a shim for ES5
171
171
  <script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
172
172
  ```
173
173
 
174
- With these polyfills, `IntersectionObserver` has been tested an known to work in the following browsers:
174
+ With these polyfills, `IntersectionObserver` has been tested and known to work in the following browsers:
175
175
 
176
176
  <table>
177
177
  <tr>
@@ -210,4 +210,4 @@ With these polyfills, `IntersectionObserver` has been tested an known to work in
210
210
 
211
211
  To run the test suite for the `IntersectionObserver` polyfill, open the [`intersection-observer-test.html`](./intersection-observer-test.html) page in the browser of your choice.
212
212
 
213
- If you run the tests in a browser that support `IntersectionObserver` natively, the tests will be run against the native implementation. If it doesn't the tests will be run against the polyfill.
213
+ If you run the tests in a browser that supports `IntersectionObserver` natively, the tests will be run against the native implementation. If it doesn't, the tests will be run against the polyfill.
@@ -69,12 +69,15 @@ describe('IntersectionObserver', function() {
69
69
  io = new IntersectionObserver(noop);
70
70
  expect(io.root).to.be(null);
71
71
 
72
+ io = new IntersectionObserver(noop, {root: document});
73
+ expect(io.root).to.be(document);
74
+
72
75
  io = new IntersectionObserver(noop, {root: rootEl});
73
76
  expect(io.root).to.be(rootEl);
74
77
  });
75
78
 
76
79
 
77
- it('throws when root is not an Element', function() {
80
+ it('throws when root is not a Document or Element', function() {
78
81
  expect(function() {
79
82
  io = new IntersectionObserver(noop, {root: 'foo'});
80
83
  }).to.throwException();
@@ -958,6 +961,7 @@ describe('IntersectionObserver', function() {
958
961
 
959
962
  describe('iframe', function() {
960
963
  var iframe;
964
+ var iframeWin, iframeDoc;
961
965
  var documentElement, body;
962
966
  var iframeTargetEl1, iframeTargetEl2;
963
967
  var bodyWidth;
@@ -1039,7 +1043,7 @@ describe('IntersectionObserver', function() {
1039
1043
  });
1040
1044
  }
1041
1045
 
1042
- describe('same-origin iframe', function() {
1046
+ describe('same-origin iframe loaded in the mainframe', function() {
1043
1047
  it('iframe targets do not intersect with a top root element', function(done) {
1044
1048
  var io = new IntersectionObserver(function(unsortedRecords) {
1045
1049
  var records = sortRecords(unsortedRecords);
@@ -1397,6 +1401,89 @@ describe('IntersectionObserver', function() {
1397
1401
  io.observe(iframeTargetEl2);
1398
1402
  });
1399
1403
 
1404
+ it('handles tracking iframe viewport', function(done) {
1405
+ iframe.style.height = '100px';
1406
+ iframe.style.top = '100px';
1407
+ iframeWin.scrollTo(0, 110);
1408
+ // {root:iframeDoc} means to track the iframe viewport.
1409
+ var io = new IntersectionObserver(
1410
+ function (records) {
1411
+ io.unobserve(iframeTargetEl1);
1412
+
1413
+ var intersectionRect = rect({
1414
+ top: 0, // if root=null, then this would be 100.
1415
+ left: 0,
1416
+ height: 90,
1417
+ width: bodyWidth
1418
+ });
1419
+ expect(records.length).to.be(1);
1420
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
1421
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
1422
+ done();
1423
+ },
1424
+ { root: iframeDoc }
1425
+ );
1426
+
1427
+ io.observe(iframeTargetEl1);
1428
+ });
1429
+
1430
+ it('handles tracking iframe viewport with rootMargin', function(done) {
1431
+ iframe.style.height = '100px';
1432
+
1433
+ var io = new IntersectionObserver(
1434
+ function (records) {
1435
+ io.unobserve(iframeTargetEl1);
1436
+ var intersectionRect = rect({
1437
+ top: 0, // if root=null, then this would be 100.
1438
+ left: 0,
1439
+ height: 200,
1440
+ width: bodyWidth
1441
+ });
1442
+
1443
+ // rootMargin: 100% --> 3x width + 3x height.
1444
+ var expectedRootBounds = rect({
1445
+ top: -100,
1446
+ left: -bodyWidth,
1447
+ width: bodyWidth * 3,
1448
+ height: 100 * 3
1449
+ });
1450
+ expect(records.length).to.be(1);
1451
+ expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
1452
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
1453
+ done();
1454
+ },
1455
+ { root: iframeDoc, rootMargin: '100%' }
1456
+ );
1457
+
1458
+ io.observe(iframeTargetEl1);
1459
+ });
1460
+
1461
+ // Current spec indicates that cross-document tracking yields
1462
+ // an essentially empty IntersectionObserverEntry.
1463
+ // See: https://github.com/w3c/IntersectionObserver/issues/87
1464
+ it('does not track cross-document elements', function(done) {
1465
+ var io = new IntersectionObserver(
1466
+ function (records) {
1467
+ io.unobserve(iframeTargetEl1)
1468
+
1469
+ expect(records.length).to.be(1);
1470
+ const zeroesRect = rect({
1471
+ top: 0,
1472
+ left: 0,
1473
+ width: 0,
1474
+ height: 0
1475
+ });
1476
+ expect(rect(records[0].rootBounds)).to.eql(zeroesRect);
1477
+ expect(rect(records[0].intersectionRect)).to.eql(zeroesRect);
1478
+ expect(records.isIntersecting).false;
1479
+ done();
1480
+ },
1481
+ { root: document }
1482
+ );
1483
+
1484
+ io.observe(iframeTargetEl1);
1485
+ });
1486
+
1400
1487
  it('handles style changes', function(done) {
1401
1488
  var spy = sinon.spy();
1402
1489
 
@@ -1619,13 +1706,10 @@ describe('IntersectionObserver', function() {
1619
1706
  });
1620
1707
  });
1621
1708
 
1622
- describe('cross-origin iframe', function() {
1709
+ describe('same-origin iframe loaded in an iframe', function() {
1623
1710
  var ASYNC_TIMEOUT = 300;
1624
- var crossOriginUpdater;
1625
1711
 
1626
1712
  beforeEach(function(done) {
1627
- Object.defineProperty(iframeWin, 'frameElement', {value: null});
1628
-
1629
1713
  /* Uncomment these lines to force polyfill inside the iframe.
1630
1714
  delete iframeWin.IntersectionObserver;
1631
1715
  delete iframeWin.IntersectionObserverEntry;
@@ -1636,9 +1720,6 @@ describe('IntersectionObserver', function() {
1636
1720
  var script = iframeDoc.createElement('script');
1637
1721
  script.src = 'intersection-observer.js';
1638
1722
  script.onload = function() {
1639
- if (iframeWin.IntersectionObserver._setupCrossOriginUpdater) {
1640
- crossOriginUpdater = iframeWin.IntersectionObserver._setupCrossOriginUpdater();
1641
- }
1642
1723
  done();
1643
1724
  };
1644
1725
  iframeDoc.body.appendChild(script);
@@ -1647,12 +1728,6 @@ describe('IntersectionObserver', function() {
1647
1728
  }
1648
1729
  });
1649
1730
 
1650
- afterEach(function() {
1651
- if (IntersectionObserver._resetCrossOriginUpdater) {
1652
- IntersectionObserver._resetCrossOriginUpdater();
1653
- }
1654
- });
1655
-
1656
1731
  function computeRectIntersection(rect1, rect2) {
1657
1732
  var top = Math.max(rect1.top, rect2.top);
1658
1733
  var bottom = Math.min(rect1.bottom, rect2.bottom);
@@ -1678,25 +1753,19 @@ describe('IntersectionObserver', function() {
1678
1753
  };
1679
1754
  }
1680
1755
 
1681
- function checkRootBoundsAreNull(records) {
1756
+ function checkRootBounds(records) {
1682
1757
  if (!supportsNativeIntersectionObserver(iframeWin)) {
1683
1758
  records.forEach(function(record) {
1684
- expect(record.rootBounds).to.be(null);
1759
+ expect(rect(record.rootBounds)).to.eql(getRootRect(document));
1685
1760
  });
1686
1761
  }
1687
1762
  }
1688
1763
 
1689
1764
  function applyParentRect(parentRect) {
1690
- if (crossOriginUpdater) {
1691
- var parentIntersectionRect = computeRectIntersection(
1692
- parentRect, getRootRect(document));
1693
- crossOriginUpdater(parentRect, parentIntersectionRect);
1694
- } else {
1695
- iframe.style.top = parentRect.top + 'px';
1696
- iframe.style.left = parentRect.left + 'px';
1697
- iframe.style.height = parentRect.height + 'px';
1698
- iframe.style.width = parentRect.width + 'px';
1699
- }
1765
+ iframe.style.top = parentRect.top + 'px';
1766
+ iframe.style.left = parentRect.left + 'px';
1767
+ iframe.style.height = parentRect.height + 'px';
1768
+ iframe.style.width = parentRect.width + 'px';
1700
1769
  }
1701
1770
 
1702
1771
  function createObserver(callback, options, parentRect) {
@@ -1712,7 +1781,7 @@ describe('IntersectionObserver', function() {
1712
1781
  var io = createObserver(function(unsortedRecords) {
1713
1782
  var records = sortRecords(unsortedRecords);
1714
1783
  expect(records.length).to.be(3);
1715
- checkRootBoundsAreNull(records);
1784
+ checkRootBounds(records);
1716
1785
 
1717
1786
  // The documentElement is partially visible.
1718
1787
  expect(rect(records[0].boundingClientRect))
@@ -1765,7 +1834,7 @@ describe('IntersectionObserver', function() {
1765
1834
  var io = createObserver(function(unsortedRecords) {
1766
1835
  var records = sortRecords(unsortedRecords);
1767
1836
  expect(records.length).to.be(3);
1768
- checkRootBoundsAreNull(records);
1837
+ checkRootBounds(records);
1769
1838
 
1770
1839
  // The documentElement is partially visible.
1771
1840
  expect(rect(records[0].boundingClientRect))
@@ -1818,7 +1887,7 @@ describe('IntersectionObserver', function() {
1818
1887
  var io = createObserver(function(unsortedRecords) {
1819
1888
  var records = sortRecords(unsortedRecords);
1820
1889
  expect(records.length).to.be(3);
1821
- checkRootBoundsAreNull(records);
1890
+ checkRootBounds(records);
1822
1891
 
1823
1892
  // The documentElement is partially visible.
1824
1893
  expect(rect(records[0].boundingClientRect))
@@ -1879,7 +1948,7 @@ describe('IntersectionObserver', function() {
1879
1948
  var io = createObserver(function(unsortedRecords) {
1880
1949
  var records = sortRecords(unsortedRecords);
1881
1950
  expect(records.length).to.be(3);
1882
- checkRootBoundsAreNull(records);
1951
+ checkRootBounds(records);
1883
1952
 
1884
1953
  // The documentElement is partially visible.
1885
1954
  expect(rect(records[0].boundingClientRect))
@@ -1933,7 +2002,7 @@ describe('IntersectionObserver', function() {
1933
2002
  var io = createObserver(function(unsortedRecords) {
1934
2003
  var records = sortRecords(unsortedRecords);
1935
2004
  expect(records.length).to.be(3);
1936
- checkRootBoundsAreNull(records);
2005
+ checkRootBounds(records);
1937
2006
 
1938
2007
  // The documentElement is partially visible.
1939
2008
  expect(rect(records[0].boundingClientRect))
@@ -1994,7 +2063,7 @@ describe('IntersectionObserver', function() {
1994
2063
  var io = createObserver(function(unsortedRecords) {
1995
2064
  var records = sortRecords(unsortedRecords);
1996
2065
  expect(records.length).to.be(4);
1997
- checkRootBoundsAreNull(records);
2066
+ checkRootBounds(records);
1998
2067
 
1999
2068
  // The documentElement is partially visible.
2000
2069
  expect(rect(records[0].boundingClientRect))
@@ -2075,7 +2144,7 @@ describe('IntersectionObserver', function() {
2075
2144
  var io = createObserver(function(unsortedRecords) {
2076
2145
  var records = sortRecords(unsortedRecords);
2077
2146
  expect(records.length).to.be(3);
2078
- checkRootBoundsAreNull(records);
2147
+ checkRootBounds(records);
2079
2148
 
2080
2149
  var emptyRect = rect({
2081
2150
  top: 0,
@@ -2118,72 +2187,6 @@ describe('IntersectionObserver', function() {
2118
2187
  io.observe(iframeTargetEl1);
2119
2188
  });
2120
2189
 
2121
- it('blocks until crossOriginUpdater is called first time', function(done) {
2122
- if (supportsNativeIntersectionObserver(iframeWin)) {
2123
- // Skip: not possible to emulate with the native observer.
2124
- done();
2125
- return;
2126
- }
2127
-
2128
- var spy = sinon.spy();
2129
-
2130
- var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2131
-
2132
- var io = createObserver(spy, {});
2133
- io.observe(iframeTargetEl1);
2134
-
2135
- runSequence([
2136
- function(done) {
2137
- setTimeout(function() {
2138
- expect(spy.callCount).to.be(0);
2139
-
2140
- // Issue the first update.
2141
- crossOriginUpdater(parentRect, null);
2142
-
2143
- done();
2144
- }, ASYNC_TIMEOUT);
2145
- },
2146
- function(done) {
2147
- setTimeout(function() {
2148
- expect(spy.callCount).to.be(1);
2149
- var records = sortRecords(spy.lastCall.args[0]);
2150
- expect(records.length).to.be(1);
2151
- expect(records[0].intersectionRatio).to.be(0);
2152
- expect(records[0].isIntersecting).to.be(false);
2153
- done();
2154
- }, ASYNC_TIMEOUT);
2155
- },
2156
- function(done) {
2157
- io.disconnect();
2158
- done();
2159
- }
2160
- ], done);
2161
- });
2162
-
2163
- it('doesn\'t block with a root specified', function(done) {
2164
- var spy = sinon.spy();
2165
-
2166
- var io = createObserver(spy, {root: body});
2167
- io.observe(iframeTargetEl1);
2168
-
2169
- runSequence([
2170
- function(done) {
2171
- setTimeout(function() {
2172
- expect(spy.callCount).to.be(1);
2173
- var record = sortRecords(spy.lastCall.args[0])[0];
2174
- expect(record.intersectionRatio).to.be(1);
2175
- expect(record.isIntersecting).to.be(true);
2176
- expect(rect(record.rootBounds)).to.eql(rect(body.getBoundingClientRect()));
2177
- done();
2178
- }, ASYNC_TIMEOUT);
2179
- },
2180
- function(done) {
2181
- io.disconnect();
2182
- done();
2183
- }
2184
- ], done);
2185
- });
2186
-
2187
2190
  it('handles style changes', function(done) {
2188
2191
  var spy = sinon.spy();
2189
2192
 
@@ -2315,7 +2318,7 @@ describe('IntersectionObserver', function() {
2315
2318
  expect(spy.callCount).to.be(1);
2316
2319
  var records = sortRecords(spy.lastCall.args[0]);
2317
2320
  expect(records.length).to.be(2);
2318
- checkRootBoundsAreNull(records);
2321
+ checkRootBounds(records);
2319
2322
  expect(records[0].intersectionRatio).to.be(1);
2320
2323
  expect(records[0].isIntersecting).to.be(true);
2321
2324
  expect(records[1].intersectionRatio).to.be(0);
@@ -2332,7 +2335,7 @@ describe('IntersectionObserver', function() {
2332
2335
  expect(spy.callCount).to.be(2);
2333
2336
  var records = sortRecords(spy.lastCall.args[0]);
2334
2337
  expect(records.length).to.be(1);
2335
- checkRootBoundsAreNull(records);
2338
+ checkRootBounds(records);
2336
2339
  expect(records[0].intersectionRatio).to.be(0);
2337
2340
  expect(records[0].isIntersecting).to.be(false);
2338
2341
  // Top-level bounds.
@@ -2347,7 +2350,7 @@ describe('IntersectionObserver', function() {
2347
2350
  expect(spy.callCount).to.be(3);
2348
2351
  var records = sortRecords(spy.lastCall.args[0]);
2349
2352
  expect(records.length).to.be(1);
2350
- checkRootBoundsAreNull(records);
2353
+ checkRootBounds(records);
2351
2354
  expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
2352
2355
  expect(records[0].isIntersecting).to.be(true);
2353
2356
  // Top-level bounds.
@@ -2362,6 +2365,806 @@ describe('IntersectionObserver', function() {
2362
2365
  ], done);
2363
2366
  });
2364
2367
  });
2368
+
2369
+ describe('cross-origin iframe', function() {
2370
+ var ASYNC_TIMEOUT = 300;
2371
+ var crossOriginUpdater;
2372
+
2373
+ beforeEach(function(done) {
2374
+ Object.defineProperty(iframeWin, 'frameElement', {value: null});
2375
+
2376
+ /* Uncomment these lines to force polyfill inside the iframe.
2377
+ delete iframeWin.IntersectionObserver;
2378
+ delete iframeWin.IntersectionObserverEntry;
2379
+ */
2380
+
2381
+ // Install polyfill right into the iframe.
2382
+ if (!iframeWin.IntersectionObserver) {
2383
+ var script = iframeDoc.createElement('script');
2384
+ script.src = 'intersection-observer.js';
2385
+ script.onload = function() {
2386
+ if (iframeWin.IntersectionObserver._setupCrossOriginUpdater) {
2387
+ crossOriginUpdater = iframeWin.IntersectionObserver._setupCrossOriginUpdater();
2388
+ }
2389
+ done();
2390
+ };
2391
+ iframeDoc.body.appendChild(script);
2392
+ } else {
2393
+ done();
2394
+ }
2395
+ });
2396
+
2397
+ afterEach(function() {
2398
+ if (IntersectionObserver._resetCrossOriginUpdater) {
2399
+ IntersectionObserver._resetCrossOriginUpdater();
2400
+ }
2401
+ });
2402
+
2403
+ function computeRectIntersection(rect1, rect2) {
2404
+ var top = Math.max(rect1.top, rect2.top);
2405
+ var bottom = Math.min(rect1.bottom, rect2.bottom);
2406
+ var left = Math.max(rect1.left, rect2.left);
2407
+ var right = Math.min(rect1.right, rect2.right);
2408
+ var width = right - left;
2409
+ var height = bottom - top;
2410
+
2411
+ return (width >= 0 && height >= 0) && {
2412
+ top: top,
2413
+ bottom: bottom,
2414
+ left: left,
2415
+ right: right,
2416
+ width: width,
2417
+ height: height
2418
+ } || {
2419
+ top: 0,
2420
+ bottom: 0,
2421
+ left: 0,
2422
+ right: 0,
2423
+ width: 0,
2424
+ height: 0
2425
+ };
2426
+ }
2427
+
2428
+ function checkRootBoundsAreNull(records) {
2429
+ if (!supportsNativeIntersectionObserver(iframeWin)) {
2430
+ records.forEach(function(record) {
2431
+ expect(record.rootBounds).to.be(null);
2432
+ });
2433
+ }
2434
+ }
2435
+
2436
+ function applyParentRect(parentRect) {
2437
+ if (crossOriginUpdater) {
2438
+ var parentIntersectionRect = computeRectIntersection(
2439
+ parentRect, getRootRect(document));
2440
+ crossOriginUpdater(parentRect, parentIntersectionRect);
2441
+ } else {
2442
+ iframe.style.top = parentRect.top + 'px';
2443
+ iframe.style.left = parentRect.left + 'px';
2444
+ iframe.style.height = parentRect.height + 'px';
2445
+ iframe.style.width = parentRect.width + 'px';
2446
+ }
2447
+ }
2448
+
2449
+ function createObserver(callback, options, parentRect) {
2450
+ var io = new iframeWin.IntersectionObserver(callback, options);
2451
+ if (parentRect) {
2452
+ applyParentRect(parentRect);
2453
+ }
2454
+ return io;
2455
+ }
2456
+
2457
+ it('calculates rects for a fully visible frame', function(done) {
2458
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2459
+ var io = createObserver(function(unsortedRecords) {
2460
+ var records = sortRecords(unsortedRecords);
2461
+ expect(records.length).to.be(3);
2462
+ checkRootBoundsAreNull(records);
2463
+
2464
+ // The documentElement is partially visible.
2465
+ expect(rect(records[0].boundingClientRect))
2466
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2467
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2468
+ top: 0,
2469
+ left: 0,
2470
+ width: bodyWidth,
2471
+ height: 300
2472
+ }));
2473
+ expect(records[0].isIntersecting).to.be(true);
2474
+ // 300 / 404 == ~0.743
2475
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2476
+
2477
+ // The document.body is partially visible.
2478
+ expect(rect(records[1].boundingClientRect))
2479
+ .to.eql(rect(body.getBoundingClientRect()));
2480
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2481
+ top: 0,
2482
+ left: 0,
2483
+ width: bodyWidth,
2484
+ height: 300
2485
+ }));
2486
+ expect(records[1].isIntersecting).to.be(true);
2487
+ // 300 / 402 == ~0.746
2488
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2489
+
2490
+ // The target1 is fully visible.
2491
+ var clientRect1 = rect({
2492
+ top: 0,
2493
+ left: 0,
2494
+ width: bodyWidth,
2495
+ height: 200
2496
+ });
2497
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2498
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
2499
+ expect(records[2].isIntersecting).to.be(true);
2500
+ expect(records[2].intersectionRatio).to.be(1);
2501
+
2502
+ done();
2503
+ io.disconnect();
2504
+ }, {}, parentRect);
2505
+ io.observe(documentElement);
2506
+ io.observe(body);
2507
+ io.observe(iframeTargetEl1);
2508
+ });
2509
+
2510
+ it('calculates rects for a fully visible and offset frame', function(done) {
2511
+ var parentRect = rect({top: 10, left: 20, height: 300, width: 100});
2512
+ var io = createObserver(function(unsortedRecords) {
2513
+ var records = sortRecords(unsortedRecords);
2514
+ expect(records.length).to.be(3);
2515
+ checkRootBoundsAreNull(records);
2516
+
2517
+ // The documentElement is partially visible.
2518
+ expect(rect(records[0].boundingClientRect))
2519
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2520
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2521
+ top: 0,
2522
+ left: 0,
2523
+ width: bodyWidth,
2524
+ height: 300
2525
+ }));
2526
+ expect(records[0].isIntersecting).to.be(true);
2527
+ // 300 / 404 == ~0.743
2528
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2529
+
2530
+ // The document.body is partially visible.
2531
+ expect(rect(records[1].boundingClientRect))
2532
+ .to.eql(rect(body.getBoundingClientRect()));
2533
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2534
+ top: 0,
2535
+ left: 0,
2536
+ width: bodyWidth,
2537
+ height: 300
2538
+ }));
2539
+ expect(records[1].isIntersecting).to.be(true);
2540
+ // 300 / 402 == ~0.746
2541
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2542
+
2543
+ // The target1 is fully visible.
2544
+ var clientRect1 = rect({
2545
+ top: 0,
2546
+ left: 0,
2547
+ width: bodyWidth,
2548
+ height: 200
2549
+ });
2550
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2551
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
2552
+ expect(records[2].isIntersecting).to.be(true);
2553
+ expect(records[2].intersectionRatio).to.be(1);
2554
+
2555
+ done();
2556
+ io.disconnect();
2557
+ }, {}, parentRect);
2558
+ io.observe(documentElement);
2559
+ io.observe(body);
2560
+ io.observe(iframeTargetEl1);
2561
+ });
2562
+
2563
+ it('calculates rects for a clipped frame on top', function(done) {
2564
+ var parentRect = rect({top: -10, left: 20, height: 300, width: 100});
2565
+ var io = createObserver(function(unsortedRecords) {
2566
+ var records = sortRecords(unsortedRecords);
2567
+ expect(records.length).to.be(3);
2568
+ checkRootBoundsAreNull(records);
2569
+
2570
+ // The documentElement is partially visible.
2571
+ expect(rect(records[0].boundingClientRect))
2572
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2573
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2574
+ top: 10,
2575
+ left: 0,
2576
+ width: bodyWidth,
2577
+ height: 300 - 10
2578
+ }));
2579
+ expect(records[0].isIntersecting).to.be(true);
2580
+ // (300 - 10) / 404 == ~0.717
2581
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2582
+
2583
+ // The document.body is partially visible.
2584
+ expect(rect(records[1].boundingClientRect))
2585
+ .to.eql(rect(body.getBoundingClientRect()));
2586
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2587
+ top: 10,
2588
+ left: 0,
2589
+ width: bodyWidth,
2590
+ height: 300 - 10
2591
+ }));
2592
+ expect(records[1].isIntersecting).to.be(true);
2593
+ // (300 - 10) / 402 == ~0.721
2594
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2595
+
2596
+ // The target1 is clipped at the top by the iframe's clipping.
2597
+ var clientRect1 = rect({
2598
+ top: 0,
2599
+ left: 0,
2600
+ width: bodyWidth,
2601
+ height: 200
2602
+ });
2603
+ var intersectRect1 = rect({
2604
+ left: 0,
2605
+ width: bodyWidth,
2606
+ // Top is clipped.
2607
+ top: 10,
2608
+ height: 200 - 10
2609
+ });
2610
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2611
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2612
+ expect(records[2].isIntersecting).to.be(true);
2613
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
2614
+
2615
+ done();
2616
+ io.disconnect();
2617
+ }, {}, parentRect);
2618
+ io.observe(documentElement);
2619
+ io.observe(body);
2620
+ io.observe(iframeTargetEl1);
2621
+ });
2622
+
2623
+ it('calculates rects for a clipped frame on bottom', function(done) {
2624
+ var rootRect = getRootRect(document);
2625
+ var parentRect = rect({top: rootRect.bottom - 300 + 10, left: 20, height: 300, width: 100});
2626
+ var io = createObserver(function(unsortedRecords) {
2627
+ var records = sortRecords(unsortedRecords);
2628
+ expect(records.length).to.be(3);
2629
+ checkRootBoundsAreNull(records);
2630
+
2631
+ // The documentElement is partially visible.
2632
+ expect(rect(records[0].boundingClientRect))
2633
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2634
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2635
+ top: 0,
2636
+ left: 0,
2637
+ width: bodyWidth,
2638
+ height: 300 - 10
2639
+ }));
2640
+ expect(records[0].isIntersecting).to.be(true);
2641
+ // (300 - 10) / 404 == ~0.717
2642
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2643
+
2644
+ // The document.body is partially visible.
2645
+ expect(rect(records[1].boundingClientRect))
2646
+ .to.eql(rect(body.getBoundingClientRect()));
2647
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2648
+ top: 0,
2649
+ left: 0,
2650
+ width: bodyWidth,
2651
+ height: 300 - 10
2652
+ }));
2653
+ expect(records[1].isIntersecting).to.be(true);
2654
+ // (300 - 10) / 402 == ~0.721
2655
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2656
+
2657
+ // The target1 is clipped at the top by the iframe's clipping.
2658
+ var clientRect1 = rect({
2659
+ top: 0,
2660
+ left: 0,
2661
+ width: bodyWidth,
2662
+ height: 200
2663
+ });
2664
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2665
+ expect(rect(records[2].intersectionRect)).to.eql(clientRect1);
2666
+ expect(records[2].isIntersecting).to.be(true);
2667
+ expect(records[2].intersectionRatio).to.be(1);
2668
+
2669
+ done();
2670
+ io.disconnect();
2671
+ }, {}, parentRect);
2672
+ io.observe(documentElement);
2673
+ io.observe(body);
2674
+ io.observe(iframeTargetEl1);
2675
+ });
2676
+
2677
+ it('calculates rects for a fully visible and scrolled frame', function(done) {
2678
+ iframeWin.scrollTo(0, 10);
2679
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2680
+ var io = createObserver(function(unsortedRecords) {
2681
+ var records = sortRecords(unsortedRecords);
2682
+ expect(records.length).to.be(3);
2683
+ checkRootBoundsAreNull(records);
2684
+
2685
+ // The documentElement is partially visible.
2686
+ expect(rect(records[0].boundingClientRect))
2687
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2688
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2689
+ top: 0,
2690
+ left: 0,
2691
+ width: bodyWidth,
2692
+ height: 300
2693
+ }));
2694
+ expect(records[0].isIntersecting).to.be(true);
2695
+ // 300 / 404 == ~0.743
2696
+ expect(records[0].intersectionRatio).to.be.within(0.74, 0.75);
2697
+
2698
+ // The document.body is partially visible.
2699
+ expect(rect(records[1].boundingClientRect))
2700
+ .to.eql(rect(body.getBoundingClientRect()));
2701
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2702
+ top: 0,
2703
+ left: 0,
2704
+ width: bodyWidth,
2705
+ height: 300
2706
+ }));
2707
+ expect(records[1].isIntersecting).to.be(true);
2708
+ // 300 / 402 == ~0.746
2709
+ expect(records[1].intersectionRatio).to.be.within(0.74, 0.75);
2710
+
2711
+ // The target1 is fully visible.
2712
+ var clientRect1 = rect({
2713
+ top: -10,
2714
+ left: 0,
2715
+ width: bodyWidth,
2716
+ height: 200
2717
+ });
2718
+ var intersectRect1 = rect({
2719
+ top: 0,
2720
+ left: 0,
2721
+ width: bodyWidth,
2722
+ // Height is only for the visible area.
2723
+ height: 200 - 10
2724
+ });
2725
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2726
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2727
+ expect(records[2].isIntersecting).to.be(true);
2728
+ expect(records[2].intersectionRatio).to.within(0.94, 0.96); // ~0.95
2729
+
2730
+ done();
2731
+ io.disconnect();
2732
+ }, {}, parentRect);
2733
+ io.observe(documentElement);
2734
+ io.observe(body);
2735
+ io.observe(iframeTargetEl1);
2736
+ });
2737
+
2738
+ it('calculates rects for a clipped frame on top and scrolled', function(done) {
2739
+ iframeWin.scrollTo(0, 10);
2740
+ var parentRect = rect({top: -10, left: 0, height: 300, width: 100});
2741
+ var io = createObserver(function(unsortedRecords) {
2742
+ var records = sortRecords(unsortedRecords);
2743
+ expect(records.length).to.be(4);
2744
+ checkRootBoundsAreNull(records);
2745
+
2746
+ // The documentElement is partially visible.
2747
+ expect(rect(records[0].boundingClientRect))
2748
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2749
+ expect(rect(records[0].intersectionRect)).to.eql(rect({
2750
+ top: 10,
2751
+ left: 0,
2752
+ width: bodyWidth,
2753
+ height: 300 - 10
2754
+ }));
2755
+ expect(records[0].isIntersecting).to.be(true);
2756
+ // (300 - 10) / 404 == ~0.717
2757
+ expect(records[0].intersectionRatio).to.be.within(0.71, 0.72);
2758
+
2759
+ // The document.body is partially visible.
2760
+ expect(rect(records[1].boundingClientRect))
2761
+ .to.eql(rect(body.getBoundingClientRect()));
2762
+ expect(rect(records[1].intersectionRect)).to.eql(rect({
2763
+ top: 10,
2764
+ left: 0,
2765
+ width: bodyWidth,
2766
+ height: 300 - 10
2767
+ }));
2768
+ expect(records[1].isIntersecting).to.be(true);
2769
+ // (300 - 10) / 402 == ~0.721
2770
+ expect(records[1].intersectionRatio).to.be.within(0.72, 0.73);
2771
+
2772
+ // The target1 is clipped at the top by the iframe's clipping.
2773
+ var clientRect1 = rect({
2774
+ top: -10,
2775
+ left: 0,
2776
+ width: bodyWidth,
2777
+ height: 200
2778
+ });
2779
+ var intersectRect1 = rect({
2780
+ left: 0,
2781
+ width: bodyWidth,
2782
+ // Top is clipped.
2783
+ top: 10,
2784
+ // The height is less by both: offset and scroll.
2785
+ height: 200 - 10 - 10
2786
+ });
2787
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2788
+ expect(rect(records[2].intersectionRect)).to.eql(intersectRect1);
2789
+ expect(records[2].isIntersecting).to.be(true);
2790
+ expect(records[2].intersectionRatio).to.within(0.89, 0.91); // ~0.9
2791
+
2792
+ // The target2 is partially visible.
2793
+ var clientRect2 = rect({
2794
+ top: 202 - 10,
2795
+ left: 0,
2796
+ width: bodyWidth,
2797
+ height: 200
2798
+ });
2799
+ var intersectRect2 = rect({
2800
+ top: 202 - 10,
2801
+ left: 0,
2802
+ width: bodyWidth,
2803
+ // The bottom is clipped off.
2804
+ bottom: 300
2805
+ });
2806
+ expect(rect(records[3].boundingClientRect)).to.eql(clientRect2);
2807
+ expect(rect(records[3].intersectionRect)).to.eql(intersectRect2);
2808
+ expect(records[3].isIntersecting).to.be(true);
2809
+ expect(records[3].intersectionRatio).to.be.within(0.53, 0.55); // ~0.54
2810
+
2811
+ done();
2812
+ io.disconnect();
2813
+ }, {}, parentRect);
2814
+ io.observe(documentElement);
2815
+ io.observe(body);
2816
+ io.observe(iframeTargetEl1);
2817
+ io.observe(iframeTargetEl2);
2818
+ });
2819
+
2820
+ it('calculates rects for a fully clipped frame', function(done) {
2821
+ var parentRect = rect({top: -400, left: 20, height: 300, width: 100});
2822
+ var io = createObserver(function(unsortedRecords) {
2823
+ var records = sortRecords(unsortedRecords);
2824
+ expect(records.length).to.be(3);
2825
+ checkRootBoundsAreNull(records);
2826
+
2827
+ var emptyRect = rect({
2828
+ top: 0,
2829
+ left: 0,
2830
+ width: 0,
2831
+ height: 0
2832
+ });
2833
+
2834
+ // The documentElement is completely invisible.
2835
+ expect(rect(records[0].boundingClientRect))
2836
+ .to.eql(rect(documentElement.getBoundingClientRect()));
2837
+ expect(rect(records[0].intersectionRect)).to.eql(emptyRect);
2838
+ expect(records[0].isIntersecting).to.be(false);
2839
+ expect(records[0].intersectionRatio).to.be(0);
2840
+
2841
+ // The document.body is completely invisible.
2842
+ expect(rect(records[1].boundingClientRect))
2843
+ .to.eql(rect(body.getBoundingClientRect()));
2844
+ expect(rect(records[1].intersectionRect)).to.eql(emptyRect);
2845
+ expect(records[1].isIntersecting).to.be(false);
2846
+ expect(records[1].intersectionRatio).to.be(0);
2847
+
2848
+ // The target1 is completely invisible.
2849
+ var clientRect1 = rect({
2850
+ top: 0,
2851
+ left: 0,
2852
+ width: bodyWidth,
2853
+ height: 200
2854
+ });
2855
+ expect(rect(records[2].boundingClientRect)).to.eql(clientRect1);
2856
+ expect(rect(records[2].intersectionRect)).to.eql(emptyRect);
2857
+ expect(records[2].isIntersecting).to.be(false);
2858
+ expect(records[2].intersectionRatio).to.be(0);
2859
+
2860
+ done();
2861
+ io.disconnect();
2862
+ }, {}, parentRect);
2863
+ io.observe(documentElement);
2864
+ io.observe(body);
2865
+ io.observe(iframeTargetEl1);
2866
+ });
2867
+
2868
+ it('blocks until crossOriginUpdater is called first time', function(done) {
2869
+ if (supportsNativeIntersectionObserver(iframeWin)) {
2870
+ // Skip: not possible to emulate with the native observer.
2871
+ done();
2872
+ return;
2873
+ }
2874
+
2875
+ var spy = sinon.spy();
2876
+
2877
+ var parentRect = rect({top: 0, left: 20, height: 300, width: 100});
2878
+
2879
+ var io = createObserver(spy, {});
2880
+ io.observe(iframeTargetEl1);
2881
+
2882
+ runSequence([
2883
+ function(done) {
2884
+ setTimeout(function() {
2885
+ expect(spy.callCount).to.be(0);
2886
+
2887
+ // Issue the first update.
2888
+ crossOriginUpdater(parentRect, null);
2889
+
2890
+ done();
2891
+ }, ASYNC_TIMEOUT);
2892
+ },
2893
+ function(done) {
2894
+ setTimeout(function() {
2895
+ expect(spy.callCount).to.be(1);
2896
+ var records = sortRecords(spy.lastCall.args[0]);
2897
+ expect(records.length).to.be(1);
2898
+ expect(records[0].intersectionRatio).to.be(0);
2899
+ expect(records[0].isIntersecting).to.be(false);
2900
+ done();
2901
+ }, ASYNC_TIMEOUT);
2902
+ },
2903
+ function(done) {
2904
+ io.disconnect();
2905
+ done();
2906
+ }
2907
+ ], done);
2908
+ });
2909
+
2910
+ it('doesn\'t block with a root specified', function(done) {
2911
+ var spy = sinon.spy();
2912
+
2913
+ var io = createObserver(spy, {root: body});
2914
+ io.observe(iframeTargetEl1);
2915
+
2916
+ runSequence([
2917
+ function(done) {
2918
+ setTimeout(function() {
2919
+ expect(spy.callCount).to.be(1);
2920
+ var record = sortRecords(spy.lastCall.args[0])[0];
2921
+ expect(record.intersectionRatio).to.be(1);
2922
+ expect(record.isIntersecting).to.be(true);
2923
+ expect(rect(record.rootBounds)).to.eql(rect(body.getBoundingClientRect()));
2924
+ done();
2925
+ }, ASYNC_TIMEOUT);
2926
+ },
2927
+ function(done) {
2928
+ io.disconnect();
2929
+ done();
2930
+ }
2931
+ ], done);
2932
+ });
2933
+
2934
+ it('handles style changes', function(done) {
2935
+ var spy = sinon.spy();
2936
+
2937
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2938
+
2939
+ // When first element becomes invisible, the second element will show.
2940
+ // And in reverse: when the first element becomes visible again, the
2941
+ // second element will disappear.
2942
+ var io = createObserver(spy, {}, parentRect);
2943
+ io.observe(iframeTargetEl1);
2944
+ io.observe(iframeTargetEl2);
2945
+
2946
+ runSequence([
2947
+ function(done) {
2948
+ setTimeout(function() {
2949
+ expect(spy.callCount).to.be(1);
2950
+ var records = sortRecords(spy.lastCall.args[0]);
2951
+ expect(records.length).to.be(2);
2952
+ expect(records[0].intersectionRatio).to.be(1);
2953
+ expect(records[0].isIntersecting).to.be(true);
2954
+ expect(records[1].intersectionRatio).to.be(0);
2955
+ expect(records[1].isIntersecting).to.be(false);
2956
+ done();
2957
+ }, ASYNC_TIMEOUT);
2958
+ },
2959
+ function(done) {
2960
+ iframeTargetEl1.style.display = 'none';
2961
+ setTimeout(function() {
2962
+ expect(spy.callCount).to.be(2);
2963
+ var records = sortRecords(spy.lastCall.args[0]);
2964
+ expect(records.length).to.be(2);
2965
+ expect(records[0].intersectionRatio).to.be(0);
2966
+ expect(records[0].isIntersecting).to.be(false);
2967
+ expect(records[1].intersectionRatio).to.be(1);
2968
+ expect(records[1].isIntersecting).to.be(true);
2969
+ done();
2970
+ }, ASYNC_TIMEOUT);
2971
+ },
2972
+ function(done) {
2973
+ iframeTargetEl1.style.display = '';
2974
+ setTimeout(function() {
2975
+ expect(spy.callCount).to.be(3);
2976
+ var records = sortRecords(spy.lastCall.args[0]);
2977
+ expect(records.length).to.be(2);
2978
+ expect(records[0].intersectionRatio).to.be(1);
2979
+ expect(records[0].isIntersecting).to.be(true);
2980
+ expect(records[1].intersectionRatio).to.be(0);
2981
+ expect(records[1].isIntersecting).to.be(false);
2982
+ done();
2983
+ }, ASYNC_TIMEOUT);
2984
+ },
2985
+ function(done) {
2986
+ io.disconnect();
2987
+ done();
2988
+ }
2989
+ ], done);
2990
+ });
2991
+
2992
+ it('handles scroll changes', function(done) {
2993
+ var spy = sinon.spy();
2994
+
2995
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
2996
+
2997
+ // Scrolling to the middle of the iframe shows the second box and
2998
+ // hides the first.
2999
+ var io = createObserver(spy, {}, parentRect);
3000
+ io.observe(iframeTargetEl1);
3001
+ io.observe(iframeTargetEl2);
3002
+
3003
+ runSequence([
3004
+ function(done) {
3005
+ setTimeout(function() {
3006
+ expect(spy.callCount).to.be(1);
3007
+ var records = sortRecords(spy.lastCall.args[0]);
3008
+ expect(records.length).to.be(2);
3009
+ expect(records[0].intersectionRatio).to.be(1);
3010
+ expect(records[0].isIntersecting).to.be(true);
3011
+ expect(records[1].intersectionRatio).to.be(0);
3012
+ expect(records[1].isIntersecting).to.be(false);
3013
+ done();
3014
+ }, ASYNC_TIMEOUT);
3015
+ },
3016
+ function(done) {
3017
+ iframeWin.scrollTo(0, 202);
3018
+ setTimeout(function() {
3019
+ expect(spy.callCount).to.be(2);
3020
+ var records = sortRecords(spy.lastCall.args[0]);
3021
+ expect(records.length).to.be(2);
3022
+ expect(records[0].intersectionRatio).to.be(0);
3023
+ expect(records[0].isIntersecting).to.be(false);
3024
+ expect(records[1].intersectionRatio).to.be(1);
3025
+ expect(records[1].isIntersecting).to.be(true);
3026
+ done();
3027
+ }, ASYNC_TIMEOUT);
3028
+ },
3029
+ function(done) {
3030
+ iframeWin.scrollTo(0, 0);
3031
+ setTimeout(function() {
3032
+ expect(spy.callCount).to.be(3);
3033
+ var records = sortRecords(spy.lastCall.args[0]);
3034
+ expect(records.length).to.be(2);
3035
+ expect(records[0].intersectionRatio).to.be(1);
3036
+ expect(records[0].isIntersecting).to.be(true);
3037
+ expect(records[1].intersectionRatio).to.be(0);
3038
+ expect(records[1].isIntersecting).to.be(false);
3039
+ done();
3040
+ }, ASYNC_TIMEOUT);
3041
+ },
3042
+ function(done) {
3043
+ io.disconnect();
3044
+ done();
3045
+ }
3046
+ ], done);
3047
+ });
3048
+
3049
+ it('handles parent rect changes', function(done) {
3050
+ var spy = sinon.spy();
3051
+
3052
+ var parentRect = rect({top: 0, left: 0, height: 200, width: 100});
3053
+
3054
+ // Iframe goes off screen and returns.
3055
+ var io = createObserver(spy, {}, parentRect);
3056
+ io.observe(iframeTargetEl1);
3057
+ io.observe(iframeTargetEl2);
3058
+
3059
+ runSequence([
3060
+ function(done) {
3061
+ setTimeout(function() {
3062
+ expect(spy.callCount).to.be(1);
3063
+ var records = sortRecords(spy.lastCall.args[0]);
3064
+ expect(records.length).to.be(2);
3065
+ checkRootBoundsAreNull(records);
3066
+ expect(records[0].intersectionRatio).to.be(1);
3067
+ expect(records[0].isIntersecting).to.be(true);
3068
+ expect(records[1].intersectionRatio).to.be(0);
3069
+ expect(records[1].isIntersecting).to.be(false);
3070
+ // Top-level bounds.
3071
+ expect(records[0].intersectionRect.height).to.be(200);
3072
+ done();
3073
+ }, ASYNC_TIMEOUT);
3074
+ },
3075
+ function(done) {
3076
+ // Completely off screen.
3077
+ applyParentRect(rect({top: -202, left: 0, height: 200, width: 100}));
3078
+ setTimeout(function() {
3079
+ expect(spy.callCount).to.be(2);
3080
+ var records = sortRecords(spy.lastCall.args[0]);
3081
+ expect(records.length).to.be(1);
3082
+ checkRootBoundsAreNull(records);
3083
+ expect(records[0].intersectionRatio).to.be(0);
3084
+ expect(records[0].isIntersecting).to.be(false);
3085
+ // Top-level bounds.
3086
+ expect(records[0].intersectionRect.height).to.be(0);
3087
+ done();
3088
+ }, ASYNC_TIMEOUT);
3089
+ },
3090
+ function(done) {
3091
+ // Partially returns.
3092
+ applyParentRect(rect({top: -100, left: 0, height: 200, width: 100}));
3093
+ setTimeout(function() {
3094
+ expect(spy.callCount).to.be(3);
3095
+ var records = sortRecords(spy.lastCall.args[0]);
3096
+ expect(records.length).to.be(1);
3097
+ checkRootBoundsAreNull(records);
3098
+ expect(records[0].intersectionRatio).to.be.within(0.45, 0.55);
3099
+ expect(records[0].isIntersecting).to.be(true);
3100
+ // Top-level bounds.
3101
+ expect(records[0].intersectionRect.height / 200).to.be.within(0.45, 0.55);
3102
+ done();
3103
+ }, ASYNC_TIMEOUT);
3104
+ },
3105
+ function(done) {
3106
+ io.disconnect();
3107
+ done();
3108
+ }
3109
+ ], done);
3110
+ });
3111
+
3112
+ it('handles tracking iframe viewport', function(done) {
3113
+ iframe.style.height = '100px';
3114
+ iframe.style.top = '100px';
3115
+ iframeWin.scrollTo(0, 110);
3116
+ // {root:iframeDoc} means to track the iframe viewport.
3117
+ var io = createObserver(
3118
+ function (records) {
3119
+ io.unobserve(iframeTargetEl1);
3120
+ var intersectionRect = rect({
3121
+ top: 0, // if root=null, then this would be 100.
3122
+ left: 0,
3123
+ height: 90,
3124
+ width: bodyWidth
3125
+ });
3126
+ expect(records.length).to.be(1);
3127
+ expect(rect(records[0].rootBounds)).to.eql(getRootRect(iframeDoc));
3128
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
3129
+ done();
3130
+ },
3131
+ { root: iframeDoc }
3132
+ );
3133
+
3134
+ io.observe(iframeTargetEl1);
3135
+ });
3136
+
3137
+ it('handles tracking iframe viewport with rootMargin', function(done) {
3138
+ iframe.style.height = '100px';
3139
+
3140
+ var io = createObserver(
3141
+ function (records) {
3142
+ io.unobserve(iframeTargetEl1);
3143
+ var intersectionRect = rect({
3144
+ top: 0, // if root=null, then this would be 100.
3145
+ left: 0,
3146
+ height: 200,
3147
+ width: bodyWidth
3148
+ });
3149
+
3150
+ // rootMargin: 100% --> 3x width + 3x height.
3151
+ var expectedRootBounds = rect({
3152
+ top: -100,
3153
+ left: -bodyWidth,
3154
+ width: bodyWidth * 3,
3155
+ height: 100 * 3
3156
+ });
3157
+ expect(records.length).to.be(1);
3158
+ expect(rect(records[0].rootBounds)).to.eql(expectedRootBounds);
3159
+ expect(rect(records[0].intersectionRect)).to.eql(intersectionRect);
3160
+ done();
3161
+ },
3162
+ { root: iframeDoc, rootMargin: '100%' }
3163
+ );
3164
+
3165
+ io.observe(iframeTargetEl1);
3166
+ });
3167
+ });
2365
3168
  });
2366
3169
  });
2367
3170
 
@@ -33,12 +33,32 @@ if ('IntersectionObserver' in window &&
33
33
  return;
34
34
  }
35
35
 
36
-
37
36
  /**
38
- * A local reference to the document.
37
+ * Returns the embedding frame element, if any.
38
+ * @param {!Document} doc
39
+ * @return {!Element}
39
40
  */
40
- var document = window.document;
41
+ function getFrameElement(doc) {
42
+ try {
43
+ return doc.defaultView && doc.defaultView.frameElement || null;
44
+ } catch (e) {
45
+ // Ignore the error.
46
+ return null;
47
+ }
48
+ }
41
49
 
50
+ /**
51
+ * A local reference to the root document.
52
+ */
53
+ var document = (function(startDoc) {
54
+ var doc = startDoc;
55
+ var frame = getFrameElement(doc);
56
+ while (frame) {
57
+ doc = frame.ownerDocument;
58
+ frame = getFrameElement(doc);
59
+ }
60
+ return doc;
61
+ })(window.document);
42
62
 
43
63
  /**
44
64
  * An IntersectionObserver registry. This registry exists to hold a strong
@@ -111,8 +131,12 @@ function IntersectionObserver(callback, opt_options) {
111
131
  throw new Error('callback must be a function');
112
132
  }
113
133
 
114
- if (options.root && options.root.nodeType != 1) {
115
- throw new Error('root must be an Element');
134
+ if (
135
+ options.root &&
136
+ options.root.nodeType != 1 &&
137
+ options.root.nodeType != 9
138
+ ) {
139
+ throw new Error('root must be a Document or Element');
116
140
  }
117
141
 
118
142
  // Binds and throttles `this._checkForIntersections`.
@@ -375,7 +399,9 @@ IntersectionObserver.prototype._monitorIntersections = function(doc) {
375
399
  });
376
400
 
377
401
  // Also monitor the parent.
378
- if (doc != (this.root && this.root.ownerDocument || document)) {
402
+ var rootDoc =
403
+ (this.root && (this.root.ownerDocument || this.root)) || document;
404
+ if (doc != rootDoc) {
379
405
  var frame = getFrameElement(doc);
380
406
  if (frame) {
381
407
  this._monitorIntersections(frame.ownerDocument);
@@ -395,7 +421,8 @@ IntersectionObserver.prototype._unmonitorIntersections = function(doc) {
395
421
  return;
396
422
  }
397
423
 
398
- var rootDoc = (this.root && this.root.ownerDocument || document);
424
+ var rootDoc =
425
+ (this.root && (this.root.ownerDocument || this.root)) || document;
399
426
 
400
427
  // Check if any dependent targets are still remaining.
401
428
  var hasDependentTargets =
@@ -473,11 +500,18 @@ IntersectionObserver.prototype._checkForIntersections = function() {
473
500
  var intersectionRect = rootIsInDom && rootContainsTarget &&
474
501
  this._computeTargetAndRootIntersection(target, targetRect, rootRect);
475
502
 
503
+ var rootBounds = null;
504
+ if (!this._rootContainsTarget(target)) {
505
+ rootBounds = getEmptyRect();
506
+ } else if (!crossOriginUpdater || this.root) {
507
+ rootBounds = rootRect;
508
+ }
509
+
476
510
  var newEntry = item.entry = new IntersectionObserverEntry({
477
511
  time: now(),
478
512
  target: target,
479
513
  boundingClientRect: targetRect,
480
- rootBounds: crossOriginUpdater && !this.root ? null : rootRect,
514
+ rootBounds: rootBounds,
481
515
  intersectionRect: intersectionRect
482
516
  });
483
517
 
@@ -598,12 +632,13 @@ IntersectionObserver.prototype._computeTargetAndRootIntersection =
598
632
  */
599
633
  IntersectionObserver.prototype._getRootRect = function() {
600
634
  var rootRect;
601
- if (this.root) {
635
+ if (this.root && !isDoc(this.root)) {
602
636
  rootRect = getBoundingClientRect(this.root);
603
637
  } else {
604
638
  // Use <html>/<body> instead of window since scroll bars affect size.
605
- var html = document.documentElement;
606
- var body = document.body;
639
+ var doc = isDoc(this.root) ? this.root : document;
640
+ var html = doc.documentElement;
641
+ var body = doc.body;
607
642
  rootRect = {
608
643
  top: 0,
609
644
  left: 0,
@@ -694,8 +729,12 @@ IntersectionObserver.prototype._rootIsInDom = function() {
694
729
  * @private
695
730
  */
696
731
  IntersectionObserver.prototype._rootContainsTarget = function(target) {
697
- return containsDeep(this.root || document, target) &&
698
- (!this.root || this.root.ownerDocument == target.ownerDocument);
732
+ var rootDoc =
733
+ (this.root && (this.root.ownerDocument || this.root)) || document;
734
+ return (
735
+ containsDeep(rootDoc, target) &&
736
+ (!this.root || rootDoc == target.ownerDocument)
737
+ );
699
738
  };
700
739
 
701
740
 
@@ -782,8 +821,8 @@ function removeEvent(node, event, fn, opt_useCapture) {
782
821
  if (typeof node.removeEventListener == 'function') {
783
822
  node.removeEventListener(event, fn, opt_useCapture || false);
784
823
  }
785
- else if (typeof node.detatchEvent == 'function') {
786
- node.detatchEvent('on' + event, fn);
824
+ else if (typeof node.detachEvent == 'function') {
825
+ node.detachEvent('on' + event, fn);
787
826
  }
788
827
  }
789
828
 
@@ -945,32 +984,26 @@ function getParentNode(node) {
945
984
  return getFrameElement(node);
946
985
  }
947
986
 
987
+ // If the parent has element that is assigned through shadow root slot
988
+ if (parent && parent.assignedSlot) {
989
+ parent = parent.assignedSlot.parentNode
990
+ }
991
+
948
992
  if (parent && parent.nodeType == 11 && parent.host) {
949
993
  // If the parent is a shadow root, return the host element.
950
994
  return parent.host;
951
995
  }
952
996
 
953
- if (parent && parent.assignedSlot) {
954
- // If the parent is distributed in a <slot>, return the parent of a slot.
955
- return parent.assignedSlot.parentNode;
956
- }
957
-
958
997
  return parent;
959
998
  }
960
999
 
961
-
962
1000
  /**
963
- * Returns the embedding frame element, if any.
964
- * @param {!Document} doc
965
- * @return {!Element}
1001
+ * Returns true if `node` is a Document.
1002
+ * @param {!Node} node
1003
+ * @returns {boolean}
966
1004
  */
967
- function getFrameElement(doc) {
968
- try {
969
- return doc.defaultView && doc.defaultView.frameElement || null;
970
- } catch (e) {
971
- // Ignore the error.
972
- return null;
973
- }
1005
+ function isDoc(node) {
1006
+ return node && node.nodeType === 9;
974
1007
  }
975
1008
 
976
1009
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "intersection-observer",
3
- "version": "0.10.0",
3
+ "version": "0.12.1",
4
4
  "description": "A polyfill for IntersectionObserver",
5
- "main": "intersection-observer",
5
+ "main": "intersection-observer.js",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git@github.com:w3c/IntersectionObserver.git"