-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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(top-app-bar): add default scroll behavior #2417
feat(top-app-bar): add default scroll behavior #2417
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2417 +/- ##
==========================================
- Coverage 98.64% 98.62% -0.03%
==========================================
Files 103 104 +1
Lines 4143 4226 +83
Branches 518 530 +12
==========================================
+ Hits 4087 4168 +81
- Misses 56 58 +2
Continue to review full report at Codecov.
|
// Used to verify when the top app bar is completely showing or completely hidden | ||
/** @private {number} */ | ||
this.topAppBarHeight_ = this.adapter_.getTopAppBarHeight(); | ||
// isDocked_ is used to indicate if the top app bar is 100% showing or hidden |
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.
Is this calculated off of topAppBarHeight? The 2 comments are very similar
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, the isDocked_
variable is used to determine if the top app bar is already docked. The topAppBarHeight_
and the currentAppBarScrollPosition_
is what we use to set that value.
this.resizeThrottleId_ = -1; | ||
// The timeout that's used to debounce toggling the isCurrentlyBeingResized_ variable after a resize | ||
/** @private {number} */ | ||
this.resizeDebounceId_ = -1; | ||
|
||
this.navClickHandler_ = () => this.adapter_.notifyNavigationIconClicked(); | ||
this.scrollHandler_ = () => this.shortAppBarScrollHandler_(); |
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 it makes more sense to rename defaultScrollHandler to scrollHandler, and scrollHandler to shortScrollHandler.
const fullyShown = this.currentAppBarScrollPosition_ === -this.topAppBarHeight_; | ||
let updateRequired = false; | ||
|
||
if (this.isDocked_) { |
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.
Is isDocked the same as fullyShown?
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.
In short, no.
fullyShown
and fullyHidden
refer to the actual location of the top app bar. isDocked_
refers to wether the previous state was fullyShown
or fullyHidden
. It's used to track when the top app bar reaches each terminal state (100% on/off screen) so we can ignore DOM updates if they don't change anything.
} | ||
|
||
/** | ||
* Used to set the initial style of the short top app bar | ||
* Used to set the initial style of the short top app bar. |
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.
add @Private
@@ -105,16 +144,128 @@ class MDCTopAppBarFoundation extends MDCFoundation { | |||
const currentScroll = this.adapter_.getViewportScrollY(); |
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.
add @Private to this method too
this.resizeDebounceId_ = setTimeout((function() { | ||
this.topAppBarScrollHandler_(); | ||
this.isCurrentlyBeingResized_ = false; | ||
}).bind(this), 100); |
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 the 100ms a constant
this.resizeThrottleId_ = setTimeout((function() { | ||
this.resizeThrottleId_ = -1; | ||
this.throttledResizeHandler_(); | ||
}).bind(this), 100); |
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 the 100ms a constant
packages/mdc-top-app-bar/index.js
Outdated
getViewportScrollY: () => window.pageYOffset, | ||
getTotalActionItems: () => | ||
this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR).length, | ||
|
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.
remove line
}); | ||
|
||
test('top app bar : resize events should set isCurrentlyBeingResized_ to true', () => { | ||
// const clock = lolex.install(); |
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.
remove
resizeHandler(); | ||
assert.isFalse(foundation.resizeThrottleId_ === -1); | ||
resizeHandler(); | ||
clock.tick(101); |
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 can probably use that constant here that you create
demos/top-app-bar.html
Outdated
|
||
shortCheckbox.disabled = this.checked; | ||
|
||
// Needs to reset the toolbar height for the default scroll behavior | ||
appBar.destroy(); |
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.
Would this be a good case for a layout
method or something to reset the scrolling? I can't think of a use case outside of the demo pages for it though.
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 only required for the demo page since we're showcasing several different toolbars on a single page.
|
||
if (this.isDocked_) { | ||
// If it's already docked and the user is still scrolling in the same direction, do nothing | ||
if (!(fullyShown || fullyHidden)) { |
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.
instead of doing 2 variables fullyshown and fullyhidden, could you do
partiallyShowing = this.currentAppBarScrollPosition > 0 && this.currentAppBarScrollPosition < -this.topAppBarHeight_;
export {strings, cssClasses}; | ||
/** @enum {number} */ | ||
const numbers = { | ||
MAX_TOP_APP_BAR_HEIGHT: 128, |
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.
swap these for alpha order
let updateRequired = false; | ||
|
||
if (this.isDocked_) { | ||
// If it was previously already docked but now is partially showing, it's no longer docked |
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.
end in .
* limitations under the License. | ||
*/ | ||
|
||
/** |
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.
Looks like you have 2 licenses in here. You probably want to delete past this comment,
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); | ||
foundation.init(); | ||
foundation.destroy(); | ||
td.verify(mockAdapter.deregisterScrollHandler(td.matchers.isA(Function)), {times: 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.
I think you can omit the {times:1} since it is the 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.
Judging by td's docs, times: 1 is not the default; the default behavior passes if it was called > 0 times.
assert.isTrue(foundation.topAppBarHeight_ === 56); | ||
}); | ||
|
||
test('top app bar scroll: throttledResizeHandler_ does not update topAppBarHeight_ if height is the same', () => { |
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 test is essentially testing the same thing as the above test. Does this cover another branch that the above does not?
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, this is testing two different branches.
foundation.currentAppBarScrollPosition_ = -1; // Indicates 1px scrolled up | ||
foundation.isUpdateRequired_ = () => true; | ||
foundation.moveTopAppBar_(); | ||
td.verify(mockAdapter.setStyle('top', '-1px'), {times: 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.
If we decide to remove {times:1}, then remove this too.
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.
Looks good to me
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 still need to review the logic in packages/mdc-top-app-bar/standard/foundation.js
/** @enum {number} */ | ||
const numbers = { | ||
MAX_TOP_APP_BAR_HEIGHT: 128, | ||
DEBOUNCE_THROTTLE_RESIZE_TIME: 100, |
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.
For time values stored as integers, it's usually a good idea to include the unit of measurement in the variable name.
E.g., DEBOUNCE_THROTTLE_RESIZE_TIME_MS
*/ | ||
constructor(adapter) { | ||
super(adapter); | ||
// Used for diffs of current scroll position vs previous scroll position |
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: Combine these two comment lines, and add a blank line before the comment.
Ditto below.
E.g.:
super(adapter);
/**
* Used for diffs of current scroll position vs previous scroll position
* @private {number}
*/
this.lastScrollPosition_ = this.adapter_.getViewportScrollY();
/**
* Used to verify when the top app bar is completely showing or completely hidden
* @private {number}
*/
this.topAppBarHeight_ = this.adapter_.getTopAppBarHeight();
...
super.destroy(); | ||
this.adapter_.deregisterScrollHandler(this.scrollHandler_); | ||
this.adapter_.deregisterResizeHandler(this.resizeHandler_); | ||
this.adapter_.setStyle('top', 'auto'); |
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 might be better to clear this inline style instead of giving it an explicit value of auto
.
E.g., setStyle('top', '')
That way, it will revert to whatever was set by the baseline CSS classes, and we won't have to keep the values in sync between JS and Sass.
/** | ||
* Function to determine if the DOM needs to update. | ||
* @private | ||
* @returns {boolean} |
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.
@return
(no trailing s
).
IntelliJ auto-generates JSDocs with a trailing s
on @return
for some reason, and I can't find any way to change it.
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.
Nice catch! After some research, I found that it uses the last one you chose when you were presented with the dropdown. Start typing @retur
and select @return
from the dropdown and the next time it generates the JSDoc comments for you it'll use @return
.
Also, JSDoc default is @returns
, but it accepts @return
as a synonym.
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.
Ahh that explains it! Thanks for solving this mystery for me!
|
||
/** | ||
* Function to determine if the DOM needs to update. | ||
* @private |
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: @private
is usually the last annotation, after @param
and @return
.
if (this.isDocked_) { | ||
// If it was previously already docked but now is partially showing, it's no longer docked. | ||
if (partiallyShowing) { | ||
this.isDocked_ = false; |
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 feels a little weird to set the value of an instance variable inside a getter method.
It's not obvious that calling isUpdateRequired_()
will have side effects.
Could we structure this a little differently and have isDocked_
be a get
property or something? So that it always returns the most up-to-date value (because it recomputes the value every time it's called).
Or, could we rename this method to something like checkForUpdate_()
, so that it doesn't sound like a pure getter?
WDYT?
// If it was previously already docked but now is partially showing, it's no longer docked. | ||
if (partiallyShowing) { | ||
this.isDocked_ = false; | ||
updateRequired = 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.
Can we just return true
here and remove the updateRequired
variable?
* @return {boolean} | ||
* @private | ||
*/ | ||
checkForUpdate_() { |
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.
Some of the logic is a little hard to mentally parse cuz it's so abstract.
I'm wondering if adding some local variables might make it easier to grok (and maintain).
E.g.:
checkForUpdate_() {
const offscreenBoundaryTop = -this.topAppBarHeight_;
const hasAnyPixelsOffscreen = this.currentAppBarScrollPosition_ < 0;
const hasAnyPixelsOnscreen = this.currentAppBarScrollPosition_ > offscreenBoundaryTop;
const partiallyShowing = hasAnyPixelsOffscreen && hasAnyPixelsOnscreen;
// If it was previously already docked but now is partially showing, it's no longer docked.
if (this.wasDocked_ && partiallyShowing) {
this.wasDocked_ = false;
}
// If it's not previously docked and not partially showing, it just became docked.
else if (!this.wasDocked_ && !partiallyShowing) {
this.wasDocked_ = true;
}
return partiallyShowing;
}
f78e542
to
e0004c0
Compare
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.
Just a few nits, otherwise LGTM!
packages/mdc-top-app-bar/adapter.js
Outdated
@@ -47,6 +47,19 @@ class MDCTopAppBarAdapter { | |||
*/ | |||
hasClass(className) {} | |||
|
|||
/** | |||
* Adds the specified attribute and value to the root Element. |
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: Update comment to something like:
Sets the specified inline style property on the root Element to the given value.
packages/mdc-top-app-bar/adapter.js
Outdated
* @param {string} attribute | ||
* @param {string} value | ||
*/ | ||
setStyle(attribute, value) {} |
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: Rename attribute
to property
@@ -43,11 +48,15 @@ class MDCTopAppBarFoundation extends MDCFoundation { | |||
hasClass: (/* className: string */) => {}, | |||
addClass: (/* className: string */) => {}, | |||
removeClass: (/* className: string */) => {}, | |||
setStyle: (/* attribute: string, value: string */) => {}, |
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: Rename attribute
to property
* Variable for current scroll position of the top app bar | ||
* @private {number} | ||
*/ | ||
this.currentAppBarScrollPosition_ = 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.
Nit: This variable name is a little confusing to me. Can we rename it to something that conveys its use and purpose?
E.g., currentAppBarOffsetTop_
* The timeout that's used to throttle the resize events | ||
* @private {number} | ||
*/ | ||
this.resizeThrottleId_ = -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.
Nit: Create a file-level constant for this "magic number" (ditto for all other inline -1
values below)
if (!this.wasDocked_) { | ||
this.wasDocked_ = true; | ||
return true; | ||
} else { |
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.
Nitty: Combine this else
with the if
statement directly below it.
E.g.:
} else if (this.isDockedShowing_...) {
*/ | ||
topAppBarResizeHandler_() { | ||
// Throttle resize events 10 p/s | ||
if (this.resizeThrottleId_ === -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.
Nit: Magic constants
Fixes: #2424
Adds the default scrolling behavior of the top app bar (scrolls off the screen when scrolling down, first thing to appear when scrolling up).