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:
parent
b836aca999
commit
4ccb2269a5
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue