diff --git a/demos/top-app-bar.html b/demos/top-app-bar.html index 95988e62f7e..2b28c32e438 100644 --- a/demos/top-app-bar.html +++ b/demos/top-app-bar.html @@ -26,17 +26,15 @@ - +
- menu - Title + menu + San Francisco, CA
-
- file_download - print - bookmark +
+ file_download
@@ -81,7 +79,7 @@ -
+

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. @@ -119,25 +117,113 @@

Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est.

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+

+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. Donec eu libero sit amet quam egestas semper. Aenean ultricies mi vitae est. +

+
+
+

Demo Controls

+
+ + +
+
+ + +
+
+ + +
+
+ diff --git a/demos/top-app-bar.scss b/demos/top-app-bar.scss index 698d4f36ba3..c2153aa88c3 100644 --- a/demos/top-app-bar.scss +++ b/demos/top-app-bar.scss @@ -29,3 +29,25 @@ padding: 16px; } } + +.demo-body { + padding: 0; + margin: 0; + box-sizing: border-box; +} + +.demo-main { + padding-left: 16px; + padding-right: 16px; + padding-bottom: 16px; + overflow: auto; +} + +.demo-controls-container { + @include mdc-elevation(4); + position: fixed; + bottom: 0; + right: 0; + background-color: white; + padding: 10px; +} diff --git a/packages/mdc-top-app-bar/README.md b/packages/mdc-top-app-bar/README.md index f90425b997b..2326f5b72d0 100644 --- a/packages/mdc-top-app-bar/README.md +++ b/packages/mdc-top-app-bar/README.md @@ -1,7 +1,6 @@ # Top App Bar -MDC Top App Bar acts as a container for items such as application title, navigation menu, and action items, among other -things. Top app bars scroll with content by default. +MDC Top App Bar acts as a container for items such as application title, navigation icon, and action items. Top app bars scroll with content by default. ## Design & API Documentation @@ -28,26 +27,42 @@ npm install --save @material/top-app-bar
- menu + menu Title
``` -Top app bars can accommodate multiple icons on the right: +Top app bars can accommodate multiple action items on the opposite side of the navigation icon: ```html
+
+``` + +Short top app bars should only be used with one action item: + +```html +
+
+
+ menu + Title +
+
@@ -70,16 +85,19 @@ Top app bars can accommodate multiple icons on the right: Class | Description --- | --- -`mdc-top-app-bar` | Mandatory +`mdc-top-app-bar` | Mandatory. +`mdc-top-app-bar--short` | Class used to style the top app bar as a short top app bar. +`mdc-top-app-bar--short-collapsed` | Class used to indicate the short top app bar is collapsed. ### Sass Mixins Mixin | Description --- | --- -`mdc-top-app-bar-ink-color($color)` | Sets the ink color of the top app bar -`mdc-top-app-bar-icon-ink-color($color)` | Sets the ink color of the top app bar icons -`mdc-top-app-bar-fill-color($color)` | Sets the fill color of the top app bar -`mdc-top-app-bar-fill-color-accessible($color)` | Sets the fill color of the top app bar and automatically sets a high-contrast ink color +`mdc-top-app-bar-ink-color($color)` | Sets the ink color of the top app bar. +`mdc-top-app-bar-icon-ink-color($color)` | Sets the ink color of the top app bar icons. +`mdc-top-app-bar-fill-color($color)` | Sets the fill color of the top app bar. +`mdc-top-app-bar-fill-color-accessible($color)` | Sets the fill color of the top app bar and automatically sets a high-contrast ink color. +`mdc-top-app-bar-short-border-radius($border-radius)` | Sets the `border-bottom-radius` property on the action item side. Used only for the short top app bar when collapsed. ### `MDCTopAppBar` @@ -95,6 +113,10 @@ Method Signature | Description `registerNavigationIconInteractionHandler(evtType: string, handler: EventListener) => void` | Registers an event listener on the native navigation icon element for a given event. `deregisterNavigationIconInteractionHandler(evtType: string, handler: EventListener) => void` | Deregisters an event listener on the native navigation icon element for a given event. `notifyNavigationIconClicked() => void` | Emits a custom event `MDCTopAppBar:nav` when the navigation icon is clicked. +`registerScrollHandler(handler) => void` | Registers a handler to be called when user scrolls. Our default implementation adds the handler as a listener to the window's `scroll` event. +`deregisterScrollHandler(handler) => void` | Unregisters a handler to be called when user scrolls. Our default implementation removes the handler as a listener to the window's `scroll` event. +`getViewportScrollY() => number` | Gets the number of pixels that the content of body is scrolled from the top of the page. +`getTotalActionItems() => number` | Gets the number of action items in the top app bar. ### Events diff --git a/packages/mdc-top-app-bar/_mixins.scss b/packages/mdc-top-app-bar/_mixins.scss index 2c28ab47dec..2ce5949e8e4 100644 --- a/packages/mdc-top-app-bar/_mixins.scss +++ b/packages/mdc-top-app-bar/_mixins.scss @@ -14,9 +14,10 @@ // limitations under the License. // +@import "@material/animation/variables"; +@import "@material/ripple/mixins"; @import "@material/theme/variables"; // for mdc-theme-accessible-ink-color @import "@material/theme/mixins"; -@import "@material/ripple/mixins"; @import "./variables"; // @@ -37,19 +38,57 @@ } @mixin mdc-top-app-bar-icon-ink-color($color) { - .mdc-top-app-bar__icon, - .mdc-top-app-bar__menu-icon { + .mdc-top-app-bar__action-item, + .mdc-top-app-bar__navigation-icon { @include mdc-theme-prop(color, $color); @include mdc-states($color); } } +@mixin mdc-top-app-bar-short-border-radius($border-radius: $mdc-top-app-bar-short-collapsed-border-radius) { + @include mdc-rtl-reflexive_(border-bottom-left-radius, 0, border-bottom-right-radius, $mdc-top-app-bar-short-collapsed-border-radius); +} + +@mixin mdc-top-app-bar-mobile-breakpoint_($mobile-breakpoint: $mdc-top-app-bar-mobile-breakpoint) { + @media (max-width: $mobile-breakpoint) { + .mdc-top-app-bar { + .mdc-top-app-bar__row { + min-height: $mdc-top-app-bar-mobile-row-height; + } + + .mdc-top-app-bar__section { + padding: $mdc-top-app-bar-mobile-section-padding; + } + + .mdc-top-app-bar__title { + @include mdc-rtl-reflexive-box(padding, left, $mdc-top-app-bar-mobile-title-left-padding); + } + } + + .mdc-top-app-bar--short { + transition: width 200ms $mdc-animation-standard-curve-timing-function; + + .mdc-top-app-bar__row { + min-height: $mdc-top-app-bar-mobile-row-height; + } + } + + .mdc-top-app-bar--short-collapsed { + transition: width 250ms $mdc-animation-standard-curve-timing-function; + + .mdc-top-app-bar__section--align-end { + @include mdc-rtl-reflexive-box(padding, right, $mdc-top-app-bar-short-collapsed-right-icon-padding); + } + } + } +} + // // Private // // Applies styles to the different types of icons that can exist in top-app-bars. -// Both .mdc-top-app-bar__icon and .mdc-top-app-bar__menu-icon share all styles except for +// Both .mdc-top-app-bar__icon and .mdc-top-app-bar__navigation-icon share all styles except for // horizontal padding. @mixin mdc-top-app-bar-icon_() { @include mdc-ripple-surface; @@ -57,6 +96,7 @@ display: flex; position: relative; + flex-shrink: 0; align-items: center; justify-content: center; width: $mdc-top-app-bar-icon-size; diff --git a/packages/mdc-top-app-bar/_variables.scss b/packages/mdc-top-app-bar/_variables.scss index 6b53bae5660..d56450955e7 100644 --- a/packages/mdc-top-app-bar/_variables.scss +++ b/packages/mdc-top-app-bar/_variables.scss @@ -21,10 +21,14 @@ $mdc-top-app-bar-section-horizontal-padding: 12px; $mdc-top-app-bar-icon-padding: 12px; -$mdc-top-app-bar-mobile-breakpoint: 599px; +$mdc-top-app-bar-mobile-breakpoint: 599px !default; $mdc-top-app-bar-mobile-row-height: 56px; $mdc-top-app-bar-mobile-title-left-padding: 12px; $mdc-top-app-bar-mobile-section-padding: 4px; $mdc-top-app-bar-icon-size: 24px; + +$mdc-top-app-bar-short-collapsed-border-radius: 10px; +$mdc-top-app-bar-short-collapsed-width: 56px; +$mdc-top-app-bar-short-collapsed-right-icon-padding: 12px; diff --git a/packages/mdc-top-app-bar/adapter.js b/packages/mdc-top-app-bar/adapter.js index b4b3c793bc2..6b63e016613 100644 --- a/packages/mdc-top-app-bar/adapter.js +++ b/packages/mdc-top-app-bar/adapter.js @@ -47,7 +47,6 @@ class MDCTopAppBarAdapter { */ hasClass(className) {} - /** * Registers an event handler on the navigation icon element for a given event. * @param {string} type @@ -66,6 +65,18 @@ class MDCTopAppBarAdapter { * Emits an event when the navigation icon is clicked. */ notifyNavigationIconClicked() {} + + /** @param {function(!Event)} handler */ + registerScrollHandler(handler) {} + + /** @param {function(!Event)} handler */ + deregisterScrollHandler(handler) {} + + /** @return {number} */ + getViewportScrollY() {} + + /** @return {number} */ + getTotalActionItems() {} } export default MDCTopAppBarAdapter; diff --git a/packages/mdc-top-app-bar/constants.js b/packages/mdc-top-app-bar/constants.js index 5df19786358..c846c3944c1 100644 --- a/packages/mdc-top-app-bar/constants.js +++ b/packages/mdc-top-app-bar/constants.js @@ -18,9 +18,17 @@ /** @enum {string} */ const strings = { NAVIGATION_EVENT: 'MDCTopAppBar:nav', + ROOT_SELECTOR: '.mdc-top-app-bar', TITLE_SELECTOR: '.mdc-top-app-bar__title', - MENU_ICON_SELECTOR: '.mdc-top-app-bar__menu-icon', - ACTION_ICON_SELECTOR: '.mdc-top-app-bar__icon', + NAVIGATION_ICON_SELECTOR: '.mdc-top-app-bar__navigation-icon', + ACTION_ITEM_SELECTOR: '.mdc-top-app-bar__action-item', }; -export {strings}; +/** @enum {string} */ +const cssClasses = { + SHORT_CLASS: 'mdc-top-app-bar--short', + SHORT_HAS_ACTION_ITEM_CLASS: 'mdc-top-app-bar--short-has-action-item', + SHORT_COLLAPSED_CLASS: 'mdc-top-app-bar--short-collapsed', +}; + +export {strings, cssClasses}; diff --git a/packages/mdc-top-app-bar/foundation.js b/packages/mdc-top-app-bar/foundation.js index f75f64537b2..99b6068ee95 100644 --- a/packages/mdc-top-app-bar/foundation.js +++ b/packages/mdc-top-app-bar/foundation.js @@ -15,10 +15,8 @@ * limitations under the License. */ -import {strings} from './constants'; - +import {strings, cssClasses} from './constants'; import MDCTopAppBarAdapter from './adapter'; - import MDCFoundation from '@material/base/foundation'; /** @@ -31,6 +29,11 @@ class MDCTopAppBarFoundation extends MDCFoundation { return strings; } + /** @return enum {string} */ + static get cssClasses() { + return cssClasses; + } + /** * {@see MDCTopAppBarAdapter} for typing information on parameters and return * types. @@ -44,6 +47,10 @@ class MDCTopAppBarFoundation extends MDCFoundation { registerNavigationIconInteractionHandler: (/* type: string, handler: EventListener */) => {}, deregisterNavigationIconInteractionHandler: (/* type: string, handler: EventListener */) => {}, notifyNavigationIconClicked: () => {}, + registerScrollHandler: (/* handler: EventListener */) => {}, + deregisterScrollHandler: (/* handler: EventListener */) => {}, + getViewportScrollY: () => /* number */ 0, + getTotalActionItems: () => /* number */ 0, }); } @@ -52,16 +59,56 @@ class MDCTopAppBarFoundation extends MDCFoundation { */ constructor(adapter) { super(Object.assign(MDCTopAppBarFoundation.defaultAdapter, adapter)); + // State variable for the current top app bar state + this.isCollapsed = false; this.navClickHandler_ = () => this.adapter_.notifyNavigationIconClicked(); + this.scrollHandler_ = () => this.shortAppBarScrollHandler_(); } init() { this.adapter_.registerNavigationIconInteractionHandler('click', this.navClickHandler_); + + const isShortTopAppBar = this.adapter_.hasClass(cssClasses.SHORT_CLASS); + + if (isShortTopAppBar) { + this.adapter_.registerScrollHandler(this.scrollHandler_); + this.initShortAppBar_(); + } } destroy() { this.adapter_.deregisterNavigationIconInteractionHandler('click', this.navClickHandler_); + this.adapter_.deregisterScrollHandler(this.scrollHandler_); + } + + /** + * Used to set the initial style of the short top app bar + */ + initShortAppBar_() { + if (this.adapter_.getTotalActionItems() > 0) { + this.adapter_.addClass(cssClasses.SHORT_HAS_ACTION_ITEM_CLASS); + } + } + + /** + * Scroll handler for applying/removing the closed modifier class + * on the short top app bar. + */ + shortAppBarScrollHandler_() { + const currentScroll = this.adapter_.getViewportScrollY(); + + if (currentScroll <= 0) { + if (this.isCollapsed) { + this.adapter_.removeClass(cssClasses.SHORT_COLLAPSED_CLASS); + this.isCollapsed = false; + } + } else { + if (!this.isCollapsed) { + this.adapter_.addClass(cssClasses.SHORT_COLLAPSED_CLASS); + this.isCollapsed = true; + } + } } } diff --git a/packages/mdc-top-app-bar/index.js b/packages/mdc-top-app-bar/index.js index 42c3f35847a..0bd9bbdf226 100644 --- a/packages/mdc-top-app-bar/index.js +++ b/packages/mdc-top-app-bar/index.js @@ -20,6 +20,7 @@ import MDCTopAppBarFoundation from './foundation'; import MDCComponent from '@material/base/component'; import {MDCRipple} from '@material/ripple/index'; import {strings} from './constants'; +import * as util from './util'; /** * @extends {MDCComponent} @@ -39,10 +40,10 @@ class MDCTopAppBar extends MDCComponent { initialize( rippleFactory = (el) => MDCRipple.attachTo(el)) { - this.navIcon_ = this.root_.querySelector(strings.MENU_ICON_SELECTOR); + this.navIcon_ = this.root_.querySelector(strings.NAVIGATION_ICON_SELECTOR); // Get all icons in the toolbar and instantiate the ripples - const icons = [].slice.call(this.root_.querySelectorAll(strings.ACTION_ICON_SELECTOR)); + const icons = [].slice.call(this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR)); icons.push(this.navIcon_); this.iconRipples_ = icons.map((icon) => { @@ -54,6 +55,7 @@ class MDCTopAppBar extends MDCComponent { destroy() { this.iconRipples_.forEach((iconRipple) => iconRipple.destroy()); + super.destroy(); } /** @@ -86,10 +88,15 @@ class MDCTopAppBar extends MDCComponent { notifyNavigationIconClicked: () => { this.emit(strings.NAVIGATION_EVENT, {}); }, + registerScrollHandler: (handler) => window.addEventListener('scroll', handler, util.applyPassive()), + deregisterScrollHandler: (handler) => window.removeEventListener('scroll', handler), + getViewportScrollY: () => window.pageYOffset, + getTotalActionItems: () => + this.root_.querySelectorAll(strings.ACTION_ITEM_SELECTOR).length, }) ) ); } } -export {MDCTopAppBar, MDCTopAppBarFoundation}; +export {MDCTopAppBar, MDCTopAppBarFoundation, util}; diff --git a/packages/mdc-top-app-bar/mdc-top-app-bar.scss b/packages/mdc-top-app-bar/mdc-top-app-bar.scss index 90a8b53d019..add28743c9c 100644 --- a/packages/mdc-top-app-bar/mdc-top-app-bar.scss +++ b/packages/mdc-top-app-bar/mdc-top-app-bar.scss @@ -14,6 +14,8 @@ // limitations under the License. // +@import "@material/elevation/mixins"; +@import "@material/animation/variables"; @import "@material/rtl/mixins"; @import "@material/typography/mixins"; @import "./mixins"; @@ -42,7 +44,7 @@ &__section { display: inline-flex; - flex: 1; + flex: 1 1 auto; align-items: center; min-width: 0; padding: $mdc-top-app-bar-section-vertical-padding $mdc-top-app-bar-section-horizontal-padding; @@ -61,27 +63,72 @@ &__title { @include mdc-typography(title); - @include mdc-rtl-reflexive-box(margin, left, $mdc-top-app-bar-title-left-padding); + @include mdc-rtl-reflexive-box(padding, left, $mdc-top-app-bar-title-left-padding); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; z-index: 1; } - &__icon, - &__menu-icon { + &__action-item, + &__navigation-icon { @include mdc-top-app-bar-icon_; } +} - @media (max-width: $mdc-top-app-bar-mobile-breakpoint) { - .mdc-top-app-bar__row { - min-height: $mdc-top-app-bar-mobile-row-height; - } +.mdc-top-app-bar--short { + position: fixed; + top: 0; + right: auto; + left: 0; + width: 100%; + transition: width 250ms $mdc-animation-standard-curve-timing-function; + z-index: 4; - .mdc-top-app-bar__section { - padding: $mdc-top-app-bar-mobile-section-padding; - } + @include mdc-rtl { + right: 0; + left: auto; + } - .mdc-top-app-bar__title { - @include mdc-rtl-reflexive-box(margin, left, $mdc-top-app-bar-mobile-title-left-padding); - } + .mdc-top-app-bar__row { + min-height: $mdc-top-app-bar-mobile-row-height; + } + + .mdc-top-app-bar__section { + padding: $mdc-top-app-bar-mobile-section-padding; + } + + .mdc-top-app-bar__title { + transition: opacity 200ms $mdc-animation-standard-curve-timing-function; + opacity: 1; + } +} + +.mdc-top-app-bar--short-collapsed { + @include mdc-top-app-bar-short-border-radius; + @include mdc-elevation(4); + + width: $mdc-top-app-bar-short-collapsed-width; + transition: width 300ms $mdc-animation-standard-curve-timing-function; + + .mdc-top-app-bar__title { + display: none; + } + + .mdc-top-app-bar__action-item { + transition: padding 150ms $mdc-animation-standard-curve-timing-function; } } + +// stylelint-disable-next-line plugin/selector-bem-pattern +.mdc-top-app-bar--short-collapsed.mdc-top-app-bar--short-has-action-item { + width: $mdc-top-app-bar-short-collapsed-width * 2; + + .mdc-top-app-bar__section--align-end { + @include mdc-rtl-reflexive-box(padding, right, 12px); + } +} + +// Mobile Styles +@include mdc-top-app-bar-mobile-breakpoint_; diff --git a/packages/mdc-top-app-bar/package.json b/packages/mdc-top-app-bar/package.json index 5d9ea017e32..e3b95d93a72 100644 --- a/packages/mdc-top-app-bar/package.json +++ b/packages/mdc-top-app-bar/package.json @@ -15,7 +15,9 @@ "topappbar" ], "dependencies": { + "@material/animation": "^0.25.0", "@material/base": "^0.29.0", + "@material/elevation": "^0.28.0", "@material/ripple": "^0.31.0", "@material/rtl": "^0.30.0", "@material/theme": "^0.30.0", diff --git a/packages/mdc-top-app-bar/util.js b/packages/mdc-top-app-bar/util.js new file mode 100644 index 00000000000..cc845c1ce3e --- /dev/null +++ b/packages/mdc-top-app-bar/util.js @@ -0,0 +1,40 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +let supportsPassive_; + +/** + * Determine whether the current browser supports passive event listeners, and if so, use them. + * @param {!Window=} globalObj + * @param {boolean=} forceRefresh + * @return {boolean|{passive: boolean}} + */ +function applyPassive(globalObj = window, forceRefresh = false) { + if (supportsPassive_ === undefined || forceRefresh) { + let isSupported = false; + try { + globalObj.document.addEventListener('test', null, {get passive() { + isSupported = true; + }}); + } catch (e) { } + + supportsPassive_ = isSupported; + } + + return supportsPassive_ ? {passive: true} : false; +} + +export {applyPassive}; diff --git a/test/unit/mdc-top-app-bar/foundation.test.js b/test/unit/mdc-top-app-bar/foundation.test.js index c12a689e660..6109c777536 100644 --- a/test/unit/mdc-top-app-bar/foundation.test.js +++ b/test/unit/mdc-top-app-bar/foundation.test.js @@ -20,7 +20,8 @@ import {captureHandlers} from '../helpers/foundation'; import {verifyDefaultAdapter} from '../helpers/foundation'; import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation'; -import {strings} from '../../../packages/mdc-top-app-bar/constants'; +import {strings, cssClasses} from '../../../packages/mdc-top-app-bar/constants'; +import {createMockRaf} from '../helpers/raf'; suite('MDCTopAppBarFoundation'); @@ -29,10 +30,16 @@ test('exports strings', () => { assert.deepEqual(MDCTopAppBarFoundation.strings, strings); }); +test('exports cssClasses', () => { + assert.isTrue('cssClasses' in MDCTopAppBarFoundation); + assert.deepEqual(MDCTopAppBarFoundation.cssClasses, cssClasses); +}); + test('defaultAdapter returns a complete adapter implementation', () => { verifyDefaultAdapter(MDCTopAppBarFoundation, [ 'hasClass', 'addClass', 'removeClass', 'registerNavigationIconInteractionHandler', - 'deregisterNavigationIconInteractionHandler', 'notifyNavigationIconClicked', + 'deregisterNavigationIconInteractionHandler', 'notifyNavigationIconClicked', 'registerScrollHandler', + 'deregisterScrollHandler', 'getViewportScrollY', 'getTotalActionItems', ]); }); @@ -44,7 +51,19 @@ const setupTest = () => { return {foundation, mockAdapter}; }; -test('on click emits navigation icon event', () => { +const createMockHandlers = (foundation, mockAdapter, mockRaf) => { + let scrollHandler; + td.when(mockAdapter.registerScrollHandler(td.matchers.isA(Function))).thenDo((fn) => { + scrollHandler = fn; + }); + + foundation.init(); + mockRaf.flush(); + td.reset(); + return {scrollHandler}; +}; + +test('click on navigation icon emits a navigation event', () => { const {foundation, mockAdapter} = setupTest(); const handlers = captureHandlers(mockAdapter, 'registerNavigationIconInteractionHandler'); foundation.init(); @@ -58,3 +77,68 @@ test('click handler removed from navigation icon during destroy', () => { foundation.destroy(); td.verify(mockAdapter.deregisterNavigationIconInteractionHandler('click', td.matchers.isA(Function))); }); + +test('short top app bar: scroll listener is registered on init', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); + foundation.init(); + td.verify(mockAdapter.registerScrollHandler(td.matchers.isA(Function)), {times: 1}); +}); + +test('short top app bar: scroll listener is removed on destroy', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); + foundation.init(); + foundation.destroy(); + td.verify(mockAdapter.deregisterScrollHandler(td.matchers.isA(Function)), {times: 1}); +}); + +test('short top app bar: class is added once when page is scrolled from the top', () => { + const {foundation, mockAdapter} = setupTest(); + const mockRaf = createMockRaf(); + + td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); + + const {scrollHandler} = createMockHandlers(foundation, mockAdapter, mockRaf); + + td.when(mockAdapter.getViewportScrollY()).thenReturn(1); + scrollHandler(); + scrollHandler(); + + td.verify(mockAdapter.addClass(MDCTopAppBarFoundation.cssClasses.SHORT_COLLAPSED_CLASS), {times: 1}); +}); + +test('short top app bar: class is removed once when page is scrolled to the top', () => { + const {foundation, mockAdapter} = setupTest(); + const mockRaf = createMockRaf(); + + td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); + + const {scrollHandler} = createMockHandlers(foundation, mockAdapter, mockRaf); + // Apply the closed class + td.when(mockAdapter.getViewportScrollY()).thenReturn(1); + scrollHandler(); + + // Test removing it + td.when(mockAdapter.getViewportScrollY()).thenReturn(0); + scrollHandler(); + scrollHandler(); + + td.verify(mockAdapter.removeClass(MDCTopAppBarFoundation.cssClasses.SHORT_COLLAPSED_CLASS), {times: 1}); +}); + +test('short top app bar: class is added if it has an action item', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); + td.when(mockAdapter.getTotalActionItems()).thenReturn(1); + foundation.init(); + td.verify(mockAdapter.addClass(MDCTopAppBarFoundation.cssClasses.SHORT_HAS_ACTION_ITEM_CLASS), {times: 1}); +}); + +test('short top app bar: class is not added if it does not have an action item', () => { + const {foundation, mockAdapter} = setupTest(); + td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS)).thenReturn(true); + td.when(mockAdapter.getTotalActionItems()).thenReturn(0); + foundation.init(); + td.verify(mockAdapter.addClass(MDCTopAppBarFoundation.cssClasses.SHORT_HAS_ACTION_ITEM_CLASS), {times: 0}); +}); diff --git a/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js b/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js index 7627ed846bc..82a04eca560 100644 --- a/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js +++ b/test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js @@ -20,6 +20,7 @@ import domEvents from 'dom-events'; import td from 'testdouble'; import {MDCTopAppBar} from '../../../packages/mdc-top-app-bar'; +import {strings} from '../../../packages/mdc-top-app-bar/constants'; function getFixture(removeIcon) { const html = bel` @@ -27,16 +28,17 @@ function getFixture(removeIcon) {
- menu + menu Title
- + file_download - + print - bookmark
@@ -53,7 +55,7 @@ function getFixture(removeIcon) { `; if (removeIcon) { - const icon = html.querySelector('.mdc-top-app-bar__menu-icon'); + const icon = html.querySelector(strings.NAVIGATION_ICON_SELECTOR); icon.parentNode.removeChild(icon); } @@ -70,12 +72,11 @@ class FakeRipple { function setupTest(removeIcon = false) { const fixture = getFixture(removeIcon); - const root = fixture.querySelector('.mdc-top-app-bar'); - const adjust = fixture.querySelector('.mdc-top-app-bar-fixed-adjust'); - const icon = root.querySelector('.mdc-top-app-bar__menu-icon'); + const root = fixture.querySelector(strings.ROOT_SELECTOR); + const icon = root.querySelector(strings.NAVIGATION_ICON_SELECTOR); const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el)); - return {root, adjust, component, icon}; + return {root, component, icon}; } suite('MDCTopAppBar'); @@ -86,7 +87,8 @@ test('attachTo initializes and returns an MDCTopAppBar instance', () => { test('constructor instantiates icon ripples', () => { const {root, component} = setupTest(); - const totalIcons = root.querySelectorAll('.mdc-top-app-bar__icon, .mdc-top-app-bar__menu-icon').length; + const selector = strings.ACTION_ITEM_SELECTOR + ',' + strings.NAVIGATION_ICON_SELECTOR; + const totalIcons = root.querySelectorAll(selector).length; assert.isTrue(component.iconRipples_.length === totalIcons); }); @@ -123,7 +125,7 @@ test('adapter#removeClass removes a class from the root element', () => { assert.isFalse(root.classList.contains('foo')); }); -test('registerNavigationIconInteractionHandler does not add a handler to the nav icon ', () => { +test('registerNavigationIconInteractionHandler does not add a handler to the nav icon if the nav icon is null', () => { const {component} = setupTest(true); const handler = td.func('eventHandler'); @@ -140,6 +142,14 @@ test('#adapter.registerNavigationIconInteractionHandler adds a handler to the na td.verify(handler(td.matchers.anything())); }); +test('#adapter.deregisterScrollHandler does not remove a handler from the nav icon if the nav icon is null ', () => { + const {component} = setupTest(true); + const handler = td.func('eventHandler'); + + assert.doesNotThrow( + () => component.getDefaultFoundation().adapter_.deregisterNavigationIconInteractionHandler('click', handler)); +}); + test('#adapter.deregisterNavigationIconInteractionHandler removes a handler from the nav icon ' + 'element for a given event', () => { const {component, icon} = setupTest(); @@ -150,3 +160,45 @@ test('#adapter.deregisterNavigationIconInteractionHandler removes a handler from domEvents.emit(icon, 'click'); td.verify(handler(td.matchers.anything()), {times: 0}); }); + +test('#adapter.registerScrollHandler adds a scroll handler to the window ' + + 'element for a given event', () => { + const {component} = setupTest(); + const handler = td.func('scrollHandler'); + component.getDefaultFoundation().adapter_.registerScrollHandler(handler); + + domEvents.emit(window, 'scroll'); + try { + td.verify(handler(td.matchers.anything())); + } finally { + // Just to be safe + window.removeEventListener('scroll', handler); + } +}); + +test('#adapter.deregisterScrollHandler removes a scroll handler from the window ' + + 'element for a given event', () => { + const {component} = setupTest(); + const handler = td.func('scrollHandler'); + window.addEventListener('scroll', handler); + component.getDefaultFoundation().adapter_.deregisterScrollHandler(handler); + domEvents.emit(window, 'scroll'); + try { + td.verify(handler(td.matchers.anything()), {times: 0}); + } finally { + // Just to be safe + window.removeEventListener('scroll', handler); + } +}); + +test('adapter#getViewportScrollY returns scroll distance', () => { + const {component} = setupTest(); + assert.equal(component.getDefaultFoundation().adapter_.getViewportScrollY(), window.pageYOffset); +}); + +test('adapter#getTotalActionItems returns the number of action items on the opposite side of the menu', () => { + const {root, component} = setupTest(); + const adapterReturn = component.getDefaultFoundation().adapter_.getTotalActionItems(); + const actual = root.querySelectorAll(strings.ACTION_ITEM_SELECTOR).length; + assert.isTrue(adapterReturn === actual); +}); diff --git a/test/unit/mdc-top-app-bar/util.test.js b/test/unit/mdc-top-app-bar/util.test.js new file mode 100644 index 00000000000..585f36460d9 --- /dev/null +++ b/test/unit/mdc-top-app-bar/util.test.js @@ -0,0 +1,42 @@ +/** + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {assert} from 'chai'; +import * as util from '../../../packages/mdc-top-app-bar/util'; + +suite('MDCTopAppBar - util'); + +test('applyPassive returns an options object for browsers that support passive event listeners', () => { + const mockWindow = { + document: { + addEventListener: function(name, method, options) { + return options.passive; + }, + }, + }; + assert.deepEqual(util.applyPassive(mockWindow, true), {passive: true}); +}); + +test('applyPassive returns false for browsers that do not support passive event listeners', () => { + const mockWindow = { + document: { + addEventListener: function() { + throw new Error(); + }, + }, + }; + assert.isFalse(util.applyPassive(mockWindow, true)); +});