-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
amp-position-observer and redesign of scrollbound/visibility-based behaviour #10818
Conversation
/to @dvoytenko This is ready for review. (thanks!). It should be a fairly complete PR with docs, integration and functional tests. Remaining items:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approve of .md file changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when none of the attributes are used on this custom element?
# limitations under the license. | ||
# | ||
|
||
tags: { # amp-position-observer script |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two spaces between {
and #
and omit word script
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all done, if nothing is specified, defaults kick in (0 and 0 for both margin and ratios)
attrs: { | ||
name: "intersection-ratios" | ||
# "0", "0.1" , "1", "0 1", "0 0.5", "0.5 1", "1 1", etc... | ||
value_regex: "^([0]*?\.\d*$|1$|0$)|([0]*?\.\d*|1|0)\s{1}([0]*?\.\d*$|1$|0$)" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that value_regex
needs \
to be escaped to work properly. For instance \d
should be \\d
. To handle this, we usually include the unescaped version above in a comment. For example see amp-timeago.
value_regex: "^([0]*?\.\d*$|1$|0$)|([0]*?\.\d*|1|0)\s{1}([0]*?\.\d*$|1$|0$)" | ||
} | ||
attrs: { | ||
name: "exclusion-margins" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please alphabetize the attributes. E.g. this would go above intersection-ratios
.
attrs: { | ||
name: "exclusion-margins" | ||
# "100", "100px", "100vh", "100 100px", "100vh 100", "100px 100vh", etc.. | ||
value_regex: "^(\d+$|\d+px$|\d+vh$)|((\d+|\d+px|\d+vh)\s{1}(\d+$|\d+px$|\d+vh$))" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, needs escaping and comment with unescaped.
<amp-position-observer exclusion-margins="100vh 200px" layout="nodisplay"></amp-position-observer> | ||
<amp-position-observer exclusion-margins="100 200vh" layout="nodisplay"></amp-position-observer> | ||
|
||
<!-- Invalid --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We usually group invalid ones with comments stating why they're invalid.
E.g. <!-- Invalid: missing required layout value -->
, <!-- Invalid: interesection-ratios not valid entries -->
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding this since I forgot to check request changes.
} | ||
this.maybeCreateRunner_().then(() => { | ||
if (this.visible_ && this.triggered_) { | ||
this.runner_.resume(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reads strange. maybeCreateRunner_
reads as "runner may end up created and maybe not", but inside the runner_
is used w/o check. Is this because promise won't be resolved or will be rejected if runner is not created?
} | ||
// percent based seek | ||
const percent = parseFloat(invocation.args && invocation.args['percent']); | ||
if (isFiniteNumber(percent) && percent <= 1 && percent >= 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A better way, i think, would be to just do minmax[percent, 0, 1]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added clamp
to math.js
@@ -262,7 +289,7 @@ export class AmpAnimation extends AMP.BaseElement { | |||
setVisible_(visible) { | |||
if (this.visible_ != visible) { | |||
this.visible_ = visible; | |||
if (this.triggered_) { | |||
if (this.triggered_ && this.triggerOnVisibility_) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Why would it be triggered then? Would it be better to reset triggered_
to false instead?
* @private | ||
*/ | ||
maybeCreateRunner_(opt_args) { | ||
if (!this.runnerPromise_ || !this.runner_) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how to read this: why would one exist but the other would not? Further, if runner_
exists but promise doesn't, why do we recreate the runner? Shouldn't the previous runner be destroyed/stopped/etc? It will continue playing otherwise. This is from reading the code, so maybe just some asserts missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All done as discussed. Re-did the actions integrations tests so they mock less
* @return {!Promise} | ||
* @private | ||
*/ | ||
maybeCreateRunner_(opt_args) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are potential consequences of this? Is it strictly necessary? Is it to be able to seek right away? Maybe it's better to animate()
right before the first seek
? Feels like a biggish change from before, but maybe it's not.
} | ||
} | ||
|
||
this.targetSelector_ = this.element.getAttribute('target-selector'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I think the target="id"
form is better for this use case. Selector kind of implies behavior for multiple targets. This also clarifies typical confusion around the direction of DOM search, e.g. it's not a closest
thing, etc. Asking folk to add an ID for this use case is very reasonable. Especially since it's optional anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
the target is entering/exiting from top (0 will be used) or bottom (1 will be used). | ||
|
||
|
||
#### exclusion-margins (optional) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit bikeshed on name. Would it be better to rename to root margins or viewport margins to get it closer to InOb terminology which we use elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
viewport-margins it is
const totalDuration = adjustedViewportRect.height + | ||
positionRect.height - totalDurationOffset; | ||
|
||
const topOffset = Math.abs( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't the topRatio
used in this formula?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The formula aligns everything to create an equivalent case where top ratio is 1.
* @private | ||
*/ | ||
discoverScene_() { | ||
if (this.targetSelector_) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above about target=id
. But if selector is used and specified by the doc - it could be a bad ID and needs to be defined as such via try{}catch(){user.error("bad selector")}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NA now
discoverScene_() { | ||
if (this.targetSelector_) { | ||
this.scene_ = user().assertElement( | ||
this.getAmpDoc().getRootNode().querySelector(this.targetSelector_), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow up question: is this code guaranteed to run after the DOM has been fully parsed? Typically we wrap query selectors into ampdoc.whenReady().then(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added whenReady
@dvoytenko all done, PTAL |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validator changes look good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with a nit or two.
* @param {number} max1 the upper bound of the source range | ||
* @param {number} min2 the lower bound of the target range | ||
* @param {number} max2 the upper bound of the target range | ||
* @param {!number} val the value in the source range |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: numbers and other primitives are non-nullable by default, so no need for !
Closes #8411 #9301 #10880
New Approach to scrollbound and visibility-based behaviour
amp-animation
<>amp-position-observer
(scrollbound animations and visibility-based animation scenes)amp-animation
<>amp-video-player-x
(Not implemented in this PR, syncing animation and videos)amp-video-player-x
<>amp-position-observer
(Not implemented in this PR, scrollbound behaviour for videos)enter
exit
progress
events (e.g. range input, carousel, XHR call, etc..)amp-position-observer
enter
,exit
,scroll(percent)
LOW-TRUST events. (i.e. has "timeline")ratios
andmargins
(similar to IntersectionObserver)Examples
Play my animation scene when 50% visible and pause when 50% invisible
Play my animation using scrolling instead of time.
Animation starts when scene is fully visible and pauses as soon as partially invisible.
Play a video when a heading is scrolled into view.
Imagine a position fixed video that would play automatically when user scrolls to a related heading somewhere in the page.
Sync my animation timeline with this video (Not implemented in this PR)
Animation starts when video starts, pauses when video is paused, seeking the video also seeks the animation, animation ends when video ends.
Play my video using scrolling instead of time (Not implemented in this PR)
Similar to scrollbound animation, but with a video instead