diff --git a/extensions/amp-story/1.0/_locales/default.js b/extensions/amp-story/1.0/_locales/default.js index 027078afc810..1bee4146654e 100644 --- a/extensions/amp-story/1.0/_locales/default.js +++ b/extensions/amp-story/1.0/_locales/default.js @@ -60,8 +60,15 @@ export default /** @const {!LocalizedStringBundleDef} */ ({ [LocalizedStringId.AMP_STORY_SYSTEM_LAYER_SHARE_WIDGET_LABEL]: { string: 'Share', }, + [LocalizedStringId.AMP_STORY_WARNING_DESKTOP_HEIGHT_SIZE_TEXT]: { + string: 'Expand the height of your window to view this experience', + }, [LocalizedStringId.AMP_STORY_WARNING_DESKTOP_SIZE_TEXT]: { - string: 'Expand your window to view this experience', + string: 'Expand both the height and width of your window to view this ' + + 'experience', + }, + [LocalizedStringId.AMP_STORY_WARNING_DESKTOP_WIDTH_SIZE_TEXT]: { + string: 'Expand the width of your window to view this experience', }, [LocalizedStringId.AMP_STORY_WARNING_EXPERIMENT_DISABLED_TEXT]: { string: 'You must enable the amp-story experiment to view this content.', diff --git a/extensions/amp-story/1.0/_locales/en.js b/extensions/amp-story/1.0/_locales/en.js index 7e9681b68e57..3ed5f4e18e0e 100644 --- a/extensions/amp-story/1.0/_locales/en.js +++ b/extensions/amp-story/1.0/_locales/en.js @@ -170,11 +170,22 @@ export default /** @const {!LocalizedStringBundleDef} */ ({ description: 'Label in the tooltip text for when a Twitter embed is ' + 'expandable.', }, + [LocalizedStringId.AMP_STORY_WARNING_DESKTOP_HEIGHT_SIZE_TEXT]: { + string: 'Expand the height of your window to view this experience', + description: 'Text for a warning screen that informs the user that ' + + 'stories are only supported in taller browser windows.', + }, [LocalizedStringId.AMP_STORY_WARNING_DESKTOP_SIZE_TEXT]: { - string: 'Expand your window to view this experience', + string: 'Expand both the height and width of your window to view this ' + + 'experience', description: 'Text for a warning screen that informs the user that ' + 'stories are only supported in larger browser windows.', }, + [LocalizedStringId.AMP_STORY_WARNING_DESKTOP_WIDTH_SIZE_TEXT]: { + string: 'Expand the width of your window to view this experience', + description: 'Text for a warning screen that informs the user that ' + + 'stories are only supported in wider browser windows.', + }, [LocalizedStringId.AMP_STORY_WARNING_EXPERIMENT_DISABLED_TEXT]: { string: 'You must enable the amp-story experiment to view this content.', description: 'Text for a warning screen that informs the user that ' + diff --git a/extensions/amp-story/1.0/amp-story-store-service.js b/extensions/amp-story/1.0/amp-story-store-service.js index 7af44512d82e..0c52112f1fc9 100644 --- a/extensions/amp-story/1.0/amp-story-store-service.js +++ b/extensions/amp-story/1.0/amp-story-store-service.js @@ -90,7 +90,6 @@ export let InteractiveComponentDef; * hasSidebarState: boolean, * infoDialogState: boolean, * interactiveEmbeddedComponentState: !InteractiveComponentDef, - * landscapeState: boolean, * mutedState: boolean, * pageAudioState: boolean, * pausedState: boolean, @@ -102,6 +101,7 @@ export let InteractiveComponentDef; * supportedBrowserState: boolean, * systemUiIsVisibleState: boolean, * uiState: !UIType, + * viewportWarningState: boolean, * actionsWhitelist: !Array<{tagOrTarget: string, method: string}>, * consentId: ?string, * currentPageId: string, @@ -130,7 +130,6 @@ export const StateProperty = { HAS_SIDEBAR_STATE: 'hasSidebarState', INFO_DIALOG_STATE: 'infoDialogState', INTERACTIVE_COMPONENT_STATE: 'interactiveEmbeddedComponentState', - LANDSCAPE_STATE: 'landscapeState', MUTED_STATE: 'mutedState', PAGE_HAS_AUDIO_STATE: 'pageAudioState', PAUSED_STATE: 'pausedState', @@ -144,6 +143,7 @@ export const StateProperty = { STORY_HAS_BACKGROUND_AUDIO_STATE: 'storyHasBackgroundAudioState', SYSTEM_UI_IS_VISIBLE_STATE: 'systemUiIsVisibleState', UI_STATE: 'uiState', + VIEWPORT_WARNING_STATE: 'viewportWarningState', // App data. ACTIONS_WHITELIST: 'actionsWhitelist', @@ -167,7 +167,6 @@ export const Action = { TOGGLE_BOOKEND: 'toggleBookend', TOGGLE_INFO_DIALOG: 'toggleInfoDialog', TOGGLE_INTERACTIVE_COMPONENT: 'toggleInteractiveComponent', - TOGGLE_LANDSCAPE: 'toggleLandscape', TOGGLE_MUTED: 'toggleMuted', TOGGLE_PAGE_HAS_AUDIO: 'togglePageHasAudio', TOGGLE_PAUSED: 'togglePaused', @@ -180,6 +179,7 @@ export const Action = { TOGGLE_STORY_HAS_BACKGROUND_AUDIO: 'toggleStoryHasBackgroundAudio', TOGGLE_SYSTEM_UI_IS_VISIBLE: 'toggleSystemUiIsVisible', TOGGLE_UI: 'toggleUi', + TOGGLE_VIEWPORT_WARNING: 'toggleViewportWarning', }; @@ -265,9 +265,6 @@ const actions = (state, action, data) => { case Action.TOGGLE_STORY_HAS_BACKGROUND_AUDIO: return /** @type {!State} */ (Object.assign({}, state, {[StateProperty.STORY_HAS_BACKGROUND_AUDIO_STATE]: !!data})); - case Action.TOGGLE_LANDSCAPE: - return /** @type {!State} */ (Object.assign( - {}, state, {[StateProperty.LANDSCAPE_STATE]: !!data})); // Mutes or unmutes the story media. case Action.TOGGLE_MUTED: return /** @type {!State} */ (Object.assign( @@ -313,6 +310,9 @@ const actions = (state, action, data) => { [StateProperty.DESKTOP_STATE]: data === UIType.DESKTOP_PANELS, [StateProperty.UI_STATE]: data, })); + case Action.TOGGLE_VIEWPORT_WARNING: + return /** @type {!State} */ (Object.assign( + {}, state, {[StateProperty.VIEWPORT_WARNING_STATE]: !!data})); case Action.SET_CONSENT_ID: return /** @type {!State} */ (Object.assign( {}, state, {[StateProperty.CONSENT_ID]: data})); @@ -434,7 +434,6 @@ export class AmpStoryStoreService { [StateProperty.INTERACTIVE_COMPONENT_STATE]: { state: EmbeddedComponentState.HIDDEN, }, - [StateProperty.LANDSCAPE_STATE]: false, [StateProperty.MUTED_STATE]: true, [StateProperty.PAGE_HAS_AUDIO_STATE]: false, [StateProperty.PAUSED_STATE]: false, @@ -446,6 +445,7 @@ export class AmpStoryStoreService { [StateProperty.STORY_HAS_BACKGROUND_AUDIO_STATE]: false, [StateProperty.SYSTEM_UI_IS_VISIBLE_STATE]: true, [StateProperty.UI_STATE]: UIType.MOBILE, + [StateProperty.VIEWPORT_WARNING_STATE]: false, // amp-story only allows actions on a case-by-case basis to preserve UX // behaviors. By default, no actions are allowed. [StateProperty.ACTIONS_WHITELIST]: [], diff --git a/extensions/amp-story/1.0/amp-story-viewport-warning-layer.js b/extensions/amp-story/1.0/amp-story-viewport-warning-layer.js index 85f6b25d821f..e0eb547f6043 100644 --- a/extensions/amp-story/1.0/amp-story-viewport-warning-layer.js +++ b/extensions/amp-story/1.0/amp-story-viewport-warning-layer.js @@ -23,9 +23,10 @@ import { getStoreService, } from './amp-story-store-service'; import {createShadowRootWithStyle} from './utils'; -import {dict} from './../../../src/utils/object'; +import {htmlFor} from '../../../src/static-template'; import {isExperimentOn} from '../../../src/experiments'; -import {renderAsElement} from './simple-template'; +import {listen} from '../../../src/event-helper'; +import {throttle} from '../../../src/utils/rate-limit'; /** @@ -34,69 +35,28 @@ import {renderAsElement} from './simple-template'; */ const LANDSCAPE_OVERLAY_CLASS = 'i-amphtml-story-landscape'; - -/** - * Full viewport layer advising the user to rotate his device. Mobile only. - * @private @const {!./simple-template.ElementDef} - */ -const LANDSCAPE_ORIENTATION_WARNING_TEMPLATE = { - tag: 'div', - attrs: dict({ - 'class': 'i-amphtml-story-no-rotation-overlay ' + - 'i-amphtml-story-system-reset'}), - children: [ - { - tag: 'div', - attrs: dict({'class': 'i-amphtml-overlay-container'}), - children: [ - { - tag: 'div', - attrs: dict({'class': 'i-amphtml-rotate-icon'}), - }, - { - tag: 'div', - attrs: dict({'class': 'i-amphtml-story-overlay-text'}), - localizedStringId: - LocalizedStringId.AMP_STORY_WARNING_LANDSCAPE_ORIENTATION_TEXT, - }, - ], - }, - ], -}; +/** @const {number} */ +const RESIZE_THROTTLE_MS = 300; /** - * Full viewport layer advising the user to expand his window. Only displayed - * for small desktop viewports. - * @private @const {!./simple-template.ElementDef} + * Viewport warning layer template. + * @param {!Element} element + * @return {!Element} */ -const DESKTOP_SIZE_WARNING_TEMPLATE = { - tag: 'div', - attrs: dict({ - 'class': 'i-amphtml-story-no-rotation-overlay ' + - 'i-amphtml-story-system-reset'}), - children: [ - { - tag: 'div', - attrs: dict({'class': 'i-amphtml-overlay-container'}), - children: [ - { - tag: 'div', - attrs: dict({'class': 'i-amphtml-desktop-size-icon'}), - }, - { - tag: 'div', - attrs: dict({'class': 'i-amphtml-story-overlay-text'}), - localizedStringId: - LocalizedStringId.AMP_STORY_WARNING_DESKTOP_SIZE_TEXT, - }, - ], - }, - ], +const getTemplate = element => { + return htmlFor(element)` +
+
+
+
+
+
+ `; }; - /** * Viewport warning layer UI. */ @@ -104,14 +64,29 @@ export class ViewportWarningLayer { /** * @param {!Window} win * @param {!Element} storyElement Element where to append the component + * @param {number} desktopWidthThreshold Threshold in px. + * @param {number} desktopHeightThreshold Threshold in px. */ - constructor(win, storyElement) { + constructor(win, storyElement, desktopWidthThreshold, + desktopHeightThreshold) { /** @private @const {!Window} */ this.win_ = win; + /** @private {number} */ + this.desktopHeightThreshold_ = desktopHeightThreshold; + + /** @private {number} */ + this.desktopWidthThreshold_ = desktopWidthThreshold; + /** @private {boolean} */ this.isBuilt_ = false; + // TODO: at this point the localization service is not registered yet. We + // should refactor the way it is registered it so it works like the store + // and analytics services. + /** @private {?./localization.LocalizationService} */ + this.localizationService_ = null; + /** @private {?Element} */ this.overlayEl_ = null; @@ -124,6 +99,9 @@ export class ViewportWarningLayer { /** @private @const {!Element} */ this.storyElement_ = storyElement; + /** @private {?Function} */ + this.unlistenResizeEvents_ = null; + /** @const @private {!../../../src/service/vsync-impl.Vsync} */ this.vsync_ = Services.vsyncFor(this.win_); @@ -138,14 +116,16 @@ export class ViewportWarningLayer { return; } - const template = this.getViewportWarningOverlayTemplate_(); - if (!template) { + this.overlayEl_ = this.getViewportWarningOverlayTemplate_(); + + if (!this.overlayEl_) { return; } + this.localizationService_ = Services.localizationService(this.win_); + this.isBuilt_ = true; const root = this.win_.document.createElement('div'); - this.overlayEl_ = renderAsElement(this.win_.document, template); createShadowRootWithStyle(root, this.overlayEl_, CSS); @@ -174,22 +154,23 @@ export class ViewportWarningLayer { this.onUIStateUpdate_(uiState); }, true /** callToInitialize */); - this.storeService_.subscribe(StateProperty.LANDSCAPE_STATE, isLandscape => { - this.onLandscapeStateUpdate_(isLandscape); - }, true /** callToInitialize */); + this.storeService_.subscribe( + StateProperty.VIEWPORT_WARNING_STATE, viewportWarningState => { + this.onViewportWarningStateUpdate_(viewportWarningState); + }, true /** callToInitialize */); } /** - * Reacts to the landscape state update, only on mobile. - * @param {boolean} isLandscape + * Reacts to the viewport warning state update, only on mobile. + * @param {boolean} viewportWarningState * @private */ - onLandscapeStateUpdate_(isLandscape) { + onViewportWarningStateUpdate_(viewportWarningState) { const isMobile = this.storeService_.get(StateProperty.UI_STATE) === UIType.MOBILE; // Adds the landscape class if we are mobile landscape. - const shouldShowLandscapeOverlay = isMobile && isLandscape; + const shouldShowLandscapeOverlay = isMobile && viewportWarningState; // Don't build the layer until we need to display it. if (!shouldShowLandscapeOverlay && !this.isBuilt()) { @@ -198,6 +179,18 @@ export class ViewportWarningLayer { this.build(); + // Listen to resize events to update the UI message. + if (viewportWarningState) { + const resizeThrottle = + throttle(this.win_, () => this.onResize_(), RESIZE_THROTTLE_MS); + this.unlistenResizeEvents_ = listen(this.win_, 'resize', resizeThrottle); + } else if (this.unlistenResizeEvents_) { + this.unlistenResizeEvents_(); + this.unlistenResizeEvents_ = null; + } + + this.updateTextContent_(); + this.vsync_.mutate(() => { this.overlayEl_.classList.toggle( LANDSCAPE_OVERLAY_CLASS, shouldShowLandscapeOverlay); @@ -221,18 +214,87 @@ export class ViewportWarningLayer { }); } + /** + * @private + */ + onResize_() { + this.updateTextContent_(); + } + /** * Returns the overlay corresponding to the device currently used. - * @return {?./simple-template.ElementDef} template + * @return {?Element} template * @private */ getViewportWarningOverlayTemplate_() { + const template = getTemplate(this.storyElement_); + const iconEl = template.querySelector('.i-amphtml-story-overlay-icon'); + if (this.platform_.isIos() || this.platform_.isAndroid()) { - return LANDSCAPE_ORIENTATION_WARNING_TEMPLATE; + iconEl.classList.add('i-amphtml-rotate-icon'); + return template; } if (!isExperimentOn(this.win_, 'disable-amp-story-desktop')) { - return DESKTOP_SIZE_WARNING_TEMPLATE; + iconEl.classList.add('i-amphtml-desktop-size-icon'); + return template; + } + + return null; + } + + /** + * Updates the UI message displayed to the user. + * @private + */ + updateTextContent_() { + const textEl = + this.overlayEl_.querySelector('.i-amphtml-story-overlay-text'); + let textContent; + + this.vsync_.run({ + measure: () => { + textContent = this.getTextContent_(); + }, + mutate: () => { + if (!textContent) { + return; + } + + textEl.textContent = textContent; + }, + }); + } + + /** + * Gets the localized message to display, depending on the viewport size. Has + * to run during a measure phase. + * @return {?string} + * @private + */ + getTextContent_() { + if (this.platform_.isIos() || this.platform_.isAndroid()) { + return this.localizationService_.getLocalizedString( + LocalizedStringId.AMP_STORY_WARNING_LANDSCAPE_ORIENTATION_TEXT); + } + + const viewportHeight = this.win_./*OK*/innerHeight; + const viewportWidth = this.win_./*OK*/innerWidth; + + if (viewportHeight < this.desktopHeightThreshold_ && + viewportWidth < this.desktopWidthThreshold_) { + return this.localizationService_.getLocalizedString( + LocalizedStringId.AMP_STORY_WARNING_DESKTOP_SIZE_TEXT); + } + + if (viewportWidth < this.desktopWidthThreshold_) { + return this.localizationService_.getLocalizedString( + LocalizedStringId.AMP_STORY_WARNING_DESKTOP_WIDTH_SIZE_TEXT); + } + + if (viewportHeight < this.desktopHeightThreshold_) { + return this.localizationService_.getLocalizedString( + LocalizedStringId.AMP_STORY_WARNING_DESKTOP_HEIGHT_SIZE_TEXT); } return null; diff --git a/extensions/amp-story/1.0/amp-story.js b/extensions/amp-story/1.0/amp-story.js index fc9e72121f53..63b1c858279d 100644 --- a/extensions/amp-story/1.0/amp-story.js +++ b/extensions/amp-story/1.0/amp-story.js @@ -260,7 +260,8 @@ export class AmpStory extends AMP.BaseElement { this.unsupportedBrowserLayer_ = new UnsupportedBrowserLayer(this.win); /** Instantiates the viewport warning layer. */ - new ViewportWarningLayer(this.win, this.element); + new ViewportWarningLayer(this.win, this.element, DESKTOP_WIDTH_THRESHOLD, + DESKTOP_HEIGHT_THRESHOLD); /** @private {!Array} */ this.pages_ = []; @@ -1449,9 +1450,8 @@ export class AmpStory extends AMP.BaseElement { this.storeService_.dispatch(Action.TOGGLE_UI, uiState); if (uiState !== UIType.MOBILE || this.isLandscapeSupported_()) { - // TODO: Rename the TOGGLE_LANDSCAPE action. (#19670) // Hides the UI that prevents users from using the LANDSCAPE orientation. - this.storeService_.dispatch(Action.TOGGLE_LANDSCAPE, false); + this.storeService_.dispatch(Action.TOGGLE_VIEWPORT_WARNING, false); return; } @@ -1463,10 +1463,10 @@ export class AmpStory extends AMP.BaseElement { state.isLandscape = offsetWidth > offsetHeight; }, mutate: state => { - const landscapeState = - this.storeService_.get(StateProperty.LANDSCAPE_STATE); + const viewportWarningState = + this.storeService_.get(StateProperty.VIEWPORT_WARNING_STATE); - if (landscapeState === state.isLandscape) { + if (viewportWarningState === state.isLandscape) { return; } @@ -1474,11 +1474,11 @@ export class AmpStory extends AMP.BaseElement { this.pausedStateToRestore_ = !!this.storeService_.get(StateProperty.PAUSED_STATE); this.storeService_.dispatch(Action.TOGGLE_PAUSED, true); - this.storeService_.dispatch(Action.TOGGLE_LANDSCAPE, true); + this.storeService_.dispatch(Action.TOGGLE_VIEWPORT_WARNING, true); } else { this.storeService_ .dispatch(Action.TOGGLE_PAUSED, this.pausedStateToRestore_); - this.storeService_.dispatch(Action.TOGGLE_LANDSCAPE, false); + this.storeService_.dispatch(Action.TOGGLE_VIEWPORT_WARNING, false); } }, }, {}); diff --git a/extensions/amp-story/1.0/localization.js b/extensions/amp-story/1.0/localization.js index 625c50331c9d..c60b1eef4912 100644 --- a/extensions/amp-story/1.0/localization.js +++ b/extensions/amp-story/1.0/localization.js @@ -25,7 +25,7 @@ import {parseJson} from '../../../src/json'; * - NOT be reused; to deprecate an ID, comment it out and prefix its key with * the string "DEPRECATED_" * - * Next ID: 37 + * Next ID: 39 * * @const @enum {string} */ @@ -61,7 +61,9 @@ export const LocalizedStringId = { AMP_STORY_SHARING_PROVIDER_NAME_WHATSAPP: '16', AMP_STORY_SYSTEM_LAYER_SHARE_WIDGET_LABEL: '17', AMP_STORY_TOOLTIP_EXPAND_TWEET: '36', + AMP_STORY_WARNING_DESKTOP_HEIGHT_SIZE_TEXT: '37', AMP_STORY_WARNING_DESKTOP_SIZE_TEXT: '18', + AMP_STORY_WARNING_DESKTOP_WIDTH_SIZE_TEXT: '38', AMP_STORY_WARNING_EXPERIMENT_DISABLED_TEXT: '19', AMP_STORY_WARNING_LANDSCAPE_ORIENTATION_TEXT: '20', AMP_STORY_WARNING_UNSUPPORTED_BROWSER_TEXT: '21', diff --git a/extensions/amp-story/1.0/test/test-amp-story-store-service.js b/extensions/amp-story/1.0/test/test-amp-story-store-service.js index bd3ab48abb5e..cc5253289c97 100644 --- a/extensions/amp-story/1.0/test/test-amp-story-store-service.js +++ b/extensions/amp-story/1.0/test/test-amp-story-store-service.js @@ -148,10 +148,10 @@ describes.fakeWin('amp-story-store-service actions', {}, env => { expect(listenerSpy).to.have.been.calledWith(true); }); - it('should toggle the landscape state', () => { + it('should toggle the viewport warning state', () => { const listenerSpy = sandbox.spy(); - storeService.subscribe(StateProperty.LANDSCAPE_STATE, listenerSpy); - storeService.dispatch(Action.TOGGLE_LANDSCAPE, true); + storeService.subscribe(StateProperty.VIEWPORT_WARNING_STATE, listenerSpy); + storeService.dispatch(Action.TOGGLE_VIEWPORT_WARNING, true); expect(listenerSpy).to.have.been.calledOnce; expect(listenerSpy).to.have.been.calledWith(true); });