diff --git a/public/docs/_examples/cb-a11y/ts/.gitignore b/public/docs/_examples/cb-a11y/ts/.gitignore new file mode 100644 index 0000000000..0390f46835 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/.gitignore @@ -0,0 +1,11 @@ +styles.css +typings +typings.json +*.js.map +package.json +karma.conf.js +karma-test-shim.js +tsconfig.json +npm-debug*. +**/protractor.config.js + diff --git a/public/docs/_examples/cb-a11y/ts/a11y.css b/public/docs/_examples/cb-a11y/ts/a11y.css new file mode 100644 index 0000000000..8541969447 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/a11y.css @@ -0,0 +1,105 @@ +label { + color: #424242; + width: 100%; +} + +hr { + border-top: 3px double #8c8b8b; +} + +h3 { + outline: 0; +} + +.hide-element { + display: none !important; +} + +.btn { + margin-bottom: 15px; +} + +.background-contrast { + background-color: #0143A3; + color: #fff; +} + +nav a:visited, a:link { + color: darkblue; +} + +.label-default { + background-color: #3F3F3F; +} + +.skiplink { + min-height: 20px; +} + +/* #docregion cb-a11y-managing-focus-skiplinks-style */ +.skiplink a { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + color: darkblue; +} + +.skiplink a:active, +.skiplink a:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} + +/* #enddocregion */ + +/* #docregion cb-a11y-managing-focus-custom-outline */ +.custom-outline:focus { + border-color: #7F0037; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(127, 0, 55, .6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(127, 0, 55, .6); +} + +/* #enddocregion */ + +/* #docregion cb-a11y-form-controls-visually-hidden-style*/ +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} + +/* #enddocregion */ + +.like-bootstrap { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} diff --git a/public/docs/_examples/cb-a11y/ts/app/a11y-index.component.html b/public/docs/_examples/cb-a11y/ts/app/a11y-index.component.html new file mode 100644 index 0000000000..b6ac43137d --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/a11y-index.component.html @@ -0,0 +1,13 @@ + diff --git a/public/docs/_examples/cb-a11y/ts/app/a11y-index.component.ts b/public/docs/_examples/cb-a11y/ts/app/a11y-index.component.ts new file mode 100644 index 0000000000..e4e76d7d2a --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/a11y-index.component.ts @@ -0,0 +1,11 @@ +import {Component} from "angular2/core"; +import {ROUTER_DIRECTIVES} from "angular2/router"; + +@Component({ + selector: 'a11y-index', + templateUrl: './app/a11y-index.component.html', + directives: [ROUTER_DIRECTIVES] +}) +export class A11yIndex{ + +} diff --git a/public/docs/_examples/cb-a11y/ts/app/app.component.html b/public/docs/_examples/cb-a11y/ts/app/app.component.html new file mode 100644 index 0000000000..f4c9d73c8d --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/app.component.html @@ -0,0 +1,6 @@ + +
+ +
diff --git a/public/docs/_examples/cb-a11y/ts/app/app.component.ts b/public/docs/_examples/cb-a11y/ts/app/app.component.ts new file mode 100644 index 0000000000..5c3f52453b --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/app.component.ts @@ -0,0 +1,29 @@ +import {Component} from "angular2/core"; +import {ROUTER_DIRECTIVES, RouteConfig, ROUTER_PROVIDERS} from "angular2/router"; +import {A11yFormControls} from "./form-controls/a11y-form-controls.component"; +import {A11yIndex} from "./a11y-index.component"; +import {A11yHelper} from "./services/a11y-helper.service"; +import {A11yManagingFocus} from "./managing-focus/a11y-managing-focus.component"; +import {A11yComponentRoles} from "./component-roles/a11y-component-roles.component"; + +@Component({ + selector: 'app', + templateUrl: 'app/app.component.html', + directives:[ + ROUTER_DIRECTIVES, + A11yIndex + ], + providers: [ + ROUTER_PROVIDERS, + A11yHelper + ] +}) +@RouteConfig([ + {path:'/', name: 'Index', component: A11yIndex}, + {path:'/form-controls', name: 'FormControls', component: A11yFormControls}, + {path:'/managing-focus', name: 'ManagingFocus', component: A11yManagingFocus}, + {path:'/component-roles', name: 'ComponentRoles', component: A11yComponentRoles} +]) +export class AppComponent { + +} diff --git a/public/docs/_examples/cb-a11y/ts/app/component-roles/a11y-component-roles.component.html b/public/docs/_examples/cb-a11y/ts/app/component-roles/a11y-component-roles.component.html new file mode 100644 index 0000000000..7b68ed2c83 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/component-roles/a11y-component-roles.component.html @@ -0,0 +1,47 @@ + + +
+
+

Roles for custom component widgets

+
+ +
+
+

Roles in the template

+
+
+ + + I set the role in my template: + + + + +
+ +
+
+

Roles of the host element

+
+
+ +
+
+ + Do something... + +
+
+ +
+
+ +
+
+ +
+ +
diff --git a/public/docs/_examples/cb-a11y/ts/app/component-roles/a11y-component-roles.component.ts b/public/docs/_examples/cb-a11y/ts/app/component-roles/a11y-component-roles.component.ts new file mode 100644 index 0000000000..247d991917 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/component-roles/a11y-component-roles.component.ts @@ -0,0 +1,35 @@ +import {Component} from "angular2/core"; +import {A11yHelper} from "../services/a11y-helper.service"; +import {A11yCustomControl} from "../shared/a11y-custom-control.component"; +import {A11yValueHelper} from "../shared/a11y-value-helper.component"; +import {A11yCustomButton} from "../shared/a11y-custom-button.component"; + +@Component({ + selector: 'a11y-component-roles', + templateUrl: './app/component-roles/a11y-component-roles.component.html', + directives: [ + A11yCustomControl, + A11yValueHelper, + A11yCustomButton + ] +}) +export class A11yComponentRoles { + + inputDivModel: string = ''; + buttonClicks: number = 0; + + constructor(private _a11yHelper: A11yHelper){} + + onClick():void { + this.buttonClicks++; + } + + generateSkiplink(hash:string){ + return this._a11yHelper.getInternalLink(hash, 'ComponentRoles'); + } + + generateButtonString(): string{ + return `Button has been clicked ${this.buttonClicks} times`; + } + +} diff --git a/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-form-controls.component.html b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-form-controls.component.html new file mode 100644 index 0000000000..480006d0d5 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-form-controls.component.html @@ -0,0 +1,179 @@ + + +
+
+

Accessible form control labels

+
+ +
+
+

Implicit labeling

+
+
+ + +
+ +
+ + + + + +
+ +
+ + + + + +
+ + What do you like most about Angular 2? + +
+ +
+
+ + + + + +
+ + Choose your favourite Angular 2 language: + +
+ +
+
+ + + + + +
+ +
+ + + +
+ +
+
+

Explicit labeling

+
+
+ + +
+ + +
+ + + + +
+ +
+
+

Hiding labels

+
+
+ + +
+ + +
+ + + + + +
+ +
+ + + + +
+ +
+
+

Labeling custom controls

+
+
+ + + + Write in this labeled div: + + + + + + + + Write in this wrapped input: + + + + + + +
+ Back to index... +
+ + diff --git a/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-form-controls.component.ts b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-form-controls.component.ts new file mode 100644 index 0000000000..fe6d87dc48 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-form-controls.component.ts @@ -0,0 +1,70 @@ +import {Component, OnInit} from "angular2/core"; +import {CORE_DIRECTIVES, FORM_DIRECTIVES} from "angular2/common"; +import {A11yHelper} from "../services/a11y-helper.service"; +import {ROUTER_DIRECTIVES} from "angular2/router"; +import {A11yInputWrapper} from "./a11y-input-wrapper.component"; +import {A11yValueHelper} from "../shared/a11y-value-helper.component"; +import {A11yCustomControl} from "../shared/a11y-custom-control.component"; + +@Component({ + selector: 'a11y-form-controls', + templateUrl: './app/form-controls/a11y-form-controls.component.html', + directives: [ + CORE_DIRECTIVES, + FORM_DIRECTIVES, + ROUTER_DIRECTIVES, + A11yCustomControl, + A11yInputWrapper, + A11yValueHelper + ] +}) +export class A11yFormControls implements OnInit { + + checkBoxes:any; + radioButtons:any; + selectOptions:any; + + inputModel:string; + inputExplicitModel: string; + inputWrappedModel: string; + inputWrappedSaveModel:string = ''; + inputDivModel: string = ''; + textModel:string; + selectModel: string = 'Curiosity'; + searchModel: string; + filterModel: string; + + radioModel:string = 'TypeScript'; + checkboxModel:Array = ["Observables", "Components"]; + + + constructor(private _a11yHelper:A11yHelper) { + } + + generateSkiplink(hash:string){ + return this._a11yHelper.getInternalLink(hash, 'FormControls'); + } + + isChecked(item:string):boolean { + return this._a11yHelper.isStringInArray(this.checkboxModel, item); + } + + toggleCheckbox(item:string):void { + this._a11yHelper.toggleItemInArray(this.checkboxModel, item); + } + + onSave(){ + this.inputWrappedSaveModel = this.inputWrappedModel; + } + + ngOnInit() { + this.checkBoxes = this._a11yHelper.getCheckboxModel(); + this.radioButtons = this._a11yHelper.getRadiobuttonsModel(); + this.selectOptions = this._a11yHelper.getSelectOptions(); + } + + updateSelect(value: string):void{ + this.selectModel = value; + } + +} diff --git a/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.css b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.css new file mode 100644 index 0000000000..14f209046a --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.css @@ -0,0 +1,27 @@ +/* #docregion */ +:host input { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +:host input:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6); +} +/* #enddocregion */ diff --git a/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.html b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.html new file mode 100644 index 0000000000..544ea235fc --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.html @@ -0,0 +1,19 @@ + +
+
+ +
+
+ diff --git a/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.ts b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.ts new file mode 100644 index 0000000000..5798b37660 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.ts @@ -0,0 +1,18 @@ +import {Component, Output, EventEmitter} from "angular2/core"; + +// #docregion +@Component({ + selector: 'a11y-input-wrapper', + templateUrl: './app/form-controls/a11y-input-wrapper.component.html', + styleUrls: ['./app/form-controls/a11y-input-wrapper.component.css'] +}) +export class A11yInputWrapper{ + + @Output() + onSave: EventEmitter = new EventEmitter(); + + save(){ + this.onSave.emit(null); + } +} +// #enddocregion diff --git a/public/docs/_examples/cb-a11y/ts/app/main.ts b/public/docs/_examples/cb-a11y/ts/app/main.ts new file mode 100644 index 0000000000..4dc919769e --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/main.ts @@ -0,0 +1,5 @@ +import {bootstrap} from 'angular2/platform/browser'; +import {AppComponent} from './app.component'; + + +bootstrap(AppComponent); \ No newline at end of file diff --git a/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-error-demo.component.html b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-error-demo.component.html new file mode 100644 index 0000000000..52a6e22017 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-error-demo.component.html @@ -0,0 +1,18 @@ + + + + diff --git a/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-error-demo.component.ts b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-error-demo.component.ts new file mode 100644 index 0000000000..4d1a80535f --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-error-demo.component.ts @@ -0,0 +1,19 @@ +import {Component} from "angular2/core"; + +// #docregion +@Component({ + selector: 'a11y-error-demo', + templateUrl: './app/managing-focus/a11y-error-demo.component.html' +}) +export class A11yErrorDemo { + + hideErrorConfirmation:boolean = true; + + setFocusOn(element: any) { + this.hideErrorConfirmation = false; + setTimeout(() => { + element.focus(); + }, 200); + } +} +// #enddocregion diff --git a/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html new file mode 100644 index 0000000000..9507810c3f --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html @@ -0,0 +1,94 @@ + + + + +
+
+

