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

feat(tabs): paginate tab header when exceeds width #2084

Merged
merged 22 commits into from
Dec 8, 2016

Conversation

andrewseguin
Copy link
Contributor

No description provided.

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Dec 5, 2016
@andrewseguin andrewseguin added pr: needs review and removed cla: yes PR author has agreed to Google's Contributor License Agreement labels Dec 5, 2016
encapsulation: ViewEncapsulation.None,
host: {
'[class.md-tab-header]': 'true',
'[class.scroll]': '_isScrollingEnabled()',
Copy link
Member

Choose a reason for hiding this comment

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

One thing to bear in mind here: the _isScrollingEnabled will be run on every change detection cycle. Since it uses offsetWidth and scrollWidth to determine whether the element is scrollable, it will end up causing a lot of layout recalculations. This was an issue in Material 1 and it might be better to address it earlier for Material 2.

Copy link
Member

Choose a reason for hiding this comment

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

This is a very good point. We should recalculate this as infrequently as possible.

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 agree that this is not ideal. Do you have any suggestions on how to reduce the amount of times we call this but still have an up to date pagination? It seems that checking this in ngAfterViewChecked will be a little better but still way more than we should need.

Copy link
Member

@crisbeto crisbeto Dec 6, 2016

Choose a reason for hiding this comment

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

I think that caching the width and only updating it in the following cases should be fine:

  1. When initialized.
  2. When a tab gets added/removed.
  3. When a tab's heading changes.

The third one might be tricky to get right since we'd have to either get the innerText from the DOM on every change detection (which apparently also causes a reflow), or use a MutationObserver per tab. We could also try to have have one "main" MutationObserver on the header, but I don't know whether that won't end up firing too often, because AFAIK, you need to enable a bunch of flags to get it working.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I chatted with Andrew yesterday about this and that's pretty much example what we're doing. MutationObserver per-tab, going to do that part in a follow-up PR (and just handle init, adding/removing tabs in this PR).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, I think init and tabs add/remove should handle the vast majority of use cases. I'll include just that logic in this PR.

In another PR, I'll attempt to check tab heading changes (text node modifications) using the MutationObserver.

I think one more thing to consider is that we could check when a change is made to the selected tab or focused tab. This would provide a fallback that could be triggered by the user who may try to get the header to update the scrolling after some event has caused the width to change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Talked with Jeremy - adding a check to select/focus will probably be overkill. But adding a check on resize will probably be nice.

Also, Jeremy had a good idea to make the mutation observer logic as a separate directive that could be placed into core for other components that need similar checks.

Copy link
Member

Choose a reason for hiding this comment

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

I like the idea of the MutationObserver directive, although isn't it basically 10 lines of boilerplate anyway?

Copy link
Member

Choose a reason for hiding this comment

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

I figure it will be a nice convenience if we ever need to do something like this again

@googlebot googlebot added the cla: yes PR author has agreed to Google's Contributor License Agreement label Dec 5, 2016
}
}

.dynamic-tabs-demo {
Copy link
Member

Choose a reason for hiding this comment

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

.demo-dynamic-tabs

]
],
providers: [
{ provide: Dir, useFactory: () => { return {value: 'ltr'}; }
Copy link
Member

Choose a reason for hiding this comment

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

Why not put a real dir directive above the tabs?

@@ -0,0 +1,23 @@
<div class="md-tab-label-pagination before md-elevation-z4"
Copy link
Member

Choose a reason for hiding this comment

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

What's this before class?

<div class="md-tab-label-pagination before md-elevation-z4"
aria-hidden="true"
md-ripple [md-ripple-disabled]="_disableScrollBefore"
[class.disabled]="_disableScrollBefore"
Copy link
Member

Choose a reason for hiding this comment

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

class.md-tab-header-disabled?


export type ScrollDirection = 'after' | 'before';

@Component({
Copy link
Member

Choose a reason for hiding this comment

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

Needs a class description.

return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr';
}

_updateScrollPosition() {
Copy link
Member

Choose a reason for hiding this comment

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

These scrolling methods could use some descriptions

}

this._renderer.setElementStyle(this._tabList.nativeElement,
'transform', `translate3d(${translateX}, 0, 0)`);
Copy link
Member

Choose a reason for hiding this comment

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

See the applyCssTransform function

this.scrollDistance -= viewLength / 3;
} else {
this.scrollDistance += viewLength / 3;
}
Copy link
Member

Choose a reason for hiding this comment

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

this.scrollDistance = (scrollDir == 'before' ? -1 : 1) * viewLength / 3;

?

Also, explain where this calculation comes from?


// If move is required, overscroll by a small amount to provide an affordance to click the
// next label.
const exaggeratedOverscroll = 60;
Copy link
Member

Choose a reason for hiding this comment

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

Make this a file-level constant?

const selectedLabel = this._labelWrappers.toArray()[labelIndex];
const viewLength = this._tabListContainer.nativeElement.offsetWidth;

let labelBeforePos: number, labelAfterPos: number;
Copy link
Member

Choose a reason for hiding this comment

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

Comment explaining what these variables contain?

@andrewseguin
Copy link
Contributor Author

Shuffled the logic so that the majority of scrolling stuff is evaluated on content check and checks are only made conditionally (e.g. tab list changed, selected index changed, etc)

Really appreciate the reviews, the amount of layout reflows has dropped significantly.

Copy link
Member

@jelbourn jelbourn left a comment

Choose a reason for hiding this comment

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

LGTM, one last nit

/**
* Waits one frame for the view to update, then updates the ink bar and scroll.
* Note: This must be run outside of the zone or it will create an infinite change detection loop
* TODO: internal
Copy link
Member

Choose a reason for hiding this comment

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

No need for TODO: internal any more

@andrewseguin
Copy link
Contributor Author

Removed the internal comment.

I'd like to add a reference drawing into the folder to help people understand better about how the element container widths/offsets work. Here's the WIP: https://docs.google.com/drawings/d/1fhtiM-NabWhKP8pVsz8hCAQkzWz1QXrUG0LH-xQdpqI/edit?usp=sharing

@jelbourn jelbourn added the action: merge The PR is ready for merge by the caretaker label Dec 7, 2016
@mmalerba
Copy link
Contributor

mmalerba commented Dec 7, 2016

Property {'_disableScrollBefore', '_disableScrollAfter', '_handleKeydown'} is private and only accessible within class 'MdTabHeader'

@mmalerba mmalerba added action: merge The PR is ready for merge by the caretaker pr: needs rebase and removed action: merge The PR is ready for merge by the caretaker labels Dec 7, 2016
@andrewseguin
Copy link
Contributor Author

Okay, should be good to merge!

@andrewseguin andrewseguin added action: merge The PR is ready for merge by the caretaker and removed pr: needs rebase labels Dec 8, 2016
@mmalerba mmalerba merged commit 92e26d7 into angular:master Dec 8, 2016
@andrewseguin andrewseguin deleted the tabs-pagination branch December 8, 2016 19:11
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 6, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker cla: yes PR author has agreed to Google's Contributor License Agreement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants