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
Implement lightbox attribute for carousels #12771
Conversation
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.
awesome. couple of requests.
this.resources_ = Services.resourcesForDoc(this.getAmpDoc()); | ||
this.buildMask_(); | ||
this.element.appendChild(this.container_); | ||
this.vsync_.mutate(() => { |
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.
on second thought, viewer.whenFirstVisible().then
is probably better than a mutate. Difference is that buildCallback
is called during prerender mode. Initializing a lightbox is too much work IMO for prerender mode. viewer.whenFirstVisible().
will ensure initializing code is delayed until page is actually shown.
* @private | ||
*/ | ||
getSlidesFromCarousel_(element) { | ||
const type = element.getAttribute('type'); |
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.
I think for both types of carousels, each slide has amp-carousel-slide
class attached to it. So this could become
toArray(element.getElementsByClassname('amp-carousel-slide');
I think there is also a race-condition here. There is no gaurantee that carousel has finish building by the time it is discovered. so we would need to wait for the "LOAD_END" signal of carousel before querying it. Something like element.whenSignal(CommonSignals.LOAD_END).then should do.
let carouselSlides = []; | ||
if (type == 'slides') { | ||
iterateCursor( | ||
scopedQuerySelectorAll(element, SLIDE_ITEM_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.
scopedQuerySelectorAll
is usually only needed when the query itself has space
or >
to query descendants. For id, class or attribute selectors, normalyl querySelector is fine
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.
This was intentional because there might be multiple carousels in the page, so you only want to select the slides of that one particular carousel.
* @private | ||
*/ | ||
getSlidesFromCarousel_(element) { | ||
const type = element.getAttribute('type'); |
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.
I'm not sure this approach is best. Maybe we could add a new public method getSlides
to the base-carousel
that slidescroll
and scrollable-carousel
could each implement to expose their slide elements. That way you don't need to have the branches here, and if the carousels ever update how they contain their slides, the implementer won't need to be aware of this code here.
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.
I like this idea a lot actually. Seems a lot better than my current hack.
const container = dev().assertElement( | ||
scopedQuerySelector(element, CAROUSEL_CONTAINER_SELECTOR) | ||
); | ||
carouselSlides = childElements(container, () => true); |
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.
You could use the Element.children
property instead.
if (type == 'slides') { | ||
iterateCursor( | ||
scopedQuerySelectorAll(element, SLIDE_ITEM_SELECTOR), | ||
slide => carouselSlides.push(childElement(slide, () => true)) |
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 () => true
looks fishy. You should use Element.firstElementChild
instead
|
||
/** | ||
* Counter tracking number of carousels without ids | ||
* @private {!number} |
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: primitives are non nullable by default
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.
I truly thought I had rid all these files of this thing... welp. Tyty.
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.
np, honestly we should have a tool that shouts at us for it. I'm surprised closure doesn't by default
if (element.tagName == 'AMP-CAROUSEL') { | ||
// TODO: special case carousel | ||
if (element.tagName.toLowerCase() == CAROUSEL_TAG) { | ||
let lightboxGroupId = element.getAttribute('lightbox'); |
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.
This should be simplified to const lightboxGroupId = element.getAttribute('lightbox') || ...
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.
Does this still read well?
const lightboxGroupId = element.getAttribute('lightbox') ||
'carousel' + (element.getAttribute('id') || this.counter_++);
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.
imho it's more readable since the const
and ||
communicates a fallback value better than a let
and an if
statement. But if you feel like it's more cluttered this way, follow your own style-compass : )
.push(dev().assertElement(element)); | ||
if (this.meetsHeuristicsForTap_(element)) { | ||
const viewer = elementByTag(this.ampdoc_.getRootNode(), VIEWER_TAG); | ||
element.setAttribute('on', 'tap:' + viewer.id + '.activate'); |
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.
Feel free to use es6 template strings for simple string interpolation like this.
this.container_ = this.win.document.createElement('div'); | ||
this.container_.classList.add('i-amphtml-lbv'); | ||
this.element.appendChild(this.container_); | ||
this.manager_.maybeInit(); |
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 do you need to call maybeInit
here now? Does it not need to be lazily initialized any more?
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.
It cannot be lazy initialized on open
anymore because it handles adding on tap handlers to everything that has the lightbox
attribute. open
is usually triggered on tap.
*/ | ||
getSlidesFromCarousel_(element) { | ||
return element.signals().whenSignal(CommonSignals.LOAD_END).then(() => { | ||
return toArray(scopedQuerySelectorAll(element, SLIDE_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.
This is fine but note that element.querySelectorAll('.someClassName)
will still only return someClassName
descendants of element
.
The whole thing is pretty confusing and something I learned recently. When querySelector goes wrong section of this article https://developers.google.com/web/updates/2013/03/What-s-the-CSS-scope-pseudo-class-for has a good explanation of what :scope
pseudo-selector (which our scopedQuerySelectorAll
is based on) is and what issues it fixes, it is worth a quick read.
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.
Yes, I think the intention is to return someClassName
descendants of element
. The element
here is the carousel that I'm lightboxing. I'm looking for the slides of that carousel only, which are necessarily descendants of the lightbox. If I don't scope this selector, I might accidentally get slides of other carousels in the document. So I think this implementation is consistent with the intention?
Maybe what's confusing here is the naming of the input parameter. It's currently called element
, but should really be carousel
.
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.
I think querySelector
returns only one element, whereas querySelectorAll
returns all elements that match the selector. We do want querySelectorAll
. As to why not use element.querySelectorAll
, I think that's precisely due to the issue in the article. I think the usage of scopedQuerySelectorAll
here is the correct one (the other ones also don't lint =P ).
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.
Oh I see. There's a performance hit. Basically element.querySelectorAll
will still work, but won't have the perf hit associated with scopedQuerySelectorAll
. Since this use case isn't necessary for scopedQuerySelectorAll
, it's better not to have the perf it and just to /*OK*/
lint. Is that what you mean @aghassemi ?
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.
correct. The reason I brought it up wasn't necessarily for the perf gains (which are not much if any), but mostly to tell what I learned regarding scropeSelector recently which was unintuitive to me so though it is worth sharing.
Alss note that the reasons our presubmit complains about element.querySelectorAll(VARIABLE)
is that a variable
is passed to it so it can not make a the right judgment whether the query is one of the problematic ones or not. If a string literal was passed to it, it would not have complained.
@cvializ PTAL? Just one unresolved discussion here about |
* Draft work on getting carousels to work with the lightbox attribute * Address comments * Use signals for detecting carousel load in amp-lightbox-viewer as well * Change scopedQuerySelectorAll to querySelectorAll for perf benefit
* Draft work on getting carousels to work with the lightbox attribute * Address comments * Use signals for detecting carousel load in amp-lightbox-viewer as well * Change scopedQuerySelectorAll to querySelectorAll for perf benefit
Partially addresses #12583. Enables lightboxing carousels and installs on-tap handlers.
Basically implements a special case if the tag name is
amp-carousel
to lightbox all its children instead. Finding the children has different logic depending on whether the type of theamp-carousel
iscarousel
orslides
.Known Bugs:
type='slides'
. This is due to us initializing the image viewer on buildCarousel, and expecting theamp-img
to be laid out when it isn't. The intended fix is to promoteImageViewer
to its own standalone component and to call its layout callback when we are one slide away and ready to lay out that slide in the lightbox.