From 6549bb8a362e73a654f5e006f766882cbd74d710 Mon Sep 17 00:00:00 2001
From: Dima Voytenko
Date: Mon, 22 Feb 2021 09:49:04 -0800
Subject: [PATCH] Support display observer in the lightbox (#32701)
* Display observer in nested fixed overlays
* Support display observer in the lightbox
* cleanup
* docs
* fix types
* minor
* Combine observers and containers together
* eager reset of container observations
* shortcircuit
---
examples/amp-lightbox.amp.html | 9 +
extensions/amp-lightbox/0.1/amp-lightbox.js | 6 +
src/utils/display-observer.js | 320 +++++++++++++++++---
test/unit/utils/test-display-observer.js | 240 ++++++++++++++-
4 files changed, 523 insertions(+), 52 deletions(-)
diff --git a/examples/amp-lightbox.amp.html b/examples/amp-lightbox.amp.html
index 7e1c8628e567..f232cb5e9464 100644
--- a/examples/amp-lightbox.amp.html
+++ b/examples/amp-lightbox.amp.html
@@ -39,6 +39,7 @@
+
@@ -76,6 +77,14 @@ Scrollable Lightbox
Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit.
Lorem ipsum dolor sit amet, has nisl nihil convenire et, vim at aeque inermis reprehendunt. Propriae tincidunt id nec, elit nusquam te mea, ius noster platonem in. Mea an idque minim, sit sale deleniti apeirian et. Omnium legendos tractatos cu mea. Vix in stet dolorem accusamus. Iisque rationibus consetetur in cum, quo unum nulla legere ut. Simul numquam saperet no sit.
+
+
+
diff --git a/extensions/amp-lightbox/0.1/amp-lightbox.js b/extensions/amp-lightbox/0.1/amp-lightbox.js
index 3430b1930b97..2d401be08195 100644
--- a/extensions/amp-lightbox/0.1/amp-lightbox.js
+++ b/extensions/amp-lightbox/0.1/amp-lightbox.js
@@ -39,6 +39,10 @@ import {dict, hasOwn} from '../../../src/utils/object';
import {getMode} from '../../../src/mode';
import {htmlFor} from '../../../src/static-template';
import {isInFie} from '../../../src/iframe-helper';
+import {
+ registerContainer,
+ unregisterContainer,
+} from '../../../src/utils/display-observer';
import {toArray} from '../../../src/types';
import {tryFocus} from '../../../src/dom';
@@ -283,6 +287,7 @@ class AmpLightbox extends AMP.BaseElement {
return;
}
this.initialize_();
+ registerContainer(this.element, this.container_);
this.boundCloseOnEscape_ = /** @type {?function(this:AmpLightbox, Event)} */ (this.closeOnEscape_.bind(
this
));
@@ -557,6 +562,7 @@ class AmpLightbox extends AMP.BaseElement {
if (!this.active_) {
return;
}
+ unregisterContainer(this.element);
if (this.isScrollable_) {
setStyle(this.element, 'webkitOverflowScrolling', '');
}
diff --git a/src/utils/display-observer.js b/src/utils/display-observer.js
index d33751697654..fda814cffbe3 100644
--- a/src/utils/display-observer.js
+++ b/src/utils/display-observer.js
@@ -23,6 +23,23 @@ const SERVICE_ID = 'DisplayObserver';
const DISPLAY_THRESHOLD = 0.51;
+const CUSTOM_CONTAINER_OFFSET = 2;
+
+/**
+ * @typedef {function(boolean, !Element)}
+ */
+let ObserverCallbackDef;
+
+/**
+ * @typedef {{
+ * container: ?Element,
+ * root: ?Element,
+ * contains: function(!Element):boolean,
+ * io: ?IntersectionObserver,
+ * }}
+ */
+let ObserverDef;
+
/**
* Observes whether the specified target is displayable. The initial observation
* is returned shortly after observing, and subsequent observations are
@@ -38,7 +55,7 @@ const DISPLAY_THRESHOLD = 0.51;
* not in the viewport, it's considered to be "displayable".
*
* @param {!Element} target
- * @param {function(boolean)} callback
+ * @param {!ObserverCallbackDef} callback
*/
export function observeDisplay(target, callback) {
getObserver(target).observe(target, callback);
@@ -67,6 +84,27 @@ export function measureDisplay(target) {
});
}
+/**
+ * Registers the container to provide additional display intersection info
+ * for other targets. Mainly aimed for fixed and/or scrollable containers
+ * that can provide display information in addition to the document flow.
+ *
+ * @param {!Element} container
+ * @param {?Element=} opt_root The subelement inside the container to be
+ * used as an intersection root. If not specified, the container will be
+ * used as an intersection root.
+ */
+export function registerContainer(container, opt_root) {
+ getObserver(container).registerContainer(container, opt_root);
+}
+
+/**
+ * @param {!Element} container
+ */
+export function unregisterContainer(container) {
+ getObserver(container).unregisterContainer(container);
+}
+
/**
* @implements {Disposable}
* @visibleForTesting
@@ -77,24 +115,39 @@ export class DisplayObserver {
* @param {!AmpDoc} ampdoc
*/
constructor(ampdoc) {
+ /** @private @const */
+ this.ampdoc_ = ampdoc;
+
const {win} = ampdoc;
+ const body = ampdoc.getBody();
+
+ this.observed_ = this.observed_.bind(this);
+ this.containerObserved_ = this.containerObserved_.bind(this);
- /** @private @const {!Array} */
+ /** @private @const {!Array} */
this.observers_ = [];
- const boundObserved = this.observed_.bind(this);
- this.observers_.push(
- new win.IntersectionObserver(boundObserved, {
- root: ampdoc.getBody(),
- threshold: DISPLAY_THRESHOLD,
- })
- );
+
// Viewport observer is only needed because `postion:fixed` elements
// are not observable by a documentElement or body's root.
- this.observers_.push(
- new win.IntersectionObserver(boundObserved, {
+ this.observers_.push({
+ container: null,
+ root: null,
+ contains: () => true,
+ io: new win.IntersectionObserver(this.observed_, {
+ threshold: DISPLAY_THRESHOLD,
+ }),
+ });
+
+ // Body observer: very close to `display:none` observer.
+ this.observers_.push({
+ container: body,
+ root: body,
+ contains: () => true,
+ io: new win.IntersectionObserver(this.observed_, {
+ root: body,
threshold: DISPLAY_THRESHOLD,
- })
- );
+ }),
+ });
/** @private {boolean} */
this.isDocDisplay_ = computeDocIsDisplayed(ampdoc.getVisibilityState());
@@ -108,7 +161,7 @@ export class DisplayObserver {
}
});
- /** @private @const {!Map>} */
+ /** @private @const {!Map>} */
this.targetObserverCallbacks_ = new Map();
/** @private @const {!Map>} */
@@ -117,24 +170,103 @@ export class DisplayObserver {
/** @override */
dispose() {
- this.observers_.forEach((observer) => observer.disconnect());
+ this.observers_.forEach((observer) => {
+ if (observer.io) {
+ observer.io.disconnect();
+ }
+ });
+ this.observers_.length = 0;
this.visibilityUnlisten_();
this.visibilityUnlisten_ = null;
this.targetObserverCallbacks_.clear();
this.targetObservations_.clear();
}
+ /**
+ * @param {!Element} container
+ * @param {?Element=} opt_root
+ */
+ registerContainer(container, opt_root) {
+ const existing = findObserverByContainer(this.observers_, container);
+ if (existing != -1) {
+ return;
+ }
+
+ /** @type {!ObserverDef} */
+ const observer = {
+ container,
+ root: opt_root || container,
+ contains: (target) => containsNotSelf(container, target),
+ // Start with null as IntersectionObserver. Will be initialized when
+ // the container itself becomes displayed.
+ io: null,
+ };
+ const index = this.observers_.length;
+ this.observers_.push(observer);
+ this.observe(container, this.containerObserved_);
+
+ this.targetObserverCallbacks_.forEach((_, target) => {
+ // Reset observation to `null` and wait for the actual measurement.
+ const value = observer.contains(target) ? null : false;
+ this.setObservation_(target, index, value, /* callbacks */ null);
+ });
+ }
+
+ /**
+ * @param {!Element} container
+ */
+ unregisterContainer(container) {
+ const index = findObserverByContainer(this.observers_, container);
+ if (index < CUSTOM_CONTAINER_OFFSET) {
+ // The container has been unregistered already.
+ return;
+ }
+
+ // Remove observer.
+ const observer = this.observers_[index];
+ this.observers_.splice(index, 1);
+ if (observer.io) {
+ observer.io.disconnect();
+ }
+
+ // Unobserve the container itself.
+ this.unobserve(container, this.containerObserved_);
+
+ // Remove observations.
+ this.targetObserverCallbacks_.forEach((callbacks, target) => {
+ const observations = this.targetObservations_.get(target);
+ if (!observations || observations.length <= index) {
+ return;
+ }
+ const oldDisplay = computeDisplay(observations, this.isDocDisplay_);
+ observations.splice(index, 1);
+ const newDisplay = computeDisplay(observations, this.isDocDisplay_);
+ notifyIfChanged(callbacks, target, newDisplay, oldDisplay);
+ });
+ }
+
/**
* @param {!Element} target
- * @param {function(boolean)} callback
+ * @param {!ObserverCallbackDef} callback
*/
observe(target, callback) {
let callbacks = this.targetObserverCallbacks_.get(target);
if (!callbacks) {
callbacks = [];
this.targetObserverCallbacks_.set(target, callbacks);
+
+ // Subscribe observers.
for (let i = 0; i < this.observers_.length; i++) {
- this.observers_[i].observe(target);
+ const observer = this.observers_[i];
+ if (observer.io && observer.contains(target)) {
+ // Reset observation to `null` and wait for the actual measurement.
+ this.setObservation_(target, i, null, /* callbacks */ null);
+ observer.io.observe(target);
+ } else {
+ // The `false` value will essentially ignore this observe when
+ // computing the display value.
+ this.setObservation_(target, i, false, /* callbacks */ null);
+ }
}
}
if (pushIfNotExist(callbacks, callback)) {
@@ -146,7 +278,7 @@ export class DisplayObserver {
this.isDocDisplay_
);
if (display != null) {
- callCallbackNoInline(callback, display);
+ callCallbackNoInline(callback, target, display);
}
});
}
@@ -155,7 +287,7 @@ export class DisplayObserver {
/**
* @param {!Element} target
- * @param {function()} callback
+ * @param {!ObserverCallbackDef} callback
*/
unobserve(target, callback) {
const callbacks = this.targetObserverCallbacks_.get(target);
@@ -167,7 +299,10 @@ export class DisplayObserver {
this.targetObserverCallbacks_.delete(target);
this.targetObservations_.delete(target);
for (let i = 0; i < this.observers_.length; i++) {
- this.observers_[i].unobserve(target);
+ const observer = this.observers_[i];
+ if (observer.io) {
+ observer.io.unobserve(target);
+ }
}
}
}
@@ -178,16 +313,56 @@ export class DisplayObserver {
const observations = this.targetObservations_.get(target);
const oldDisplay = computeDisplay(observations, !this.isDocDisplay_);
const newDisplay = computeDisplay(observations, this.isDocDisplay_);
- notifyIfChanged(callbacks, newDisplay, oldDisplay);
+ notifyIfChanged(callbacks, target, newDisplay, oldDisplay);
+ });
+ }
+
+ /**
+ * @param {boolean} isDisplayed
+ * @param {!Element} container
+ * @private
+ */
+ containerObserved_(isDisplayed, container) {
+ const index = findObserverByContainer(this.observers_, container);
+ if (index < CUSTOM_CONTAINER_OFFSET) {
+ // The container has been unregistered already.
+ return;
+ }
+
+ const observer = this.observers_[index];
+ if (isDisplayed && observer.io) {
+ // Has already been initialized.
+ return;
+ }
+
+ if (isDisplayed) {
+ const {win} = this.ampdoc_;
+ observer.io = new win.IntersectionObserver(this.observed_, {
+ root: observer.root,
+ threshold: DISPLAY_THRESHOLD,
+ });
+ } else if (observer.io) {
+ observer.io.disconnect();
+ observer.io = null;
+ }
+
+ this.targetObserverCallbacks_.forEach((callbacks, target) => {
+ if (observer.io && observer.contains(target)) {
+ // Reset observation to `null` and wait for the actual measurement.
+ this.setObservation_(target, index, null, callbacks);
+ observer.io.observe(target);
+ } else {
+ this.setObservation_(target, index, false, callbacks);
+ }
});
}
/**
* @param {!Array} entries
- * @param {!IntersectionObserver} observer
+ * @param {!IntersectionObserver} io
* @private
*/
- observed_(entries, observer) {
+ observed_(entries, io) {
const seen = new Set();
for (let i = entries.length - 1; i >= 0; i--) {
const {target, isIntersecting} = entries[i];
@@ -196,19 +371,39 @@ export class DisplayObserver {
}
seen.add(target);
const callbacks = this.targetObserverCallbacks_.get(target);
- if (!callbacks) {
+ const index = findObserverByIo(this.observers_, io);
+ if (!callbacks || index == -1) {
continue;
}
- let observations = this.targetObservations_.get(target);
- if (!observations) {
- observations = emptyObservations(this.observers_.length);
- this.targetObservations_.set(target, observations);
+ this.setObservation_(target, index, isIntersecting, callbacks);
+ }
+ }
+
+ /**
+ * @param {!Element} target
+ * @param {number} index
+ * @param {?boolean} value
+ * @param {?Array} callbacks
+ * @private
+ */
+ setObservation_(target, index, value, callbacks) {
+ let observations = this.targetObservations_.get(target);
+ if (!observations) {
+ const observers = this.observers_;
+ observations = new Array(observers.length);
+ for (let i = 0; i < observers.length; i++) {
+ observations[i] = observers[i].io ? null : false;
}
+ this.targetObservations_.set(target, observations);
+ }
+
+ if (callbacks) {
const oldDisplay = computeDisplay(observations, this.isDocDisplay_);
- const index = this.observers_.indexOf(observer);
- observations[index] = isIntersecting;
+ observations[index] = value;
const newDisplay = computeDisplay(observations, this.isDocDisplay_);
- notifyIfChanged(callbacks, newDisplay, oldDisplay);
+ notifyIfChanged(callbacks, target, newDisplay, oldDisplay);
+ } else {
+ observations[index] = value;
}
}
}
@@ -222,18 +417,6 @@ function getObserver(target) {
return /** @type {!DisplayObserver} */ (getServiceForDoc(target, SERVICE_ID));
}
-/**
- * @param {number} length
- * @return {!Array}
- */
-function emptyObservations(length) {
- const result = new Array(length);
- for (let i = 0; i < length; i++) {
- result[i] = null;
- }
- return result;
-}
-
/**
* @param {?Array} observations
* @param {boolean} isDocDisplay
@@ -243,7 +426,7 @@ function computeDisplay(observations, isDocDisplay) {
if (!isDocDisplay) {
return false;
}
- if (!observations) {
+ if (!observations || observations.length == 0) {
// Unknown yet.
return null;
}
@@ -282,26 +465,65 @@ function displayReducer(acc, value) {
return null;
}
+/**
+ * @param {!Array} observers
+ * @param {!IntersectionObserver} io
+ * @return {number}
+ */
+function findObserverByIo(observers, io) {
+ for (let i = 0; i < observers.length; i++) {
+ if (observers[i].io === io) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * @param {!Array} observers
+ * @param {!Element} container
+ * @return {number}
+ */
+function findObserverByContainer(observers, container) {
+ for (let i = 0; i < observers.length; i++) {
+ if (observers[i].container === container) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+/**
+ * @param {!Element} container
+ * @param {!Element} child
+ * @return {boolean}
+ */
+function containsNotSelf(container, child) {
+ return child !== container && container.contains(child);
+}
+
/**
* @param {!Array} callbacks
+ * @param {!Element} target
* @param {boolean} newDisplay
* @param {boolean} oldDisplay
*/
-function notifyIfChanged(callbacks, newDisplay, oldDisplay) {
+function notifyIfChanged(callbacks, target, newDisplay, oldDisplay) {
if (newDisplay != null && newDisplay !== oldDisplay) {
for (let i = 0; i < callbacks.length; i++) {
- callCallbackNoInline(callbacks[i], newDisplay);
+ callCallbackNoInline(callbacks[i], target, newDisplay);
}
}
}
/**
* @param {!ObserverCallbackDef} callback
- * @param {../layout-rect.LayoutSizeDef} size
+ * @param {!Element} target
+ * @param {boolean} isDisplayed
*/
-function callCallbackNoInline(callback, size) {
+function callCallbackNoInline(callback, target, isDisplayed) {
try {
- callback(size);
+ callback(isDisplayed, target);
} catch (e) {
rethrowAsync(e);
}
diff --git a/test/unit/utils/test-display-observer.js b/test/unit/utils/test-display-observer.js
index d3ad5afb4b1e..39031a9d6b91 100644
--- a/test/unit/utils/test-display-observer.js
+++ b/test/unit/utils/test-display-observer.js
@@ -18,14 +18,16 @@ import {Deferred} from '../../../src/utils/promise';
import {
measureDisplay,
observeDisplay,
+ registerContainer,
unobserveDisplay,
+ unregisterContainer,
} from '../../../src/utils/display-observer';
import {removeItem} from '../../../src/utils/array';
describes.realWin('display-observer', {amp: true}, (env) => {
let win, doc, ampdoc;
let element;
- let docObserver, viewportObserver;
+ let docObserver, viewportObserver, containerObservers;
beforeEach(() => {
win = env.win;
@@ -42,7 +44,9 @@ describes.realWin('display-observer', {amp: true}, (env) => {
this.elements = [];
}
- disconnect() {}
+ disconnect() {
+ this.elements.length = 0;
+ }
observe(element) {
if (this.elements.includes(element)) {
@@ -53,12 +57,17 @@ describes.realWin('display-observer', {amp: true}, (env) => {
unobserve(element) {
if (!this.elements.includes(element)) {
- throw new Error('not observed');
+ throw new Error(
+ 'not observed: ' + element.id + ' on ' + this.options?.root?.id
+ );
}
removeItem(this.elements, element);
}
notify(entries) {
+ if (!entries.some(({target}) => this.elements.includes(target))) {
+ throw new Error('unobserved target');
+ }
const {callback} = this;
return Promise.resolve().then(() => {
callback(entries, this);
@@ -68,6 +77,7 @@ describes.realWin('display-observer', {amp: true}, (env) => {
docObserver = null;
viewportObserver = null;
+ containerObservers = new Map();
env.sandbox
.stub(win, 'IntersectionObserver')
.value(function (callback, options) {
@@ -80,6 +90,14 @@ describes.realWin('display-observer', {amp: true}, (env) => {
return (docObserver =
docObserver || new FakeIntersectionObserver(callback, options));
}
+ if (options.root) {
+ const containerObserver = new FakeIntersectionObserver(
+ callback,
+ options
+ );
+ containerObservers.set(options.root, containerObserver);
+ return containerObserver;
+ }
return new FakeIntersectionObserver(callback, options);
});
});
@@ -291,4 +309,220 @@ describes.realWin('display-observer', {amp: true}, (env) => {
expect(display3).to.be.true;
});
});
+
+ describe('registerContainer', () => {
+ let container;
+ let topElement;
+
+ beforeEach(() => {
+ container = doc.createElement('div');
+ container.id = 'container1';
+ doc.body.appendChild(container);
+ container.appendChild(element);
+
+ topElement = doc.createElement('div');
+ topElement.id = 'topElement1';
+ doc.body.appendChild(topElement);
+ });
+
+ it('should create observer only after container display is known', async () => {
+ registerContainer(container);
+ expect(containerObservers.get(container)).to.not.exist;
+
+ await viewportObserver.notify([
+ {target: container, isIntersecting: false},
+ ]);
+ expect(containerObservers.get(container)).to.not.exist;
+
+ await viewportObserver.notify([
+ {target: container, isIntersecting: true},
+ ]);
+ expect(containerObservers.get(container)).to.exist;
+ });
+
+ it('should only observe contained elements', async () => {
+ const elementCallback = createCallbackCaller();
+ observeDisplay(element, elementCallback);
+ const topElementCallback = createCallbackCaller();
+ observeDisplay(topElement, topElementCallback);
+
+ viewportObserver.notify([{target: element, isIntersecting: false}]);
+ docObserver.notify([{target: element, isIntersecting: false}]);
+ viewportObserver.notify([{target: topElement, isIntersecting: false}]);
+ docObserver.notify([{target: topElement, isIntersecting: false}]);
+
+ const display1 = await elementCallback.next();
+ const display2 = await topElementCallback.next();
+ expect(display1).to.be.false;
+ expect(display2).to.be.false;
+
+ registerContainer(container);
+ await viewportObserver.notify([
+ {target: container, isIntersecting: true},
+ ]);
+
+ const containerObserver = containerObservers.get(container);
+ expect(containerObserver.elements).to.include(element);
+ expect(containerObserver.elements).to.not.include(topElement);
+
+ containerObserver.notify([{target: element, isIntersecting: true}]);
+ const display3 = await elementCallback.next();
+ const display4 = await topElementCallback.next();
+ expect(display3).to.be.true;
+ expect(display4).to.be.false; // no change.
+ });
+
+ it('should unregister observer', async () => {
+ const elementCallback = createCallbackCaller();
+ observeDisplay(element, elementCallback);
+
+ viewportObserver.notify([{target: element, isIntersecting: false}]);
+ docObserver.notify([{target: element, isIntersecting: false}]);
+
+ const display1 = await elementCallback.next();
+ expect(display1).to.be.false;
+
+ registerContainer(container);
+ await viewportObserver.notify([
+ {target: container, isIntersecting: true},
+ ]);
+ const containerObserver = containerObservers.get(container);
+ containerObserver.notify([{target: element, isIntersecting: true}]);
+ const display2 = await elementCallback.next();
+ expect(display2).to.be.true;
+
+ unregisterContainer(container);
+ expect(docObserver.elements).to.not.include(container);
+ expect(containerObserver.elements).to.not.include(element);
+
+ const display3 = await elementCallback.next();
+ expect(display3).to.be.false;
+ });
+
+ it('should change display when container observer is notified', async () => {
+ const elementCallback = createCallbackCaller();
+ observeDisplay(element, elementCallback);
+
+ viewportObserver.notify([{target: element, isIntersecting: false}]);
+ docObserver.notify([{target: element, isIntersecting: false}]);
+
+ const display1 = await elementCallback.next();
+ expect(display1).to.be.false;
+
+ registerContainer(container);
+ await viewportObserver.notify([
+ {target: container, isIntersecting: true},
+ ]);
+
+ const containerObserver = containerObservers.get(container);
+ containerObserver.notify([{target: element, isIntersecting: true}]);
+ const display2 = await elementCallback.next();
+ expect(display2).to.be.true;
+
+ containerObserver.notify([{target: element, isIntersecting: false}]);
+ const display3 = await elementCallback.next();
+ expect(display3).to.be.false;
+ });
+
+ it('should change display when container display has changed', async () => {
+ const elementCallback = createCallbackCaller();
+ observeDisplay(element, elementCallback);
+
+ viewportObserver.notify([{target: element, isIntersecting: false}]);
+ docObserver.notify([{target: element, isIntersecting: false}]);
+
+ const display1 = await elementCallback.next();
+ expect(display1).to.be.false;
+
+ registerContainer(container);
+ await docObserver.notify([{target: container, isIntersecting: false}]);
+ await viewportObserver.notify([
+ {target: container, isIntersecting: true},
+ ]);
+
+ const containerObserver = containerObservers.get(container);
+ containerObserver.notify([{target: element, isIntersecting: true}]);
+ const display2 = await elementCallback.next();
+ expect(display2).to.be.true;
+
+ await viewportObserver.notify([
+ {target: container, isIntersecting: false},
+ ]);
+ const display3 = await elementCallback.next();
+ expect(display3).to.be.false;
+ });
+
+ it('should compute display for nested observers', async () => {
+ const childContainer = doc.createElement('div');
+ childContainer.id = 'child-container1';
+ container.appendChild(childContainer);
+ childContainer.appendChild(element);
+
+ const elementCallback = createCallbackCaller();
+ observeDisplay(element, elementCallback);
+ await viewportObserver.notify([{target: element, isIntersecting: false}]);
+ expect(elementCallback.isEmpty()).to.be.true;
+
+ await docObserver.notify([{target: element, isIntersecting: false}]);
+ expect(await elementCallback.next()).to.be.false;
+
+ // 1. Register childContainer.
+ registerContainer(childContainer);
+ expect(containerObservers.get(childContainer)).to.not.exist;
+ expect(elementCallback.isEmpty()).to.be.true;
+
+ // 2. Make child container undisplayed.
+ await viewportObserver.notify([
+ {target: childContainer, isIntersecting: false},
+ ]);
+ await docObserver.notify([
+ {target: childContainer, isIntersecting: false},
+ ]);
+ expect(containerObservers.get(childContainer)).to.not.exist;
+ expect(await elementCallback.next()).to.be.false;
+
+ // 3. Register parent container.
+ registerContainer(container);
+ expect(containerObservers.get(container)).to.not.exist;
+ expect(elementCallback.isEmpty()).to.be.true;
+
+ // 4. Make parent container displayed, but child is still undisplayed.
+ await docObserver.notify([{target: container, isIntersecting: true}]);
+ expect(containerObservers.get(container)).to.exist;
+ expect(elementCallback.isEmpty()).to.be.true;
+
+ // 5. Intersect the child container inside the parent container.
+ await containerObservers
+ .get(container)
+ .notify([{target: childContainer, isIntersecting: true}]);
+ expect(containerObservers.get(childContainer)).to.exist;
+ expect(elementCallback.isEmpty()).to.be.true;
+
+ // 6. Intesect the element inside the child container.
+ await containerObservers
+ .get(childContainer)
+ .notify([{target: element, isIntersecting: true}]);
+ expect(await elementCallback.next()).to.be.true;
+ });
+
+ it('should not interrupt observations for the unrelated targets', async () => {
+ const elementCallback = createCallbackCaller();
+ observeDisplay(topElement, elementCallback);
+
+ // 1. Register a container, but not observations on it yet.
+ registerContainer(container);
+ expect(elementCallback.isEmpty()).to.be.true;
+
+ await viewportObserver.notify([
+ {target: topElement, isIntersecting: false},
+ ]);
+ await docObserver.notify([{target: topElement, isIntersecting: false}]);
+ expect(await elementCallback.next()).to.be.false;
+
+ // 2. Provide observations for the container.
+ await docObserver.notify([{target: container, isIntersecting: true}]);
+ expect(containerObservers.get(container)).to.exist;
+ expect(elementCallback.isEmpty()).to.be.true;
+ });
+ });
});