Skip to content

Commit

Permalink
Universal IntersectionObserver polyfill (#27595)
Browse files Browse the repository at this point in the history
* Universal IntersectionObserver polyfill

* fix starting condition

* Implement the actual polyfill

* Review fixes

* New version of polyfill that supports FIE

* Experiment guard

* tests

* Update polyfill to 0.9.0 and remove inabox experiment

* fixing tests...

* isIntersecting auto-polyfill

* Allow some code dependencies to src/polyfills/in-ob.hs

* Split code to simplify dep-check rules

* Move scheduleUpgradeIfNeeded to stub modules

* lints

* esm dist command

* remove --esm dist

* fix deps
  • Loading branch information
Dima Voytenko committed Apr 22, 2020
1 parent b45e30d commit 419131a
Show file tree
Hide file tree
Showing 16 changed files with 930 additions and 3 deletions.
6 changes: 6 additions & 0 deletions build-system/compile/bundles.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,12 @@ exports.extensionBundles = [
latestVersion: '0.1',
type: TYPES.MISC,
},
{
name: 'amp-intersection-observer-polyfill',
version: '0.1',
latestVersion: '0.1',
type: TYPES.MISC,
},
{
name: 'amp-izlesene',
version: '0.1',
Expand Down
2 changes: 2 additions & 0 deletions build-system/compile/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ const COMMON_GLOBS = [
'third_party/webcomponentsjs/ShadowCSS.js',
'node_modules/dompurify/package.json',
'node_modules/dompurify/dist/purify.es.js',
'node_modules/intersection-observer/package.json',
'node_modules/intersection-observer/intersection-observer.install.js',
'node_modules/promise-pjs/package.json',
'node_modules/promise-pjs/promise.mjs',
'node_modules/web-animations-js/package.json',
Expand Down
9 changes: 8 additions & 1 deletion build-system/global-configs/experiments-config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
{
"experimentA": {},
"experimentA": {
"name": "IntersectionObserverPolyfill",
"environment": "AMP",
"command": "gulp dist --define_experiment_constant=INTERSECTION_OBSERVER_POLYFILL",
"issue": "https://github.com/ampproject/amphtml/issues/27807",
"expirationDateUTC": "2020-05-31",
"defineExperimentConstant": "INTERSECTION_OBSERVER_POLYFILL"
},
"experimentB": {},
"experimentC": {}
}
1 change: 1 addition & 0 deletions build-system/tasks/presubmit-checks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,7 @@ const forbiddenTermsSrcInclusive = {
whitelist: [
'src/element-stub.js',
'src/friendly-iframe-embed.js',
'src/polyfillstub/intersection-observer-stub.js',
'src/runtime.js',
'src/service/extensions-impl.js',
'src/service/lightbox-manager-discovery.js',
Expand Down
24 changes: 22 additions & 2 deletions build-system/tasks/update-packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function writeIfUpdated(patchedName, file) {
}

/**
* Patches Web Animations API by wrapping its body into `install` function.
* Patches Web Animations polyfill by wrapping its body into `install` function.
* This gives us an option to call polyfill directly on the main window
* or a friendly iframe.
*/
Expand Down Expand Up @@ -76,6 +76,25 @@ function patchWebAnimations() {
writeIfUpdated(patchedName, file);
}

/**
* Patches Intersection Observer polyfill by wrapping its body into `install`
* function.
* This gives us an option to control when and how the polyfill is installed.
* The polyfill can only be installed on the root context.
*/
function patchIntersectionObserver() {
// Copies intersection-observer into a new file that has an export.
const patchedName =
'node_modules/intersection-observer/intersection-observer.install.js';
let file = fs
.readFileSync('node_modules/intersection-observer/intersection-observer.js')
.toString();

// Wrap the contents inside the install function.
file = `export function installIntersectionObserver() {\n${file}\n}\n`;
writeIfUpdated(patchedName, file);
}

/**
* Does a yarn check on node_modules, and if it is outdated, runs yarn.
*/
Expand Down Expand Up @@ -118,13 +137,14 @@ function maybeUpdatePackages() {

/**
* Installs custom lint rules, updates node_modules (for local dev), and patches
* web-animations-js if necessary.
* polyfills if necessary.
*/
async function updatePackages() {
if (!isTravisBuild()) {
runYarnCheck();
}
patchWebAnimations();
patchIntersectionObserver();
}

module.exports = {
Expand Down
22 changes: 22 additions & 0 deletions build-system/test-configs/dep-check-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,26 @@ exports.rules = [
filesMatching: 'extensions/**/*.js',
mustNotDependOn: 'src/base-element.js',
},
{
filesMatching: 'src/polyfills/**/*.js',
mustNotDependOn: '**/*.js',
allowlist: [
'src/polyfills/fetch.js->src/log.js',
'src/polyfills/fetch.js->src/types.js',
'src/polyfills/fetch.js->src/json.js',
'src/polyfills/fetch.js->src/utils/object.js',
'src/polyfills/fetch.js->src/utils/bytes.js',
'src/polyfills/intersection-observer.js->src/polyfillstub/intersection-observer-stub.js',
'src/polyfills/promise.js->node_modules/promise-pjs/promise.js',
],
},
{
filesMatching: 'src/polyfillstub/**/*.js',
mustNotDependOn: '**/*.js',
allowlist: [
'src/polyfillstub/intersection-observer-stub.js->src/services.js',
],
},
{
filesMatching: '**/*.js',
mustNotDependOn: 'src/polyfills/**/*.js',
Expand All @@ -390,9 +410,11 @@ exports.rules = [
'src/polyfills.js->src/polyfills/promise.js',
'src/polyfills.js->src/polyfills/array-includes.js',
'src/polyfills.js->src/polyfills/custom-elements.js',
'src/polyfills.js->src/polyfills/intersection-observer.js',
'src/friendly-iframe-embed.js->src/polyfills/custom-elements.js',
'src/friendly-iframe-embed.js->src/polyfills/document-contains.js',
'src/friendly-iframe-embed.js->src/polyfills/domtokenlist.js',
'src/friendly-iframe-embed.js->src/polyfills/intersection-observer.js',
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {installIntersectionObserver} from 'intersection-observer/intersection-observer.install';
import {registerServiceBuilder} from '../../../src/service';
import {upgradePolyfill} from '../../../src/polyfillstub/intersection-observer-stub';

const TAG = 'amp-intersection-observer-polyfill';

/**
* @param {!Window} win
* @return {!Object}
*/
function upgrade(win) {
upgradePolyfill(win, () => {
installIntersectionObserver();
// TODO(dvoytenko): hookup 3p host updater for inabox.
});
return {};
}

/**
* Registers the polyfill.
* @param {!Window} win
*/
export function upgradeIntersectionObserverPolyfill(win) {
registerServiceBuilder(win, TAG, upgrade, /* instantiate */ true);
}

// eslint-disable-next-line no-unused-vars
AMP.extension(TAG, '0.1', function (AMP) {
upgradeIntersectionObserverPolyfill(window);
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@ampproject/viewer-messaging": "1.1.0",
"@ampproject/worker-dom": "0.24.0",
"dompurify": "2.0.7",
"intersection-observer": "0.9.0",
"moment": "2.24.0",
"preact": "10.2.1",
"promise-pjs": "1.1.4",
Expand Down
6 changes: 6 additions & 0 deletions src/friendly-iframe-embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ import {
setParentWindow,
} from './service';
import {escapeHtml} from './dom';
import {getMode} from './mode';
import {installAmpdocServices} from './service/core-services';
import {install as installCustomElements} from './polyfills/custom-elements';
import {install as installDOMTokenList} from './polyfills/domtokenlist';
import {install as installDocContains} from './polyfills/document-contains';
import {installForChildWin as installIntersectionObserver} from './polyfills/intersection-observer';
import {installStylesForDoc, installStylesLegacy} from './style-installer';
import {installTimerInEmbedWindow} from './service/timer-impl';
import {isDocumentReady} from './document-ready';
Expand Down Expand Up @@ -850,6 +852,10 @@ function installPolyfillsInChildWindow(parentWin, childWin) {
// The anonymous class parameter allows us to detect native classes vs
// transpiled classes.
installCustomElements(childWin, class {});
// eslint-disable-next-line no-undef
if (INTERSECTION_OBSERVER_POLYFILL || getMode().localDev || getMode().test) {
installIntersectionObserver(parentWin, childWin);
}
}

/**
Expand Down
6 changes: 6 additions & 0 deletions src/polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

/** @fileoverview */

import {getMode} from './mode';
import {install as installArrayIncludes} from './polyfills/array-includes';
import {install as installCustomElements} from './polyfills/custom-elements';
import {install as installDOMTokenList} from './polyfills/domtokenlist';
import {install as installDocContains} from './polyfills/document-contains';
import {install as installFetch} from './polyfills/fetch';
import {install as installGetBoundingClientRect} from './get-bounding-client-rect';
import {install as installIntersectionObserver} from './polyfills/intersection-observer';
import {install as installMathSign} from './polyfills/math-sign';
import {install as installObjectAssign} from './polyfills/object-assign';
import {install as installObjectValues} from './polyfills/object-values';
Expand All @@ -42,6 +44,10 @@ if (self.document) {
// The anonymous class parameter allows us to detect native classes vs
// transpiled classes.
installCustomElements(self, class {});
// eslint-disable-next-line no-undef
if (INTERSECTION_OBSERVER_POLYFILL || getMode().localDev || getMode().test) {
installIntersectionObserver(self);
}
}

// TODO(#18268, erwinm): For whatever reason imports to modules that have no
Expand Down
71 changes: 71 additions & 0 deletions src/polyfills/intersection-observer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview
* See https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver.
*/

import {IntersectionObserverStub} from '../polyfillstub/intersection-observer-stub';

/**
* @param {!Window} win
*/
export function install(win) {
if (!win.IntersectionObserver) {
win.IntersectionObserver = /** @type {typeof IntersectionObserver} */ (IntersectionObserverStub);
}
fixEntry(win);
}

/**
* @param {!Window} parentWin
* @param {!Window} childWin
*/
export function installForChildWin(parentWin, childWin) {
if (childWin.IntersectionObserver) {
fixEntry(childWin);
} else if (parentWin.IntersectionObserver) {
Object.defineProperties(childWin, {
IntersectionObserver: {get: () => parentWin.IntersectionObserver},
IntersectionObserverEntry: {
get: () => parentWin.IntersectionObserverEntry,
},
});
}
}

/** @param {!Window} win */
function fixEntry(win) {
// Minimal polyfill for Edge 15's lack of `isIntersecting`
// See: https://github.com/w3c/IntersectionObserver/issues/211
if (
win.IntersectionObserverEntry &&
!('isIntersecting' in win.IntersectionObserverEntry.prototype)
) {
Object.defineProperty(
win.IntersectionObserverEntry.prototype,
'isIntersecting',
{
enumerable: true,
configurable: true,
get() {
return this.intersectionRatio > 0;
},
}
);
}
}

0 comments on commit 419131a

Please sign in to comment.