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
|
||||
|
||||
// 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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue