Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [Story Player] Add parameter to configure amp-story-player animation #34204

Merged
merged 13 commits into from
May 11, 2021
6 changes: 5 additions & 1 deletion css/amp-story-player-iframe.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
transform-style: preserve-3d;
}

.i-amphtml-story-player-loaded iframe {
.i-amphtml-story-player-loaded .story-player-iframe {
opacity: 1;
transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
}

.i-amphtml-story-player-no-navigation-transition .story-player-iframe {
transition-duration: 0.01s; /* Set to low value so transitionend is emitted */
}

.i-amphtml-story-player-main-container iframe:nth-of-type(1),
.i-amphtml-story-player-main-container .story-player-iframe[i-amphtml-iframe-position="0"] {
transform: translate3d(0, 0, 1px);
Expand Down
94 changes: 75 additions & 19 deletions examples/amp-story/player.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,87 @@
line-height: 1.25;
text-shadow: 3px 3px #000;
}
.actions .circle {
width: 65px;
height: 65px;
background-size: cover;
overflow: hidden;
border-radius: 50%;
display: inline-block;
margin: 10px;
box-shadow:
0 0 0 3px white,
0 0 0 7px hsl(20, 79%, 60%);
color: white;
text-align: center;
padding-top: 20px;
box-sizing: border-box;
text-shadow: 0px 2px 4px #000000;
cursor: pointer;
}

.actions .circle.active {
box-shadow:
0 0 0 3px white,
0 0 0 7px hsl(257, 79%, 60%);
}
</style>
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<script>
window.onload = () => {
const player = document.querySelector('amp-story-player');
document.querySelector('.actions').addEventListener('click', (e) => {
if (e.target.hasAttribute('data-story')) {
player.show(e.target.getAttribute('data-story'), null, {animate: false});
}
})
player.addEventListener('navigation', (e) => {
document.querySelectorAll('.actions .circle').forEach((circle, index) => {
circle.classList.toggle('active', index === e.detail.index)
})
});
};
</script>
</head>
<body>
<h1>This is a NON-AMP page that embeds a story below:</h1>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras viverra neque ex, sit amet varius sem maximus sed. Suspendisse potenti. Donec erat purus, sagittis sit amet tincidunt ut, maximus sit amet massa. Maecenas venenatis fringilla dui vitae vestibulum. Fusce imperdiet euismod lobortis. Nullam sagittis nunc at tristique mattis. In sodales consectetur mollis. Maecenas sollicitudin, ex vel tempor rutrum, turpis eros interdum enim, sit amet volutpat tortor dolor at ipsum. Nam posuere velit vel urna vulputate interdum. Aenean eu vulputate lorem. Praesent nec nunc sodales, egestas orci sed, hendrerit mauris. Ut blandit turpis non erat sagittis, quis fermentum odio feugiat.</div>
<amp-story-player style="width: 360px; height: 600px;">
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-new-york-city/"
class="story">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-new-york-city/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in New York City</span>
</a>
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-miami/">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-miami/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in Miami</span>
</a>
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-mexico-city/">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-mexico-city/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in Mexico City</span>
</a>
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-rome/">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-rome/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in Rome</span>
</a>
</amp-story-player>
<div>
<amp-story-player style="width: 360px; height: 600px;">
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-new-york-city/"
class="story">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-new-york-city/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in New York City</span>
</a>
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-miami/">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-miami/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in Miami</span>
</a>
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-mexico-city/">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-mexico-city/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in Mexico City</span>
</a>
<a href="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-rome/">
<img data-amp-story-player-poster-img src="https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-rome/img/promo3x4.jpg" width="360" height="600" loading="lazy">
<span class="title">A local’s guide to what to eat and do in Rome</span>
</a>
</amp-story-player>

<div class="actions">
<div class="circle active"
data-story="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-new-york-city/"
style="background-image: url('https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-new-york-city/img/42a59562-834d-11e9-b585-e36b16a531aa.jpg')">New York</div>
<div class="circle"
data-story="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-miami/"
style="background-image: url('https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-miami/img/9f1d2656-6b7a-11e9-bbe7-1c798fb80536.jpg')">Miami</div>
<div class="circle"
data-story="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-mexico-city/"
style="background-image: url('https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-mexico-city/img/2e0f5178-746c-11e9-9331-30bc5836f48e.jpg')">Mexico City</div>
<div class="circle"
data-story="https://www-washingtonpost-com.cdn.ampproject.org/v/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-rome/"
style="background-image: url('https://www-washingtonpost-com.cdn.ampproject.org/i/s/www.washingtonpost.com/graphics/2019/lifestyle/travel/amp-stories/a-locals-guide-to-what-to-eat-and-do-in-rome/img/f5903e2e-86fa-11e9-9d73-e2ba6bbf1b9b.jpg')">Rome</div>
</div>
</div>
<div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras viverra neque ex, sit amet varius sem maximus sed. Suspendisse potenti. Donec erat purus, sagittis sit amet tincidunt ut, maximus sit amet massa. Maecenas venenatis fringilla dui vitae vestibulum. Fusce imperdiet euismod lobortis. Nullam sagittis nunc at tristique mattis. In sodales consectetur mollis. Maecenas sollicitudin, ex vel tempor rutrum, turpis eros interdum enim, sit amet volutpat tortor dolor at ipsum. Nam posuere velit vel urna vulputate interdum. Aenean eu vulputate lorem. Praesent nec nunc sodales, egestas orci sed, hendrerit mauris. Ut blandit turpis non erat sagittis, quis fermentum odio feugiat.</div>
</body>
</html>
4 changes: 4 additions & 0 deletions spec/amp-story-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,27 +182,31 @@ playerEl.load();

- number: the story in the player to which you want to move, relative to the current story.
- number (optional): the page of the story to which you want to move, relative to the current page.
- {animate: boolean} (optional): options for the navigation (animate: whether to animate the story transition).

If the player is currently on the third story out of five stories:

- `player.go(1)` will go forward one story to the fourth story
- `player.go(-1)` will go backward one story to the second story
- `player.go(-1, 1)` will go backward one story and navigate one page backwards
- `player.go(0, 5)` will stay in the current story and navigate 5 pages forward
- `player.go(1, 0, {animate: false})` will go to the next page without the swipe animation

#### show

**Parameters**

- string or null: the URL of the story to show.
- string (optional): the ID attribute of the page element.
- {animate: boolean} (optional): options for the navigation (animate: whether to animate the story transition).

Will change the current story being displayed by the player.

```javascript
player.show('cool-story.html'); // Will display cool-story.html
player.show('cool-story.html', 'page-4'); // Will display cool-story.html and switch to page-4
player.show(null, 'page-4'); // Stay on current story and switch to page-4
player.show('cool-story.html', null, {animate: false}); // Will display cool-story.html without the swipe animation
```

#### rewind
Expand Down
45 changes: 36 additions & 9 deletions src/amp-story-player/amp-story-player-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
serializeQueryString,
} from '../url';
import {applySandbox} from '../3p-frame';
import {createCustomEvent} from '../event-helper';
import {createCustomEvent, listenOnce} from '../event-helper';
import {dict} from '../core/types/object';
import {isJsonScriptTag, tryFocus} from '../dom';
// Source for this constant is css/amp-story-player-iframe.css
Expand Down Expand Up @@ -115,6 +115,10 @@ const STORY_MESSAGE_STATE_TYPE = {
/** @const {string} */
export const AMP_STORY_PLAYER_EVENT = 'AMP_STORY_PLAYER_EVENT';

/** @const {string} */
const CLASS_NO_NAVIGATION_TRANSITION =
'i-amphtml-story-player-no-navigation-transition';

/** @typedef {{ state:string, value:(boolean|string) }} */
let DocumentStateTypeDef;

Expand All @@ -135,17 +139,27 @@ let StoryDef;

/**
* @typedef {{
* on: string,
* action: string,
* endpoint: string,
* on: ?string,
* action: ?string,
* endpoint: ?string,
* pageScroll: ?boolean,
* autoplay: ?boolean,
* }}
*/
let BehaviorDef;

/**
* @typedef {{
* controls: (!Array<!ViewerControlDef>),
* behavior: !BehaviorDef,
* attribution: ?string,
* }}
*/
let DisplayDef;

/**
* @typedef {{
* controls: ?Array<!ViewerControlDef>,
* behavior: ?BehaviorDef,
* display: ?DisplayDef,
* }}
*/
let ConfigDef;
Expand Down Expand Up @@ -756,16 +770,27 @@ export class AmpStoryPlayer {
* Shows the story provided by the URL in the player and go to the page if provided.
* @param {?string} storyUrl
* @param {string=} pageId
* @param {{animate: boolean?}} options
* @return {!Promise}
*/
show(storyUrl, pageId = null) {
show(storyUrl, pageId = null, options = {}) {
const story = this.getStoryFromUrl_(storyUrl);

let renderPromise = Promise.resolve();
if (story.idx !== this.currentIdx_) {
this.currentIdx_ = story.idx;

renderPromise = this.render_();

if (options.animate === false) {
this.rootEl_.classList.toggle(
CLASS_NO_NAVIGATION_TRANSITION,
!options.animate
mszylkowski marked this conversation as resolved.
Show resolved Hide resolved
);
listenOnce(story.iframe, 'transitionend', () => {
Enriqe marked this conversation as resolved.
Show resolved Hide resolved
this.rootEl_.classList.remove(CLASS_NO_NAVIGATION_TRANSITION);
});
}
this.onNavigation_();
}

Expand Down Expand Up @@ -945,8 +970,9 @@ export class AmpStoryPlayer {
* Navigates stories given a number.
* @param {number} storyDelta
* @param {number=} pageDelta
* @param {{animate: boolean?}} options
*/
go(storyDelta, pageDelta = 0) {
go(storyDelta, pageDelta = 0, options = {}) {
if (storyDelta === 0 && pageDelta === 0) {
return;
}
Expand All @@ -969,7 +995,7 @@ export class AmpStoryPlayer {

let showPromise = Promise.resolve();
if (this.currentIdx_ !== newStory.idx) {
showPromise = this.show(newStory.href);
showPromise = this.show(newStory.href, /* pageId */ null, options);
}

showPromise.then(() => {
Expand All @@ -980,6 +1006,7 @@ export class AmpStoryPlayer {
/**
* Updates story position.
* @param {!StoryDef} story
* @return {!Promise}
* @private
*/
updatePosition_(story) {
Expand Down
67 changes: 67 additions & 0 deletions test/unit/test-amp-story-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {AmpStoryComponentManager} from '../../src/amp-story-player/amp-story-com
import {AmpStoryPlayer} from '../../src/amp-story-player/amp-story-player-impl';
import {Messaging} from '@ampproject/viewer-messaging';
import {PageScroller} from '../../src/amp-story-player/page-scroller';
import {expect} from 'chai';

describes.realWin('AmpStoryPlayer', {amp: false}, (env) => {
let win;
Expand Down Expand Up @@ -1440,5 +1441,71 @@ describes.realWin('AmpStoryPlayer', {amp: false}, (env) => {
},
});
});

it('supress navigation animation if called go with options.animate = false', async () => {
const playerEl = win.document.createElement('amp-story-player');
attachPlayerWithStories(playerEl, 2);
playerEl.appendChild(buildCircularWrappingConfig());

const player = new AmpStoryPlayer(win, playerEl);

await player.load();
await nextTick();

player.go(1, 0, {animate: false});

expect(
player
.getElement()
.querySelector('.i-amphtml-story-player-main-container')
.classList.contains('i-amphtml-story-player-no-navigation-transition')
).to.be.true;
});

it('not supress navigation animation if called go with options.animate = true', async () => {
const playerEl = win.document.createElement('amp-story-player');
attachPlayerWithStories(playerEl, 2);
playerEl.appendChild(buildCircularWrappingConfig());

const player = new AmpStoryPlayer(win, playerEl);

await player.load();
await nextTick();

player.go(1, 0, {animate: true});

expect(
player
.getElement()
.querySelector('.i-amphtml-story-player-main-container')
.classList.contains('i-amphtml-story-player-no-navigation-transition')
).to.be.false;
});

it('revert navigation animation after transition ends', async () => {
const playerEl = win.document.createElement('amp-story-player');
attachPlayerWithStories(playerEl, 2);
playerEl.appendChild(buildCircularWrappingConfig());

const player = new AmpStoryPlayer(win, playerEl);

await player.load();
await nextTick();

player.go(1, 0, {animate: false});

const rootEl = player
.getElement()
.querySelector('.i-amphtml-story-player-main-container');

// Wait for event dispatched to be listened
await new Promise((resolve) => setTimeout(() => resolve(), 50));
Comment on lines +1501 to +1502
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a hard-coded timeout, you may be able to use yield here.

Suggested change
// Wait for event dispatched to be listened
await new Promise((resolve) => setTimeout(() => resolve(), 50));
yield macroTask();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'm about to send a new PR to fix the flaky test


expect(
rootEl.classList.contains(
'i-amphtml-story-player-no-navigation-transition'
)
).to.be.false;
});
});
});