Skip to content

Commit

Permalink
feat(top-app-bar): Add --fixed variant to top app bar (#2474)
Browse files Browse the repository at this point in the history
  • Loading branch information
williamernest committed Apr 5, 2018
1 parent 797c7e6 commit 1d40fa9
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 21 deletions.
63 changes: 43 additions & 20 deletions demos/top-app-bar.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
<input type="checkbox" id="dense-checkbox"/>
<label for="dense-checkbox">Dense</label>
</div>
<div>
<input type="checkbox" id="fixed-checkbox"/>
<label for="fixed-checkbox">Fixed</label>
</div>
<div>
<input type="checkbox" id="prominent-checkbox"/>
<label for="prominent-checkbox">Prominent</label>
Expand Down Expand Up @@ -210,23 +214,18 @@ <h3 class="mdc-typography--title">Demo Controls</h3>

var rtlCheckbox = document.getElementById('rtl-checkbox');
var noActionItemCheckbox = document.getElementById('no-action-item-checkbox');
var shortCheckbox = document.getElementById('short-checkbox');
var alwaysCollapsedCheckbox = document.getElementById('always-collapsed-checkbox');
var fixedCheckbox = document.getElementById('fixed-checkbox');
var denseCheckbox = document.getElementById('dense-checkbox');
var prominentCheckbox = document.getElementById('prominent-checkbox');
var shortCheckbox = document.getElementById('short-checkbox');
var alwaysCollapsedCheckbox = document.getElementById('always-collapsed-checkbox');


appBarEl.addEventListener('MDCTopAppBar:nav', function() {
drawer.open = true;
});

rtlCheckbox.addEventListener('change', function() {
document.body.setAttribute('dir', this.checked ? 'rtl': 'ltr');
appBarEl.classList.remove('mdc-top-app-bar--short-has-action-item');

appBar.destroy();
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);
});

// Generic Options
noActionItemCheckbox.addEventListener('change', function() {
if (this.checked) {
rightSection.removeChild(rightItemEl);
Expand All @@ -237,6 +236,39 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
}
});

rtlCheckbox.addEventListener('change', function() {
document.body.setAttribute('dir', this.checked ? 'rtl': 'ltr');
appBarEl.classList.remove('mdc-top-app-bar--short-has-action-item');

appBar.destroy();
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);
});

// Top App Bar Specific Options
denseCheckbox.addEventListener('change', function() {
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--dense');

shortCheckbox.disabled = this.checked || prominentCheckbox.checked || fixedCheckbox.checked;
});

fixedCheckbox.addEventListener('change', function() {
var addScrolledClass = this.checked ? window.pageYOffset > 0 : false;
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--fixed');
appBarEl.classList[addScrolledClass ? 'add' : 'remove']('mdc-top-app-bar--fixed-scrolled');

appBar.destroy();
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);

shortCheckbox.disabled = this.checked || prominentCheckbox.checked || denseCheckbox.checked;
});

prominentCheckbox.addEventListener('change', function() {
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--prominent');

shortCheckbox.disabled = this.checked || denseCheckbox.checked || fixedCheckbox.checked;
});

// Short Top App Bar Specific Options
shortCheckbox.addEventListener('change', function() {
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--short');
appBarEl.classList.remove('mdc-top-app-bar--short-has-action-item');
Expand All @@ -253,6 +285,7 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
alwaysCollapsedCheckbox.disabled = !this.checked;
prominentCheckbox.disabled = this.checked;
denseCheckbox.disabled = this.checked;
fixedCheckbox.disabled = this.checked;
});

alwaysCollapsedCheckbox.addEventListener('change', function() {
Expand All @@ -262,17 +295,7 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);
});

denseCheckbox.addEventListener('change', function() {
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--dense');

shortCheckbox.disabled = this.checked || prominentCheckbox.checked;
});

prominentCheckbox.addEventListener('change', function() {
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--prominent');

shortCheckbox.disabled = this.checked || denseCheckbox.checked;
});
});
</script>
</body>
Expand Down
15 changes: 15 additions & 0 deletions packages/mdc-top-app-bar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ Top app bars can accommodate multiple action items on the opposite side of the n
</header>
```

Top app bars can be fixed at the top of the page:

```html
<header class="mdc-top-app-bar mdc-top-app-bar--fixed">
<div class="mdc-top-app-bar__row">
<section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start">
<a href="#" class="material-icons mdc-top-app-bar__navigation-icon">menu</a>
<span class="mdc-top-app-bar__title">Title</span>
</section>
</div>
</header>
```

Short top app bars should only be used with one action item:

```html
Expand Down Expand Up @@ -102,6 +115,8 @@ Short top app bars can be configured to always appear collapsed by applying the
Class | Description
--- | ---
`mdc-top-app-bar` | Mandatory.
`mdc-top-app-bar--fixed` | Class used to style the top app bar as a fixed top app bar.
`mdc-top-app-bar--prominent` | Class used to style the top app bar as a prominent top app bar.
`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.

Expand Down
2 changes: 2 additions & 0 deletions packages/mdc-top-app-bar/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const strings = {

/** @enum {string} */
const cssClasses = {
FIXED_CLASS: 'mdc-top-app-bar--fixed',
FIXED_SCROLLED_CLASS: 'mdc-top-app-bar--fixed-scrolled',
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',
Expand Down
69 changes: 69 additions & 0 deletions packages/mdc-top-app-bar/fixed/foundation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright 2018 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 {cssClasses} from '../constants';
import MDCTopAppBarAdapter from '../adapter';
import MDCTopAppBarFoundation from '../foundation';

/**
* @extends {MDCTopAppBarFoundation<!MDCFixedTopAppBarFoundation>}
* @final
*/
class MDCFixedTopAppBarFoundation extends MDCTopAppBarFoundation {
/**
* @param {!MDCTopAppBarAdapter} adapter
*/
constructor(adapter) {
super(adapter);
/** State variable for the previous scroll iteration top app bar state */
this.wasScrolled_ = false;

this.scrollHandler_ = () => this.fixedScrollHandler_();
}

init() {
super.init();
this.adapter_.registerScrollHandler(this.scrollHandler_);
}

destroy() {
super.destroy();
this.adapter_.deregisterScrollHandler(this.scrollHandler_);
}

/**
* Scroll handler for applying/removing the modifier class
* on the fixed top app bar.
*/
fixedScrollHandler_() {
const currentScroll = this.adapter_.getViewportScrollY();

if (currentScroll <= 0) {
if (this.wasScrolled_) {
this.adapter_.removeClass(cssClasses.FIXED_SCROLLED_CLASS);
this.wasScrolled_ = false;
}
} else {
if (!this.wasScrolled_) {
this.adapter_.addClass(cssClasses.FIXED_SCROLLED_CLASS);
this.wasScrolled_ = true;
}
}
}
}

export default MDCFixedTopAppBarFoundation;
5 changes: 4 additions & 1 deletion packages/mdc-top-app-bar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import MDCTopAppBarFoundation from './foundation';
import MDCComponent from '@material/base/component';
import {MDCRipple} from '@material/ripple/index';
import {cssClasses, strings} from './constants';
import MDCFixedTopAppBarFoundation from './fixed/foundation';
import MDCShortTopAppBarFoundation from './short/foundation';

/**
Expand Down Expand Up @@ -99,6 +100,8 @@ class MDCTopAppBar extends MDCComponent {
let foundation;
if (this.root_.classList.contains(cssClasses.SHORT_CLASS)) {
foundation = new MDCShortTopAppBarFoundation(adapter);
} else if (this.root_.classList.contains(cssClasses.FIXED_CLASS)) {
foundation = new MDCFixedTopAppBarFoundation(adapter);
} else {
foundation = new MDCTopAppBarFoundation(adapter);
}
Expand All @@ -107,4 +110,4 @@ class MDCTopAppBar extends MDCComponent {
}
}

export {MDCTopAppBar, MDCTopAppBarFoundation, MDCShortTopAppBarFoundation};
export {MDCTopAppBar, MDCTopAppBarFoundation, MDCFixedTopAppBarFoundation, MDCShortTopAppBarFoundation};
11 changes: 11 additions & 0 deletions packages/mdc-top-app-bar/mdc-top-app-bar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,17 @@
}
}

.mdc-top-app-bar--fixed {
position: fixed;
transition: box-shadow 200ms linear;
}

.mdc-top-app-bar--fixed-scrolled {
@include mdc-elevation(4);

transition: box-shadow 200ms linear;
}

// Specific styles for prominent and dense styled top app bar
// stylelint-disable plugin/selector-bem-pattern
.mdc-top-app-bar--dense.mdc-top-app-bar--prominent {
Expand Down
98 changes: 98 additions & 0 deletions test/unit/mdc-top-app-bar/fixed.foundation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @license
* Copyright 2018 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 td from 'testdouble';

import MDCFixedTopAppBarFoundation from '../../../packages/mdc-top-app-bar/fixed/foundation';
import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation';
import {createMockRaf} from '../helpers/raf';

suite('MDCFixedTopAppBarFoundation');

const setupTest = () => {
const mockAdapter = td.object(MDCTopAppBarFoundation.defaultAdapter);

const foundation = new MDCFixedTopAppBarFoundation(mockAdapter);

return {foundation, mockAdapter};
};

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('fixed top app bar: scroll listener is registered on init', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS)).thenReturn(true);
foundation.init();
td.verify(mockAdapter.registerScrollHandler(td.matchers.isA(Function)), {times: 1});
});

test('fixed top app bar: scroll listener is removed on destroy', () => {
const {foundation, mockAdapter} = setupTest();
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS)).thenReturn(true);
foundation.init();
foundation.destroy();
td.verify(mockAdapter.deregisterScrollHandler(td.matchers.isA(Function)), {times: 1});
});

test('fixed 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.FIXED_CLASS)).thenReturn(true);
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS)).thenReturn(false);
td.when(mockAdapter.getTotalActionItems()).thenReturn(0);
td.when(mockAdapter.getViewportScrollY()).thenReturn(0);

const {scrollHandler} = createMockHandlers(foundation, mockAdapter, mockRaf);
td.when(mockAdapter.getViewportScrollY()).thenReturn(1);

scrollHandler();
scrollHandler();

td.verify(mockAdapter.addClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS), {times: 1});
});

test('fixed 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.FIXED_CLASS)).thenReturn(true);
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS)).thenReturn(false);
td.when(mockAdapter.getTotalActionItems()).thenReturn(0);

const {scrollHandler} = createMockHandlers(foundation, mockAdapter, mockRaf);
// Apply the scrolled 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.FIXED_SCROLLED_CLASS), {times: 1});
});
30 changes: 30 additions & 0 deletions test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import td from 'testdouble';

import {MDCTopAppBar} from '../../../packages/mdc-top-app-bar';
import {strings} from '../../../packages/mdc-top-app-bar/constants';
import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation';
import MDCFixedTopAppBarFoundation from '../../../packages/mdc-top-app-bar/fixed/foundation';
import MDCShortTopAppBarFoundation from '../../../packages/mdc-top-app-bar/short/foundation';

function getFixture(removeIcon) {
const html = bel`
Expand Down Expand Up @@ -101,6 +104,33 @@ test('destroy destroys icon ripples', () => {
});
});