Managing focus

+
+ +
+
+

The focus outline

+
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +

Focus flow

+ +
+
+ + +
+
+ +
+
+ +
+
+ + + +
+ +
+
+

Focusing custom controls

+
+
+ +
+
+ + Do something... + +
+
+ +
+
+ +
+
+
+ +
+
+

Internal focus in a component

+
+
+ + +
+ + Back to index... + +
+ diff --git a/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.ts b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.ts new file mode 100644 index 0000000000..ca1d7846a8 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.ts @@ -0,0 +1,44 @@ +import {Component, OnInit} from "angular2/core"; +import {A11yHelper} from "../services/a11y-helper.service"; +import {CORE_DIRECTIVES} from "angular2/common"; +import {A11yErrorDemo} from "./a11y-error-demo.component"; +import {ROUTER_DIRECTIVES} from "angular2/router"; +import {A11yCustomButton} from "../shared/a11y-custom-button.component"; +import {A11yValueHelper} from "../shared/a11y-value-helper.component"; + +@Component({ + selector: 'a11y-managing-focus', + templateUrl: './app/managing-focus/a11y-managing-focus.component.html', + directives: [ + CORE_DIRECTIVES, + ROUTER_DIRECTIVES, + A11yCustomButton, + A11yValueHelper, + A11yErrorDemo + ] +}) +export class A11yManagingFocus implements OnInit{ + + countriesWorkedIn: Array; + buttonClicks: number = 0; + + constructor(private _a11yHelper:A11yHelper) { + } + + generateSkiplink(hash:string) { + return this._a11yHelper.getInternalLink(hash, 'ManagingFocus'); + } + + onClick():void { + this.buttonClicks++; + } + + generateButtonString(): string{ + return `Button has been clicked ${this.buttonClicks} times`; + } + + ngOnInit():void{ + this.countriesWorkedIn = this._a11yHelper.getCountriesWorkedIn() + } + +} diff --git a/public/docs/_examples/cb-a11y/ts/app/services/a11y-helper.service.ts b/public/docs/_examples/cb-a11y/ts/app/services/a11y-helper.service.ts new file mode 100644 index 0000000000..eab283b6c6 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/services/a11y-helper.service.ts @@ -0,0 +1,111 @@ +import {Injectable} from "angular2/core"; +import {Router} from "angular2/router"; + +@Injectable() +export class A11yHelper { + + constructor(private _router: Router){} + + getInternalLink(hash: string, instructionName: string): string{ + let instruction = this._router.generate([instructionName]); + let path = '/' + instruction.toUrlPath() + hash; + return path; + } + + generateUniqueIdString():string { + return (this.randomGuidSnippet() + + this.randomGuidSnippet() + "-" + + this.randomGuidSnippet() + "-4" + + this.randomGuidSnippet().substr(0, 3) + "-" + + this.randomGuidSnippet() + "-" + + this.randomGuidSnippet() + + this.randomGuidSnippet() + + this.randomGuidSnippet()).toLowerCase(); + } + + getCheckboxModel():any { + return [ + { + name: 'Template syntax', + value: 'Template syntax' + }, + { + name: 'Observables', + value: 'Observables' + }, + { + name: 'Components', + value: 'Components' + }, + { + name: 'Forms', + value: 'Forms' + } + ]; + } + + getRadiobuttonsModel():any { + return [ + { + name: 'TypeScript', + value: 'TypeScript' + }, + { + name: 'JavaScript', + value: 'JavaScript' + }, + { + name: 'ES6', + value: 'ES6' + }, + { + name: 'Dart', + value: 'Dart' + } + ]; + } + + getSelectOptions():any { + return [ + { + name: 'Curiosity', + value: 'Curiosity' + }, + { + name: 'Increased userbase', + value: 'Increased userbase' + }, + { + name: 'Legal reasons', + value: 'Legal reasons' + } + ]; + } + + getCountriesWorkedIn():Array{ + return ['The USA', 'The Netherlands', 'South Africa', 'Germany', 'The UK']; + } + + toggleItemInArray(stringArray:Array, item:string): void { + var entryIndex = stringArray.indexOf(item); + if (entryIndex != -1) { + stringArray.splice(entryIndex, 1); + } else { + stringArray.push(item); + } + } + + isStringInArray(stringArray: Array, item: string): boolean { + return stringArray.indexOf(item.toString()) != -1; + } + + removeHtmlStringBreaks(inputValue: string):string{ + return inputValue.replace(new RegExp('
', 'g'), '') + .replace(new RegExp('
', 'g'), '\n') + .replace(new RegExp('
', 'g'), '') + } + + private randomGuidSnippet():string { + return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); + } +} diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-button.component.html b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-button.component.html new file mode 100644 index 0000000000..ceb79a69d3 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-button.component.html @@ -0,0 +1,3 @@ + + + diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-button.component.ts b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-button.component.ts new file mode 100644 index 0000000000..0c03b7bf9b --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-button.component.ts @@ -0,0 +1,25 @@ +import {Component, EventEmitter, Output} from "angular2/core"; + +// #docregion +@Component({ + selector: 'a11y-custom-button', + templateUrl: './app/shared/a11y-custom-button.component.html', + host: { + 'role': 'button', + 'tabindex': '0', + 'class': 'btn btn-primary', + '(keydown.space)': 'onKeyDown()', + '(keydown.enter)': 'onKeyDown()' + } +}) +export class A11yCustomButton { + + @Output() + click:EventEmitter = new EventEmitter(); + + onKeyDown() { + this.click.emit(null); + } + +} +// #enddocregion diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.css b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.css new file mode 100644 index 0000000000..85aa2a3e40 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.css @@ -0,0 +1,6 @@ +/* #docregion */ +div.edit-box{ + height: auto; + min-height: 50px; +} +/* #enddocregion */ diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.html b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.html new file mode 100644 index 0000000000..21fdf24fbe --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.html @@ -0,0 +1,18 @@ + +
+
+ +
+
+
+ diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.ts b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.ts new file mode 100644 index 0000000000..a62782d9ba --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-custom-control.component.ts @@ -0,0 +1,68 @@ +import {Component, OnInit, Provider, forwardRef,} from "angular2/core"; +import {A11yHelper} from "../services/a11y-helper.service"; +import {NG_VALUE_ACCESSOR, ControlValueAccessor} from "angular2/common"; + +// #docregion +const noop = () => { +}; + +const A11Y_CUSTOM_CONTROL_VALUE_ACCESSOR = new Provider( + NG_VALUE_ACCESSOR, { + useExisting: forwardRef(() => A11yCustomControl), + multi: true + }); + +@Component({ + selector: 'a11y-custom-control', + templateUrl: './app/shared/a11y-custom-control.component.html', + styleUrls: ['./app/shared/a11y-custom-control.component.css'], + providers: [A11Y_CUSTOM_CONTROL_VALUE_ACCESSOR] +}) +export class A11yCustomControl implements OnInit, ControlValueAccessor { + + uniqueId:string; + + private _innerValue:any = ''; + outerValue:string = ''; + + private _onTouchedCallback:() => void = noop; + private _onChangeCallback:(_:any) => void = noop; + + constructor(private _a11yHelper:A11yHelper) { + } + + onChange(event:any, value:string) { + if (event.keyCode == 13) { + event.preventDefault(); + } + else { + this._innerValue = this._a11yHelper.removeHtmlStringBreaks(value); + this._onChangeCallback(this._innerValue); + } + } + + onBlur(){ + this._onTouchedCallback(); + } + + writeValue(value:any) { + if (value != this._innerValue) { + this._innerValue = value; + this.outerValue = value; + } + } + + registerOnChange(fn:any) { + this._onChangeCallback = fn; + } + + registerOnTouched(fn:any) { + this._onTouchedCallback = fn; + } + + ngOnInit():void { + this.uniqueId = this._a11yHelper.generateUniqueIdString(); + } + +} +// #enddocregion diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-value-helper.component.html b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-value-helper.component.html new file mode 100644 index 0000000000..bde42e27d4 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-value-helper.component.html @@ -0,0 +1,5 @@ + + Current value: {{displayValue}} + diff --git a/public/docs/_examples/cb-a11y/ts/app/shared/a11y-value-helper.component.ts b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-value-helper.component.ts new file mode 100644 index 0000000000..6443299ec6 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/app/shared/a11y-value-helper.component.ts @@ -0,0 +1,18 @@ +import {Component, Input} from "angular2/core"; + +@Component({ + selector: 'a11y-value-helper', + templateUrl: './app/shared/a11y-value-helper.component.html', + styles: [` + .value-label { + position:relative; + top: -15px; + } +`] +}) +export class A11yValueHelper { + + @Input() + displayValue: any; + +} diff --git a/public/docs/_examples/cb-a11y/ts/example-config.json b/public/docs/_examples/cb-a11y/ts/example-config.json new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/example-config.json @@ -0,0 +1 @@ + diff --git a/public/docs/_examples/cb-a11y/ts/index.html b/public/docs/_examples/cb-a11y/ts/index.html new file mode 100644 index 0000000000..e56cff83f3 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/index.html @@ -0,0 +1,42 @@ + + + + + A11y demonstration + + + + + + + + + + + + + + + + + + + + + + +loading... + + + diff --git a/public/docs/_examples/cb-a11y/ts/plnkr.json b/public/docs/_examples/cb-a11y/ts/plnkr.json new file mode 100644 index 0000000000..8e4e448188 --- /dev/null +++ b/public/docs/_examples/cb-a11y/ts/plnkr.json @@ -0,0 +1,8 @@ +{ + "description": "A11y Cookbook samples", + "files":[ + "!**/*.d.ts", + "!**/*.js" + ], + "tags":["cookbook", "a11y"] +} \ No newline at end of file diff --git a/public/docs/dart/latest/cookbook/_data.json b/public/docs/dart/latest/cookbook/_data.json index ce8315d497..3600c7ca08 100644 --- a/public/docs/dart/latest/cookbook/_data.json +++ b/public/docs/dart/latest/cookbook/_data.json @@ -4,14 +4,20 @@ "navTitle": "Overview", "intro": "A collection of recipes for common Angular application scenarios" }, - + "a1-a2-quick-reference": { "title": "Angular 1 to 2 Quick Reference", "navTitle": "Angular 1 to 2 Quick Ref", "intro": "Learn how Angular 1 concepts and techniques map to Angular 2", "hide": true }, - + + "a11y": { + "title": "ARIA / Accessibility (a11y) reference", + "navTitle": "ARIA / Accessibility (a11y)", + "intro": "Learn how to make your Angular 2 sites accessible for everyone" + }, + "component-communication": { "title": "Component Interaction", "intro": "Share information between different directives and components" @@ -22,7 +28,7 @@ "intro": "Techniques for Dependency Injection", "hide": true }, - + "dynamic-forms": { "title": "Dynamic Form", "intro": "Render dynamic forms with NgFormModel", @@ -34,4 +40,4 @@ "intro": "Convert Angular 2 TypeScript examples into ES5 JavaScript", "hide": true } -} \ No newline at end of file +} diff --git a/public/docs/dart/latest/cookbook/a11y.jade b/public/docs/dart/latest/cookbook/a11y.jade new file mode 100644 index 0000000000..e44128c6a2 --- /dev/null +++ b/public/docs/dart/latest/cookbook/a11y.jade @@ -0,0 +1,2 @@ +!= partial("../../../_includes/_ts-temp") + diff --git a/public/docs/js/latest/cookbook/_data.json b/public/docs/js/latest/cookbook/_data.json index a2040caccf..38bcf23120 100644 --- a/public/docs/js/latest/cookbook/_data.json +++ b/public/docs/js/latest/cookbook/_data.json @@ -11,6 +11,12 @@ "intro": "Learn how Angular 1 concepts and techniques map to Angular 2" }, + "a11y": { + "title": "ARIA / Accessibility (a11y) reference", + "navTitle": "ARIA / Accessibility (a11y)", + "intro": "Learn how to make your Angular 2 sites accessible for everyone" + }, + "component-communication": { "title": "Component Interaction", "intro": "Share information between different directives and components" @@ -20,7 +26,7 @@ "title": "Dependency Injection", "intro": "Techniques for Dependency Injection" }, - + "dynamic-forms": { "title": "Dynamic Form", "intro": "Render dynamic forms with NgFormModel" diff --git a/public/docs/js/latest/cookbook/a11y.jade b/public/docs/js/latest/cookbook/a11y.jade new file mode 100644 index 0000000000..6778b6af28 --- /dev/null +++ b/public/docs/js/latest/cookbook/a11y.jade @@ -0,0 +1 @@ +!= partial("../../../_includes/_ts-temp") diff --git a/public/docs/ts/latest/cookbook/_data.json b/public/docs/ts/latest/cookbook/_data.json index e4363318ef..11d69c8e0c 100644 --- a/public/docs/ts/latest/cookbook/_data.json +++ b/public/docs/ts/latest/cookbook/_data.json @@ -4,13 +4,19 @@ "navTitle": "Overview", "description": "A collection of recipes for common Angular application scenarios" }, - + "a1-a2-quick-reference": { "title": "Angular 1 to 2 Quick Reference", "navTitle": "Angular 1 to 2 Quick Ref", "intro": "Learn how Angular 1 concepts and techniques map to Angular 2" }, - + + "a11y": { + "title": "ARIA / Accessibility (a11y) reference", + "navTitle": "ARIA / Accessibility (a11y)", + "intro": "Learn how to make your Angular 2 sites accessible for everyone" + }, + "component-communication": { "title": "Component Interaction", "intro": "Share information between different directives and components" diff --git a/public/docs/ts/latest/cookbook/a11y.jade b/public/docs/ts/latest/cookbook/a11y.jade new file mode 100644 index 0000000000..f021ac2381 --- /dev/null +++ b/public/docs/ts/latest/cookbook/a11y.jade @@ -0,0 +1,1080 @@ +include ../_util-fns + +:marked + ## Welcome to A11y! + + You are about to learn how to make your Angular 2 application accessible for + as many people as possible. This is no small goal as there is a large group of people out there who find it + very hard or even impossible to use applications that have not been built with these concepts in mind. + + We will show you how to integrate these concepts with little to no extra effort on your part, + provided you take them into account right from the design phase of your application. + + So page through this cookbook, apply these recipes and watch your user base grow. + + If you see the terms `Web Accessibility`, `ARIA` or `a11y` for the first time, you are at the start of an + incredible journey. For those who already enjoy the benefits of `Accessible Web Applications`, we will + show you how to use your knowledge in the new and exciting world of Angular 2. + +.l-main-section +:marked + ## A11y and ARIA in a nutshell + + `Accessibility` is often called `a11y`. This is because we want to say as lot while we write less. As + it has eleven letters, starts with an `a` and ends with a `y`, we shorten this word to `a11y`. We will + refer to `a11y` when we want to say `accessibility`. + +.l-sub-section + :marked + In short, `a11y` refers to creating web applications that everyone can use, making it accessible to everyone. + +:marked + If you are totally new to the term you may want to have a look at what the folks at the `W3C` have to say about + [a11y](https://www.w3.org/WAI/intro/accessibility.php), to put the rest of the article into perspective. + + What is that other word? + +.l-sub-section + :marked + `ARIA`, or `Accessible Rich Internet Applications` refers to a standard set of attributes for adding accessibility + information to `HTML` and `SVG`, which allows us to bring `a11y` concepts into + Internet applications like those we are building with Angular 2. + +:marked + You can also read what they say about [ARIA](https://www.w3.org/WAI/intro/aria) at the `W3C`. We will be right here + waiting for you when you come back. + +.callout.is-important + header ARIA terminology confusion alert! + :marked + In `ARIA` we refer to the `aria-...` attributes as `ARIA States` or `ARIA Properties`. The difference between + the two should become clear as you progress through this cookbook. However, `ARIA Properties` are not + real `HTML` element properties but decorating attributes referring to properties. When we refer to an + `ARIA Property` in the code you will **HAVE** to do + it with an Angular 2 attribute binding. This is simply a terminology clash. + +.l-main-section +:marked + ## Native elements vs. custom components + + Angular 2 gives you a lot of power at your fingertips to create extremely powerful components. Spare a + thought for the native `HTML` elements before you decide to build something new. + + The makers of the browsers have spent a lot of time thinking about the very same issue that brought + you to this page today and have provided you with a lot of functionality out-of-the-box when you make use of + native `HTML` elements. + + We would like to suggest the following rule of thumb when building your applications: + +.l-sub-section + :marked + If there is already a native element inside `HTML` providing the function that you need, use + that element instead of writing your own. + +:marked + So, if you want to accept user text input, use the `input` element instead of constructing something new. + + This way you have to think less about things like focus management, tabindex, etc. and have more time to think about + your code. + + We know this is not always possible, so this guide will also show you what you can do to make your own + components as accessible as possible. + + So without further ado, let us see how easy it is to get big `a11y` wins in our applications! + +.l-main-section +:marked + ## Important note on styling in this chapter + +.callout.is-important + header Angular Pages Do Not Require A Style Library + :marked + Where you see `CSS` classes in the example code, these classes are independent of Angular 2. It does not need + the styles of any external library. We are free to style our apps with any `CSS library`, our own `Custom CSS` or use no + `CSS` at all. + +:marked + Classes like `container`, `row`, `col-xs-12`, `checkbox`, `radio`, `form-group`, `form-control`, etc, + come from [Twitter Bootstrap](http://getbootstrap.com/css/). This is a purely cosmetic addition to prettify + the examples so that they are as visually appealing as they are accessible. + + Always remember, just because something looks good, it does not mean that it is accessible. + + The good news is, here we strive to do both! + +.l-main-section + +:marked + ## Table of contents + + [Accessible form control labels](#form-control-labels) + + [Managing focus](#managing-focus) + + [Roles for custom component widgets](#component-roles) + +.l-sub-section + :marked + In the example application code when we need unique element id's we will be generating `GUID's`to make sure that they are + unique. You can use your own method to do this, as long as every id on a page is unique. More on this later... + +:marked + **Feel free to follow along in this [live example](/resources/live-examples/cb-a11y/ts/plnkr.html)**. + +.l-main-section + +:marked + ## Accessible form control labels + + Whether we are using native interactive `HTML` elements or creating our own rich custom interactive components, + it is crucial to + label them. Imagine coming face-to-face with a customer detail page on your favourite online store, and + be greeted with a screen filled with unlabeled input fields! + + That would be a nightmare right? The users will leave so fast the bounce rate counter will be able to power a small town, it will be + spinning *THAT* fast. + + We can avoid this from ever happening by simply adding a label for each field. The challenge is that + many users of our website will not be able to see or recognize these labels without help. + + For this reason it is not only important to visually mark any form component, or `form controls` as they are sometimes called, + but to do so in a way that also exposes it to assistive technologies. + + We will discuss some ways to do this. + +.l-sub-section + :marked + You will see the `ng-content` tag making its appearance in some examples. This is because we are making use + of `Content Projection` to load content into the templates of our components. + +.l-sub-section + :marked + It is very important to note that a `label` element can describe one and only form input element. You cannot + label multiple form fields with one label. However, this is something we can do with `ARIA`. + +.l-sub-section + :marked + The label text position also matters. For `inputs`, `textareas` and `selects`, the label text precedes + the element and for `checkboxes` and `radiobuttons`, the text should follow the element in the flow. + +:marked + ### Implicit labeling + + Firstly we will look at the easiest, quickest way to give our form controls accessible labels, + and that is with a syntax called `implicit labeling`. + +.l-sub-section + :marked + Use this method to label your `form controls` when possible. As you will see later, the other methods of + labeling rely on generating unique id's for your elements. As we are often building reusable components + in Angular 2, you will need to make sure that every id you create is unique on a page, no matter how often you use + your component! Save yourself the trouble and label implicitly. + +:marked + We label implicitly like this: + +code-example(language="html" escape="html"). + + +:marked + Easy, isn't it? Of course, here we can substitute the `input` element with any native `HTML` form control. + + Let us now explore how we can use `implicit labeling` to decorate the commonly used native `HTML` form controls + in our Angular 2 components. + +:marked + #### Inputs and textareas + + Labeling an input: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-input-implicit') + +:marked + Labeling a textarea: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-textarea-implicit') + +:marked + #### Checkboxes and radiobuttons + +.l-sub-section + :marked + Because of the many `input` fields making up a `checkbox group` or `radiobutton group`, the usual rule applies + for each input but the entire group also needs labeling and we do this by using `fieldset` and `legend`. + +:marked + Labeling checkboxes: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-checkboxes-implicit') + +:marked + Labeling radiobuttons: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-radiobuttons-implicit') + +:marked + #### Select lists + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-select-implicit') + +:marked + ### Explicit labeling + + There could be a number of reasons why you prefer not using the `implicit labeling` syntax described above. Maybe + you need to write the input outside the label for positioning or styling purposes, or maybe your elements are + already furnished with id's. + + As an alternative, you can also use the `explicit labeling` syntax. For this syntax + the `HTML` form element needs an `id`, which is then connected via a `for / id` pair *[The `for` attribute of the label has to + match the `id` of the element being labeled]* . + + As an example, we will only look at adding an `explicit label` to a `text input`. The examples under the `implicit labeling` + section can then easily be adjusted to use this syntax. + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html', 'cb-a11y-form-controls-input-explicit') + +:marked + ### Hidden labels + + Sometimes, the design of your page makes more sense without a visible label. Think about `search` fields. Do you have + to give a visual label for each one? + + The answer is no, but does that mean we get away with not labeling those fields at all? + + Again the answer is no. + + So how do we solve it? + + We can either use an `explicitly labeled` input and hide the label with style, or we can use `aria-label`, which + is an `ARIA Property`. + +.l-sub-section + :marked + We cannot hide the label using the `display` css property. Hidden fields are also hidden to assistive + technologies so we have to use some css magic to either place the label outside the visible page or + shrink it right down. We will not see it, but screen readers will still find it and read it while + still linking it to the correct input via the `for / id` linkup. The following example has such a + `visually hidden` style. + +:marked + Example of a good visually hidden css style: + ++makeExample('cb-a11y/ts/a11y.css', 'cb-a11y-form-controls-visually-hidden-style') + +:marked + Applying the style to a control to hide the label: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html', 'cb-a11y-form-controls-hidden-label-explicit') + +:marked + Or the `aria-label` alternative: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-hidden-label-aria') + +:marked + This `aria-label` attribute serves the same `a11y` purpose as our `label` tags above and tells screen readers the label + of the field. + +.l-sub-section + :marked + So why not simply always use `aria-label`? This is because adding the `label` element not only provides the visual + label, but linking a `label` to a `native form control` also means that clicking on the `label` will select the + `form control` itself. This assists users with motor disabilities by providing a larger clickable area, thereby also + touching on another important area of `a11y`. + +:marked + Let's have a look at a quick comparison between and `input` with no label versus one labeled with `aria-label`. + + The following shows what happens if we have an `input` for filtering criteria that does not take any `a11y` into account. We will + use the accessibility tools in the newest development version of `Chrome`, although you can use one of a number of internal + and external web browser tools to test accessibility. + + Here we see the information that assistive technologies will use when reading our field out: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/invisible-label-input-not-labeled.png" alt="Input with invisible label not labeled correctly") + +:marked + The only description a user with a screen reader will get is **Enter a value**. What value? + + Just imagine if there are more than one input on the same page with the same placeholder. Utter chaos! + + Now let's get some `a11y` going there and look at the same input again: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/invisible-label-input-labeled.png" alt="Input with invisible label labeled correctly") + +:marked + Immediately it is clear what this `input` field is all about and what to do with it! + +:marked + ### Labeling custom form controls + + So you need to do something and try as you might you cannot construct it with native `HTML` elements + **OR** you need to give that legacy application an `a11y` makeover without the luxury of rewriting the + entire code base. + + How would you go about making these custom form controls accessible? + + Fear not, there is a way! We introduce the next addition to our `ARIA` toolkit, and that is `aria-labelledby`. + + Wait, why do we even need this? We just use the `for / id` binding of the `label` element, right? + + Wrong again! The `for / id` function of the `label` element is only recognized when used with + the native `HTML` form control elements such as `input` and `textarea`. To label anything else, like a `div` or a + `custom element` we need to create this link by using `aria-labelledby`. + + Let's illustrate this by recreating the native `input` element with a component that makes use of `div's`. + +.l-sub-section + :marked + Please note that creating an `input` out of `div's` is **NOT** recommended, but only serves as an illustration that + it is possible to make any form control accessible. Also note that as we are focusing on `a11y`, our component is not + production ready but only + implements the basics of functionality to make it function as an `input`, for example adding the machinery to make it play nicely + with `ngModel`. The full implementation would become + even larger and more complex before we can use it in an enterprise application. We hope that this illustrates + further why native `HTML` elements should preferably be used. + +:marked + Our component: + ++makeTabs('cb-a11y/ts/app/shared/a11y-custom-control.component.html,cb-a11y/ts/app/shared/a11y-custom-control.component.ts,cb-a11y/ts/app/shared/a11y-custom-control.component.css', +null, 'a11y-custom-control.component.html,a11y-custom-control.component.ts, a11y-custom-control.component.css') + +:marked + This can now be used in our `HTML` as follows: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-custom-control-usage') + +:marked + Let's have a look at what is rendered out. *For clarity sake, we omit the style attributes added by Angular 2 + for the component style.*: + +code-example(language="html" escape="html" format="linenums"). + +
+ +
+
+
+ +:marked + The first thing that we should note is the `role` attribute. This is also part of `ARIA` and we use these when we need + to tell assistive technologies that the semantic role of an `HTML` element has changed. The `div` element was certainly + never specified as a textbox! Here we are doing just that, so our custom control needs the role of + `textarea`. We will look at `ARIA Roles` in more detail later. + + Next we will look at the `aria-labelledby` attribute. As you can see, this has the `id` of the `label` field. This + is how we tell screen readers to use that specific `label` element to label our input control. + +.l-sub-section + :marked + Besides the need to generate unique `id's`, there is one more warning in using `aria-labelledby`. Even when using this + with an actual `label` element, clicking the label will **NOT** focus the input as it does when used with native + `HTML` form control elements. Therefore, this construct will always be slightly inferior to the native approach, as you lose the + accessibility gain the `label click` gives you. + +:marked + We also snuck in yet another `ARIA` property called `aria-multiline`. Yes folks, + we need to tell someone who is unable to see if our input accepts single or multiple lines of text. By using + `aria-multiline`, we are able to tell the screen reader whether it is single or multiline field. + + *That was certainly a mouthful! Aren't there ways where we can use the power of Angular 2, but keep the + built-in `a11y` wins the native `HTML` elements give us?* + + There certainly are and let's look at one such option that uses `Content Projection`. + +:marked + ### Labeling options with Content Projection + + One of the biggest wins in using Angular 2 components is that they are reusable. This means less code to write, + which makes everyone happy as it leads to a more maintainable code base and building + functionality quicker. + + The syntax patterns we suggested earlier in this section could, of course, work against this very strength. Duplicating + all the `HTML` for each well styled and functional form control is not what we are aiming for either. Neither + are we suggesting throwing out all the power of Angular 2. Hey, we built it after all! + + There are mechanisms inside Angular 2 which allows you to use what you have just learnt in clever ways to + gain the biggest wins with accessibility of your form controls, while also benefiting from using the framework. + + As an example of what you could do, we will be looking at using `Content Projection` and `Implicit Labeling` to build + a component that can decorate any input: + ++makeTabs('cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.html,cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.ts,cb-a11y/ts/app/form-controls/a11y-input-wrapper.component.css', +null, 'a11y-input-wrapper.component.html,a11y-input-wrapper.component.ts, a11y-input-wrapper.component.css') + +:marked + How do we use it? Like this: + ++makeExample('cb-a11y/ts/app/form-controls/a11y-form-controls.component.html','cb-a11y-form-controls-custom-control-wrapped-usage') + +:marked + Let's quickly dive into the code. + + We used `Content Projection` with `multiple projection slots` to project our `input` and the `label` content into our component's + template. This way we keep direct access to and direct control of the `label` content as well as the `input`. + + Our component gets to do all the hard work for us while we have got all the benefits of using the native `HTML input` + element directly! + + We did not have to add any of the extra code of the previous component and our resultant decorated `input` + control is fully accessible. + + With `a11y` in Angular 2 it truly seems that you can indeed have your cake.... and eat it! + +:marked + ### Section summary + + In this section we looked at how we can give accessible labels to our native, as well as custom, form controls. + + We looked at `implicit labeling` versus `explicit labeling` and how `implicit labeling` can often save you + a lot of extra lines of code and trouble. + + We also saw that even when a control does not need a visual label, it still needs a label for those who + cannot see it and we looked at ways to hide these labels in an accessible way. + + Finally, we looked at how we can label even the most inaccessible of controls with `ARIA` and how we could use + what lies in our Angular 2 toolbox in clever ways so that we could benefit from combining the raw power of + Angular 2 with the `a11y` implementations of native `HTML` elements. + + Keep reading for more on `a11y` in Angular 2 or [go back to the table of contents](#toc) + +.l-main-section + +:marked + ## Managing focus + + Often when developers think of `a11y`, they only think of people with visual disabilities and screen readers. + + While we do a lot to make sure that screen readers can properly consume and read our pages, `a11y` is + about much, much more. Visual disabilities is only one of the main groups of disabilities that hamper access to the web. + There is also a vast group of people out there who finds it difficult to use our websites due to `motor disabilities`. + + We need to spare a thought for those people out there who would love to use our shiny new Angular 2 application, but + simply cannot use a mouse due to a variety of reasons. Some people have no hands whilst others + are unable to use their hands temporarily, due to injury. Just like those with visual disabilities, this group also + relies strongly on the keyboard or other types of assistive technologies when they navigate the web. + +.l-sub-section + :marked + One of the most important aspects of `a11y` is called `keyboard accessibility`. This means that every page we make + **MUST** be navigable and operable through use of the keyboard alone. + +:marked + So how do `keyboard accessibility` relate to `managing focus`? + + For a user totally dependent on a screen reader and/or keyboard, his interaction with our site is completely based on + where the current focus of the application lies. + +.l-sub-section + :marked + Focus is broken down into `keyboard focus`, which refers to the area of the page that will next be affected by a + keyboard action and `reading focus`, which refers to where the screen reader will next start reading from. + +:marked + In this section we will be looking primarily at `keyboard focus`. By correctly managing `keyboard focus`, the + `reading focus` will usually also be correct. + +.l-sub-section + :marked + For those using keyboard only navigation, focus is typically set one focusable element forward using `Tab` + or one element backward using `Shift+Tab`. Why not try it now? Go to your favorite online shopping site and + see if you can order your next product using only `Tab` and `Shift+Tab` for navigation and `Enter` to select + actions. Walking a mile in the shoes of someone who cannot use a mouse can give us a lot of respect for why we + should make our websites accessible. + +:marked + ### Outline marks the spot + + We have all seen it, the blue box that web browsers draw around the currently focused `element`. This is rendered + through the `outline` style property. + +figure.image-display + img(src="/resources/images/cookbooks/a11y/standard-focus-outline.png" alt="Standard browser focus outline box") + +:marked + It clearly indicates where the current `keyboard focus` of the application lies. As it turns out, this is one + of the non-negotiables of `a11y`. We **HAVE** to visually show where the current `keyboard focus` lies. + Someone navigating a website with keyboard input alone cannot do so unless this is always clear. + + Unfortunately, the solid blue box does not meet everyone's approval in a world filled with opacities and box-shadows. + This leads to one of the most infamous of unforgivable sins of `a11y`. + +.callout.is-important + header Leave the focus outline intact! + :marked + **DO NOT** under any circumstances remove the focus outline for interactive elements. **NEVER** completely + remove this with the style commands `outline:0` or `outline:none`, unless you intend to implement your own. + You will instantly make your site unusable for any sighted user who uses the keyboard or related assistive + technologies. + +:marked + If you really hate the default implementation, please **DO** replace this with another style that shows up when + the interactive element receives focus. + + You could do something like this: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/custom-focus-outline.png" alt="Standard browser focus outline box") + +:marked + We accomplish this with the following style by taking a cue from `Twitter Bootstrap`: + ++makeExample('cb-a11y/ts/a11y.css', 'cb-a11y-managing-focus-custom-outline') + +:marked + Here we use the forbidden `outline:0`, **BUT** we then immediately give it another visual focus style. + +:marked + ### Focus flow + + In Angular 2 we have unequaled power to create easily reusable components. Let us think for a moment + about the pages we put together with these components. + + If you tried out any `keyboard navigation` in the previous section, you would have noticed that the `keyboard focus` simply + moves from one element to the next on the page. Our page layouts should support this to create a natural flow. + + How much would you enjoy it if anyone forced you to scroll up and down on a page to find the next element you want to interact + with? Not at all much? Well, we should not force this on any user. We should design a logical flow of focus throughout every page + in our application. + +.l-sub-section + :marked + Unless modified through script, the `normal flow of focus` will jump one focusable element up or down in the order that they + appear in the `HTML DOM Tree`, regardless of visual page position. By ensuring that your `HTML` has a logical + structure, you make sure that all users can navigate your pages correctly. Where you place the elements + with `CSS` does not affect this order. We call this the `Separation of Content and Presentation`. + +:marked + We saw in the labeling section that we get a lot of standard `a11y` functionality out of the box when we use + `native form controls`, and here it is no different. **DO NOT** change the focus order with script unless + it is for a very specific functional reason you cannot solve with the default flow, like focusing + on an error message the user needs to see. + + Let's have a look at a basic example of separating content from presentation. We have a basic layout based on a list + of countries, asking for two pieces of information per country. It looks like this: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/focus-flow-clean.png" alt="Collection of inputs based on country list separated into columns per information type") + +.l-sub-section + :marked + Note how we repeated the country name in both related `input labels`. Had we instead chosen for `How many months did you work + there`, we would have made yet another common `a11y` mistake. **DO NOT** rely on visual context alone + when labeling `HTML elements`. A person with a visual disability cannot see this context so you need to tell them, + through the screen reader, what the current element relates to. + +:marked + If we create this layout in the `HTML DOM Tree`, one column below the other, we end up with the following focus flow: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/focus-flow-bad.png" alt="Incorrect focus flow grouping taborder into columns") + +:marked + The example's simplicity and the choice of labeling make this usable, but the flow is illogical. Generally, + the user would want to think about one country at a time. This flow could force a user to + think about each country twice. + + A far superior focus flow is: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/focus-flow-good.png" alt="Correctly flowing focus by country") + +:marked + We do this by managing the focus flow through the `content` with `HTML` alone and changing the visual `presentation` with + `CSS`. Just like that we are able to create the required flow with absolutely no scripting! + + Here is the `HTML`: + ++makeExample('cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html', 'cb-a11y-managing-focus-flow') + +:marked + ### Skiplinks + + We now know about `keyboard focus` and how it flows on the page. Let's zoom out a bit and look at the complete page. + For a sighted person who navigates the web using a mouse, it is very easy to skip sections of web pages. + This could be because they are returning to a website they know very well, or because they can immediately see and interact + with a specific section of interest. + + Now imagine being forced to click on **EVERY** menu and **EVERY** link and **EVERY** field and... You get the picture. + That is as much fun as watching the grass grow! Now what if we tell you that a person navigating the web + with his keyboard alone often gets forced into exactly this situation? + + If we force these users to repeatedly navigate through entire pages to find one area they want to interact with, it + adds a serious barrier. + + We can give quick links to subsections of our pages that remain completely hidden until focused through + `keyboard navigation`. These are called `skiplinks` and they (could) look like this: + +figure.image-display + img(src="/resources/images/cookbooks/a11y/skiplinks.png" alt="Correctly flowing focus by country") + +:marked + You can, of course, give any styling you like, but here is how we made ours: + ++makeExample('cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html','cb-a11y-managing-focus-skiplinks-links') + +:marked + These links point to internal `id's` and we build them with a function leveraging the `router`. *Please refer to the + section on `routing` in the documentation for a more detailed explanation.* + + They are then rendered out as `internal links`: + +code-example(language="html" escape="html" format="linenums"). + Go directly to focus flow + +:marked + If the target of this link is not an interactive element, we can make that element focusable by adding + `tabindex="-1"`. If we do not, it will not work in all browsers! + +.l-sub-section + :marked + When we use `tabindex="-1"` we are allowing an element to + accept the current `keyboard focus`. However, this tells the browser to keep the element out of the normal + `keyboard navigation` flow. It can only accept focus via internal links, clicks or script. + +:marked + The `HTML` of the target now looks like this: + ++makeExample('cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html','cb-a11y-managing-focus-skiplinks-destination') + +:marked + Finally we need some styling magic to make it all work by only showing focused links: + ++makeExample('cb-a11y/ts/a11y.css', 'cb-a11y-managing-focus-skiplinks-style') + +:marked + Quite easy, isn't it? Yet it is such an extremely powerful feature for a large group of users out there. + + Up to this point of this section, we have made very little use of Angular 2. Yes folks, + this is standard `HTML` functionality and we need to know about if we are to make accessible Angular 2 applications! + + Now that we know more about how browsers handle focus and what we can do to leverage it, it is time to look at what + we can do inside our custom Angular 2 components to keep them accessible. + +:marked + ### Interactive components should accept focus + + Unlike native interactive `HTML` elements, `custom interactive components` created with Angular 2 need our help + to accept focus within the normal focus flow of the page during `keyboard navigation`. + + This means that a native `button` element will accept focus, but a `button control` built as a + `custom element` from non interactive `HTML` elements won't. Not unless we do something about it. + + So let's go a bit crazy and do exactly that, but first, another word of warning. + +.l-sub-section + :marked + Again we need to stress that re-creating any `native HTML element` out of `custom elements` is **NOT** recommended, nor + do we make any promises about the production readiness of our example. Our focus remains on `a11y` and how + we have the tools to make the most stubbornly inaccessible component accessible. + +:marked + *So what doth a great button make?* + + A `button` should tell everyone that it is a button. And that includes people (whether they can or can't see), + browsers and assistive technologies like screen readers. It should look like a button, act like a buttons, and + click like a button. + + A `button` should accept focus, react to the mouse `click` event and react to the keyboard `enter` and `space` events. + *There is a set of rules governing which keyboard events should ideally be implemented per widget and you can read + about these [Common Widget Design Patterns](https://www.w3.org/WAI/intro/accessibility.php) at the `W3C`*. + + This is our mission, and we choose to accept it: + ++makeTabs('cb-a11y/ts/app/shared/a11y-custom-button.component.ts,cb-a11y/ts/app/shared/a11y-custom-button.component.html', +null, 'a11y-custom-button.component.ts,a11y-custom-button.component.html') + +:marked + We manipulate the `Host` element of our component and *Hey Presto*, it can now be used like the standard `button` element: + ++makeExample('cb-a11y/ts/app/managing-focus/a11y-managing-focus.component.html','cb-a11y-custom-button-usage') + +:marked + Looking at the generated `HTML` we see: + +code-example(language="html" escape="html" format="linenums"). + + Do something... + + +:marked + **Important**: Note the `role` and `tabindex`. + + The `ARIA role` of this element is `button`. It tells any assistive technologies that this element is + a button, regardless of the original design of the `HTML` element. More information on this later. + + Setting the `tabindex` to `0` inserts the element in the default flow of`keyboard navigation` focus. This means that our + element also becomes accessible via the keyboard! + +.l-sub-section + :marked + **DO NOT** use a `tabindex` value of `1` or greater as this will change the default keyboard navigation. + +:marked + ### Internal focus management for components + +.callout.is-important + header There be monsters here! + :marked + Let's kick of this topic with a warning. If our code changes focus in a way that the user does not expect, + it can **VERY EASILY** + break `a11y` in our application. We should set the focus programmatically very sparingly and only where + the default focus flow does not work. For example when opening a `modal window` we should make sure that we + set and contain the focus + in it, or when focusing the attention on an incorrect input so that the user can easily correct it. **DO NOT** use this + to navigate on the user's behalf. Everyone uses web pages in a personal way and enforcing **our way** on + our users can make our pages unusable and confusing. + +:marked + Usually, setting focus programmatically is only required in complex widgets. Often it is possible to solve + the problems in far less code using standard `HTML` or `ARIA` function. We would still like to show how you can + do it. + + As this is an `a11y` cookbook and not `Dial-A-Widget`, we are going to seriously trim down on functionality and + get down to the nuts and bolts of what we need to do. The concepts demonstrated in this code can easily be applied + in any component, regardless of the complexity. + + We have created a `button` that shows an `alert`, then sets focus on the `alert` and later allow the + user to close the error message with a `close button`. Extremely useful, we know... + ++makeTabs('cb-a11y/ts/app/managing-focus/a11y-error-demo.component.html,cb-a11y/ts/app/managing-focus/a11y-error-demo.component.ts', +null, 'a11y-error-demo.component.html,a11y-error-demo.component.ts') + +:marked + We are setting focus on an `alert` that starts out hidden. As `hidden` and `disabled` elements cannot accept focus, + we first need this element to become visible again. To give the browser time to apply this change we set our focus using + a `timeout` function. + + Also note how we are using the `local template variable` to easily set focus right inside our template code! + + We also use `ARIA` to apply the role of `alert` and manage the `aria-hidden` property. Yes, we even have something + in `ARIA` we can use to tell screen readers when an element is hidden or not. Pretty neat, hey? + +.l-sub-section + :marked + When you have to adjust the focus programatically, make sure that you test the result with a screen reader! The results + are often not what you expect. + +:marked + ### Section summary + + In this section we saw how important `keyboard focus` is to make sure that many of our users are able to + navigate our web pages. + + We looked at displaying the current `keyboard focus` and how to build our component templates with a + natural flow of focus in mind. + + Finally we looked at what we can do to make sure that our own `components` can accept `focus` and how to + programmatically manage `focus` in our components. + + Keep reading to further explore `a11y` in Angular 2 or [go back to the table of contents](#toc) + +.l-main-section + +:marked + ## Roles for custom component widgets + + Angular 2, and its predecessor, opens up web development to a large group of developers, including those who + are developing web interfaces for the first time. In fact, this `Web Development Platform` makes it so easy to + create web applications and manipulate the `DOM` that it is often easy to forget that `Web Development` should be founded on `HTML`. + + `HTML` is the only way to tell + the browsers what you would like them to show on-screen. Even when we manipulate the `DOM` with `script`, we are simply + manipulating a logical structure created from our `HTML`. + + Yes, we know that many of you already know this, but it is very important to realise that without an understanding + of `HTML`, and how browsers go about interpreting our `markup`, it is very hard to get a good understanding + of `a11y`. + + This is not as scary as it sounds, though. In the earlier sections we have already built up a solid understanding + of some key `HTML` concepts that support `a11y` and now we are about to give you the next weapon in your + `a11y` arsenal! + +:marked + ### Roles in HTML + +.l-sub-section + :marked + Throughout this cookbook, we have often looked at the benefits of using native `HTML` elements to + create the functionality we seek. The constant repetition is intentional and we will look at it again + as this remains such an important concept in building accessible web applications. + +:marked + Browsers are built to interpret our `HTML` according to the + [HTML 5 specification](https://www.w3.org/TR/html5/). Within this document we find each `HTML` element's definition. + +.l-sub-section + :marked + We assume the use of `HTML 5`. However, the `ARIA` concepts also work with `HTML 4`. + +:marked + We could say that each `HTML` element has a `Type` and that the browsers react based on this specific type + when encountering a specific element. + + This is one way of looking at `Roles` in `HTML`. Each element plays a specific `role` on the page based on the specification. + +.l-sub-section + :marked + Each browser knows what to do when it encounters an `input` or a `button` and in the wonderful world of `a11y` the + browsers pass this information through to screen readers and other assistive technologies through another + information tree called the [Accessibility Tree](https://www.w3.org/WAI/PF/aria-implementation/#intro_treetypes). + +:marked + With Angular 2 we can extend or change existing elements' functions, or we can create new `Custom Elements`. + + We can make a `div` act like an `input` or a `button`, or we can create a reusable widget + called `my-seriously-cool-widget`, which is in itself a collection of known `HTML` elements or other custom controls. + + However, browsers still expect the known elements to act according to the specification, + whilst they have no idea what our new custom elements do. This means that the `Accessibility Tree` will not be correct, + and this sends users who depend on assistive technologies on a wild goose chase. + + Fear not! We have all the tools we need at out fingertips to tell the browsers, and indirectly the screen readers, + what type of element we are creating! + +:marked + ### ARIA Roles to the rescue + + Within the realms of `ARIA`, we can apply roles to elements that either tell the browser to override the + current `role` of a known element, or give a familiar role to a totally new custom element. + + In this way we can give all the information that assistive technologies need, regardless of the `HTML` structure! + +.l-sub-section + :marked + It is important to note that applying an `ARIA Role` overrides the implicit role of native elements. Therefore, unless + you are changing the `role`, do not apply an `ARIA Role` to an element if the implicit role is correct. + + +:marked + ### How to apply an ARIA Role in Angular 2 + + We give an `ARIA Role` to an element by setting the value of the `role` attribute. When we write this directly in our `HTML` + it is as simple as: + +code-example(language="html" escape="html" format="linenums"). +

I am an alert.

+ +:marked + That was easy! After all the discussion at the start of the chapter you would be forgiven for expecting a very complex + implementation. This is really all there is to it. + + To see how we can use this in an Angular 2 template, we turn to an old friend from our `labeling` section: + ++makeExample('cb-a11y/ts/app/shared/a11y-custom-control.component.html') + +:marked + We will often need to apply a role directly to the custom elements we create with our components. So + how do we do that? + + In Angular 2 we refer to this new custom element as the `Host Element` of the component, because this is the element + in our `HTML` that hosts our component's implementation. + + Angular 2 gives us everything we need to manipulate our `Host Element` through the `Host Property` + of our component definition. Again a familiar example makes a repeat appearance: + ++makeExample('cb-a11y/ts/app/shared/a11y-custom-button.component.ts') + +:marked + You can see that we apply the `role` of `button` to the host element. We can even check and see that this is rendered into the resultant + `DOM` element: + +code-example(language="html" escape="html" format="linenums"). + + Do something... + + +:marked + Now our browser, and any attached assistive technologies, suddenly know that `a11y-custom-button` is a `button`! + + This is really super easy, but there is one more missing ingredient. We need to know which roles we can use, of course. + + Let's look at the two main sections of `roles` we can use to make our applications accessible. + +.l-sub-section + :marked + There are more roles available. You can read the full documentation at the `W3C`, but these two sections of roles + are what we really need when looking at the `structure` and `roles` of the applications and widgets we create. + +:marked + ### ARIA Roles: The landmark roles + + The first section of roles we look at is `Landmark Roles`. These refer to `navigational landmarks` or the regions of the + page the user may want quick access to. Screen readers are also aware of these regions and this helps to give the user + a clearer *picture* of the page layout. + + In a way these roles are the most difficult to master as they refer to the overall page structure and require us to think about + the layout of components on a page to create the `landmarks`. Therefore, these roles should usually be + used inside our `Smart Components` as they need some knowledge of the application structure and general layout. + + Visit the `W3C` to read more about the following [Landmark Roles](https://www.w3.org/TR/wai-aria/roles#landmark_roles): + + - application + - banner + - complementary + - contentinfo + - form + - main + - navigation + - search + +.callout.is-important + header Avoid role="application" + :marked + The `application` role exists to tell assistive technologies that it is about to enter a heavily scripted web application + and switch into `application mode`. However, if our `HTML` follows the rules we have discussed so far, this role does more + harm than good. Use this sparingly and avoid altogether unless you know when to use it and why you are using it for + the specific application. + +:marked + `HTML 5` provides native `semantic elements` that implicitly carry many of these roles and we recommend that you use these + when possible. + + Let's have a look at a high level `HTML` layout for a page using the `HTML 5 Semantic Elements`: + +code-example(language="html" escape="html" format="linenums"). +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+ +.l-sub-section + :marked + *"But you just told us not to apply the `role` attribute to elements when they already imply the `role`! What gives?"* + This is true, but for these landmark elements we should also give the roles as some browsers do not implement + the native `semantic elements` properly. Don't worry you still get enough benefit from using them. + +.l-sub-section + :marked + There is another piece of technology that reads our web pages almost like a screen reader. And that is the `search engine`. + Yes, using a proper + document structure with `semantic elements` also helps search engine crawlers to read and index our websites more easily. + Talk about winning on three fronts! `Readability`, `a11y` and `Search Engine Optimization`. + +:marked + When it is totally impossible to use these elements, like when you need to support `HTML 4`, we can still + create this structure in our `HTML` using `ARIA Roles`: + +code-example(language="html" escape="html" format="linenums"). +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+ +.l-sub-section + :marked + In the previous section we looked at `skiplinks`. The `landmarks` are great skiplink destinations. Yet another + benefit of using a proper `HTML` layout to structure your page. It also becomes clearer for + you as well. + +:marked + ### ARIA Roles: The widget roles + + The other section of `ARIA Roles` we will briefly look at is `Widget Roles`. These roles are of particular + interest to us as Angular 2 developers, and because we often build highly functional widgets, it is important + to give them the appropriate role, where possible. + + Visit the `W3C` to read more about these [Widget Roles](https://www.w3.org/TR/wai-aria/roles#widget_roles): + + The following roles are for standalone widgets: + - alert + - alertdialog + - button + - checkbox + - dialog + - gridcell + - link + - log + - marquee + - menuitem + - menuitemcheckbox + - menuitemradio + - option + - progressbar + - radio + - scrollbar + - slider + - spinbutton + - status + - tab + - tabpanel + - textbox + - timer + - tooltip + - treeitem + + There is also a set of roles for `composite widgets`. Those are widgets built up from other widgets. + - combobox + - grid + - listbox + - menu + - menubar + - radiogroup + - tablist + - tree + - treegrid + + Wow, we have a role for every base type of widget! We have shown how easy it is to apply these roles to our templates + and components, so again we saw that writing accessible applications really does not take much extra effort once + the secrets of `a11y` have been demystified. + +.l-sub-section + :marked + The names of these widget roles are self-explanatory, so we will not dive into a further discussion. Visit the + `W3C` documentation, if in doubt. + +:marked + ### Section summary + + In this section we looked at how we tell the browser what type of `custom widget component` we are making. We also + saw how we can override the role of a native `HTML` element. + + We saw that Angular 2 makes applying a `role` to our `custom elements` easy by using + the `Host Element`. + + Finally, we had a look at the most interesting `ARIA Roles` for us as Angular 2 developers. + + [Go back to the table of contents](#toc) + diff --git a/public/resources/images/cookbooks/a11y/custom-focus-outline.png b/public/resources/images/cookbooks/a11y/custom-focus-outline.png new file mode 100644 index 0000000000..664d2babbe Binary files /dev/null and b/public/resources/images/cookbooks/a11y/custom-focus-outline.png differ diff --git a/public/resources/images/cookbooks/a11y/focus-flow-bad.png b/public/resources/images/cookbooks/a11y/focus-flow-bad.png new file mode 100644 index 0000000000..f8b5b800bc Binary files /dev/null and b/public/resources/images/cookbooks/a11y/focus-flow-bad.png differ diff --git a/public/resources/images/cookbooks/a11y/focus-flow-clean.png b/public/resources/images/cookbooks/a11y/focus-flow-clean.png new file mode 100644 index 0000000000..c4f702ff0c Binary files /dev/null and b/public/resources/images/cookbooks/a11y/focus-flow-clean.png differ diff --git a/public/resources/images/cookbooks/a11y/focus-flow-good.png b/public/resources/images/cookbooks/a11y/focus-flow-good.png new file mode 100644 index 0000000000..4bfa2ccbd0 Binary files /dev/null and b/public/resources/images/cookbooks/a11y/focus-flow-good.png differ diff --git a/public/resources/images/cookbooks/a11y/invisible-label-input-labeled.png b/public/resources/images/cookbooks/a11y/invisible-label-input-labeled.png new file mode 100644 index 0000000000..e41e4ca914 Binary files /dev/null and b/public/resources/images/cookbooks/a11y/invisible-label-input-labeled.png differ diff --git a/public/resources/images/cookbooks/a11y/invisible-label-input-not-labeled.png b/public/resources/images/cookbooks/a11y/invisible-label-input-not-labeled.png new file mode 100644 index 0000000000..601c7c14a9 Binary files /dev/null and b/public/resources/images/cookbooks/a11y/invisible-label-input-not-labeled.png differ diff --git a/public/resources/images/cookbooks/a11y/skiplinks.png b/public/resources/images/cookbooks/a11y/skiplinks.png new file mode 100644 index 0000000000..6b07bd4bdf Binary files /dev/null and b/public/resources/images/cookbooks/a11y/skiplinks.png differ diff --git a/public/resources/images/cookbooks/a11y/standard-focus-outline.png b/public/resources/images/cookbooks/a11y/standard-focus-outline.png new file mode 100644 index 0000000000..ee72ad0f46 Binary files /dev/null and b/public/resources/images/cookbooks/a11y/standard-focus-outline.png differ