-
Notifications
You must be signed in to change notification settings - Fork 6.8k
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
Conversation
encapsulation: ViewEncapsulation.None, | ||
host: { | ||
'[class.md-tab-header]': 'true', | ||
'[class.scroll]': '_isScrollingEnabled()', |
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.
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.
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 a very good point. We should recalculate this as infrequently as possible.
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 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.
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 that caching the width and only updating it in the following cases should be fine:
- When initialized.
- When a tab gets added/removed.
- 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.
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.
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).
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.
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.
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.
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.
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 the idea of the MutationObserver directive, although isn't it basically 10 lines of boilerplate 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.
I figure it will be a nice convenience if we ever need to do something like this again
41eda70
to
f7a97de
Compare
} | ||
} | ||
|
||
.dynamic-tabs-demo { |
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.
.demo-dynamic-tabs
] | ||
], | ||
providers: [ | ||
{ provide: Dir, useFactory: () => { return {value: 'ltr'}; } |
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 not put a real dir
directive above the tabs?
@@ -0,0 +1,23 @@ | |||
<div class="md-tab-label-pagination before md-elevation-z4" |
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'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" |
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.
class.md-tab-header-disabled
?
|
||
export type ScrollDirection = 'after' | 'before'; | ||
|
||
@Component({ |
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.
Needs a class description.
return this._dir && this._dir.value === 'rtl' ? 'rtl' : 'ltr'; | ||
} | ||
|
||
_updateScrollPosition() { |
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.
These scrolling methods could use some descriptions
} | ||
|
||
this._renderer.setElementStyle(this._tabList.nativeElement, | ||
'transform', `translate3d(${translateX}, 0, 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.
See the applyCssTransform
function
this.scrollDistance -= viewLength / 3; | ||
} else { | ||
this.scrollDistance += viewLength / 3; | ||
} |
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.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; |
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.
Make this a file-level constant?
const selectedLabel = this._labelWrappers.toArray()[labelIndex]; | ||
const viewLength = this._tabListContainer.nativeElement.offsetWidth; | ||
|
||
let labelBeforePos: number, labelAfterPos: 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.
Comment explaining what these variables contain?
1237037
to
cfcbdbc
Compare
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. |
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, 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 |
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.
No need for TODO: internal
any more
875a8e4
to
4ee83d9
Compare
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 |
Property {'_disableScrollBefore', '_disableScrollAfter', '_handleKeydown'} is private and only accessible within class 'MdTabHeader' |
5e5122f
to
1b29725
Compare
Okay, should be good to merge! |
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
No description provided.