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

Implement lightbox attribute for carousels #12771

Merged
merged 4 commits into from Jan 11, 2018

Conversation

cathyxz
Copy link
Contributor

@cathyxz cathyxz commented Jan 10, 2018

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 the amp-carousel is carousel or slides.

Known Bugs:

  • There's a known issue with rendering the third image onwards for carousels with type='slides'. This is due to us initializing the image viewer on buildCarousel, and expecting the amp-img to be laid out when it isn't. The intended fix is to promote ImageViewer 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.

Copy link
Contributor

@aghassemi aghassemi left a 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(() => {
Copy link
Contributor

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');
Copy link
Contributor

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),
Copy link
Contributor

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

Copy link
Contributor Author

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');
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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))
Copy link
Contributor

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}
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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');
Copy link
Contributor

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') || ...

Copy link
Contributor Author

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_++);

Copy link
Contributor

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');
Copy link
Contributor

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();
Copy link
Contributor

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?

Copy link
Contributor Author

@cathyxz cathyxz Jan 10, 2018

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));
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 ).

Copy link
Contributor Author

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 ?

Copy link
Contributor

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.

@cathyxz
Copy link
Contributor Author

cathyxz commented Jan 11, 2018

@cvializ PTAL? Just one unresolved discussion here about element.querySelectorAll and scopedQuerySelectorAll. =)

@aghassemi
Copy link
Contributor

LGTM

@cathyxz cathyxz dismissed cvializ’s stale review January 11, 2018 21:59

comments all addressed

@cathyxz cathyxz merged commit fe4b18c into ampproject:master Jan 11, 2018
@cathyxz cathyxz deleted the feature/carousel-lightbox branch January 11, 2018 22:32
gzgogo pushed a commit to gzgogo/amphtml that referenced this pull request Jan 26, 2018
* 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
RanAbram pushed a commit to RanAbram/amphtml that referenced this pull request Mar 12, 2018
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
No open projects
Lightbox 2.0 [cathyxz]
  
Awaiting triage
Development

Successfully merging this pull request may close these issues.

None yet

4 participants