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(top-app-bar): add default scroll behavior #2417

Merged
merged 26 commits into from
Apr 6, 2018

Conversation

williamernest
Copy link
Contributor

@williamernest williamernest commented Mar 16, 2018

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

@codecov-io
Copy link

codecov-io commented Mar 16, 2018

Codecov Report

Merging #2417 into master will decrease coverage by 0.02%.
The diff coverage is 97.67%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
packages/mdc-top-app-bar/short/foundation.js 100% <ø> (ø) ⬆️
packages/mdc-top-app-bar/constants.js 100% <100%> (ø) ⬆️
packages/mdc-top-app-bar/standard/foundation.js 100% <100%> (ø)
packages/mdc-top-app-bar/index.js 100% <100%> (ø) ⬆️
packages/mdc-top-app-bar/foundation.js 93.1% <75%> (-6.9%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update afe5367...ce71bba. Read the comment docs.

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

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

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

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?

Copy link
Contributor Author

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

@moog16 moog16 Mar 16, 2018

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

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

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

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

getViewportScrollY: () => window.pageYOffset,
getTotalActionItems: () =>
this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR).length,

Copy link
Contributor

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

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

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


shortCheckbox.disabled = this.checked;

// Needs to reset the toolbar height for the default scroll behavior
appBar.destroy();
Copy link
Contributor

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.

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

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

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

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.
*/

/**
Copy link
Contributor

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});
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 you can omit the {times:1} since it is the default.

Copy link
Contributor

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.

https://github.com/testdouble/testdouble.js/blob/master/docs/6-verifying-invocations.md#times

assert.isTrue(foundation.topAppBarHeight_ === 56);
});

test('top app bar scroll: throttledResizeHandler_ does not update topAppBarHeight_ if height is the same', () => {
Copy link
Contributor

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?

Copy link
Contributor Author

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

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.

Copy link
Contributor

@moog16 moog16 left a 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

Copy link
Contributor

@acdvorak acdvorak left a 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,
Copy link
Contributor

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

@acdvorak acdvorak Mar 19, 2018

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

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

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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

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

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

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

@acdvorak acdvorak Mar 28, 2018

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;
}

@williamernest williamernest force-pushed the feat/top-app-bar/add-default-scroll-behavior branch from f78e542 to e0004c0 Compare March 28, 2018 22:12
Copy link
Contributor

@acdvorak acdvorak left a 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!

@@ -47,6 +47,19 @@ class MDCTopAppBarAdapter {
*/
hasClass(className) {}

/**
* Adds the specified attribute and value to the root Element.
Copy link
Contributor

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.

* @param {string} attribute
* @param {string} value
*/
setStyle(attribute, value) {}
Copy link
Contributor

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

(See https://css-tricks.com/css-ruleset-terminology/)

@@ -43,11 +48,15 @@ class MDCTopAppBarFoundation extends MDCFoundation {
hasClass: (/* className: string */) => {},
addClass: (/* className: string */) => {},
removeClass: (/* className: string */) => {},
setStyle: (/* attribute: string, value: string */) => {},
Copy link
Contributor

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

(See https://css-tricks.com/css-ruleset-terminology/)

* Variable for current scroll position of the top app bar
* @private {number}
*/
this.currentAppBarScrollPosition_ = 0;
Copy link
Contributor

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

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

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

Choose a reason for hiding this comment

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

Nit: Magic constants

@williamernest williamernest merged commit 18be342 into master Apr 6, 2018
@williamernest williamernest deleted the feat/top-app-bar/add-default-scroll-behavior branch April 6, 2018 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants