From 81b870fcb7674b4849347b95d74768f85d5957f0 Mon Sep 17 00:00:00 2001 From: Alan Orozco Date: Mon, 25 Feb 2019 22:43:49 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8auto-lightbox=20carousels=20under=20ex?= =?UTF-8?q?periment=20(#20910)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduces criteria to accept any carousel that: 1. Has one image on every slide 2. Has no valid `on=tap` actions. * Since `amp-img` has to be measured against the slide element, some APIs are now async. * Introduces a `mixed` manual test for several cases. --- .../0.1/amp-auto-lightbox.js | 119 ++++++++++---- .../0.1/carousel-criteria.js | 118 ++++++++++++++ .../0.1/test/test-amp-auto-lightbox.js | 9 +- .../0.1/test/test-carousel-criteria.js | 136 ++++++++++++++++ .../amp-auto-lightbox/0.1/utils/promise.js | 19 +++ .../0.1/service/lightbox-manager-impl.js | 96 +++++------ src/auto-lightbox.js | 31 ++++ test/manual/auto-lightbox/mixed.html | 152 ++++++++++++++++++ tools/experiments/experiments.js | 6 + 9 files changed, 608 insertions(+), 78 deletions(-) create mode 100644 extensions/amp-auto-lightbox/0.1/carousel-criteria.js create mode 100644 extensions/amp-auto-lightbox/0.1/test/test-carousel-criteria.js create mode 100644 extensions/amp-auto-lightbox/0.1/utils/promise.js create mode 100644 test/manual/auto-lightbox/mixed.html diff --git a/extensions/amp-auto-lightbox/0.1/amp-auto-lightbox.js b/extensions/amp-auto-lightbox/0.1/amp-auto-lightbox.js index d4300c1f5015b..67a66fe66939e 100644 --- a/extensions/amp-auto-lightbox/0.1/amp-auto-lightbox.js +++ b/extensions/amp-auto-lightbox/0.1/amp-auto-lightbox.js @@ -24,14 +24,17 @@ import {AmpEvents} from '../../../src/amp-events'; import {AutoLightboxEvents} from '../../../src/auto-lightbox'; +import {CarouselCriteria} from './carousel-criteria'; import {CommonSignals} from '../../../src/common-signals'; import {Services} from '../../../src/services'; import { closestAncestorElementBySelector, + matches, whenUpgradedToCustomElement, } from '../../../src/dom'; import {dev} from '../../../src/log'; import {getMode} from '../../../src/mode'; +import {resolveFalse, resolveTrue} from './utils/promise'; import {toArray} from '../../../src/types'; import {tryParseJson} from '../../../src/json'; @@ -70,21 +73,34 @@ export const RENDER_AREA_RATIO = 1.2; /** Factor of renderArea vs viewportArea to lightbox. */ export const VIEWPORT_AREA_RATIO = 0.25; +/** @const {!Array} */ +const CANDIDATES = ['amp-img', 'amp-carousel']; + /** - * Selector for subnodes for which the auto-lightbox treatment does not apply. + * Selector for subnodes by attribute for which the auto-lightbox treatment + * does not apply. These can be set directly on the candidate or on an ancestor. */ -const DISABLED_ANCESTORS = [ +const DISABLED_BY_ATTR = [ // Runtime-specific. '[placeholder]', // Explicitly opted out. '[data-amp-auto-lightbox-disable]', + // Considered "actionable", i.e. that are bound to a default + // onclick action(e.g. `button`) or where it cannot be determined whether + // they're actionable or not (e.g. `amp-script`). + 'amp-selector [option]', +].join(','); + +/** + * Selector for subnodes for which the auto-lightbox treatment does not apply. + */ +const DISABLED_ANCESTORS = [ // Ancestors considered "actionable", i.e. that are bound to a default // onclick action(e.g. `button`) or where it cannot be determined whether // they're actionable or not (e.g. `amp-script`). 'a[href]', - 'amp-selector [option]', 'amp-script', 'amp-story', 'button', @@ -93,8 +109,6 @@ const DISABLED_ANCESTORS = [ 'amp-lightbox', // Special treatment. - // TODO(alanorozco): Allow and possibly group carousels where images are the - // only content. 'amp-carousel', ].join(','); @@ -117,13 +131,65 @@ export class Criteria { /** * @param {!Element} element - * @return {boolean} + * @return {!Promise} */ static meetsAll(element) { - return Criteria.meetsSizingCriteria(element) && - Criteria.meetsTreeShapeCriteria(element); + if (!Criteria.meetsSimpleCriteria(element) || + !Criteria.meetsTreeShapeCriteria(element)) { + return resolveFalse(); + } + return Criteria.meetsComplexCriteria(element); + } + + /** + * Criteria that is "simple", ie runs quickly and discards elements in order + * to shortcircuit. + * @param {!Element} element + * @return {boolean} + */ + static meetsSimpleCriteria(element) { + if (element.tagName.toUpperCase() == 'AMP-IMG') { + return ImageCriteria.meetsSizingCriteria(element); + } + return true; + } + + /** + * Criteria that is "complex", ie takes longer to run and discards elements + * after they're likely to be good candidates per previous conditions. + * @param {!Element} element + * @return {!Promise} + */ + static meetsComplexCriteria(element) { + if (element.tagName.toUpperCase() == 'AMP-CAROUSEL') { + return CarouselCriteria.meetsAll(element); + } + return resolveTrue(); } + /** + * @param {!Element} element + * @return {boolean} + */ + static meetsTreeShapeCriteria(element) { + const disabledSelector = `${DISABLED_ANCESTORS},${DISABLED_BY_ATTR}`; + const disabledAncestor = + closestAncestorElementBySelector(element, disabledSelector); + // since we lookup both amp-img and amp-carousel at the same level, and + // we'd like to give amp-carousel special treatment by containing amp-img's, + // we need to filter out images inside carousels, but not carousels + // themselves. + if (disabledAncestor && + (disabledAncestor != element || + matches(disabledAncestor, DISABLED_BY_ATTR))) { + return false; + } + const actions = Services.actionServiceForDoc(element); + return !actions.hasResolvableAction(element, 'tap'); + } +} + +class ImageCriteria { /** * @param {!Element} element * @return {boolean} @@ -145,18 +211,6 @@ export class Criteria { vw, vh); } - - /** - * @param {!Element} element - * @return {boolean} - */ - static meetsTreeShapeCriteria(element) { - if (closestAncestorElementBySelector(element, DISABLED_ANCESTORS)) { - return false; - } - const actions = Services.actionServiceForDoc(element); - return !actions.hasResolvableAction(element, 'tap'); - } } @@ -208,6 +262,15 @@ function markAsVisited(candidate) { } +/** + * @param {string} tagName + * @return {string} + */ +function candidateSelector(tagName) { + return `${tagName}:not([${LIGHTBOXABLE_ATTR}]):not([${VISITED_ATTR}])`; +} + + /** * @param {!Element} element * @return {!Promise} @@ -226,8 +289,7 @@ export class Scanner { * @return {!Array} */ static getCandidates(root) { - const selector = - `amp-img:not([${LIGHTBOXABLE_ATTR}]):not([${VISITED_ATTR}])`; + const selector = CANDIDATES.map(candidateSelector).join(','); const candidates = toArray(root.querySelectorAll(selector)); candidates.forEach(markAsVisited); return candidates; @@ -410,12 +472,13 @@ export function apply(ampdoc, element) { export function runCandidates(ampdoc, candidates) { return candidates.map(candidate => whenLoaded(candidate).then(() => { - if (!Criteria.meetsAll(candidate)) { - dev().info(TAG, 'discarded', candidate); - return; - } - dev().info(TAG, 'apply', candidate); - return apply(ampdoc, candidate); + return Criteria.meetsAll(candidate).then(meetsAll => { + if (!meetsAll) { + return; + } + dev().info(TAG, 'apply', candidate); + return apply(ampdoc, candidate); + }); }, NOOP)); } diff --git a/extensions/amp-auto-lightbox/0.1/carousel-criteria.js b/extensions/amp-auto-lightbox/0.1/carousel-criteria.js new file mode 100644 index 0000000000000..8bf6b7f749357 --- /dev/null +++ b/extensions/amp-auto-lightbox/0.1/carousel-criteria.js @@ -0,0 +1,118 @@ +/** + * Copyright 2019 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 {devAssert} from '../../../src/log'; +import {isActionableByTap} from '../../../src/auto-lightbox'; +import {isExperimentOn} from '../../../src/experiments'; +import {iterateCursor} from '../../../src/dom'; +import {resolveFalse, resolveTrue} from './utils/promise'; +import {toWin} from '../../../src/types'; +import {tryResolve} from '../../../src/utils/promise'; + +const MIN_IMG_SLIDE_AREA_RATIO = 0.5; + +export class CarouselCriteria { + /** + * @param {!Element} element + * @return {!Promise} + */ + static meetsAll(element) { + const win = toWin(element.ownerDocument.defaultView); + + if (!isExperimentOn(win, 'amp-auto-lightbox-carousel')) { + return resolveFalse(); + } + + const slides = element.querySelectorAll('.amp-carousel-slide'); + const images = element.querySelectorAll('amp-img'); + + if (images.length < 1) { + return resolveFalse(); + } + + if (slides.length != images.length) { + return resolveFalse(); + } + + let promise = resolveTrue(); + + iterateCursor(slides, slide => { + promise = promise.then(previousWasAccepted => { + if (!previousWasAccepted) { + return false; + } + return SlideCriteria.meetsAll(slide); + }); + }); + + return promise; + } +} + +class SlideCriteria { + + /** + * @param {!Element} element + * @return {!Promise} + */ + static meetsAll(element) { + if (element.tagName == 'AMP-IMG') { + return tryResolve(() => !isActionableByTap(element)); + } + + const img = element.querySelector('amp-img'); + if (!img) { + return resolveFalse(); + } + + const slideMeetsSizingPromise = + SlideCriteria.meetsSizingCriteria(img, element); + + return slideMeetsSizingPromise.then(slideMeetsSizingCriteria => { + if (!slideMeetsSizingCriteria) { + return false; + } + return !isActionableByTap(element); + }); + } + + /** + * @param {!AmpElement} img + * @param {!Element} slide + * @return {!Promise} + */ + static meetsSizingCriteria(img, slide) { + devAssert(img.tagName == 'AMP-IMG'); + + return img.getImpl().then(impl => new Promise(resolve => { + impl.measureElement(() => { + const { + width: imgWidth, + height: imgHeight, + } = img.getLayoutBox(); + + const { + width: slideWidth, + height: slideHeight, + } = slide./*OK*/getBoundingClientRect(); + + const imgArea = imgWidth * imgHeight; + const slideArea = slideWidth * slideHeight; + + resolve((imgArea / slideArea) >= MIN_IMG_SLIDE_AREA_RATIO); + }); + })); + } +} diff --git a/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js b/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js index 63eda6376effa..fbd9e89f7b019 100644 --- a/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js +++ b/extensions/amp-auto-lightbox/0.1/test/test-amp-auto-lightbox.js @@ -67,7 +67,8 @@ describes.realWin(TAG, { } const stubAllCriteriaMet = () => env.sandbox.stub(Criteria, 'meetsAll'); - const mockAllCriteriaMet = isMet => stubAllCriteriaMet().returns(isMet); + const mockAllCriteriaMet = isMet => + stubAllCriteriaMet().returns(tryResolve(() => isMet)); function mockCandidates(candidates) { env.sandbox.stub(Scanner, 'getCandidates').returns(candidates); @@ -434,9 +435,9 @@ describes.realWin(TAG, { const allCriteriaMet = stubAllCriteriaMet(); - allCriteriaMet.withArgs(matchEquals(a)).returns(true); - allCriteriaMet.withArgs(matchEquals(b)).returns(false); - allCriteriaMet.withArgs(matchEquals(c)).returns(true); + allCriteriaMet.withArgs(matchEquals(a)).returns(tryResolve(() => true)); + allCriteriaMet.withArgs(matchEquals(b)).returns(tryResolve(() => false)); + allCriteriaMet.withArgs(matchEquals(c)).returns(tryResolve(() => true)); mockCandidates([a, b, c]); mockIsProxyOrigin(true); diff --git a/extensions/amp-auto-lightbox/0.1/test/test-carousel-criteria.js b/extensions/amp-auto-lightbox/0.1/test/test-carousel-criteria.js new file mode 100644 index 0000000000000..f468a4f2a2621 --- /dev/null +++ b/extensions/amp-auto-lightbox/0.1/test/test-carousel-criteria.js @@ -0,0 +1,136 @@ +/** + * Copyright 2019 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 {CarouselCriteria} from '../carousel-criteria'; +import {htmlFor} from '../../../../src/static-template'; +import {toggleExperiment} from '../../../../src/experiments'; + + +const TAG = 'amp-auto-lightbox'; + + +describes.realWin(TAG, { + amp: { + amp: true, + ampdoc: 'single', + experiments: ['amp-auto-lightbox-carousel'], + }, +}, env => { + + let html; + + function buildCarousel(slides) { + const element = html``; + slides.forEach(slide => { + slide.classList.add('amp-carousel-slide'); + element.appendChild(slide); + }); + env.win.document.body.appendChild(element); + return element; + } + + beforeEach(() => { + html = htmlFor(env.win.document.body); + toggleExperiment(env.win, 'amp-auto-lightbox-carousel', true); + }); + + it('rejects carousels without ', () => { + const root = buildCarousel([ + html`
Slide 1
`, + html`
Slide 2
`, + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.false; + }); + + it('rejects carousels with but non-image slides', () => { + const root = buildCarousel([ + html``, + html``, + html`
Slide
`, + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.false; + }); + + it('accepts carousels with only ', () => { + const root = buildCarousel([ + html``, + html``, + html``, + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.true; + }); + + it('accepts carousels with only (nested)', () => { + const root = buildCarousel([ + html`
`, + html`
`, + html`
`, + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.true; + }); + + it('accepts carousels with in every slide (mixed)', () => { + const root = buildCarousel([ + html`
Hello world!
`, + html``, + html`
Hola
`, + html`

My Image

`, + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.true; + }); + + it('rejects deep trees with only ', () => { + const deep = html`
+ +
`; + + const root = buildCarousel([ + deep, + deep.cloneNode(/* deep */ true), + deep.cloneNode(/* deep */ true), + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.false; + }); + + it('rejects wide trees with only ', () => { + const wide = html`
+ +
+
+
+
+
+
+
+
+
`; + + const root = buildCarousel([ + wide, + wide.cloneNode(/* deep */ true), + wide.cloneNode(/* deep */ true), + ]); + + expect(CarouselCriteria.meetsAll(root)).to.eventually.be.false; + }); + +}); diff --git a/extensions/amp-auto-lightbox/0.1/utils/promise.js b/extensions/amp-auto-lightbox/0.1/utils/promise.js new file mode 100644 index 0000000000000..3b26513d4a11d --- /dev/null +++ b/extensions/amp-auto-lightbox/0.1/utils/promise.js @@ -0,0 +1,19 @@ +/** + * Copyright 2019 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 {tryResolve} from '../../../../src/utils/promise'; + +export const resolveFalse = () => tryResolve(() => false); +export const resolveTrue = () => tryResolve(() => true); diff --git a/extensions/amp-lightbox-gallery/0.1/service/lightbox-manager-impl.js b/extensions/amp-lightbox-gallery/0.1/service/lightbox-manager-impl.js index 511bbf1a5fb56..b20498c06766f 100644 --- a/extensions/amp-lightbox-gallery/0.1/service/lightbox-manager-impl.js +++ b/extensions/amp-lightbox-gallery/0.1/service/lightbox-manager-impl.js @@ -15,7 +15,10 @@ */ import {AmpEvents} from '../../../../src/amp-events'; -import {AutoLightboxEvents} from '../../../../src/auto-lightbox'; +import { + AutoLightboxEvents, + isActionableByTap, +} from '../../../../src/auto-lightbox'; import {CommonSignals} from '../../../../src/common-signals'; import { LIGHTBOX_THUMBNAIL_AD, @@ -53,6 +56,25 @@ const CAROUSEL_TAG = 'AMP-CAROUSEL'; const FIGURE_TAG = 'FIGURE'; const SLIDE_SELECTOR = '.amp-carousel-slide, .i-amphtml-carousel-slotted'; +/** + * @param {!Element} slide + * @return {!Element} + */ +function getBaseElementForSlide(slide) { + const tagName = slide.tagName.toUpperCase(); + if (tagName == 'AMP-IMG' || tagName == 'FIGURE') { + return slide; + } + const figure = slide.querySelector('figure'); + if (figure) { + return figure; + } + const allImages = slide.querySelectorAll('amp-img'); + userAssert(allImages.length == 1, + 'Found more than one images or none in slide!'); + return dev().assertElement(allImages[0]); +} + /** @typedef {{ * srcset: ?../../../../src/srcset.Srcset, * placeholderSrc: string, @@ -136,26 +158,6 @@ export class LightboxManager { return this.scanPromise_; } - /** - * Decides whether an already lightboxable element should automatically get - * a tap handler to open in the lightbox. - * @param {!Element} element - * @return {boolean} - */ - meetsHeuristicsForTap_(element) { - devAssert(element); - devAssert(element.hasAttribute('lightbox')); - - if (!ELIGIBLE_TAP_TAGS[element.tagName]) { - return false; - } - const actions = Services.actionServiceForDoc(element); - if (actions.hasResolvableAction(element, 'tap')) { - return false; - } - return true; - } - /** * Scans the document for lightboxable elements and updates `this.elements_` * accordingly. @@ -188,20 +190,24 @@ export class LightboxManager { */ processLightboxCarousel_(carousel) { const lightboxGroupId = carousel.getAttribute('lightbox') || - 'carousel' + (carousel.getAttribute('id') || this.counter_++); + `carousel${carousel.getAttribute('id') || this.counter_++}`; + this.getSlidesFromCarousel_(carousel).then(slides => { slides.forEach(slide => { - const shouldExcludeSlide = slide.hasAttribute('lightbox-exclude') - || (slide.hasAttribute('lightbox') + const shouldExcludeSlide = + slide.hasAttribute('lightbox-exclude') || ( + slide.hasAttribute('lightbox') && slide.getAttribute('lightbox') !== lightboxGroupId); - if (!shouldExcludeSlide) { - if (this.seen_.includes(slide)) { - return; - } - slide.setAttribute('lightbox', lightboxGroupId); - this.seen_.push(slide); - this.processBaseLightboxElement_(slide, lightboxGroupId); + if (shouldExcludeSlide) { + return; } + const baseElement = getBaseElementForSlide(slide); + if (this.seen_.includes(baseElement)) { + return; + } + baseElement.setAttribute('lightbox', lightboxGroupId); + this.seen_.push(baseElement); + this.processBaseLightboxElement_(baseElement, lightboxGroupId); }); }); } @@ -253,9 +259,8 @@ export class LightboxManager { lightboxGroupId); if (!unwrappedFigureElement) { return; - } else { - element = unwrappedFigureElement; } + element = unwrappedFigureElement; } userAssert(this.baseElementIsSupported_(element), @@ -266,7 +271,7 @@ export class LightboxManager { } this.lightboxGroups_[lightboxGroupId].push(dev().assertElement(element)); - if (!this.meetsHeuristicsForTap_(element)) { + if (isActionableByTap(element)) { return; } const gallery = elementByTag(this.ampdoc_.getRootNode(), GALLERY_TAG); @@ -298,7 +303,7 @@ export class LightboxManager { /** * Get the description for single lightboxed item. * @param {!Element} element - * @return {string|null} + * @return {?string} */ getDescription(element) { // If the element in question is the descendant of a figure element @@ -373,7 +378,7 @@ export class LightboxManager { /** * Get thumbnail srcset for single element. * @param {!Element} element - * @return {!../../../../src/srcset.Srcset|null} + * @return {?../../../../src/srcset.Srcset} * @private */ getThumbnailSrcset_(element) { @@ -390,29 +395,28 @@ export class LightboxManager { /** * Get the srcset for the user-specified placeholder for each element * @param {!Element} element - * @return {!../../../../src/srcset.Srcset|null} + * @return {?../../../../src/srcset.Srcset} * @private */ getUserPlaceholderSrcset_(element) { if (element.tagName == 'AMP-IMG') { return srcsetFromElement(element); - } else if (element.tagName == 'AMP-VIDEO') { + } + if (element.tagName == 'AMP-VIDEO') { return this.getThumbnailSrcsetForVideo_(element); // TODO: process placeholder logic for other components as added - } else { - const placeholder = childElementByAttr(element, 'placeholder'); - if (placeholder) { - return this.getUserPlaceholderSrcset_(placeholder); - } else { - return null; - } } + const placeholder = childElementByAttr(element, 'placeholder'); + if (placeholder) { + return this.getUserPlaceholderSrcset_(placeholder); + } + return null; } /** * Given an amp video, returns the thumbnail srcset. * @param {!Element} ampVideo - * @return {!../../../../src/srcset.Srcset|null} + * @return {?../../../../src/srcset.Srcset} */ getThumbnailSrcsetForVideo_(ampVideo) { const poster = ampVideo.getAttribute('poster'); diff --git a/src/auto-lightbox.js b/src/auto-lightbox.js index c0199e83253cf..673b33c4b4c22 100644 --- a/src/auto-lightbox.js +++ b/src/auto-lightbox.js @@ -16,6 +16,7 @@ import {ChunkPriority, chunk} from './chunk'; import {Services} from './services'; +import {dev} from './log'; import {isExperimentOn} from './experiments'; @@ -40,3 +41,33 @@ export function installAutoLightboxExtension(ampdoc) { .installExtensionForDoc(ampdoc, 'amp-auto-lightbox'); }, ChunkPriority.LOW); } + + +/** + * @param {!Element} element + * @return {boolean} + */ +export function isActionableByTap(element) { + if (element.tagName.toLowerCase() == 'a' && element.hasAttribute('href')) { + return true; + } + if (element.querySelector('a[href]')) { + return true; + } + const action = Services.actionServiceForDoc(element); + const hasTapAction = action.hasResolvableAction(element, 'tap', + dev().assertElement(element.parentElement)); + if (hasTapAction) { + return true; + } + const actionables = element.querySelectorAll('[on]'); + for (let i = 0; i < actionables.length; i++) { + const actionable = actionables[i]; + const hasTapAction = action.hasResolvableAction(actionable, 'tap', + dev().assertElement(actionable.parentElement)); + if (hasTapAction) { + return true; + } + } + return false; +} diff --git a/test/manual/auto-lightbox/mixed.html b/test/manual/auto-lightbox/mixed.html new file mode 100644 index 0000000000000..b9c75dda85166 --- /dev/null +++ b/test/manual/auto-lightbox/mixed.html @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + +
+

Applies

+

No tap action and should cover 25% of viewport.

+
+ +
+ +

Does not apply

+

Wrapped in hyperlink.

+ + + + +

Does not apply

+

Too small.

+
+ +
+ +

Applies

+

Carousel only has images.

+
+ + + + + + + +
+ +

Does not apply

+

Slide has a tap action.

+
+ + + + + + + +
+ +

Applies

+

Carousel only has images and text.

+ +
+ +
Lorem ipsum
+
+
+ +
Lorem ipsum
+
+
+ +
Lorem ipsum
+
+
+ +
Lorem ipsum
+
+
+ +
Lorem ipsum
+
+
+ +

Does not apply

+

Carousel only has images and text, but images are too small.

+ +
+ +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium eros a sem gravida, quis tempor sem luctus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.
+
+
+ +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium eros a sem gravida, quis tempor sem luctus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.
+
+
+ +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium eros a sem gravida, quis tempor sem luctus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.
+
+
+ +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium eros a sem gravida, quis tempor sem luctus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.
+
+
+ +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus pretium eros a sem gravida, quis tempor sem luctus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.
+
+
+ +

Applies

+

Loaded dynamically.

+ + + +
+ + I do nothing. Click me to close. + + + diff --git a/tools/experiments/experiments.js b/tools/experiments/experiments.js index bdbffc2519a9b..c5003d2e3cfe3 100644 --- a/tools/experiments/experiments.js +++ b/tools/experiments/experiments.js @@ -395,6 +395,12 @@ const EXPERIMENTS = [ spec: 'https://github.com/ampproject/amphtml/issues/20395', cleanupIssue: 'https://github.com/ampproject/amphtml/issues/20394', }, + { + id: 'amp-auto-lightbox-carousel', + name: 'Automatically detects carousels to group in a lightbox.', + spec: 'https://github.com/ampproject/amphtml/issues/20395', + cleanupIssue: 'https://github.com/ampproject/amphtml/issues/20394', + }, { id: 'fixed-elements-in-lightbox', name: 'Transfer fixed elements in lightboxes for smooth iOS scrolling',