test('getDefaultFoundation returns the appropriate foundation for default', () => {
const fixture = getFixture();
const root = fixture.querySelector(strings.ROOT_SELECTOR);
const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el));
assert.isTrue(component.foundation_ instanceof MDCTopAppBarFoundation);
assert.isFalse(component.foundation_ instanceof MDCShortTopAppBarFoundation);
assert.isFalse(component.foundation_ instanceof MDCFixedTopAppBarFoundation);
});

test('getDefaultFoundation returns the appropriate foundation for fixed', () => {
const fixture = getFixture();
const root = fixture.querySelector(strings.ROOT_SELECTOR);
root.classList.add(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS);
const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el));
assert.isFalse(component.foundation_ instanceof MDCShortTopAppBarFoundation);
assert.isTrue(component.foundation_ instanceof MDCFixedTopAppBarFoundation);
});

test('getDefaultFoundation returns the appropriate foundation for short', () => {
const fixture = getFixture();
const root = fixture.querySelector(strings.ROOT_SELECTOR);
root.classList.add(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS);
const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el));
assert.isTrue(component.foundation_ instanceof MDCShortTopAppBarFoundation);
assert.isFalse(component.foundation_ instanceof MDCFixedTopAppBarFoundation);
});

test('adapter#hasClass returns true if the root element has specified class', () => {
const {root, component} = setupTest();
root.classList.add('foo');
Expand Down

0 comments on commit 1d40fa9

Please sign in to comment.