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
// Reusable components (used inside embedded components)
import { MdTabsModule } from '@angular/material';
import { MdIconModule, MdTabsModule } from '@angular/material';
import { CodeComponent } from './code/code.component';
// Embedded Components
@ -38,7 +38,11 @@ export class EmbeddedComponents {
}
@NgModule({
imports: [ CommonModule, MdTabsModule ],
imports: [
CommonModule,
MdIconModule,
MdTabsModule
],
declarations: [
embeddedComponents,
CodeComponent,

View File

@ -1,11 +1,17 @@
<div *ngIf="hasToc" [class.closed]="isClosed">
<div *ngIf="!hasSecondary" class="toc-heading">Contents</div>
<div *ngIf="hasSecondary" class="toc-heading secondary"
(click)="toggle(false)"
title="Expand/collapse contents"
aria-label="Expand/collapse contents">
<p>Table of Contents<button type="button" class="toc-show-all material-icons" [class.closed]="isClosed"></button></p>
</div>
<div *ngIf="hasToc" [class.collapsed]="isCollapsed">
<button *ngIf="!isEmbedded" type="button" class="toc-heading"
(click)="toTop()" title="Top of page">Contents</button>
<div *ngIf="!hasSecondary && isEmbedded" class="toc-heading embedded">Contents</div>
<button *ngIf="hasSecondary" type="button" class="toc-heading embedded secondary"
(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">
<li *ngFor="let toc of tocList" title="{{toc.title}}" class="{{toc.level}}" [class.secondary]="toc.isSecondary">
@ -14,8 +20,9 @@
</ul>
<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"
aria-label="Expand/collapse contents">
aria-label="Expand/collapse contents"
[attr.aria-pressed]="!isCollapsed">
</button>
</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 { By, DOCUMENT } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@ -15,7 +15,8 @@ describe('TocComponent', () => {
let page: {
listItems: DebugElement[];
tocHeading: DebugElement;
tocHeadingButton: DebugElement;
tocHeadingButtonEmbedded: DebugElement;
tocHeadingButtonSide: DebugElement;
tocMoreButton: DebugElement;
};
@ -23,7 +24,8 @@ describe('TocComponent', () => {
return {
listItems: tocComponentDe.queryAll(By.css('ul.toc-list>li')),
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')),
};
}
@ -34,7 +36,8 @@ describe('TocComponent', () => {
providers: [
{ provide: ScrollService, useClass: TestScrollService },
{ provide: TocService, useClass: TestTocService }
]
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
});
@ -106,18 +109,18 @@ describe('TocComponent', () => {
});
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');
});
});
describe('when many TocItems', () => {
let scrollSpy: jasmine.Spy;
let scrollToTopSpy: jasmine.Spy;
beforeEach(() => {
fixture.detectChanges();
page = setPage();
scrollSpy = TestBed.get(ScrollService).scrollToTop;
scrollToTopSpy = TestBed.get(ScrollService).scrollToTop;
});
it('should have more than 4 displayed items', () => {
@ -128,16 +131,16 @@ describe('TocComponent', () => {
expect(page.listItems.length).toEqual(tocList.length);
});
it('should be in "closed" (not expanded) state at the start', () => {
expect(tocComponent.isClosed).toBeTruthy();
it('should be in "collapsed" (not expanded) state at the start', () => {
expect(tocComponent.isCollapsed).toBeTruthy();
});
it('should have "closed" class at the start', () => {
expect(tocComponentDe.children[0].classes.closed).toEqual(true);
it('should have "collapsed" class at the start', () => {
expect(tocComponentDe.children[0].classes.collapsed).toEqual(true);
});
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');
});
@ -145,7 +148,7 @@ describe('TocComponent', () => {
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', () => {
const aSecondary = page.listItems.find(item => item.classes.secondary);
expect(aSecondary).toBeTruthy('should find a secondary');
@ -155,32 +158,32 @@ describe('TocComponent', () => {
describe('after click tocHeading button', () => {
beforeEach(() => {
page.tocHeadingButton.nativeElement.click();
page.tocHeadingButtonEmbedded.nativeElement.click();
fixture.detectChanges();
});
it('should not be "closed"', () => {
expect(tocComponent.isClosed).toEqual(false);
it('should not be "collapsed"', () => {
expect(tocComponent.isCollapsed).toEqual(false);
});
it('should not have "closed" class', () => {
expect(tocComponentDe.children[0].classes.closed).toBeFalsy();
it('should not have "collapsed" class', () => {
expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy();
});
it('should not scroll', () => {
expect(scrollSpy).not.toHaveBeenCalled();
expect(scrollToTopSpy).not.toHaveBeenCalled();
});
it('should be "closed" after clicking again', () => {
page.tocHeadingButton.nativeElement.click();
it('should be "collapsed" after clicking again', () => {
page.tocHeadingButtonEmbedded.nativeElement.click();
fixture.detectChanges();
expect(tocComponent.isClosed).toEqual(true);
expect(tocComponent.isCollapsed).toEqual(true);
});
it('should not scroll after clicking again', () => {
page.tocHeadingButton.nativeElement.click();
page.tocHeadingButtonEmbedded.nativeElement.click();
fixture.detectChanges();
expect(scrollSpy).not.toHaveBeenCalled();
expect(scrollToTopSpy).not.toHaveBeenCalled();
});
});
@ -191,34 +194,34 @@ describe('TocComponent', () => {
fixture.detectChanges();
});
it('should not be "closed"', () => {
expect(tocComponent.isClosed).toEqual(false);
it('should not be "collapsed"', () => {
expect(tocComponent.isCollapsed).toEqual(false);
});
it('should not have "closed" class', () => {
expect(tocComponentDe.children[0].classes.closed).toBeFalsy();
it('should not have "collapsed" class', () => {
expect(tocComponentDe.children[0].classes.collapsed).toBeFalsy();
});
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();
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();
fixture.detectChanges();
expect(tocComponent.isClosed).toEqual(true);
expect(tocComponent.isCollapsed).toEqual(true);
});
it('should scroll after clicking again', () => {
page.tocMoreButton.nativeElement.click();
fixture.detectChanges();
expect(scrollSpy).toHaveBeenCalled();
expect(scrollToTopSpy).toHaveBeenCalled();
});
});
});
@ -226,9 +229,12 @@ describe('TocComponent', () => {
describe('when in side panel (not embedded))', () => {
let fixture: ComponentFixture<HostNotEmbeddedTocComponent>;
let scrollToTopSpy: jasmine.Spy;
beforeEach(() => {
fixture = TestBed.createComponent(HostNotEmbeddedTocComponent);
scrollToTopSpy = TestBed.get(ScrollService).scrollToTop;
tocComponentDe = fixture.debugElement.children[0];
tocComponent = tocComponentDe.componentInstance;
tocService = TestBed.get(TocService);
@ -255,9 +261,19 @@ describe('TocComponent', () => {
});
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');
});
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;
hasToc = false;
hostElement: HTMLElement;
isClosed = true;
isCollapsed = true;
isEmbedded = false;
private onDestroy = new Subject();
private primaryMax = 4;
@ -52,7 +52,11 @@ export class TocComponent implements OnInit, OnDestroy {
}
toggle(canScroll = true) {
this.isClosed = !this.isClosed;
if (canScroll && this.isClosed) { this.scrollService.scrollToTop(); }
this.isCollapsed = !this.isCollapsed;
if (canScroll && this.isCollapsed) { this.toTop(); }
}
toTop() {
this.scrollService.scrollToTop();
}
}

View File

@ -13,7 +13,8 @@
</a>
<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}}
<md-icon class="rotating-icon" svgIcon="keyboard_arrow_right"></md-icon>
</button>

View File

@ -50,51 +50,70 @@ aio-toc > div {
}
}
button.toc-show-all,
button.toc-heading,
button.toc-more-items {
cursor: pointer;
display: inline-block;
background: 0;
background-color: transparent;
border: none;
box-shadow: none;
color: $mediumgray;
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 {
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 {
content: 'expand_less';
}
button.toc-more-items.closed::after {
button.toc-more-items.collapsed::after {
content: 'more_horiz';
}
.mat-icon.collapsed {
@include rotate(0deg);
}
.mat-icon:not(.collapsed) {
@include rotate(90deg);
// margin: 4px;
}
ul.toc-list {
list-style-type: none;
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;
}