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 --fixed variant to top app bar #2474

Merged
merged 6 commits into from
Apr 5, 2018
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: You might also want to toggle/remove mdc-top-app-bar--fixed-scrolled here, if it's not too hard to do.

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

Choose a reason for hiding this comment

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

We probably want some kind of transition for box-shadow to make the transition between --fixed and --fixed-scrolled a little smoother.

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