diff --git a/extensions/amp-next-page/1.0/amp-next-page.css b/extensions/amp-next-page/1.0/amp-next-page.css index 62e7f975c622..bef1044173d0 100644 --- a/extensions/amp-next-page/1.0/amp-next-page.css +++ b/extensions/amp-next-page/1.0/amp-next-page.css @@ -27,25 +27,6 @@ overflow: visible; } -.amp-next-page-default-separator { - position: relative; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - text-align: center; - font-family: 'Roboto', Helvetica, Arial, sans-serif; - font-weight: 400; - font-size: 14px; - width: 160px; - height: 38px; - margin: auto; - border-radius: 32px; - box-shadow: 0px 6px 17px 0px rgba(0, 0, 0, 0.09); - background: black; - color: white; -} - /** Fixes collapsing margins when switching between the visible and hidden states */ .i-amphtml-next-page-document:before, .i-amphtml-next-page-document:after { @@ -56,3 +37,43 @@ .i-amphtml-next-page-placeholder { background: #eee; } + +/** Default separator styles */ +.amp-next-page-separator { + border-bottom: 1px solid rgba(0, 0, 0, 0.12); +} + +/** Default footer styles */ +.amp-next-page-footer { + border-top: 1px solid rgba(0, 0, 0, 0.1); + margin-top: 16px; +} + +.amp-next-page-footer-content { + display: flex; + flex-direction: column; +} + +.amp-next-page-footer-article { + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + display: block; + overflow: auto; + padding: 10px 0; + text-decoration: none; +} + +.amp-next-page-footer-image { + width: 72px; + height: 72px; + float: left; + margin: 0 10px; + background-size: cover; + background-position: center; +} + +.amp-next-page-footer-title { + position: relative; + margin: 5px 30px 5px 0; + color: #3c4043; + font-size: 17px; +} diff --git a/extensions/amp-next-page/1.0/page.js b/extensions/amp-next-page/1.0/page.js index 8de226de523b..a8d4a5cd89e8 100644 --- a/extensions/amp-next-page/1.0/page.js +++ b/extensions/amp-next-page/1.0/page.js @@ -127,37 +127,44 @@ export class Page { } /** + * @param {!PageState} state * @return {boolean} */ - isVisible() { - return this.visibilityState_ === VisibilityState.VISIBLE; + is(state) { + return this.state_ === state; } /** * @return {boolean} */ - isPaused() { - return this.state_ === PageState.PAUSED; + isLoaded() { + return ( + this.state_ === PageState.LOADED || + this.state_ === PageState.INSERTED || + this.state_ === PageState.PAUSED + ); } /** - * @return {!VisibilityState} - * @visibleForTesting + * @return {boolean} */ - getVisibilityState() { - return this.visibilityState_; + isVisible() { + return this.isLoaded() && this.visibilityState_ === VisibilityState.VISIBLE; } /** * @param {VisibilityState} visibilityState */ setVisibility(visibilityState) { - if (visibilityState == this.visibilityState_) { + if (!this.isLoaded() || visibilityState == this.visibilityState_) { return; } //Reload the page if necessary - if (this.isPaused() && visibilityState === VisibilityState.VISIBLE) { + if ( + this.is(PageState.PAUSED) && + visibilityState === VisibilityState.VISIBLE + ) { this.resume(); } @@ -189,7 +196,7 @@ export class Page { return Promise.resolve(); } return this.shadowDoc_.close().then(() => { - this.manager_.closeDocument(this /** page */).then(() => { + return this.manager_.closeDocument(this /** page */).then(() => { this.shadowDoc_ = null; this.visibilityState_ = VisibilityState.HIDDEN; this.state_ = PageState.PAUSED; @@ -200,25 +207,10 @@ export class Page { /** * Removes the placeholder and re-renders the page after its shadow * root has been removed + * @return {!Promise} */ resume() { - this.attach_(); - } - - /** - * @return {boolean} - */ - isFetching() { - return this.state_ === PageState.FETCHING; - } - - /** - * @return {boolean} - */ - isLoaded() { - return ( - this.state_ === PageState.LOADED || this.state_ === PageState.INSERTED - ); + return this.attach_(); } /** @@ -244,40 +236,40 @@ export class Page { .then(content => { this.state_ = PageState.LOADED; this.content_ = content; - return this.manager_.createDocumentContainerForPage(this /** page */); - }) - .then(container => { - this.container_ = container; + this.container_ = this.manager_.createDocumentContainerForPage( + this /** page */ + ); // TODO(wassgha): To further optimize, this should ideally // be parsed from the service worker instead of stored in memory - this.attach_(); + return this.attach_(); }) .catch(() => { this.state_ = PageState.FAILED; - // TOOD(wassgha): Silently skips this page, should we re-try or show an error state? - this.manager_.setLastFetchedPage(this); }); } /** * Inserts the fetched (or cached) HTML as the document's content + * @return {!Promise} */ attach_() { - const shadowDoc = this.manager_.attachDocumentToPage( - this /** page */, - /** @type {!Document} */ (devAssert(this.content_)), - this.isPaused() /** force */ - ); - - if (shadowDoc) { - this.shadowDoc_ = shadowDoc; - if (!this.isPaused()) { - this.manager_.setLastFetchedPage(this); - } - this.state_ = PageState.INSERTED; - } else { - this.state_ = PageState.FAILED; - } + return this.manager_ + .attachDocumentToPage( + this /** page */, + /** @type {!Document} */ (devAssert(this.content_)), + this.is(PageState.PAUSED) /** force */ + ) + .then(shadowDoc => { + if (!shadowDoc) { + this.state_ = PageState.FAILED; + return; + } + this.state_ = PageState.INSERTED; + this.shadowDoc_ = shadowDoc; + if (!this.is(PageState.PAUSED)) { + this.manager_.setLastFetchedPage(this); + } + }); } } diff --git a/extensions/amp-next-page/1.0/service.js b/extensions/amp-next-page/1.0/service.js index 52ff33b9cb2a..7528d51b2b93 100644 --- a/extensions/amp-next-page/1.0/service.js +++ b/extensions/amp-next-page/1.0/service.js @@ -26,6 +26,7 @@ import {VisibilityState} from '../../../src/visibility-state'; import { childElementByAttr, childElementsByTag, + insertAfterOrAtStart, isJsonScriptTag, removeChildren, removeElement, @@ -33,7 +34,7 @@ import { } from '../../../src/dom'; import {dev, devAssert, user, userAssert} from '../../../src/log'; import {escapeCssSelectorIdent} from '../../../src/css'; -import {htmlFor} from '../../../src/static-template'; +import {htmlFor, htmlRefs} from '../../../src/static-template'; import {installStylesForDoc} from '../../../src/style-installer'; import { parseFavicon, @@ -57,6 +58,8 @@ const DOC_CONTAINER_CLASS = 'i-amphtml-next-page-document-container'; const SHADOW_ROOT_CLASS = 'i-amphtml-next-page-shadow-root'; const PLACEHOLDER_CLASS = 'i-amphtml-next-page-placeholder'; +const ASYNC_NOOP = () => Promise.resolve(); + /** @enum */ export const Direction = {UP: 1, DOWN: -1}; @@ -71,6 +74,9 @@ export class NextPageService { /** @private @const {!Window} */ this.win_ = ampdoc.win; + /** @private @const {!Document} */ + this.doc_ = this.win_.document; + /** * @private * @const {!../../../src/service/viewport/viewport-interface.ViewportInterface} @@ -90,7 +96,13 @@ export class NextPageService { this.separator_ = null; /** @private {?Element} */ - this.moreBox_ = null; + this.footer_ = null; + + /** @private {function():!Promise} */ + this.refreshFooter_ = ASYNC_NOOP; + + /** @private {boolean} */ + this.finished_ = false; /** @private {?AmpElement} element */ this.host_ = null; @@ -160,11 +172,11 @@ export class NextPageService { // Get the separator and more box (and remove the provided elements in the process) this.separator_ = this.getSeparatorElement_(element); - this.moreBox_ = this.getMoreBoxElement_(element); + this.footer_ = this.getFooterElement_(element); // Create a reference to the host page this.hostPage_ = this.createHostPage(); - this.toggleHiddenAndReplaceableElements(this.win_.document); + this.toggleHiddenAndReplaceableElements(this.doc_); this.history_ = Services.historyForDoc(this.ampdoc_); this.initializeHistory(); @@ -177,14 +189,14 @@ export class NextPageService { ); this.visibilityObserver_ = new VisibilityObserver(this.ampdoc_); - // Have the suggestion box be always visible - this.host_.appendChild(this.moreBox_); - if (!this.pages_) { this.pages_ = [this.hostPage_]; this.setLastFetchedPage(this.hostPage_); } + // Have the footer be always visible + insertAfterOrAtStart(this.host_, this.footer_, null /** after */); + this.nextSrc_ = this.getHost_().getAttribute('src'); this.hasDeepParsing_ = this.getHost_().hasAttribute('deep-parsing') || !this.nextSrc_; @@ -210,6 +222,9 @@ export class NextPageService { */ updateScroll_() { this.updateScrollDirection_(); + if (this.finished_) { + return; + } this.readyPromise_.then(() => { this.maybeFetchNext(); }); @@ -220,8 +235,10 @@ export class NextPageService { * @return {!Promise} */ maybeFetchNext(force = false) { + devAssert(!this.finished_); + // If a page is already queued to be fetched, wait for it - if (this.pages_.some(page => page.isFetching())) { + if (this.pages_.some(page => page.is(PageState.FETCHING))) { return Promise.resolve(); } // If we're still too far from the bottom, early return @@ -231,22 +248,32 @@ export class NextPageService { const pageCount = this.pages_.length; const nextPage = this.pages_[this.getPageIndex_(this.lastFetchedPage_) + 1]; - if (!nextPage) { - return ( - this.getRemotePages_() - .then(pages => this.queuePages_(pages)) - // Queuing pages can result in no new pages (in case the server - // returned an empty array or the suggestions already exist in the queue) - .then(() => { - if (this.pages_.length <= pageCount) { - // Remote server did not return any new pages - return Promise.resolve(); - } - return this.maybeFetchNext(true /** force */); - }) - ); + if (nextPage) { + return nextPage.fetch().then(() => { + if (nextPage.is(PageState.FAILED)) { + // Silently skip this page and get the footer ready in case + // this page is the last one + this.setLastFetchedPage(nextPage); + return this.refreshFooter_(); + } + }); } - return nextPage.fetch(); + + // Attempt to get more pages + return ( + this.getRemotePages_() + .then(pages => this.queuePages_(pages)) + // Queuing pages can result in no new pages (in case the server + // returned an empty array or the suggestions already exist in the queue) + .then(() => { + if (this.pages_.length <= pageCount) { + // Remote server did not return any new pages, update the footer and lock the state + this.finished_ = true; + return this.refreshFooter_(); + } + return this.maybeFetchNext(true /** force */); + }) + ); } /** @@ -256,19 +283,16 @@ export class NextPageService { updateVisibility() { this.pages_.forEach((page, index) => { if ( - page.relativePos === ViewportRelativePos.INSIDE_VIEWPORT || - page.relativePos === ViewportRelativePos.CONTAINS_VIEWPORT + page.relativePos === ViewportRelativePos.OUTSIDE_VIEWPORT && + page.isVisible() ) { + page.setVisibility(VisibilityState.HIDDEN); + } else { if (!page.isVisible()) { page.setVisibility(VisibilityState.VISIBLE); } this.hidePreviousPages_(index); this.resumePausedPages_(index); - } else if ( - page.relativePos === ViewportRelativePos.OUTSIDE_VIEWPORT && - page.isVisible() - ) { - page.setVisibility(VisibilityState.HIDDEN); } }); @@ -319,13 +343,11 @@ export class NextPageService { return Promise.all( previousPages .filter(page => { - const shouldHide = - page.relativePos === ViewportRelativePos.LEAVING_VIEWPORT || - page.relativePos === ViewportRelativePos.OUTSIDE_VIEWPORT; - return shouldHide; + // Pages that are outside of the viewport should be hidden + return page.relativePos === ViewportRelativePos.OUTSIDE_VIEWPORT; }) .map((page, away) => { - // Hide all pages that are in the viewport + // Hide all pages whose visibility state have changed to hidden if (page.isVisible()) { page.setVisibility(VisibilityState.HIDDEN); } @@ -343,6 +365,7 @@ export class NextPageService { * ready to become visible soon * @param {number} index index of the page to start at * @param {number=} pausePageCountForTesting + * @return {!Promise} * @private */ resumePausedPages_(index, pausePageCountForTesting) { @@ -360,9 +383,9 @@ export class NextPageService { Math.max(0, index - pausePageCount - 1), Math.min(this.pages_.length, index + pausePageCount + 1) ) - .filter(page => page.isPaused()); + .filter(page => page.is(PageState.PAUSED)); - nearViewportPages.forEach(page => page.resume()); + return Promise.all(nearViewportPages.map(page => page.resume())); } /** @@ -383,7 +406,7 @@ export class NextPageService { return; } const {title, url} = page; - this.win_.document.title = title; + this.doc_.title = title; this.history_.replace({title, url}); } @@ -401,21 +424,23 @@ export class NextPageService { * @return {!Page} */ createHostPage() { - const doc = this.win_.document; - const {title, location} = doc; + const {title, location} = this.doc_; const {href: url} = location; const image = - parseSchemaImage(doc) || parseOgImage(doc) || parseFavicon(doc) || ''; + parseSchemaImage(this.doc_) || + parseOgImage(this.doc_) || + parseFavicon(this.doc_) || + ''; return new HostPage( this, { url, - title, + title: title || '', image, }, PageState.INSERTED /** initState */, VisibilityState.VISIBLE /** initVisibility */, - doc /** initDoc */ + this.doc_ /** initDoc */ ); } @@ -423,23 +448,15 @@ export class NextPageService { * Create a container element for the document and insert it into * the amp-next-page element * @param {!Page} page - * @return {!Promise} + * @return {!Element} */ createDocumentContainerForPage(page) { - const container = this.win_.document.createElement('div'); + const container = this.doc_.createElement('div'); container.classList.add(DOC_CONTAINER_CLASS); - this.host_.insertBefore(container, dev().assertElement(this.moreBox_)); - - // Insert the separator - const separatorInstance = this.separator_.cloneNode(true); - container.appendChild(separatorInstance); - const separatorRenderPromise = this.maybeRenderSeparatorTemplate_( - separatorInstance, - page - ); + this.host_.insertBefore(container, dev().assertElement(this.footer_)); // Insert the document - const shadowRoot = this.win_.document.createElement('div'); + const shadowRoot = this.doc_.createElement('div'); shadowRoot.classList.add(SHADOW_ROOT_CLASS); container.appendChild(shadowRoot); @@ -453,7 +470,7 @@ export class NextPageService { } ); - return separatorRenderPromise.then(() => container); + return container; } /** @@ -462,13 +479,13 @@ export class NextPageService { * @param {!Page} page * @param {!Document} content * @param {boolean=} force - * @return {?../../../src/runtime.ShadowDoc} + * @return {!Promise} */ attachDocumentToPage(page, content, force = false) { // If the user already scrolled to the bottom, prevent rendering if (this.getViewportsAway_() < NEAR_BOTTOM_VIEWPORT_COUNT && !force) { // TODO(wassgha): Append a "load next article" button? - return null; + return Promise.resolve(); } const container = dev().assertElement(page.container); @@ -481,7 +498,7 @@ export class NextPageService { // will need to replace placeholder // TODO(wassgha) This wouldn't be needed once we can resume a ShadowDoc if (!shadowRoot) { - devAssert(page.isPaused()); + devAssert(page.is(PageState.PAUSED)); const placeholder = dev().assertElement( scopedQuerySelector( container, @@ -490,7 +507,7 @@ export class NextPageService { 'Paused page does not have a placeholder' ); - shadowRoot = this.win_.document.createElement('div'); + shadowRoot = this.doc_.createElement('div'); shadowRoot.classList.add(SHADOW_ROOT_CLASS); container.replaceChild(shadowRoot, placeholder); @@ -516,10 +533,18 @@ export class NextPageService { const body = ampdoc.getBody(); body.classList.add(DOC_CLASS); - return amp; + // Insert the separator + const separatorInstance = this.separator_.cloneNode(true); + insertAfterOrAtStart(container, separatorInstance, null /** after */); + const separatorPromise = this.maybeRenderSeparatorTemplate_( + separatorInstance, + page + ); + + return separatorPromise.then(() => amp); } catch (e) { dev().error(TAG, 'failed to attach shadow document for page', e); - return null; + return Promise.resolve(); } } @@ -530,7 +555,7 @@ export class NextPageService { * @return {!Promise} */ closeDocument(page) { - if (page.isPaused()) { + if (page.is(PageState.PAUSED)) { return Promise.resolve(); } @@ -543,7 +568,7 @@ export class NextPageService { ); // Create a placeholder that gets displayed when the document becomes inactive - const placeholder = this.win_.document.createElement('div'); + const placeholder = this.doc_.createElement('div'); placeholder.classList.add(PLACEHOLDER_CLASS); let docHeight = 0; @@ -676,7 +701,7 @@ export class NextPageService { return response.text(); }) .then(html => { - const doc = this.win_.document.implementation.createHTMLDocument(''); + const doc = this.doc_.implementation.createHTMLDocument(''); doc.open(); doc.write(html); doc.close(); @@ -709,6 +734,9 @@ export class NextPageService { return Promise.resolve(); } return this.queuePages_(pages).then(() => { + // Render the initial footer template with all pages + this.refreshFooter_(); + // Mark the page as ready this.readyResolver_(); }); }); @@ -721,7 +749,7 @@ export class NextPageService { * @return {!Promise} */ queuePages_(pages) { - if (!pages.length) { + if (!pages.length || this.finished_) { return Promise.resolve(); } // Queue the given pages @@ -740,6 +768,7 @@ export class NextPageService { user().error(TAG, 'Failed to queue page due to error:', e); } }); + // To be safe, if the pages were parsed after the user // finished scrolling return this.maybeFetchNext(); @@ -817,8 +846,10 @@ export class NextPageService { const providedSeparator = childElementByAttr(element, 'separator'); if (providedSeparator) { removeElement(providedSeparator); + return providedSeparator; } - return providedSeparator || this.buildDefaultSeparator_(); + // If no separator is provided, we build a default one + return this.buildDefaultSeparator_(); } /** @@ -829,17 +860,16 @@ export class NextPageService { const html = htmlFor(this.getHost_()); return html`
- Next article -
+ > `; } /** * Renders the template inside the separator element using * data from the current article (if a template is present) + * otherwise rehydrates the default separator * * @param {!Element} separator * @param {!Page} page @@ -849,11 +879,13 @@ export class NextPageService { if (!this.templates_.hasTemplate(separator)) { return Promise.resolve(); } + const data = /** @type {!JsonObject} */ ({ title: page.title, url: page.url, image: page.image, }); + return this.templates_ .findAndRenderTemplate(separator, data) .then(rendered => { @@ -869,23 +901,103 @@ export class NextPageService { * @return {!Element} * @private */ - getMoreBoxElement_(element) { - const providedMoreBox = childElementByAttr(element, 'more-box'); - // TODO(wassgha): Use templates (amp-mustache) to render the more box - if (providedMoreBox) { - removeElement(providedMoreBox); + getFooterElement_(element) { + const providedFooter = childElementByAttr(element, 'footer'); + if (providedFooter) { + this.refreshFooter_ = this.templates_.hasTemplate(providedFooter) + ? () => this.renderFooterTemplate_() + : ASYNC_NOOP; + removeElement(providedFooter); + return providedFooter; } - return providedMoreBox || this.buildDefaultMoreBox_(); + // If no footer is provided then we build a default one + this.refreshFooter_ = () => this.refreshDefaultFooter_(); + return this.buildDefaultFooter_(); } /** * @return {!Element} * @private */ - buildDefaultMoreBox_() { - // TODO(wassgha): Better default more box - const moreBox = this.win_.document.createElement('div'); - moreBox.classList.add('amp-next-page-default-more-box'); - return moreBox; + buildDefaultFooter_() { + const html = htmlFor(this.getHost_()); + return html` + + `; + } + + /** + * Renders the template inside the footer element using + * data from the current articles + * + * @return {!Promise} + */ + renderFooterTemplate_() { + const footer = dev().assertElement(this.footer_); + devAssert(this.templates_.hasTemplate(footer)); + + const data = /** @type {!JsonObject} */ ({ + pages: (this.pages_ || []) + .filter(page => !page.isLoaded() && !page.is(PageState.FETCHING)) + .map(page => ({ + title: page.title, + url: page.url, + image: page.image, + })), + }); + + // Re-render templated footer (if needed) + return this.templates_ + .findAndRenderTemplate(footer, data) + .then(rendered => { + return this.mutator_.mutateElement(footer, () => { + removeChildren(dev().assertElement(footer)); + footer.appendChild(rendered); + }); + }); + } + + /** + * Renders the template inside the footer element using + * data from the current articles (if a template is present) + * otherwise rehydrates the default footer element + * + * @return {!Promise} + */ + refreshDefaultFooter_() { + const footer = dev().assertElement(this.footer_); + const data = /** @type {!JsonObject} */ ({ + pages: (this.pages_ || []) + .filter(page => !page.isLoaded() && !page.is(PageState.FETCHING)) + .map(page => ({ + title: page.title, + url: page.url, + image: page.image, + })), + }); + + const html = htmlFor(this.getHost_()); + const content = this.doc_.createElement('div'); + content.classList.add('amp-next-page-footer-content'); + + data['pages'].forEach(page => { + const article = html` + + + + + `; + const {image, title} = htmlRefs(article); + image.src = page.image; + title.textContent = page.title; + article.href = page.url; + + content.appendChild(article); + }); + + return this.mutator_.mutateElement(footer, () => { + removeChildren(dev().assertElement(footer)); + footer.appendChild(content); + }); } } diff --git a/extensions/amp-next-page/1.0/test/test-amp-next-page.js b/extensions/amp-next-page/1.0/test/test-amp-next-page.js index 776aa479f894..3ee46ae4daba 100644 --- a/extensions/amp-next-page/1.0/test/test-amp-next-page.js +++ b/extensions/amp-next-page/1.0/test/test-amp-next-page.js @@ -562,7 +562,7 @@ describes.realWin( }); }); - describe('default separators', () => { + describe('default separators & footers', () => { let element; let service; @@ -579,11 +579,11 @@ describes.realWin( element.parentNode.removeChild(element); }); - it('adds a default separator to for the host page', async () => { + it('adds a default separator to the host page', async () => { await fetchDocuments(service, MOCK_NEXT_PAGE_WITH_RECOMMENDATIONS); expect(service.pages_[1].container.firstElementChild).to.have.class( - 'amp-next-page-default-separator' + 'amp-next-page-separator' ); }); @@ -591,12 +591,18 @@ describes.realWin( await fetchDocuments(service, MOCK_NEXT_PAGE, 2); expect(service.pages_[2].container.firstElementChild).to.have.class( - 'amp-next-page-default-separator' + 'amp-next-page-separator' ); }); + + it('adds a default footer to the host page', async () => { + await fetchDocuments(service, MOCK_NEXT_PAGE_WITH_RECOMMENDATIONS); + + expect(element.lastElementChild).to.have.class('amp-next-page-footer'); + }); }); - describe('custom and templated separators', () => { + describe('custom and templated separators & footers', () => { let element; let service; let html; @@ -697,6 +703,57 @@ describes.realWin( expect(template1.innerText).to.equal('Rendered 1'); expect(template2.innerText).to.equal('Rendered 2'); }); + + it('correctly renders a templated footer', async () => { + const separator = html` +
+ +
+ `; + + element = await getAmpNextPage({ + inlineConfig: VALID_CONFIG, + separator, + }); + + service = Services.nextPageServiceForDoc(doc); + env.sandbox.stub(service, 'getViewportsAway_').returns(0); + const templateRenderStub = env.sandbox + .stub(service.templates_, 'findAndRenderTemplate') + .resolves( + html` + Rendered + ` + ); + + await fetchDocuments(service, MOCK_NEXT_PAGE, '1'); + expect(templateRenderStub).to.have.been.calledWith( + env.sandbox.match.any, + { + pages: [ + { + title: 'Title 1', + url: '', + image: '/examples/img/hero@1x.jpg', + }, + { + title: 'Title 2', + url: 'http://localhost:9876/document2', + image: '/examples/img/hero@1x.jpg', + }, + ], + } + ); + expect(element.lastElementChild.innerText).to.equal('Rendered'); + }); }); } ); diff --git a/extensions/amp-next-page/1.0/visibility-observer.js b/extensions/amp-next-page/1.0/visibility-observer.js index 0536b6d9ad1b..b2877c04af34 100644 --- a/extensions/amp-next-page/1.0/visibility-observer.js +++ b/extensions/amp-next-page/1.0/visibility-observer.js @@ -143,7 +143,6 @@ export class VisibilityObserverEntry { // through being scrolling into/out of the viewport in which case // we don't need to update the visibility this.relativePos_ = ViewportRelativePos.LEAVING_VIEWPORT; - return; } callback(this.relativePos_); diff --git a/test/manual/amp-next-page/1.0/amp-next-page.amp.html b/test/manual/amp-next-page/1.0/amp-next-page.amp.html index 556b39ee868f..dc94fbb25433 100644 --- a/test/manual/amp-next-page/1.0/amp-next-page.amp.html +++ b/test/manual/amp-next-page/1.0/amp-next-page.amp.html @@ -20,20 +20,16 @@ .content { padding: 20px; } - .amp-next-page-default-separator { + .amp-next-page-separator { margin-top: 16px; margin-bottom: 16px; } - .separator-content { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - } - .separator-content amp-img { - display: inline-block; - border-radius: 50%; - margin-right: 6px; + .my-footer { + padding: 24px; + text-align: center; + color: #AAAAAA; + display: block; + font-family: Arial, Helvetica, sans-serif; } @@ -301,17 +297,22 @@

Host page

} ] -
- -
-
- Here are a few more articles ... -
+ + + more footer content here +