Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit 1d40fa9

Browse files
feat(top-app-bar): Add --fixed variant to top app bar (#2474)
1 parent 797c7e6 commit 1d40fa9

File tree

8 files changed

+272
-21
lines changed

8 files changed

+272
-21
lines changed

demos/top-app-bar.html

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
177177
<input type="checkbox" id="dense-checkbox"/>
178178
<label for="dense-checkbox">Dense</label>
179179
</div>
180+
<div>
181+
<input type="checkbox" id="fixed-checkbox"/>
182+
<label for="fixed-checkbox">Fixed</label>
183+
</div>
180184
<div>
181185
<input type="checkbox" id="prominent-checkbox"/>
182186
<label for="prominent-checkbox">Prominent</label>
@@ -210,23 +214,18 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
210214

211215
var rtlCheckbox = document.getElementById('rtl-checkbox');
212216
var noActionItemCheckbox = document.getElementById('no-action-item-checkbox');
213-
var shortCheckbox = document.getElementById('short-checkbox');
214-
var alwaysCollapsedCheckbox = document.getElementById('always-collapsed-checkbox');
217+
var fixedCheckbox = document.getElementById('fixed-checkbox');
215218
var denseCheckbox = document.getElementById('dense-checkbox');
216219
var prominentCheckbox = document.getElementById('prominent-checkbox');
220+
var shortCheckbox = document.getElementById('short-checkbox');
221+
var alwaysCollapsedCheckbox = document.getElementById('always-collapsed-checkbox');
222+
217223

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

222-
rtlCheckbox.addEventListener('change', function() {
223-
document.body.setAttribute('dir', this.checked ? 'rtl': 'ltr');
224-
appBarEl.classList.remove('mdc-top-app-bar--short-has-action-item');
225-
226-
appBar.destroy();
227-
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);
228-
});
229-
228+
// Generic Options
230229
noActionItemCheckbox.addEventListener('change', function() {
231230
if (this.checked) {
232231
rightSection.removeChild(rightItemEl);
@@ -237,6 +236,39 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
237236
}
238237
});
239238

239+
rtlCheckbox.addEventListener('change', function() {
240+
document.body.setAttribute('dir', this.checked ? 'rtl': 'ltr');
241+
appBarEl.classList.remove('mdc-top-app-bar--short-has-action-item');
242+
243+
appBar.destroy();
244+
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);
245+
});
246+
247+
// Top App Bar Specific Options
248+
denseCheckbox.addEventListener('change', function() {
249+
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--dense');
250+
251+
shortCheckbox.disabled = this.checked || prominentCheckbox.checked || fixedCheckbox.checked;
252+
});
253+
254+
fixedCheckbox.addEventListener('change', function() {
255+
var addScrolledClass = this.checked ? window.pageYOffset > 0 : false;
256+
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--fixed');
257+
appBarEl.classList[addScrolledClass ? 'add' : 'remove']('mdc-top-app-bar--fixed-scrolled');
258+
259+
appBar.destroy();
260+
appBar = mdc.topAppBar.MDCTopAppBar.attachTo(appBarEl);
261+
262+
shortCheckbox.disabled = this.checked || prominentCheckbox.checked || denseCheckbox.checked;
263+
});
264+
265+
prominentCheckbox.addEventListener('change', function() {
266+
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--prominent');
267+
268+
shortCheckbox.disabled = this.checked || denseCheckbox.checked || fixedCheckbox.checked;
269+
});
270+
271+
// Short Top App Bar Specific Options
240272
shortCheckbox.addEventListener('change', function() {
241273
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--short');
242274
appBarEl.classList.remove('mdc-top-app-bar--short-has-action-item');
@@ -253,6 +285,7 @@ <h3 class="mdc-typography--title">Demo Controls</h3>
253285
alwaysCollapsedCheckbox.disabled = !this.checked;
254286
prominentCheckbox.disabled = this.checked;
255287
denseCheckbox.disabled = this.checked;
288+
fixedCheckbox.disabled = this.checked;
256289
});
257290

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

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

