Skip to content

Commit ede5fa3

Browse files
authored
✨Added play/pause button (ampproject#27815)
* added play/pause button * enable click navigation on pause * added tests for system layer and story * fix amp-story-test * fixed comments from PR * renamed flags, simplified check conditions * finish fixes on tests * updated naming to elementWithPlayable and fix more PR comments * updated test descriptions
1 parent dbc06b2 commit ede5fa3

File tree

7 files changed

+323
-4
lines changed

7 files changed

+323
-4
lines changed

extensions/amp-story/1.0/amp-story-page.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,7 @@ export class AmpStoryPage extends AMP.BaseElement {
509509
? this.maybeFinishAnimations_()
510510
: this.maybeStartAnimations_();
511511
this.checkPageHasAudio_();
512+
this.checkPageHasElementWithPlayback_();
512513
this.renderOpenAttachmentUI_();
513514
this.findAndPrepareEmbeddedComponents_();
514515
this.startMeasuringVideoPerformance_();
@@ -1360,6 +1361,7 @@ export class AmpStoryPage extends AMP.BaseElement {
13601361
return;
13611362
}
13621363

1364+
this.storeService_.dispatch(Action.TOGGLE_PAUSED, false);
13631365
this.switchTo_(pageId, NavigationDirection.PREVIOUS);
13641366
}
13651367

@@ -1382,6 +1384,7 @@ export class AmpStoryPage extends AMP.BaseElement {
13821384
return;
13831385
}
13841386

1387+
this.storeService_.dispatch(Action.TOGGLE_PAUSED, false);
13851388
this.switchTo_(pageId, NavigationDirection.NEXT);
13861389
}
13871390

@@ -1425,6 +1428,22 @@ export class AmpStoryPage extends AMP.BaseElement {
14251428
);
14261429
}
14271430

1431+
/**
1432+
* Checks if the page has elements with playback.
1433+
* @private
1434+
*/
1435+
checkPageHasElementWithPlayback_() {
1436+
const pageHasElementWithPlayback =
1437+
this.isAutoAdvance() ||
1438+
this.element.hasAttribute('background-audio') ||
1439+
this.getAllMedia_().length > 0;
1440+
1441+
this.storeService_.dispatch(
1442+
Action.TOGGLE_PAGE_HAS_ELEMENT_WITH_PLAYBACK,
1443+
pageHasElementWithPlayback
1444+
);
1445+
}
1446+
14281447
/**
14291448
* @private
14301449
*/
@@ -1554,7 +1573,9 @@ export class AmpStoryPage extends AMP.BaseElement {
15541573
this.registerMedia_(mediaPool, videoEl)
15551574
.then(() => this.preloadMedia_(mediaPool, videoEl))
15561575
.then((poolVideoEl) => {
1557-
this.playMedia_(mediaPool, poolVideoEl);
1576+
if (!this.storeService_.get(StateProperty.PAUSED_STATE)) {
1577+
this.playMedia_(mediaPool, poolVideoEl);
1578+
}
15581579
if (!this.storeService_.get(StateProperty.MUTED_STATE)) {
15591580
this.unmuteAllMedia();
15601581
}

extensions/amp-story/1.0/amp-story-store-service.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,14 @@ export let InteractiveComponentDef;
9595
* interactiveEmbeddedComponentState: !InteractiveComponentDef,
9696
* mutedState: boolean,
9797
* pageAudioState: boolean,
98+
* pageHasElementsWithPlaybackState: boolean,
9899
* pausedState: boolean,
99100
* previewState: boolean,
100101
* rtlState: boolean,
101102
* shareMenuState: boolean,
102103
* sidebarState: boolean,
103104
* storyHasAudioState: boolean,
105+
* storyHasPlaybackUiState: boolean,
104106
* storyHasBackgroundAudioState: boolean,
105107
* supportedBrowserState: boolean,
106108
* systemUiIsVisibleState: boolean,
@@ -140,6 +142,7 @@ export const StateProperty = {
140142
INTERACTIVE_COMPONENT_STATE: 'interactiveEmbeddedComponentState',
141143
MUTED_STATE: 'mutedState',
142144
PAGE_HAS_AUDIO_STATE: 'pageAudioState',
145+
PAGE_HAS_ELEMENTS_WITH_PLAYBACK_STATE: 'pageHasElementsWithPlaybackState',
143146
PAUSED_STATE: 'pausedState',
144147
// Story preview state.
145148
PREVIEW_STATE: 'previewState',
@@ -151,6 +154,8 @@ export const StateProperty = {
151154
STORY_HAS_AUDIO_STATE: 'storyHasAudioState',
152155
// amp-story has a `background-audio` attribute.
153156
STORY_HAS_BACKGROUND_AUDIO_STATE: 'storyHasBackgroundAudioState',
157+
// Any page has elements with playback.
158+
STORY_HAS_PLAYBACK_UI_STATE: 'storyHasPlaybackUiState',
154159
SYSTEM_UI_IS_VISIBLE_STATE: 'systemUiIsVisibleState',
155160
UI_STATE: 'uiState',
156161
VIEWPORT_WARNING_STATE: 'viewportWarningState',
@@ -186,13 +191,15 @@ export const Action = {
186191
TOGGLE_INTERACTIVE_COMPONENT: 'toggleInteractiveComponent',
187192
TOGGLE_MUTED: 'toggleMuted',
188193
TOGGLE_PAGE_HAS_AUDIO: 'togglePageHasAudio',
194+
TOGGLE_PAGE_HAS_ELEMENT_WITH_PLAYBACK: 'togglePageHasElementWithPlayblack',
189195
TOGGLE_PAUSED: 'togglePaused',
190196
TOGGLE_RTL: 'toggleRtl',
191197
TOGGLE_SHARE_MENU: 'toggleShareMenu',
192198
TOGGLE_SIDEBAR: 'toggleSidebar',
193199
TOGGLE_SUPPORTED_BROWSER: 'toggleSupportedBrowser',
194200
TOGGLE_STORY_HAS_AUDIO: 'toggleStoryHasAudio',
195201
TOGGLE_STORY_HAS_BACKGROUND_AUDIO: 'toggleStoryHasBackgroundAudio',
202+
TOGGLE_STORY_HAS_PLAYBACK_UI: 'toggleStoryHasPlaybackUi',
196203
TOGGLE_SYSTEM_UI_IS_VISIBLE: 'toggleSystemUiIsVisible',
197204
TOGGLE_UI: 'toggleUi',
198205
TOGGLE_VIEWPORT_WARNING: 'toggleViewportWarning',
@@ -314,6 +321,12 @@ const actions = (state, action, data) => {
314321
...state,
315322
[StateProperty.STORY_HAS_BACKGROUND_AUDIO_STATE]: !!data,
316323
});
324+
// Shows or hides the play/pause controls.
325+
case Action.TOGGLE_STORY_HAS_PLAYBACK_UI:
326+
return /** @type {!State} */ ({
327+
...state,
328+
[StateProperty.STORY_HAS_PLAYBACK_UI_STATE]: !!data,
329+
});
317330
// Mutes or unmutes the story media.
318331
case Action.TOGGLE_MUTED:
319332
return /** @type {!State} */ ({
@@ -325,6 +338,11 @@ const actions = (state, action, data) => {
325338
...state,
326339
[StateProperty.PAGE_HAS_AUDIO_STATE]: !!data,
327340
});
341+
case Action.TOGGLE_PAGE_HAS_ELEMENT_WITH_PLAYBACK:
342+
return /** @type {!State} */ ({
343+
...state,
344+
[StateProperty.PAGE_HAS_ELEMENTS_WITH_PLAYBACK_STATE]: !!data,
345+
});
328346
case Action.TOGGLE_PAUSED:
329347
return /** @type {!State} */ ({
330348
...state,
@@ -529,13 +547,15 @@ export class AmpStoryStoreService {
529547
},
530548
[StateProperty.MUTED_STATE]: true,
531549
[StateProperty.PAGE_HAS_AUDIO_STATE]: false,
550+
[StateProperty.PAGE_HAS_ELEMENTS_WITH_PLAYBACK_STATE]: false,
532551
[StateProperty.PAUSED_STATE]: false,
533552
[StateProperty.RTL_STATE]: false,
534553
[StateProperty.SHARE_MENU_STATE]: false,
535554
[StateProperty.SIDEBAR_STATE]: false,
536555
[StateProperty.SUPPORTED_BROWSER_STATE]: true,
537556
[StateProperty.STORY_HAS_AUDIO_STATE]: false,
538557
[StateProperty.STORY_HAS_BACKGROUND_AUDIO_STATE]: false,
558+
[StateProperty.STORY_HAS_PLAYBACK_UI_STATE]: false,
539559
[StateProperty.SYSTEM_UI_IS_VISIBLE_STATE]: true,
540560
[StateProperty.UI_STATE]: UIType.MOBILE,
541561
[StateProperty.VIEWPORT_WARNING_STATE]: false,

extensions/amp-story/1.0/amp-story-system-layer.css

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,9 @@
195195

196196
.i-amphtml-story-mute-audio-control,
197197
.i-amphtml-story-unmute-audio-control,
198+
.i-amphtml-paused-display,
199+
.i-amphtml-story-pause-control,
200+
.i-amphtml-story-play-control,
198201
.i-amphtml-story-mute-text,
199202
.i-amphtml-story-unmute-sound-text,
200203
.i-amphtml-story-unmute-no-sound-text,
@@ -211,6 +214,19 @@
211214
display: block !important;
212215
}
213216

217+
.i-amphtml-story-desktop-panels .i-amphtml-paused-display, .i-amphtml-story-desktop-fullbleed .i-amphtml-paused-display {
218+
display: block !important;
219+
}
220+
221+
.i-amphtml-paused-display button[disabled] {
222+
opacity: 0.3 !important;
223+
cursor: initial !important;
224+
}
225+
226+
.i-amphtml-story-has-playback-ui:not([paused]) .i-amphtml-story-pause-control,
227+
.i-amphtml-story-has-playback-ui[paused] .i-amphtml-story-play-control {
228+
display: block !important;
229+
}
214230

215231
.i-amphtml-story-has-audio[muted] .i-amphtml-story-mute-text,
216232
[i-amphtml-current-page-has-audio].i-amphtml-story-has-audio:not([muted]) .i-amphtml-story-unmute-sound-text,
@@ -231,6 +247,14 @@
231247
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/><path d="M0 0h24v24H0z" fill="none"/></svg>') !important;
232248
}
233249

250+
.i-amphtml-story-pause-control {
251+
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>') !important;
252+
}
253+
254+
.i-amphtml-story-play-control {
255+
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M8 5v14l11-7z"/><path d="M0 0h24v24H0z" fill="none"/></svg>') !important;
256+
}
257+
234258
.i-amphtml-story-sidebar-control {
235259
background-image: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 24 24" fill="#FFFFFF"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/><path d="M0 0h24v24H0z" fill="none"/></svg>') !important;
236260
}

extensions/amp-story/1.0/amp-story-system-layer.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const AD_SHOWING_ATTRIBUTE = 'ad-showing';
4040
/** @private @const {string} */
4141
const AUDIO_MUTED_ATTRIBUTE = 'muted';
4242

43+
/** @private @const {string} */
44+
const PAUSED_ATTRIBUTE = 'paused';
45+
4346
/** @private @const {string} */
4447
const HAS_INFO_BUTTON_ATTRIBUTE = 'info';
4548

@@ -49,6 +52,12 @@ const MUTE_CLASS = 'i-amphtml-story-mute-audio-control';
4952
/** @private @const {string} */
5053
const UNMUTE_CLASS = 'i-amphtml-story-unmute-audio-control';
5154

55+
/** @private @const {string} */
56+
const PAUSE_CLASS = 'i-amphtml-story-pause-control';
57+
58+
/** @private @const {string} */
59+
const PLAY_CLASS = 'i-amphtml-story-play-control';
60+
5261
/** @private @const {string} */
5362
const MESSAGE_DISPLAY_CLASS = 'i-amphtml-story-messagedisplay';
5463

@@ -178,6 +187,26 @@ const TEMPLATE = {
178187
},
179188
],
180189
},
190+
{
191+
tag: 'div',
192+
attrs: dict({
193+
'class': 'i-amphtml-paused-display',
194+
}),
195+
children: [
196+
{
197+
tag: 'button',
198+
attrs: dict({
199+
'class': PAUSE_CLASS + ' i-amphtml-story-button',
200+
}),
201+
},
202+
{
203+
tag: 'button',
204+
attrs: dict({
205+
'class': PLAY_CLASS + ' i-amphtml-story-button',
206+
}),
207+
},
208+
],
209+
},
181210
{
182211
tag: 'a',
183212
attrs: dict({
@@ -353,6 +382,10 @@ export class SystemLayer {
353382
this.onAudioIconClick_(true);
354383
} else if (matches(target, `.${UNMUTE_CLASS}, .${UNMUTE_CLASS} *`)) {
355384
this.onAudioIconClick_(false);
385+
} else if (matches(target, `.${PAUSE_CLASS}, .${PAUSE_CLASS} *`)) {
386+
this.onPausedClick_(true);
387+
} else if (matches(target, `.${PLAY_CLASS}, .${PLAY_CLASS} *`)) {
388+
this.onPausedClick_(false);
356389
} else if (matches(target, `.${SHARE_CLASS}, .${SHARE_CLASS} *`)) {
357390
this.onShareClick_(event);
358391
} else if (matches(target, `.${INFO_CLASS}, .${INFO_CLASS} *`)) {
@@ -386,6 +419,14 @@ export class SystemLayer {
386419
true /** callToInitialize */
387420
);
388421

422+
this.storeService_.subscribe(
423+
StateProperty.STORY_HAS_PLAYBACK_UI_STATE,
424+
(hasPlaybackUi) => {
425+
this.onStoryHasPlaybackUiStateUpdate_(hasPlaybackUi);
426+
},
427+
true /** callToInitialize */
428+
);
429+
389430
this.storeService_.subscribe(
390431
StateProperty.MUTED_STATE,
391432
(isMuted) => {
@@ -402,6 +443,14 @@ export class SystemLayer {
402443
true /** callToInitialize */
403444
);
404445

446+
this.storeService_.subscribe(
447+
StateProperty.PAUSED_STATE,
448+
(isPaused) => {
449+
this.onPausedStateUpdate_(isPaused);
450+
},
451+
true /** callToInitialize */
452+
);
453+
405454
this.storeService_.subscribe(
406455
StateProperty.CURRENT_PAGE_INDEX,
407456
(index) => {
@@ -426,6 +475,14 @@ export class SystemLayer {
426475
true /** callToInitialize */
427476
);
428477

478+
this.storeService_.subscribe(
479+
StateProperty.PAGE_HAS_ELEMENTS_WITH_PLAYBACK_STATE,
480+
(hasPlaybackUi) => {
481+
this.onPageHasElementsWithPlaybackStateUpdate_(hasPlaybackUi);
482+
},
483+
true /** callToInitialize */
484+
);
485+
429486
this.storeService_.subscribe(
430487
StateProperty.HAS_SIDEBAR_STATE,
431488
(hasSidebar) => {
@@ -529,6 +586,20 @@ export class SystemLayer {
529586
});
530587
}
531588

589+
/**
590+
* Reacts to story having elements with playback.
591+
* @param {boolean} hasPlaybackUi
592+
* @private
593+
*/
594+
onStoryHasPlaybackUiStateUpdate_(hasPlaybackUi) {
595+
this.vsync_.mutate(() => {
596+
this.getShadowRoot().classList.toggle(
597+
'i-amphtml-story-has-playback-ui',
598+
hasPlaybackUi
599+
);
600+
});
601+
}
602+
532603
/**
533604
* Reacts to the presence of audio on a page to determine which audio messages
534605
* to display.
@@ -551,6 +622,21 @@ export class SystemLayer {
551622
});
552623
}
553624

625+
/**
626+
* Reacts to the presence of elements with playback on the page.
627+
* @param {boolean} pageHasElementsWithPlayback
628+
* @private
629+
*/
630+
onPageHasElementsWithPlaybackStateUpdate_(pageHasElementsWithPlayback) {
631+
this.vsync_.mutate(() => {
632+
this.getShadowRoot()
633+
.querySelectorAll('.i-amphtml-paused-display button')
634+
.forEach((button) => {
635+
button.disabled = !pageHasElementsWithPlayback;
636+
});
637+
});
638+
}
639+
554640
/**
555641
* Reacts to muted state updates.
556642
* @param {boolean} isMuted
@@ -564,6 +650,19 @@ export class SystemLayer {
564650
});
565651
}
566652

653+
/**
654+
* Reacts to paused state updates.
655+
* @param {boolean} isPaused
656+
* @private
657+
*/
658+
onPausedStateUpdate_(isPaused) {
659+
this.vsync_.mutate(() => {
660+
isPaused
661+
? this.getShadowRoot().setAttribute(PAUSED_ATTRIBUTE, '')
662+
: this.getShadowRoot().removeAttribute(PAUSED_ATTRIBUTE);
663+
});
664+
}
665+
567666
/**
568667
* Hides message after elapsed time.
569668
* @param {string} message
@@ -675,6 +774,15 @@ export class SystemLayer {
675774
});
676775
}
677776

777+
/**
778+
* Handles click events on the paused and play buttons.
779+
* @param {boolean} paused Specifies if the story is being paused or not.
780+
* @private
781+
*/
782+
onPausedClick_(paused) {
783+
this.storeService_.dispatch(Action.TOGGLE_PAUSED, paused);
784+
}
785+
678786
/**
679787
* Handles click events on the share button and toggles the share menu.
680788
* @param {!Event} event

0 commit comments

Comments
 (0)