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(tab-nav-bar): add initial functionality of tab nav bar #1589

Merged
merged 10 commits into from
Oct 26, 2016
6 changes: 2 additions & 4 deletions src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,7 @@ import {SidenavDemo} from './sidenav/sidenav-demo';
import {SnackBarDemo} from './snack-bar/snack-bar-demo';
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
import {MenuDemo} from './menu/menu-demo';
import {TabsDemo} from './tabs/tab-group-demo';


import {TabsDemoModule} from './tabs/tabs-demo.module';

@NgModule({
imports: [
Expand All @@ -43,6 +41,7 @@ import {TabsDemo} from './tabs/tab-group-demo';
HttpModule,
RouterModule.forRoot(DEMO_APP_ROUTES),
MaterialModule.forRoot(),
TabsDemoModule,
],
declarations: [
BaselineDemo,
Expand Down Expand Up @@ -76,7 +75,6 @@ import {TabsDemo} from './tabs/tab-group-demo';
SliderDemo,
SlideToggleDemo,
SpagettiPanel,
TabsDemo,
ToolbarDemo,
TooltipDemo,
],
Expand Down
6 changes: 3 additions & 3 deletions src/demo-app/demo-app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Home} from './demo-app';
import {ButtonDemo} from '../button/button-demo';
import {BaselineDemo} from '../baseline/baseline-demo';
import {ButtonToggleDemo} from '../button-toggle/button-toggle-demo';
import {TabsDemo} from '../tabs/tab-group-demo';
import {TabsDemo} from '../tabs/tabs-demo';
import {GridListDemo} from '../grid-list/grid-list-demo';
import {GesturesDemo} from '../gestures/gestures-demo';
import {LiveAnnouncerDemo} from '../live-announcer/live-announcer-demo';
Expand All @@ -27,7 +27,7 @@ import {RippleDemo} from '../ripple/ripple-demo';
import {DialogDemo} from '../dialog/dialog-demo';
import {TooltipDemo} from '../tooltip/tooltip-demo';
import {SnackBarDemo} from '../snack-bar/snack-bar-demo';

import {TABS_DEMO_ROUTES} from '../tabs/routes';

export const DEMO_APP_ROUTES: Routes = [
{path: '', component: Home},
Expand All @@ -51,7 +51,7 @@ export const DEMO_APP_ROUTES: Routes = [
{path: 'live-announcer', component: LiveAnnouncerDemo},
{path: 'gestures', component: GesturesDemo},
{path: 'grid-list', component: GridListDemo},
{path: 'tabs', component: TabsDemo},
{path: 'tabs', component: TabsDemo, children: TABS_DEMO_ROUTES},
{path: 'button-toggle', component: ButtonToggleDemo},
{path: 'baseline', component: BaselineDemo},
{path: 'ripple', component: RippleDemo},
Expand Down
10 changes: 10 additions & 0 deletions src/demo-app/tabs/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Routes} from '@angular/router';

import {SunnyTabContent, RainyTabContent, FoggyTabContent} from '../tabs/tabs-demo';

export const TABS_DEMO_ROUTES: Routes = [
{ path: '', redirectTo: 'sunny-tab', pathMatch: 'full' },
{ path: 'sunny-tab', component: SunnyTabContent },
{ path: 'rainy-tab', component: RainyTabContent },
{ path: 'foggy-tab', component: FoggyTabContent },
Copy link
Member

Choose a reason for hiding this comment

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

Remove the leading/trailing spaces in object literals

];
9 changes: 0 additions & 9 deletions src/demo-app/tabs/tab-group-demo.scss

This file was deleted.

26 changes: 0 additions & 26 deletions src/demo-app/tabs/tab-group-demo.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
<h1>Tab Nav Bar</h1>

<div class="demo-nav-bar">
<md-tab-nav-bar>
<a md-tab-link
*ngFor="let tabLink of tabLinks; let i = index"
[routerLink]="tabLink.link"
[active]="activeLinkIndex === i"
(click)="activeLinkIndex = i">
{{tabLink.label}}
</a>
</md-tab-nav-bar>
<router-outlet></router-outlet>
</div>


<h1>Tab Group Demo</h1>

<md-tab-group class="demo-tab-group">
Expand All @@ -13,6 +29,7 @@ <h1>Tab Group Demo</h1>
</md-tab>
</md-tab-group>


<h1>Async Tabs</h1>

<md-tab-group class="demo-tab-group">
Expand Down
23 changes: 23 additions & 0 deletions src/demo-app/tabs/tabs-demo.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {NgModule} from '@angular/core';
import {MaterialModule} from '@angular/material';
import {FormsModule} from '@angular/forms';
import {BrowserModule} from '@angular/platform-browser';
import {RouterModule} from '@angular/router';

import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs-demo';

@NgModule({
imports: [
FormsModule,
BrowserModule,
MaterialModule,
RouterModule,
],
declarations: [
TabsDemo,
SunnyTabContent,
RainyTabContent,
FoggyTabContent,
]
})
export class TabsDemoModule {}
22 changes: 22 additions & 0 deletions src/demo-app/tabs/tabs-demo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.demo-nav-bar {
border: 1px solid #e0e0e0;
.md-tab-header {
background: #f9f9f9;
}
sunny-routed-content,
rainy-routed-content,
foggy-routed-content {
display: block;
padding: 12px;
}
}

.demo-tab-group {
border: 1px solid #e0e0e0;
.md-tab-header {
background: #f9f9f9;
}
.md-tab-body {
padding: 12px;
}
}
65 changes: 65 additions & 0 deletions src/demo-app/tabs/tabs-demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {Router} from '@angular/router';
import {Observable} from 'rxjs/Observable';

@Component({
moduleId: module.id,
selector: 'tabs-demo',
templateUrl: 'tabs-demo.html',
styleUrls: ['tabs-demo.css'],
encapsulation: ViewEncapsulation.None,
})
export class TabsDemo {
tabLinks = [
{ label: 'Sun', link: 'sunny-tab'},
{ label: 'Rain', link: 'rainy-tab'},
{ label: 'Fog', link: 'foggy-tab'},
];
activeLinkIndex = 0;

tabs = [
{ label: 'Tab One', content: 'This is the body of the first tab' },
{ label: 'Tab Two', content: 'This is the body of the second tab' },
{ label: 'Tab Three', content: 'This is the body of the third tab' },
];

asyncTabs: Observable<any>;

constructor(private router: Router) {
this.asyncTabs = Observable.create((observer: any) => {
setTimeout(() => {
observer.next(this.tabs);
}, 1000);
});

// Initialize the index by checking if a tab link is contained in the url.
// This is not an ideal check and can be removed if routerLink exposes if it is active.
// https://github.com/angular/angular/pull/12525
this.activeLinkIndex =
this.tabLinks.findIndex(routedTab => router.url.indexOf(routedTab.link) != -1);
}
}


@Component({
moduleId: module.id,
selector: 'sunny-routed-content',
template: 'This is the routed body of the sunny tab.',
})
export class SunnyTabContent {}


@Component({
moduleId: module.id,
selector: 'rainy-routed-content',
template: 'This is the routed body of the rainy tab.',
})
export class RainyTabContent {}


@Component({
moduleId: module.id,
selector: 'foggy-routed-content',
template: 'This is the routed body of the foggy tab.',
})
export class FoggyTabContent {}
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ export * from './slider/index';
export * from './slide-toggle/index';
export * from './snack-bar/index';
export * from './tabs/index';
export * from './tabs/tab-nav-bar/index';
export * from './toolbar/index';
export * from './tooltip/index';
53 changes: 4 additions & 49 deletions src/lib/tabs/tab-group.scss
Original file line number Diff line number Diff line change
@@ -1,52 +1,15 @@
@import '../core/style/variables';


$md-tab-bar-height: 48px !default;
@import './tabs-common';

:host {
display: flex;
flex-direction: column;
font-family: $md-font-family;
}

// The top section of the view; contains the tab labels
.md-tab-header {
overflow: hidden;
position: relative;
display: flex;
flex-direction: row;
flex-shrink: 0;
}

// Wraps each tab label
// Wraps each tab label
.md-tab-label {
line-height: $md-tab-bar-height;
height: $md-tab-bar-height;
padding: 0 12px;
font-size: $md-body-font-size-base;
font-family: $md-font-family;
font-weight: 500;
cursor: pointer;
box-sizing: border-box;
color: currentColor;
opacity: 0.6;
min-width: 160px;
text-align: center;
&:focus {
outline: none;
opacity: 1;
}
}

@media ($md-xsmall) {
.md-tab-label {
min-width: 72px;
}
}

.md-tab-disabled {
cursor: default;
pointer-events: none;
@include tab-label;
}

// The bottom section of the view; contains the tab bodies
Expand All @@ -67,12 +30,4 @@ $md-tab-bar-height: 48px !default;
&.md-tab-active {
display: block;
}
}

// The colored bar that underlines the active tab
md-ink-bar {
position: absolute;
bottom: 0;
height: 2px;
transition: 350ms ease-out;
}
}
1 change: 1 addition & 0 deletions src/lib/tabs/tab-nav-bar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './tab-nav-bar';
4 changes: 4 additions & 0 deletions src/lib/tabs/tab-nav-bar/tab-nav-bar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="md-tab-header">
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking that this should probably be a <nav> element instead of a div.

@marcysutton Is it appropriate to always use a <nav> element for a component that contains anchors and navigates the page, or should <nav> only be used for major site navigation?

Copy link

@marcysutton marcysutton Oct 25, 2016

Choose a reason for hiding this comment

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

Help me level-set: is the set of anchors visually like tabs, but without the associated tabpanels and ARIA tab semantics/keyboard behavior?

The difference between a <div> and <nav> is that the latter has a landmark role, making it easy for a screen reader user to discover. I think this component would be a reasonable use case for how I've seen tabs presented in the Material Design spec. Unless you anticipate so many of them on a page they dominate the landmarks list, I don't see much of a downside.

If you do end up using <nav>, I'd recommend labeling the element with an aria-label so it can be easily identified as a screen reader landmark.

Edit: I thought I had sent this comment days ago, but I'm still not used to the Review workflow apparently.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the input Marcy. Jeremy and I discussed this and agree that we can move this responsibility to the user by making this a component with an attribute selector.

Usage of this component will be written by the user as such:
<nav md-tab-nav-bar aria-label="navigation links"> ... </nav>

<ng-content></ng-content>
<md-ink-bar></md-ink-bar>
</div>
10 changes: 10 additions & 0 deletions src/lib/tabs/tab-nav-bar/tab-nav-bar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@import '../tabs-common';

:host {
display: block;
}

a[md-tab-link] {
Copy link
Member

Choose a reason for hiding this comment

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

I'd remove the a part of this selector just to reduce the specificity.

@include tab-label;
text-decoration: none;
}
55 changes: 55 additions & 0 deletions src/lib/tabs/tab-nav-bar/tab-nav-bar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {MdTabsModule} from '../tabs';
import {Component} from '@angular/core';
import {By} from '@angular/platform-browser';


describe('MdTabNavBar', () => {

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [MdTabsModule.forRoot()],
declarations: [
SimpleTabNavBarTestApp
],
});

TestBed.compileComponents();
}));

describe('basic behavior', () => {
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;

beforeEach(() => {
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
});

it('should change active index on click', () => {
let component = fixture.debugElement.componentInstance;

// select the second link
let tabLink = fixture.debugElement.queryAll(By.css('a'))[1];
tabLink.nativeElement.click();
expect(component.activeIndex).toBe(1);

// select the third link
tabLink = fixture.debugElement.queryAll(By.css('a'))[2];
tabLink.nativeElement.click();
expect(component.activeIndex).toBe(2);
});
});
});

@Component({
selector: 'test-app',
template: `
<md-tab-nav-bar>
<a md-tab-link [active]="activeIndex === 0" (click)="activeIndex = 0">Tab One</a>
<a md-tab-link [active]="activeIndex === 1" (click)="activeIndex = 1">Tab Two</a>
<a md-tab-link [active]="activeIndex === 2" (click)="activeIndex = 2">Tab Three</a>
</md-tab-nav-bar>
`
})
class SimpleTabNavBarTestApp {
activeIndex = 0;
}
Loading