268-
shortCheckbox.disabled = this.checked || prominentCheckbox.checked;
269-
});
270-
271-
prominentCheckbox.addEventListener('change', function() {
272-
appBarEl.classList[this.checked ? 'add' : 'remove']('mdc-top-app-bar--prominent');
273-
274-
shortCheckbox.disabled = this.checked || denseCheckbox.checked;
275-
});
276299
});
277300
</script>
278301
</body>

packages/mdc-top-app-bar/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,19 @@ Top app bars can accommodate multiple action items on the opposite side of the n
5252
</header>
5353
```
5454

55+
Top app bars can be fixed at the top of the page:
56+
57+
```html
58+
<header class="mdc-top-app-bar mdc-top-app-bar--fixed">
59+
<div class="mdc-top-app-bar__row">
60+
<section class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start">
61+
<a href="#" class="material-icons mdc-top-app-bar__navigation-icon">menu</a>
62+
<span class="mdc-top-app-bar__title">Title</span>
63+
</section>
64+
</div>
65+
</header>
66+
```
67+
5568
Short top app bars should only be used with one action item:
5669

5770
```html
@@ -102,6 +115,8 @@ Short top app bars can be configured to always appear collapsed by applying the
102115
Class | Description
103116
--- | ---
104117
`mdc-top-app-bar` | Mandatory.
118+
`mdc-top-app-bar--fixed` | Class used to style the top app bar as a fixed top app bar.
119+
`mdc-top-app-bar--prominent` | Class used to style the top app bar as a prominent top app bar.
105120
`mdc-top-app-bar--short` | Class used to style the top app bar as a short top app bar.
106121
`mdc-top-app-bar--short-collapsed` | Class used to indicate the short top app bar is collapsed.
107122

