Skip to content

Commit ecb956e

Browse files
Splaktarkseamon
authored andcommitted
docs-infra: address numerous a11y issues (angular#697)
* build: enable codelyzer and most default Angular CLI TSLint rules - do some merging of tslint:recommended and our rules * fix(a11y): fix theme picker keyboard access enable Codelyzer a11y rules Relates to angular#671 * fix(component-overview): header does not show up in headings list Fixes angular#695 * fix(stack-blitz-button): no accessible label and description is not on correct element Fixes angular#693 * fix(example-viewer): no accessible label on View source button Fixes angular#693 * fix(component-sidenav): apply aria-current to selected nav items & improve contrast - use Roboto font instead of system-ui for `docs-nav-content-btn`s - use a different background color for selected route, in addition to different text color - increase opacity of `docs-nav-content-btn`s to meet contrast guidelines - switch to `mat-nav-list` to get the benefits of better contrast and hover/focus styles - plus slightly more padding for touch interfaces Fixes angular#694. Relates to angular#671. * fix: use header and main tags for more semantic HTML - improves screen reader support - footer tags were already properly used Relates to angular#671. * fix(component-nav): apply aria-label to navs - so that they can be differentiated by screen readers Relates to angular#671. * fix(component-nav): remove aria-label that duplicates messages - aria-expanded already covers whether the section can be toggled or not - the button's text content already covers the accessible name Relates to angular#671. * fix(header-link): remove title - the aria-label is sufficient Relates to angular#671. * feat(theme-picker): provide a visual preview of the theme colors via an SVG * fix(theme-picker,version-picker): add/improve aria-labels and tooltips Fixes angular#671 * fix: various a11y refinements - minify theme-demo-icon.svg - use & instead of / as it reads better on a screen reader - prefix classes with `docs-` - tell screen reader users when a theme has been selected Fixes angular#671
1 parent 614db11 commit ecb956e

37 files changed

+317
-219
lines changed

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/e2e/src/app.e2e-spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('Material Docs App', () => {
88
page = new MaterialDocsAppPage();
99
});
1010

11-
it('should display welcome message', async() => {
11+
it('should display welcome message', async () => {
1212
await page.navigateTo();
1313
expect(await page.getTitleText()).toEqual('Angular Material');
1414
});

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"start": "yarn build-themes && ng serve",
99
"start:jit": "yarn build-themes && ng serve --aot=false",
1010
"start:prod": "yarn build-themes && ng serve --prod",
11-
"lint": "tslint -p tsconfig.json",
11+
"lint": "ng lint",
1212
"test": "yarn build-themes && ng test",
1313
"pree2e": "webdriver-manager update",
1414
"e2e": "yarn build-themes && ng e2e",
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
<app-navbar class="mat-elevation-z6"></app-navbar>
22
<router-outlet></router-outlet>
3-
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
<div class="docs-primary-header component-page-header">
1+
<header class="docs-primary-header component-page-header">
22
<button mat-button class="sidenav-toggle" (click)="toggleSidenav.emit()">
33
<mat-icon>menu</mat-icon>
44
</button>
55

66
<h1 focusOnNavigation>{{getTitle()}} </h1>
7-
</div>
7+
</header>

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/component-sidenav/_component-sidenav-theme.scss

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
}
1818

1919
.docs-nav-content-btn {
20-
color: rgba(mat-color($foreground, text), .5);
20+
color: rgba(mat-color($foreground, text), 0.7);
21+
font-family: Roboto, "Helvetica Neue", sans-serif;
2122

2223
&:focus {
2324
// override the default background
@@ -37,6 +38,10 @@
3738
&:hover {
3839
color: mat-color($primary, if($is-dark-theme, 200, default));
3940
}
41+
42+
&.docs-component-viewer-sidenav-item-selected {
43+
background: rgba(0, 0, 0, $nav-background-focus-opacity);
44+
}
4045
}
4146
}
4247

Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
<div class="docs-component-viewer-nav">
22
<div class="docs-component-viewer-nav-content">
3-
<nav *ngFor="let category of docItems.getCategories((params | async)?.section); let last = last;">
3+
<div *ngFor="let category of docItems.getCategories((params | async)?.section); let last = last;">
44
<button cdkAccordionItem #panel="cdkAccordionItem" (click)="panel.toggle()" expanded="true"
55
class="docs-nav-content-btn"
6-
[attr.aria-label]="category.name + ', section toggle'"
76
[attr.aria-controls]="'panel-' + category.id"
87
[attr.aria-expanded]="panel.expanded">
98
{{category.name}}
109
<mat-icon>{{panel.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'}}</mat-icon>
1110
</button>
12-
<ul [@bodyExpansion]="panel.expanded ? 'expanded' : 'collapsed'" id="panel-{{category.id}}">
13-
<li *ngFor="let component of category.items">
14-
<a [routerLink]="'/' + (params | async)?.section+ '/' + component.id"
15-
routerLinkActive="docs-component-viewer-sidenav-item-selected">
16-
{{component.name}}
17-
</a>
18-
</li>
19-
</ul>
11+
<mat-nav-list dense [@bodyExpansion]="panel.expanded ? 'expanded' : 'collapsed'"
12+
id="panel-{{category.id}}" [attr.aria-label]="category.name">
13+
<a mat-list-item *ngFor="let component of category.items"
14+
[routerLink]="'/' + (params | async)?.section+ '/' + component.id"
15+
routerLinkActive="docs-component-viewer-sidenav-item-selected"
16+
[attr.aria-current]="currentItemId === component.id ? 'page': 'false'">
17+
{{component.name}}
18+
</a>
19+
</mat-nav-list>
2020
<hr *ngIf="!last" />
21-
</nav>
21+
</div>
2222
</div>
2323
</div>

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/component-sidenav/component-sidenav.html

+5-5
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22
<!-- If on small screen, menu resides in drawer -->
33
<mat-sidenav #sidenav class="docs-component-viewer-sidenav"
44
*ngIf="(isScreenSmall | async)"
5-
[opened]="!(isScreenSmall | async)"
5+
[opened]="(isScreenSmall | async) === false"
66
[mode]="(isScreenSmall | async) ? 'over' : 'side'"
7-
[fixedInViewport]="(isScreenSmall | async) ? true : false"
7+
[fixedInViewport]="(isScreenSmall | async)"
88
[fixedTopGap]="92">
99
<app-component-nav [params]="params"></app-component-nav>
1010
</mat-sidenav>
1111
<div class="docs-component-sidenav-content">
1212
<component-page-header (toggleSidenav)="sidenav.toggle()"></component-page-header>
1313
<div class="docs-component-sidenav-inner-content">
14-
<div class="docs-component-sidenav-body-content">
14+
<main class="docs-component-sidenav-body-content">
1515
<!-- If on large screen, menu resides to left of content -->
1616
<app-component-nav
17-
*ngIf="!(isScreenSmall | async)"
17+
*ngIf="(isScreenSmall | async) === false"
1818
[params]="params">
1919
</app-component-nav>
2020
<router-outlet></router-outlet>
21-
</div>
21+
</main>
2222
<app-footer></app-footer>
2323
</div>
2424
</div>

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/component-sidenav/component-sidenav.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {HttpClientModule} from '@angular/common/http';
3737
import {StackBlitzButtonModule} from '../../shared/stack-blitz';
3838
import {SvgViewerModule} from '../../shared/svg-viewer/svg-viewer';
3939
import {ExampleModule} from '@angular/components-examples';
40+
import {MatListModule} from '@angular/material/list';
4041

4142
const SMALL_WIDTH_BREAKPOINT = 720;
4243

@@ -81,6 +82,7 @@ export class ComponentSidenav implements OnInit {
8182
export class ComponentNav implements OnInit, OnDestroy {
8283
@Input() params: Observable<Params>;
8384
expansions: {[key: string]: boolean} = {};
85+
currentItemId: string;
8486
private _onDestroy = new Subject<void>();
8587

8688
constructor(public docItems: DocumentationItems,
@@ -91,7 +93,7 @@ export class ComponentNav implements OnInit, OnDestroy {
9193
startWith(null),
9294
switchMap(() => this.params),
9395
takeUntil(this._onDestroy)
94-
).subscribe(p => this.setExpansions(p));
96+
).subscribe(params => this.setExpansions(params));
9597
}
9698

9799
ngOnDestroy() {
@@ -103,14 +105,12 @@ export class ComponentNav implements OnInit, OnDestroy {
103105
setExpansions(params: Params) {
104106
const categories = this.docItems.getCategories(params.section);
105107
for (const category of (categories || [])) {
106-
if (this.expansions[category.id]) {
107-
continue;
108-
}
109108

110109
let match = false;
111110
for (const item of category.items) {
112111
if (this._router.url.indexOf(item.id) > -1) {
113112
match = true;
113+
this.currentItemId = item.id;
114114
break;
115115
}
116116
}
@@ -167,6 +167,9 @@ const routes: Routes = [ {
167167

168168
@NgModule({
169169
imports: [
170+
MatSidenavModule,
171+
MatListModule,
172+
RouterModule,
170173
CommonModule,
171174
ComponentCategoryListModule,
172175
ComponentHeaderModule,

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/component-viewer/component-overview.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<ng-container *ngIf="componentViewer.componentDocItem | async; let docItem">
2-
<span class="cdk-visually-hidden" tabindex="-1" #initialFocusTarget>
2+
<h2 class="cdk-visually-hidden" tabindex="-1" #initialFocusTarget>
33
Overview for {{docItem?.id}}
4-
</span>
4+
</h2>
55
<doc-viewer
66
documentUrl="/docs-content/overviews/{{docItem?.packageName}}/{{docItem?.id}}.html"
77
class="docs-component-view-text-content docs-component-overview"

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/component-viewer/component-viewer.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<div class="docs-component-viewer">
2-
<nav mat-tab-nav-bar class="docs-component-viewer-tabbed-content">
2+
<nav mat-tab-nav-bar class="docs-component-viewer-tabbed-content"
3+
aria-label="Documentation Sections">
34
<a mat-tab-link class="docs-component-viewer-section-tab"
45
*ngFor="let section of sections"
56
[routerLink]="section.toLowerCase()"

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/component-viewer/component-viewer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class ComponentViewer implements OnDestroy {
3636
public _componentPageTitle: ComponentPageTitle,
3737
public docItems: DocumentationItems,
3838
) {
39-
let routeAndParentParams = [_route.params];
39+
const routeAndParentParams = [_route.params];
4040
if (_route.parent) {
4141
routeAndParentParams.push(_route.parent.params);
4242
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
<div class="docs-primary-header">
1+
<header class="docs-primary-header">
22
<h1>Guides</h1>
3-
</div>
3+
</header>
44

5-
<mat-nav-list class="docs-guide-list">
6-
<a mat-list-item *ngFor="let guide of guideItems.getAllItems()"
7-
class="docs-guide-item"
8-
[routerLink]="['/guide/', guide.id]">
9-
{{guide.name}}
10-
</a>
11-
</mat-nav-list>
5+
<main>
6+
<mat-nav-list class="docs-guide-list">
7+
<a mat-list-item *ngFor="let guide of guideItems.getAllItems()"
8+
class="docs-guide-item"
9+
[routerLink]="['/guide/', guide.id]">
10+
{{guide.name}}
11+
</a>
12+
</mat-nav-list>
13+
</main>
1214

1315
<app-footer></app-footer>

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/pages/homepage/homepage.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ <h2> Material Design components for Angular</h2>
1111
</div>
1212
</header>
1313

14-
<div class="docs-homepage-promo">
14+
<main class="docs-homepage-promo">
1515
<div class="docs-homepage-row">
1616
<div class="docs-homepage-promo-img">
1717
<docs-svg-viewer src="../assets/img/homepage/sprintzerotoapp.svg"
@@ -60,6 +60,6 @@ <h2>Optimized for Angular</h2>
6060
Get started
6161
</a>
6262
</div>
63-
</div>
63+
</main>
6464

6565
<app-footer></app-footer>

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/shared/doc-viewer/doc-viewer.spec.ts

+6-7
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,37 @@ describe('DocViewer', () => {
2222
}));
2323

2424
it('should load doc into innerHTML', () => {
25-
let fixture = TestBed.createComponent(DocViewerTestComponent);
25+
const fixture = TestBed.createComponent(DocViewerTestComponent);
2626
fixture.detectChanges();
2727

2828
const url = fixture.componentInstance.documentUrl;
2929
http.expectOne(url).flush(FAKE_DOCS[url]);
3030

31-
let docViewer = fixture.debugElement.query(By.directive(DocViewer));
31+
const docViewer = fixture.debugElement.query(By.directive(DocViewer));
3232
expect(docViewer).not.toBeNull();
3333
expect(docViewer.nativeElement.innerHTML).toBe('<div>my docs page</div>');
3434
});
3535

3636
it('should save textContent of the doc', () => {
37-
let fixture = TestBed.createComponent(DocViewerTestComponent);
37+
const fixture = TestBed.createComponent(DocViewerTestComponent);
3838
fixture.detectChanges();
3939

4040
const url = fixture.componentInstance.documentUrl;
4141
http.expectOne(url).flush(FAKE_DOCS[url]);
4242

43-
let docViewer = fixture.debugElement.query(By.directive(DocViewer));
43+
const docViewer = fixture.debugElement.query(By.directive(DocViewer));
4444
expect(docViewer.componentInstance.textContent).toBe('my docs page');
4545
});
4646

47-
4847
it('should correct hash based links', () => {
49-
let fixture = TestBed.createComponent(DocViewerTestComponent);
48+
const fixture = TestBed.createComponent(DocViewerTestComponent);
5049
fixture.componentInstance.documentUrl = `http://material.angular.io/doc-with-links.html`;
5150
fixture.detectChanges();
5251

5352
const url = fixture.componentInstance.documentUrl;
5453
http.expectOne(url).flush(FAKE_DOCS[url]);
5554

56-
let docViewer = fixture.debugElement.query(By.directive(DocViewer));
55+
const docViewer = fixture.debugElement.query(By.directive(DocViewer));
5756
// Our test runner runs at the page /context.html, so it will be the prepended value.
5857
expect(docViewer.nativeElement.innerHTML).toContain(`/context.html#test"`);
5958
});

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/shared/doc-viewer/doc-viewer.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {ComponentPortal, DomPortalHost} from '@angular/cdk/portal';
1+
import {ComponentPortal, DomPortalOutlet} from '@angular/cdk/portal';
22
import {HttpClient, HttpErrorResponse} from '@angular/common/http';
33
import {DomSanitizer} from '@angular/platform-browser';
44
import {
@@ -25,7 +25,7 @@ import {HeaderLink} from './header-link';
2525
template: 'Loading document...',
2626
})
2727
export class DocViewer implements OnDestroy {
28-
private _portalHosts: DomPortalHost[] = [];
28+
private _portalHosts: DomPortalOutlet[] = [];
2929
private _documentFetchSubscription: Subscription;
3030

3131
@Input() name: string;
@@ -102,15 +102,15 @@ export class DocViewer implements OnDestroy {
102102

103103
/** Instantiate a ExampleViewer for each example. */
104104
private _loadComponents(componentName: string, componentClass: any) {
105-
let exampleElements =
105+
const exampleElements =
106106
this._elementRef.nativeElement.querySelectorAll(`[${componentName}]`);
107107

108108
Array.prototype.slice.call(exampleElements).forEach((element: Element) => {
109-
let example = element.getAttribute(componentName);
110-
let portalHost = new DomPortalHost(
109+
const example = element.getAttribute(componentName);
110+
const portalHost = new DomPortalOutlet(
111111
element, this._componentFactoryResolver, this._appRef, this._injector);
112-
let examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
113-
let exampleViewer = portalHost.attach(examplePortal);
112+
const examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
113+
const exampleViewer = portalHost.attach(examplePortal);
114114
if (example !== null) {
115115
(exampleViewer.instance as ExampleViewer).example = example;
116116
}

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/shared/doc-viewer/header-link.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,8 @@ import {Router} from '@angular/router';
1717
@Component({
1818
selector: 'header-link',
1919
template: `
20-
<a
21-
title="Link to this heading"
22-
aria-label="Link to this heading"
23-
class="docs-markdown-a"
24-
[attr.aria-describedby]="example"
25-
[href]="_getFragmentUrl()">
20+
<a aria-label="Link to this heading" class="docs-markdown-a"
21+
[attr.aria-describedby]="example" [href]="_getFragmentUrl()">
2622
<mat-icon>link</mat-icon>
2723
</a>
2824
`

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/shared/documentation-items/documentation-items.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -583,14 +583,14 @@ const DOCS: {[key: string]: DocCategory[]} = {
583583
]
584584
};
585585

586-
for (let category of DOCS[COMPONENTS]) {
587-
for (let doc of category.items) {
586+
for (const category of DOCS[COMPONENTS]) {
587+
for (const doc of category.items) {
588588
doc.packageName = 'material';
589589
}
590590
}
591591

592-
for (let category of DOCS[CDK]) {
593-
for (let doc of category.items) {
592+
for (const category of DOCS[CDK]) {
593+
for (const doc of category.items) {
594594
doc.packageName = 'cdk';
595595
}
596596
}
@@ -619,11 +619,11 @@ export class DocumentationItems {
619619
}
620620

621621
getItemById(id: string, section: string): DocItem | undefined {
622-
const sectionLookup = section == 'cdk' ? 'cdk' : 'material';
623-
return ALL_DOCS.find(doc => doc.id === id && doc.packageName == sectionLookup);
622+
const sectionLookup = section === 'cdk' ? 'cdk' : 'material';
623+
return ALL_DOCS.find(doc => doc.id === id && doc.packageName === sectionLookup);
624624
}
625625

626626
getCategoryById(id: string): DocCategory | undefined {
627-
return ALL_CATEGORIES.find(c => c.id == id);
627+
return ALL_CATEGORIES.find(c => c.id === id);
628628
}
629629
}

material.angular.io/material.angular.io/material.angular.io/material.angular.io/material.angular.io/src/app/shared/example-viewer/example-viewer.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="docs-example-viewer-title-spacer">{{exampleData?.title}}</div>
44

55
<button mat-icon-button type="button" (click)="toggleSourceView()"
6-
[matTooltip]="'View source'">
6+
[matTooltip]="'View ' + exampleData?.title + ' source'" aria-label="View source">
77
<mat-icon>
88
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 24 24" fit="" preserveAspectRatio="xMidYMid meet" focusable="false">
99
<path fill="none" d="M0 0h24v24H0V0z"></path>

0 commit comments

Comments
 (0)