feat(aio): buttons for TOC "Contents" label

- Use buttons for the TOC “Contents” label when embedded-and-expandable or TOC on the right to satisfy a11y.
- Add aria-pressed setting for the toggles in TOC and NavItem.
- Clicking the right panel TOC “Contents” button scrolls to top.
- When embedded use same rotating caret as sidebar
- When embedded and no secondaries, “Content” is just a label.
- Gray background for focused buttons rather than outline because can’t get carets to work with outline.
This commit is contained in:
Ward Bell 2017-05-16 16:10:06 -07:00 committed by Pete Bacon Darwin
parent b836aca999
commit 4ccb2269a5
6 changed files with 128 additions and 77 deletions

View File

@ -9,7 +9,7 @@ import { PrettyPrinter } from './code/pretty-printer.service';
// It is not enough just to import them inside the AppModule // It is not enough just to import them inside the AppModule
// Reusable components (used inside embedded components) // Reusable components (used inside embedded components)
import { MdTabsModule } from '@angular/material'; import { MdIconModule, MdTabsModule } from '@angular/material';
import { CodeComponent } from './code/code.component'; import { CodeComponent } from './code/code.component';
// Embedded Components // Embedded Components
@ -38,7 +38,11 @@ export class EmbeddedComponents {
} }
@NgModule({ @NgModule({
imports: [ CommonModule, MdTabsModule ], imports: [
CommonModule,
MdIconModule,
MdTabsModule
],
declarations: [ declarations: [
embeddedComponents, embeddedComponents,
CodeComponent, CodeComponent,

View File

@ -1,11 +1,17 @@
<div *ngIf="hasToc" [class.closed]="isClosed"> <div *ngIf="hasToc" [class.collapsed]="isCollapsed">
<div *ngIf="!hasSecondary" class="toc-heading">Contents</div> <button *ngIf="!isEmbedded" type="button" class="toc-heading"
<div *ngIf="hasSecondary" class="toc-heading secondary" (click)="toTop()" title="Top of page">Contents</button>
(click)="toggle(false)"
title="Expand/collapse contents" <div *ngIf="!hasSecondary && isEmbedded" class="toc-heading embedded">Contents</div>
aria-label="Expand/collapse contents">
<p>Table of Contents<button type="button" class="toc-show-all material-icons" [class.closed]="isClosed"></button></p> <button *ngIf="hasSecondary" type="button" class="toc-heading embedded secondary"
</div> (click)="toggle(false)"
title="Expand/collapse contents"
aria-label="Expand/collapse contents"
[attr.aria-pressed]="!isCollapsed">
Contents
<md-icon class="rotating-icon" svgIcon="keyboard_arrow_right" [class.collapsed]="isCollapsed"></md-icon>
</button>
<ul class="toc-list"> <ul class="toc-list">
<li *ngFor="let toc of tocList" title="{{toc.title}}" class="{{toc.level}}" [class.secondary]="toc.isSecondary"> <li *ngFor="let toc of tocList" title="{{toc.title}}" class="{{toc.level}}" [class.secondary]="toc.isSecondary">
@ -14,8 +20,9 @@
</ul> </ul>
<button type="button" (click)="toggle()" *ngIf="hasSecondary" <button type="button" (click)="toggle()" *ngIf="hasSecondary"
class="toc-more-items material-icons" [class.closed]="isClosed" class="toc-more-items embedded material-icons" [class.collapsed]="isCollapsed"
title="Expand/collapse contents" title="Expand/collapse contents"
aria-label="Expand/collapse contents"> aria-label="Expand/collapse contents"
[attr.aria-pressed]="!isCollapsed">
</button> </button>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Component, DebugElement } from '@angular/core'; import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By, DOCUMENT } from '@angular/platform-browser'; import { By, DOCUMENT } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@ -15,7 +15,8 @@ describe('TocComponent', () => {
let page: { let page: {
listItems: DebugElement[]; listItems: DebugElement[];
tocHeading: DebugElement; tocHeading: DebugElement;
tocHeadingButton: DebugElement; tocHeadingButtonEmbedded: DebugElement;
tocHeadingButtonSide: DebugElement;
tocMoreButton: DebugElement; tocMoreButton: DebugElement;
}; };
@ -23,7 +24,8 @@ describe('TocComponent', () => {
return { return {
listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')), listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')),
tocHeading: tocComponentDe.query(By.css('.toc-heading')), tocHeading: tocComponentDe.query(By.css('.toc-heading')),
tocHeadingButton: tocComponentDe.query(By.css('.toc-heading button')), tocHeadingButtonEmbedded: tocComponentDe.query(By.css('button.toc-heading.embedded')),
tocHeadingButtonSide: tocComponentDe.query(By.css('button.toc-heading:not(.embedded)')),
tocMoreButton: tocComponentDe.query(By.css('button.toc-more-items')), tocMoreButton: tocComponentDe.query(By.css('button.toc-more-items')),
}; };
} }
@ -34,7 +36,8 @@ describe('TocComponent', () => {
providers: [ providers: [
{ provide: ScrollService, useClass: TestScrollService }, { provide: ScrollService, useClass: TestScrollService },
{ provide: TocService, useClass: TestTocService } { provide: TocService, useClass: TestTocService }
] ],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
}); });
}); });
@ -106,18 +109,18 @@ describe('TocComponent', () => {
}); });
it('should not display expando buttons', () => { it('should not display expando buttons', () => {
expect(page.tocHeadingButton).toBeFalsy('top expand/collapse button'); expect(page.tocHeadingButtonEmbedded).toBeFalsy('top expand/collapse button');
expect(page.tocMoreButton).toBeFalsy('bottom more button'); expect(page.tocMoreButton).toBeFalsy('bottom more button');
}); });
}); });
describe('when many TocItems', () => { describe('when many TocItems', () => {
let scrollSpy: jasmine.Spy; let scrollToTopSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
fixture.detectChanges(); fixture.detectChanges();
page = setPage(); page = setPage();
scrollSpy = TestBed.get(ScrollService).scrollToTop; scrollToTopSpy = TestBed.get(ScrollService).scrollToTop;
}); });
it('should have more than 4 displayed items', () => { it('should have more than 4 displayed items', () => {
@ -128,16 +131,16 @@ describe('TocComponent', () => {
expect(page.listItems.length).toEqual(tocList.length); expect(page.listItems.length).toEqual(tocList.length);
}); });
it('should be in "closed" (not expanded) state at the start', () => { it('should be in "collapsed" (not expanded) state at the start', () => {
expect(tocComponent.isClosed).toBeTruthy(); expect(tocComponent.isCollapsed).toBeTruthy();
}); });
it('should have "closed" class at the start', () => { it('should have "collapsed" class at the start', () => {
expect(tocComponentDe.children[0].classes.closed).toEqual(true); expect(tocComponentDe.children[0].classes.collapsed).toEqual(true);
}); });
it('should display expando buttons', () => { it('should display expando buttons', () => {
expect(page.tocHeadingButton).toBeTruthy('top expand/collapse button'); expect(page.tocHeadingButtonEmbedded).toBeTruthy('top expand/collapse button');
expect(page.tocMoreButton).toBeTruthy('bottom more button'); expect(page.tocMoreButton).toBeTruthy('bottom more button');
}); });
@ -145,7 +148,7 @@ describe('TocComponent', () => {
expect(tocComponent.hasSecondary).toEqual(true, 'hasSecondary flag'); expect(tocComponent.hasSecondary).toEqual(true, 'hasSecondary flag');
}); });
// CSS should hide items with the secondary class when closed // CSS should hide items with the secondary class when collapsed
it('should have secondary item with a secondary class', () => { it('should have secondary item with a secondary class', () => {
const aSecondary = page.listItems.find(item => item.classes.secondary); const aSecondary = page.listItems.find(item => item.classes.secondary);
expect(aSecondary).toBeTruthy('should find a secondary'); expect(aSecondary).toBeTruthy('should find a secondary');
@ -155,32 +158,32 @@ describe('TocComponent', () => {
describe('after click tocHeading button', () => { describe('after click tocHeading button', () => {
beforeEach(() => { beforeEach(() => {
page.tocHeadingButton.nativeElement.click(); page.tocHeadingButtonEmbedded.nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should not be "closed"', () => { it('should not be "collapsed"', () => {
expect(tocComponent.isClosed).toEqual(false); expect(tocComponent.isCollapsed).toEqual(false);
}); });
it('should not have "closed" class', () => { it('should not have "collapsed" class', () => {
expect(tocComponentDe.children[0].classes.closed).toBeFalsy(); expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy();
}); });
it('should not scroll', () => { it('should not scroll', () => {
expect(scrollSpy).not.toHaveBeenCalled(); expect(scrollToTopSpy).not.toHaveBeenCalled();
}); });
it('should be "closed" after clicking again', () => { it('should be "collapsed" after clicking again', () => {
page.tocHeadingButton.nativeElement.click(); page.tocHeadingButtonEmbedded.nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
expect(tocComponent.isClosed).toEqual(true); expect(tocComponent.isCollapsed).toEqual(true);
}); });
it('should not scroll after clicking again', () => { it('should not scroll after clicking again', () => {
page.tocHeadingButton.nativeElement.click(); page.tocHeadingButtonEmbedded.nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
expect(scrollSpy).not.toHaveBeenCalled(); expect(scrollToTopSpy).not.toHaveBeenCalled();
}); });
}); });
@ -191,34 +194,34 @@ describe('TocComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should not be "closed"', () => { it('should not be "collapsed"', () => {
expect(tocComponent.isClosed).toEqual(false); expect(tocComponent.isCollapsed).toEqual(false);
}); });
it('should not have "closed" class', () => { it('should not have "collapsed" class', () => {
expect(tocComponentDe.children[0].classes.closed).toBeFalsy(); expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy();
}); });
it('should not scroll', () => { it('should not scroll', () => {
expect(scrollSpy).not.toHaveBeenCalled(); expect(scrollToTopSpy).not.toHaveBeenCalled();
}); });
it('should be "closed" after clicking again', () => { it('should be "collapsed" after clicking again', () => {
page.tocMoreButton.nativeElement.click(); page.tocMoreButton.nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
expect(tocComponent.isClosed).toEqual(true); expect(tocComponent.isCollapsed).toEqual(true);
}); });
it('should be "closed" after clicking tocHeadingButton', () => { it('should be "collapsed" after clicking tocHeadingButton', () => {
page.tocMoreButton.nativeElement.click(); page.tocMoreButton.nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
expect(tocComponent.isClosed).toEqual(true); expect(tocComponent.isCollapsed).toEqual(true);
}); });
it('should scroll after clicking again', () => { it('should scroll after clicking again', () => {
page.tocMoreButton.nativeElement.click(); page.tocMoreButton.nativeElement.click();
fixture.detectChanges(); fixture.detectChanges();
expect(scrollSpy).toHaveBeenCalled(); expect(scrollToTopSpy).toHaveBeenCalled();
}); });
}); });
}); });
@ -226,9 +229,12 @@ describe('TocComponent', () => {
describe('when in side panel (not embedded))', () => { describe('when in side panel (not embedded))', () => {
let fixture: ComponentFixture<HostNotEmbeddedTocComponent>; let fixture: ComponentFixture<HostNotEmbeddedTocComponent>;
let scrollToTopSpy: jasmine.Spy;
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(HostNotEmbeddedTocComponent); fixture = TestBed.createComponent(HostNotEmbeddedTocComponent);
scrollToTopSpy = TestBed.get(ScrollService).scrollToTop;
tocComponentDe = fixture.debugElement.children[0]; tocComponentDe = fixture.debugElement.children[0];
tocComponent = tocComponentDe.componentInstance; tocComponent = tocComponentDe.componentInstance;
tocService = TestBed.get(TocService); tocService = TestBed.get(TocService);
@ -255,9 +261,19 @@ describe('TocComponent', () => {
}); });
it('should not display expando buttons', () => { it('should not display expando buttons', () => {
expect(page.tocHeadingButton).toBeFalsy('top expand/collapse button'); expect(page.tocHeadingButtonEmbedded).toBeFalsy('top expand/collapse button');
expect(page.tocMoreButton).toBeFalsy('bottom more button'); expect(page.tocMoreButton).toBeFalsy('bottom more button');
}); });
it('should display "Contents" button', () => {
expect(page.tocHeadingButtonSide).toBeTruthy();
});
it('should scroll to top when "Contents" button clicked', () => {
page.tocHeadingButtonSide.nativeElement.click();
fixture.detectChanges();
expect(scrollToTopSpy).toHaveBeenCalled();
});
}); });
}); });

View File

@ -15,7 +15,7 @@ export class TocComponent implements OnInit, OnDestroy {
hasSecondary = false; hasSecondary = false;
hasToc = false; hasToc = false;
hostElement: HTMLElement; hostElement: HTMLElement;
isClosed = true; isCollapsed = true;
isEmbedded = false; isEmbedded = false;
private onDestroy = new Subject(); private onDestroy = new Subject();
private primaryMax = 4; private primaryMax = 4;
@ -52,7 +52,11 @@ export class TocComponent implements OnInit, OnDestroy {
} }
toggle(canScroll = true) { toggle(canScroll = true) {
this.isClosed = !this.isClosed; this.isCollapsed = !this.isCollapsed;
if (canScroll && this.isClosed) { this.scrollService.scrollToTop(); } if (canScroll && this.isCollapsed) { this.toTop(); }
}
toTop() {
this.scrollService.scrollToTop();
} }
} }

View File

@ -13,7 +13,8 @@
</a> </a>
<button *ngIf="node.url == null" type="button" [ngClass]="classes" title="{{node.tooltip}}" <button *ngIf="node.url == null" type="button" [ngClass]="classes" title="{{node.tooltip}}"
(click)="headerClicked()" class="vertical-menu-item heading"> (click)="headerClicked()" class="vertical-menu-item heading"
[attr.aria-pressed]="isExpanded">
{{node.title}} {{node.title}}
<md-icon class="rotating-icon" svgIcon="keyboard_arrow_right"></md-icon> <md-icon class="rotating-icon" svgIcon="keyboard_arrow_right"></md-icon>
</button> </button>

View File

@ -50,51 +50,70 @@ aio-toc > div {
} }
} }
button.toc-show-all, button.toc-heading,
button.toc-more-items { button.toc-more-items {
cursor: pointer;
display: inline-block; display: inline-block;
background: 0; background: 0;
background-color: transparent; background-color: transparent;
border: none; border: none;
box-shadow: none; box-shadow: none;
color: $mediumgray;
padding: 0; padding: 0;
&:focus.embedded {
outline: none;
background: $lightgray;
}
}
button.toc-heading,
div.toc-heading {
font-size: 115%;
}
button.toc-heading {
md-icon.rotating-icon {
height: 18px;
width: 18px;
position: relative;
left: -4px;
top: 5px;
}
&:hover:not(.embedded) {
color: $accentblue;
}
}
button.toc-more-items {
color: $mediumgray;
top: 10px;
position: relative;
&:hover { &:hover {
color: $accentblue; color: $accentblue;
} }
&:focus {
outline: none;
}
}
button.toc-show-all {
min-width: 34px;
position: absolute;
top: 0;
}
button.toc-show-all::after {
content: 'expand_less';
}
button.toc-show-all.closed::after {
content: 'expand_more';
}
button.toc-more-items {
top: 10px;
position: relative;
} }
button.toc-more-items::after { button.toc-more-items::after {
content: 'expand_less'; content: 'expand_less';
} }
button.toc-more-items.closed::after { button.toc-more-items.collapsed::after {
content: 'more_horiz'; content: 'more_horiz';
} }
.mat-icon.collapsed {
@include rotate(0deg);
}
.mat-icon:not(.collapsed) {
@include rotate(90deg);
// margin: 4px;
}
ul.toc-list { ul.toc-list {
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
@ -131,6 +150,6 @@ aio-toc > div {
} }
} }
aio-toc.embedded > div.closed li.secondary { aio-toc.embedded > div.collapsed li.secondary {
display: none; display: none;
} }