packages/mdc-top-app-bar/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const strings = {
2626

2727
/** @enum {string} */
2828
const cssClasses = {
29+
FIXED_CLASS: 'mdc-top-app-bar--fixed',
30+
FIXED_SCROLLED_CLASS: 'mdc-top-app-bar--fixed-scrolled',
2931
SHORT_CLASS: 'mdc-top-app-bar--short',
3032
SHORT_HAS_ACTION_ITEM_CLASS: 'mdc-top-app-bar--short-has-action-item',
3133
SHORT_COLLAPSED_CLASS: 'mdc-top-app-bar--short-collapsed',
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @license
3+
* Copyright 2018 Google Inc. All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import {cssClasses} from '../constants';
19+
import MDCTopAppBarAdapter from '../adapter';
20+
import MDCTopAppBarFoundation from '../foundation';
21+
22+
/**
23+
* @extends {MDCTopAppBarFoundation<!MDCFixedTopAppBarFoundation>}
24+
* @final
25+
*/
26+
class MDCFixedTopAppBarFoundation extends MDCTopAppBarFoundation {
27+
/**
28+
* @param {!MDCTopAppBarAdapter} adapter
29+
*/
30+
constructor(adapter) {
31+
super(adapter);
32+
/** State variable for the previous scroll iteration top app bar state */
33+
this.wasScrolled_ = false;
34+
35+
this.scrollHandler_ = () => this.fixedScrollHandler_();
36+
}
37+
38+
init() {
39+
super.init();
40+
this.adapter_.registerScrollHandler(this.scrollHandler_);
41+
}
42+
43+
destroy() {
44+
super.destroy();
45+
this.adapter_.deregisterScrollHandler(this.scrollHandler_);
46+
}
47+
48+
/**
49+
* Scroll handler for applying/removing the modifier class
50+
* on the fixed top app bar.
51+
*/
52+
fixedScrollHandler_() {
53+
const currentScroll = this.adapter_.getViewportScrollY();
54+
55+
if (currentScroll <= 0) {
56+
if (this.wasScrolled_) {
57+
this.adapter_.removeClass(cssClasses.FIXED_SCROLLED_CLASS);
58+
this.wasScrolled_ = false;
59+
}
60+
} else {
61+
if (!this.wasScrolled_) {
62+
this.adapter_.addClass(cssClasses.FIXED_SCROLLED_CLASS);
63+
this.wasScrolled_ = true;
64+
}
65+
}
66+
}
67+
}
68+
69+
export default MDCFixedTopAppBarFoundation;

packages/mdc-top-app-bar/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import MDCTopAppBarFoundation from './foundation';
2020
import MDCComponent from '@material/base/component';
2121
import {MDCRipple} from '@material/ripple/index';
2222
import {cssClasses, strings} from './constants';
23+
import MDCFixedTopAppBarFoundation from './fixed/foundation';
2324
import MDCShortTopAppBarFoundation from './short/foundation';
2425

2526
/**
@@ -99,6 +100,8 @@ class MDCTopAppBar extends MDCComponent {
99100
let foundation;
100101
if (this.root_.classList.contains(cssClasses.SHORT_CLASS)) {
101102
foundation = new MDCShortTopAppBarFoundation(adapter);
103+
} else if (this.root_.classList.contains(cssClasses.FIXED_CLASS)) {
104+
foundation = new MDCFixedTopAppBarFoundation(adapter);
102105
} else {
103106
foundation = new MDCTopAppBarFoundation(adapter);
104107
}
@@ -107,4 +110,4 @@ class MDCTopAppBar extends MDCComponent {
107110
}
108111
}
109112

110-
export {MDCTopAppBar, MDCTopAppBarFoundation, MDCShortTopAppBarFoundation};
113+
export {MDCTopAppBar, MDCTopAppBarFoundation, MDCFixedTopAppBarFoundation, MDCShortTopAppBarFoundation};

packages/mdc-top-app-bar/mdc-top-app-bar.scss

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@
159159
}
160160
}
161161

162+
.mdc-top-app-bar--fixed {
163+
position: fixed;
164+
transition: box-shadow 200ms linear;
165+
}
166+
167+
.mdc-top-app-bar--fixed-scrolled {
168+
@include mdc-elevation(4);
169+
170+
transition: box-shadow 200ms linear;
171+
}
172+
162173
// Specific styles for prominent and dense styled top app bar
163174
// stylelint-disable plugin/selector-bem-pattern
164175
.mdc-top-app-bar--dense.mdc-top-app-bar--prominent {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* @license
3+
* Copyright 2018 Google Inc. All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import td from 'testdouble';
19+
20+
import MDCFixedTopAppBarFoundation from '../../../packages/mdc-top-app-bar/fixed/foundation';
21+
import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation';
22+
import {createMockRaf} from '../helpers/raf';
23+
24+
suite('MDCFixedTopAppBarFoundation');
25+
26+
const setupTest = () => {
27+
const mockAdapter = td.object(MDCTopAppBarFoundation.defaultAdapter);
28+
29+
const foundation = new MDCFixedTopAppBarFoundation(mockAdapter);
30+
31+
return {foundation, mockAdapter};
32+
};
33+
34+
const createMockHandlers = (foundation, mockAdapter, mockRaf) => {
35+
let scrollHandler;
36+
td.when(mockAdapter.registerScrollHandler(td.matchers.isA(Function))).thenDo((fn) => {
37+
scrollHandler = fn;
38+
});
39+
40+
foundation.init();
41+
mockRaf.flush();
42+
td.reset();
43+
return {scrollHandler};
44+
};
45+
46+
test('fixed top app bar: scroll listener is registered on init', () => {
47+
const {foundation, mockAdapter} = setupTest();
48+
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS)).thenReturn(true);
49+
foundation.init();
50+
td.verify(mockAdapter.registerScrollHandler(td.matchers.isA(Function)), {times: 1});
51+
});
52+
53+
test('fixed top app bar: scroll listener is removed on destroy', () => {
54+
const {foundation, mockAdapter} = setupTest();
55+
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS)).thenReturn(true);
56+
foundation.init();
57+
foundation.destroy();
58+
td.verify(mockAdapter.deregisterScrollHandler(td.matchers.isA(Function)), {times: 1});
59+
});
60+
61+
test('fixed top app bar: class is added once when page is scrolled from the top', () => {
62+
const {foundation, mockAdapter} = setupTest();
63+
const mockRaf = createMockRaf();
64+
65+
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS)).thenReturn(true);
66+
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS)).thenReturn(false);
67+
td.when(mockAdapter.getTotalActionItems()).thenReturn(0);
68+
td.when(mockAdapter.getViewportScrollY()).thenReturn(0);
69+
70+
const {scrollHandler} = createMockHandlers(foundation, mockAdapter, mockRaf);
71+
td.when(mockAdapter.getViewportScrollY()).thenReturn(1);
72+
73+
scrollHandler();
74+
scrollHandler();
75+
76+
td.verify(mockAdapter.addClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS), {times: 1});
77+
});
78+
79+
test('fixed top app bar: class is removed once when page is scrolled to the top', () => {
80+
const {foundation, mockAdapter} = setupTest();
81+
const mockRaf = createMockRaf();
82+
83+
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS)).thenReturn(true);
84+
td.when(mockAdapter.hasClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS)).thenReturn(false);
85+
td.when(mockAdapter.getTotalActionItems()).thenReturn(0);
86+
87+
const {scrollHandler} = createMockHandlers(foundation, mockAdapter, mockRaf);
88+
// Apply the scrolled class
89+
td.when(mockAdapter.getViewportScrollY()).thenReturn(1);
90+
scrollHandler();
91+
92+
// Test removing it
93+
td.when(mockAdapter.getViewportScrollY()).thenReturn(0);
94+
scrollHandler();
95+
scrollHandler();
96+
97+
td.verify(mockAdapter.removeClass(MDCTopAppBarFoundation.cssClasses.FIXED_SCROLLED_CLASS), {times: 1});
98+
});

test/unit/mdc-top-app-bar/mdc-top-app-bar.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import td from 'testdouble';
2121

2222
import {MDCTopAppBar} from '../../../packages/mdc-top-app-bar';
2323
import {strings} from '../../../packages/mdc-top-app-bar/constants';
24+
import MDCTopAppBarFoundation from '../../../packages/mdc-top-app-bar/foundation';
25+
import MDCFixedTopAppBarFoundation from '../../../packages/mdc-top-app-bar/fixed/foundation';
26+
import MDCShortTopAppBarFoundation from '../../../packages/mdc-top-app-bar/short/foundation';
2427

2528
function getFixture(removeIcon) {
2629
const html = bel`
@@ -101,6 +104,33 @@ test('destroy destroys icon ripples', () => {
101104
});
102105
});
103106

107+
test('getDefaultFoundation returns the appropriate foundation for default', () => {
108+
const fixture = getFixture();
109+
const root = fixture.querySelector(strings.ROOT_SELECTOR);
110+
const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el));
111+
assert.isTrue(component.foundation_ instanceof MDCTopAppBarFoundation);
112+
assert.isFalse(component.foundation_ instanceof MDCShortTopAppBarFoundation);
113+
assert.isFalse(component.foundation_ instanceof MDCFixedTopAppBarFoundation);
114+
});
115+
116+
test('getDefaultFoundation returns the appropriate foundation for fixed', () => {
117+
const fixture = getFixture();
118+
const root = fixture.querySelector(strings.ROOT_SELECTOR);
119+
root.classList.add(MDCTopAppBarFoundation.cssClasses.FIXED_CLASS);
120+
const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el));
121+
assert.isFalse(component.foundation_ instanceof MDCShortTopAppBarFoundation);
122+
assert.isTrue(component.foundation_ instanceof MDCFixedTopAppBarFoundation);
123+
});
124+
125+
test('getDefaultFoundation returns the appropriate foundation for short', () => {
126+
const fixture = getFixture();
127+
const root = fixture.querySelector(strings.ROOT_SELECTOR);
128+
root.classList.add(MDCTopAppBarFoundation.cssClasses.SHORT_CLASS);
129+
const component = new MDCTopAppBar(root, undefined, (el) => new FakeRipple(el));
130+
assert.isTrue(component.foundation_ instanceof MDCShortTopAppBarFoundation);
131+
assert.isFalse(component.foundation_ instanceof MDCFixedTopAppBarFoundation);
132+
});
133+
104134
test('adapter#hasClass returns true if the root element has specified class', () => {
105135
const {root, component} = setupTest();
106136
root.classList.add('foo');

0 commit comments

Comments
 (0)