Angular cdk 学习之 Accessibility(al
cdk ally里面提供的功能运用场景是 select, menu 这种有 list options 的组件, 负责处理键盘上下按钮时 option active 的逻辑。cdk Accessibility 部分官方文档连接https://material.angular.io/cdk/a11y/overview。
和cdk里面其他功能模块一样,cdk ally使用之前也要provider导入A11yModule模块。
import {A11yModule} from '@angular/cdk/a11y';
一 ListKeyManager
ListKeyManager主要做的事情就是去管理一堆的list 里面哪个item处于active状态(active代表一种状态,比如处于活动转换,获取焦点状态等等)。而且ListKeyManager管理的item必须实现ListKeyManagerOption接口。换句话说ListKeyManager用于管理一系列list的item,这些item就是ListKeyManagerOption。通过ListKeyManager来控制那个ListKeyManagerOption对应的item处于active状态。所以咱们先看下ListKeyManagerOption和ListKeyManager里面一些常见的方法,如下:
1.1 ListKeyManager常用方法
讲ListKeyManager方法的时候咱们顺带讲下ListKeyManagerOption里面的方法
1.1.1 ListKeyManagerOption常用方法介绍
export interface ListKeyManagerOption {
/** 当前item是否disabled. */
disabled?: boolean;
/** 获取当前item对应的label,配合ListKeyManager里面withTypeAhead函数的使用
* 适用于输入框的情况,比如输入框下面有一些list item。当输入框输入字符的时候list item 里面的active
* item 和输入的文字匹配
*/
getLabel?(): string;
}
1.1.2 ListKeyManager常用方法介绍
export declare class ListKeyManager<T extends ListKeyManagerOption> {
/**
* tab 按键的时候触发的
*/
tabOut: Subject<void>;
/** 当ListKeyManager里面的list item ,active item 改变的时候触发*/
change: Subject<number>;
/**
* 设置那些ListKeyManager管理的list里在移动(比如tab 按键切换)过程中那些item需要跳过。
*/
skipPredicate(predicate: (item: T) => boolean): this;
/**
* 设置是否循环移动(当active item是最好一个的时候,继续下一个跳到第一个)
*/
withWrap(shouldWrap?: boolean): this;
/**
* active item 移动方向垂直(对应键盘方向键的 上下按键)
*/
withVerticalOrientation(enabled?: boolean): this;
/**
* active item 移动方向水平(对应键盘方向键的 左右按键)
*/
withHorizontalOrientation(direction: 'ltr' | 'rtl' | null): this;
/**
* 用来处理组合按键的情况,比如 altr + 方向键的时候。同样达到方向按键的效果
*/
withAllowedModifierKeys(keys: ListKeyManagerModifierKey[]): this;
/**
* 设置typeahead 模式 配合ListKeyManagerOption的getLabel()函数使用,一般用于配合有输入框的情况下使用,比如输入框输入一个字符,active item 会自动设置到包含这个字符item
*/
withTypeAhead(debounceInterval?: number): this;
/**
* 设置 active item 对应的index
*/
setActiveItem(index: number): void;
/**
* 设置 active item 对应的item
*/
setActiveItem(item: T): void;
/**
* 设置按键
*/
onKeydown(event: KeyboardEvent): void;
/** 当前active item 对应的事件 */
readonly activeItemIndex: number | null;
/** 当前active item 对应的item */
readonly activeItem: T | null;
/** 设置第一个位置的item 为active item(当然了如果第一个item 是disable,则设置第二个) */
setFirstItemActive(): void;
/** 设置最后一个位置的item 为 active item */
setLastItemActive(): void;
/** 设置下一个item 为active item */
setNextItemActive(): void;
/** 设置上一个item 为active item */
setPreviousItemActive(): void;
/**
* 设置active item,但是不产生其他的效果
*/
updateActiveItem(index: number): void;
/**
* 设置active item,但是不产生其他的效果
*/
updateActiveItem(item: T): void;
}
1.2 ListKeyManager的使用
接下来咱们通过ListKeyManager的两个具体继承类ActiveDescendantKeyManager和FocusKeyManager来说明ListKeyManager的具体使用。其实ActiveDescendantKeyManager里面管理的item必须实现Highlightable接口、FocusKeyManager里面管理的item必须实现FocusableOption。
1.2.1 ActiveDescendantKeyManager的使用
ActiveDescendantKeyManager里面管理的list item 必须实现Highlightable接口。
ActiveDescendantKeyManager继承了ListKeyManager类,Highlightable实现了ListKeyManagerOption接口
ActiveDescendantKeyManager使用场景:如果想ListKeyManager管理的list里面active的item标记为活动状态,其他的标记为非活动状态情况下使用。而且每个item都必须实现Highlightable接口。接下来咱们通过一个具体的实例来简单的说明下。
咱们自定义一个item组件ItemActiveOptionComponent,这个组件实现Highlightable接口。组件里面做的事情也很简单就是去改变active item的class样式。
import {Component, HostBinding, Input} from '@angular/core';
import {Highlightable} from "@angular/cdk/a11y";
@Component({
selector: 'app-item-active-option',
template: `
<div [class.disabled]="disabled">
<ng-content></ng-content>
</div>
`,
styles: [`
.active {
background-color: lightblue;
color: #fff;
}
.disabled {
opacity: 0.3;
}
`]
})
export class ItemActiveOptionComponent implements Highlightable {
@Input() item;
@Input() disabled = false;
private _isActive = false;
@HostBinding('class.active') get isActive() {
return this._isActive;
}
/**
* 设置 active对应的class
*/
setActiveStyles() {
this._isActive = true;
}
/**
* 设置非active对应的class
*/
setInactiveStyles() {
this._isActive = false;
}
getLabel() {
return this.item.name;
}
}
cdk-active-descendant.component.less文件,组件对应样式
.form-control {
display: block;
width: 100%;
height: calc(2.25rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.form-control:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.list-group-item:first-child {
margin-top: 1rem;
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
}
.list-group-item {
position: relative;
display: block;
padding: .75rem 1.25rem;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, .125);
}
.list-group-item:last-child {
margin-bottom: 0;
border-bottom-right-radius: .25rem;
border-bottom-left-radius: .25rem;
}
.list-group-item.active {
z-index: 2;
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
ActiveDescendantKeyManager的使用,咱们放在这个组件里面实现。ActiveDescendantKeyManager里面管理的item就是咱们上面自定义的ItemActiveOptionComponent。具体的实现可以看下代码。
import {AfterViewInit, Component, QueryList, ViewChildren} from '@angular/core';
import {ItemActiveOptionComponent} from "./active-item-option/item-active-option.component";
import {ActiveDescendantKeyManager} from "@angular/cdk/a11y";
@Component({
selector: 'app-cdk-active-descendant',
template: `
<!-- 输入框 -->
<div class="form-group">
<input class="form-control" placeholder="Search..." (keyup)="onKeyDown($event)" #input>
</div>
<section class="list-group">
<!-- ActiveDescendantKeyManager要管理的item -->
<app-item-active-option *ngFor="let user of users | filter: 'name' : input.value; index as index"
[item]="user" class="list-group-item">
{{user.name}}
</app-item-active-option>
</section>
`,
styleUrls: ['cdk-active-descendant.component.less']
})
export class CdkActiveDescendantComponent implements AfterViewInit {
/**
* 找到所有的item(ListKeyManagerOption - Highlightable)
*/
@ViewChildren(ItemActiveOptionComponent) items: QueryList<ItemActiveOptionComponent>;
// list item source list
users = [
{
"id": "5b902934d965e7501f4e1c6f",
"name": "Caroline Hodges"
},
{
"id": "5b9029348f7eed8b6f5f02db",
"name": "Delores Rivas"
},
{
"id": "5b9029346f48c8407c64d0d5",
"name": "Darlene Franklin"
},
{
"id": "5b9029341eff315fa87f9e21",
"name": "Alfreda Love"
},
{
"id": "5b9029342e8917c6ccdb9865",
"name": "Marcy Ratliff"
},
{
"id": "5b9029349dbb48013460e01f",
"name": "Beulah Nielsen"
},
{
"id": "5b902934f4f1586e5e72d74a",
"name": "Morton Kerr"
},
{
"id": "5b9029347918bb204bf7014e",
"name": "Autumn Tillman"
},
{
"id": "5b902934b86f80e1fc60c626",
"name": "Diane Bennett"
},
{
"id": "5b9029348999f59215020349",
"name": "June Eaton"
}
];
private keyManager: ActiveDescendantKeyManager<ItemActiveOptionComponent>;
ngAfterViewInit() {
// new ActiveDescendantKeyManager
this.keyManager = new ActiveDescendantKeyManager(this.items)
.withWrap() // 循环
.withTypeAhead(); // 支持搜索
}
onKeyDown(event) {
// 传递事件进去
this.keyManager.onKeydown(event);
}
}
ActiveDescendantKeyManager.gif
1.2.2 FocusKeyManager的使用
FocusKeyManager里面管理的item必须实现FocusableOption接口。
FocusKeyManager继承了ListKeyManager类,FocusableOption实现了ListKeyManagerOption接口
FocusKeyManager使用场景:如果想ListKeyManager管理的list里面active item想直接接受到浏览器的焦点focus的时候使用。每个item都必须实现FocusableOption接口。
自定义一个组件ItemFocusOptionComponent,并且这个组件实现了FocusableOption接口,FocusableOption接口的focus()方法里面设置当前item获取焦点。
import {Component, ElementRef, HostBinding, Input} from '@angular/core';
import {FocusableOption, FocusOrigin} from "@angular/cdk/a11y";
@Component({
selector: 'app-item-focus-option',
template: `
<ng-content></ng-content>
`,
styles: [
`:host:focus {
background: lightblue;
color: #fff;
}`
]
})
export class ItemFocusOptionComponent implements FocusableOption {
@Input() item;
/**
* 屏蔽掉默认的键盘事件,js里面自己控制键盘事件
*/
@HostBinding('tabindex') tabindex = '-1';
constructor(private host: ElementRef) {
}
getLabel() {
return this.item.name;
}
/**
* 设置获取焦点
*/
focus(origin?: FocusOrigin): void {
this.host.nativeElement.focus();
}
}
cdk-focus.component.less 样式文件
.form-control {
display: block;
width: 100%;
height: calc(2.25rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out, box-shadow .15s ease-in-out;
}
.form-control:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.2rem rgba(0,123,255,.25);
}
.list-group-item:first-child {
margin-top: 1rem;
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
}
.list-group-item {
position: relative;
display: block;
padding: .75rem 1.25rem;
margin-bottom: -1px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, .125);
}
.list-group-item:last-child {
margin-bottom: 0;
border-bottom-right-radius: .25rem;
border-bottom-left-radius: .25rem;
}
.list-group-item.focus {
z-index: 2;
color: #fff;
background-color: #007bff;
border-color: #007bff;
}
FocusKeyManager的具体使用
import {AfterViewInit, Component, QueryList, ViewChildren} from '@angular/core';
import {FocusableOption, FocusKeyManager} from "@angular/cdk/a11y";
import {ItemFocusOptionComponent} from "./focus-item-option/item-focus-option.component";
@Component({
selector: 'app-cdk-focus',
template: `
<section class="list-group" (keyup)="onKeyDown($event)">
<app-item-focus-option *ngFor="let user of users; index as index"
[item]="user" class="list-group-item"
(click)="selectItem(index)">
{{user.name}}
</app-item-focus-option>
</section>
`,
styleUrls: ['./cdk-focus.component.less']
})
export class CdkFocusComponent implements AfterViewInit {
// 获取搜有实现了FocusableOption接口的item
@ViewChildren(ItemFocusOptionComponent) items: QueryList<ItemFocusOptionComponent>;
// list source
users = [
{
"id": "5b902934d965e7501f4e1c6f",
"name": "Caroline Hodges"
},
{
"id": "5b9029348f7eed8b6f5f02db",
"name": "Delores Rivas"
},
{
"id": "5b9029346f48c8407c64d0d5",
"name": "Darlene Franklin"
},
{
"id": "5b9029341eff315fa87f9e21",
"name": "Alfreda Love"
},
{
"id": "5b9029342e8917c6ccdb9865",
"name": "Marcy Ratliff"
},
{
"id": "5b9029349dbb48013460e01f",
"name": "Beulah Nielsen"
},
{
"id": "5b902934f4f1586e5e72d74a",
"name": "Morton Kerr"
},
{
"id": "5b9029347918bb204bf7014e",
"name": "Autumn Tillman"
},
{
"id": "5b902934b86f80e1fc60c626",
"name": "Diane Bennett"
},
{
"id": "5b9029348999f59215020349",
"name": "June Eaton"
}
];
private keyManager: FocusKeyManager<ItemFocusOptionComponent>;
ngAfterViewInit() {
// 创建FocusKeyManager对象
this.keyManager = new FocusKeyManager(this.items)
.withWrap()
.withTypeAhead();
}
/**
* 传递按键事件
* @param event
*/
onKeyDown(event) {
this.keyManager.onKeydown(event);
}
/**
* 点击选中
* @param index
*/
selectItem(index: number) {
this.keyManager.setActiveItem(index);
}
}
FocusKeyManager.gif
二 FocusTrap
FocusTrap是cdk ally模块里面提供的一个指令。用于捕获元素中的Tab键焦点。同时控制焦点的范围,Tab切换焦点的时候不会跳出这个区域。举个例子比如有一个视图元素添加了FocusTrap指令,这个视图元素里面有三个input。这样Tab键切换焦点的时候焦点会在这三个input之间切换。
Selector: [cdkTrapFocus]
Exported as: cdkTrapFocus
还有几个其他的指令可以配合FocusTrap指令来使用:cdkFocusRegionStart、cdkFocusRegionEnd、cdkFocusInitial。咱们上面不是说了FocusTrap指令会控制焦点的范围。cdkFocusRegionStart和cdkFocusRegionEnd指令有可以进一步的控制焦点的范围
- cdkFocusRegionStart:FocusTrap范围的起点
- cdkFocusRegionEnd:FocusTrap范围的终点
- cdkFocusInitial:区间出现时,一开始获取focus的item。
2.1 FocusTrap指令属性介绍
FocusTrap属性 | 类型 | 解释 |
---|---|---|
autoCapture: boolean | @Input('cdkTrapFocusAutoCapture') | 初始化的时候元素是否自动获取焦点 |
enabled: boolean | @Input('cdkTrapFocus') | false Tab切换焦点的时候会跑到外面去,true Tab切换焦点的时候焦点不会跑到外面去 |
2.2 FocusTrap指令使用
import {Component} from '@angular/core';
@Component({
selector: 'app-cdk-accessibility',
template: `
<!-- FocusTrap的使用,- 控制焦点的范围,tab切换焦点的时候不会跳出这个区域 -->
<div cdkTrapFocus style="border: solid 1px #ccc; padding: 10px">
<!-- Tab and Shift + Tab will not leave this element. -->
<input placeholder="Email">
<input type="password" placeholder="Password" style="margin-left: 10px">
<button type="submit" style="margin-left: 10px">Submit</button>
</div>
`
})
export class CdkAccessibilityComponent {
}
2.3 cdkFocusRegionStart cdkFocusRegionEnd cdkFocusInitial的使用
cdkFocusRegionStart和cdkFocusRegionEnd的使用应该很好理解。就是用来控制范围的。关键是cdkFocusInitial的使用,cdkFocusInitial必须要配合cdkTrapFocus指令的cdkTrapFocusAutoCapture=true使用。而且我试了下cdkFocusInitial一开始是没有效果,没都是隐藏显示的时候才有效果,才获取到焦点。
import {Component} from '@angular/core';
@Component({
selector: 'app-cdk-accessibility',
template: `
<div *ngIf="displayFocusTrap" cdkTrapFocus cdkTrapFocusAutoCapture="true"
style="border: solid 1px #ccc; padding: 10px; margin-top: 10px">
<input value="1">
<input style="margin-left: 10px" value="2" cdkFocusRegionStart>
<input style="margin-left: 10px" value="3" cdkFocusInitial>
<input style="margin-left: 10px" value="4" cdkFocusRegionEnd>
</div>
<button (click)="displayFocusTrap=!displayFocusTrap">显示</button>
`
})
export class CdkAccessibilityComponent {
displayFocusTrap = false;
}
三 FocusMonitor
FocusMonitor是cdk ally里面提供的一个Service。用于监控焦点。FocusMonitor里面常用方法如下
export declare class FocusMonitor implements OnDestroy {
/**
* 监听元素的焦点获取
*/
monitor(element: HTMLElement, checkChildren?: boolean): Observable<FocusOrigin>;
monitor(element: ElementRef<HTMLElement>, checkChildren?: boolean): Observable<FocusOrigin>;
/**
* 停止监听元素的焦点获取
*/
stopMonitoring(element: HTMLElement): void;
stopMonitoring(element: ElementRef<HTMLElement>): void;
/**
* 设置元素获取焦点
* @param element 元素
* @param origin 设置的焦点是通过哪个来的 'touch' | 'mouse' | 'keyboard' | 'program' | null
* @param options 用于配置focus行为的选项.
*/
focusVia(element: HTMLElement, origin: FocusOrigin, options?: FocusOptions): void;
focusVia(element: ElementRef<HTMLElement>, origin: FocusOrigin, options?: FocusOptions): void;
}
3.1 FocusMonitor的使用
一个简单的例子,咱们仅仅是在元素获取焦点的时候做一个简单的打印。
import {AfterViewInit, Component, ElementRef, OnDestroy} from '@angular/core';
import {FocusMonitor} from "@angular/cdk/a11y";
@Component({
selector: 'app-cdk-focus-monitor',
template: `
<div cdkTrapFocus style="border: solid 1px #ccc; padding: 10px; margin-top: 10px">
<input value="FocusMonitor item 1">
<input style="margin-left: 10px" value="FocusMonitor item 2">
<input style="margin-left: 10px" value="FocusMonitor item 3">
<input style="margin-left: 10px" value="FocusMonitor item 4">
</div>
`
})
export class CdkFocusMonitorComponent implements AfterViewInit, OnDestroy {
constructor(private _elementRef: ElementRef,
private _focusMonitor: FocusMonitor) {
}
ngAfterViewInit() {
/**
* 这里我们只是做了一个简单的打印
*/
this._focusMonitor.monitor(this._elementRef.nativeElement, true).subscribe(mode => {
console.log('元素获取到焦点 focused 来源 ' + mode);
});
}
ngOnDestroy() {
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
}
}
四 FocusTrapFactory
FocusTrapFactory也是cdk ally里面提供的一个Service。他的功能就是用来给元素添加cdkFocusTrap指令。FocusTrapFactory常用函数就一个,如下
export declare class FocusTrapFactory {
/**
* 给指定的元素添加cdkFocusTrap指令,deferCaptureElements参数正好对应cdkFocusTrap指令
* @Input('cdkTrapFocusAutoCapture')功能
*/
create(element: HTMLElement, deferCaptureElements?: boolean): FocusTrap;
}
4.1 FocusTrapFactory使用
通过FocusTrapFactory的crete函数达到cdkFocusTrap指令的效果。
import {AfterViewInit, Component, ElementRef} from '@angular/core';
import {FocusTrap, FocusTrapFactory} from "@angular/cdk/a11y";
@Component({
selector: 'app-cdk-focus-trap-factory',
template: `
<div style="border: solid 1px #ccc; padding: 10px; margin-top: 10px">
<input value="FocusTrapFactory item 1">
<input style="margin-left: 10px" value="FocusTrapFactory item 2">
<input style="margin-left: 10px" value="FocusTrapFactory item 3">
<input style="margin-left: 10px" value="FocusTrapFactory item 4">
</div>
`
})
export class CdkFocusTrapFactoryComponent implements AfterViewInit {
private _focusTrap: FocusTrap;
constructor(private _elementRef: ElementRef,
private _focusTrapFactory: FocusTrapFactory) {
}
ngAfterViewInit() {
this._focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement);
}
}
五 InteractivityChecker
InteractivityChecker是一个Service,用于检查元素的一些状态。常见方法如下。
export declare class InteractivityChecker {
/**
* 元素是否 disabled.
*/
isDisabled(element: HTMLElement): boolean;
/**
* 元素是否visible
*
* This will capture states like `display: none` and `visibility: hidden`, but not things like
* being clipped by an `overflow: hidden` parent or being outside the viewport.
*
* @returns Whether the element is visible.
*/
isVisible(element: HTMLElement): boolean;
/**
* 元素是否接受Tab按键,不如Tab按键切换焦点
*/
isTabbable(element: HTMLElement): boolean;
/**
* 元素是否可以获取焦点
*/
isFocusable(element: HTMLElement): boolean;
}
5.1 InteractivityChecker使用
举一个非常简单的例子,打印出button的各种状态。
import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {InteractivityChecker} from "@angular/cdk/a11y";
@Component({
selector: 'app-cdk-interactivity-checker',
template: `
<button #interactivityCheckerButton>InteractivityChecker测试</button>
<p>上面button是否disable: {{disable}}</p>
<p>上面button是否visible: {{visible}}</p>
<p>上面button是否可以tabable: {{tabable}}</p>
<p>上面button是否可以focusable: {{focusable}}</p>
`
})
export class CdkInteractivityCheckerComponent implements OnInit {
@ViewChild('interactivityCheckerButton') button: ElementRef;
disable: boolean;
visible: boolean;
tabable: boolean;
focusable: boolean;
constructor(private _interactivityChecker: InteractivityChecker) {
}
ngOnInit() {
this.disable = this._interactivityChecker.isDisabled(this.button.nativeElement);
this.visible = this._interactivityChecker.isVisible(this.button.nativeElement);
this.tabable = this._interactivityChecker.isTabbable(this.button.nativeElement);
this.focusable = this._interactivityChecker.isFocusable(this.button.nativeElement);
}
}
六 LiveAnnouncer
LiveAnnouncer也是一个Service。用于在屏幕上发布一个消息。把消息显示在屏幕上。关于这一部分的内容估计的去看下W3C关于WAI-ARIA的使用。我们就举一个简单的例子。
import {Component} from '@angular/core';
import {LiveAnnouncer} from "@angular/cdk/a11y";
@Component({
selector: 'app-cdk-live-announcer',
template: `
`
})
export class CdkLiveAnnouncerComponent {
index = 1;
/**
* 会在屏幕上输出Hey Google,三秒之后又会换成Hey Google 2
*/
constructor(private liveAnnouncer: LiveAnnouncer) {
liveAnnouncer.announce("Hey Google");
setTimeout(() => {
this.timerTask();
}, 3000);
}
timerTask() {
this.index = this.index + 1;
this.liveAnnouncer.announce("Hey Google " + this.index.toString(), "off");
}
}
关于cdk ally咱们就扯这么多,希望能对大家有点帮助。当然了里面很有很多其他的类咱们没有介绍到。大家在时机使用的时候可以对照官方文档看下 https://material.angular.io/cdk/a11y/overview。 文章中涉及到的例子连接地址 https://github.com/tuacy/angular-cdk-study