介绍

CDK Focus 是对原生 DOM focus 的上层封装和扩展。

Focus Origin

原生 DOM focus 我们只能知道 element 被 focus 了,但是无法知道它是怎么被 focus 的,但 CDK Focus 可以。

比如说,有一个 button,我们有三种方式可以 focus 它:

  1. 用 mouse 点击这个 button

  2. 用 keyboard tab

  3. 用 Script -- button.focus()

CDK Focus 在监听到 focus 后,会附带一个 origin 值,这个 origin 就阐明了 focus 的来源

touch (触屏) 和 mouse 代表用户点击了按钮导致了 focus。

keyboard 指的是用户按键 tab 导致了 focus。

program 表示是程序 Script 导致了 focus。

Descendant Focused

DOM focus 和 blur 不支持冒泡,所以无法监听子孙 element 的 focus 和 blur 事件。

通常我们会改用 focusin 和 focusout,因为它们支持冒泡。

或者用 focus + capture 提早捕获然后再自行判断。

这些繁琐的事情,CDK Focus 都替我们封装好了,只要告诉它我们是否要监听子孙 focus / blue 就可以了。

FocusMonitor

好,知道了它的用途,接着我们来看具体代码演示。

FocusMonitor 是一个 Root Level Provider,我们需要使用它来监听 focus 事件。

App Template

<button #button>click</button>

App 组件

export class AppComponent {
// 1. query button element
readonly button = viewChild.required('button', { read: ElementRef }); constructor() {
// 2. inject FocusMonitor
const focusMonitor = inject(FocusMonitor); afterNextRender(() => {
// 3. 监听 focus 事件
focusMonitor.monitor(this.button().nativeElement).subscribe(origin => { if (origin !== null) {
// 4. origin could be 'touch', 'mouse', 'keyboard', 'program'
console.log('focused by: ', origin);
}
else {
// 5. if origin null mean blur
console.log('blur');
}
});
});
}
}

几个知识点:

  1. DOM Manupulation

    FocusMonitor.monitor 是 DOM Manupulation,它底层执行的是 element.addEventListenter('focus'),所以它需要 ElementRef 或者 HtmlElement。

    最好是在组件 lifecycle AfterRenderHooks 阶段才监听。

  2. runOutsideAngular

    FocusMonitor.monitor 是 ngZone.runOutsideAngular 的,所以 focus 事件不会被 Zone.js 监听到。

    如果我们在事件 callback 想让 Angular refreshView 需要自己手动 tick。

    对 Change Detection 不熟悉的朋友,可以复习这篇 Change Detection

    如果你的项目是 Zoneless ChangeDetection 那可以忽略这第二部分。

  3. RxJS Subject

    FocusMonitor.monitor 返回的是 RxJS Subject

    subscribe 它就可以接受到 focus 和 blur 事件了。

    origin 值如果是 null 表示它是一个 blur 事件,origin 值如果是 touch,mouse,keyboard,program 表示它是一个 focus 事件,origin 值表示它触发的方式。

Unsubscribe and remove listener

由于 FocusMonitor.monitor 返回的是 RxJS Subject 而非 Observable,因此使用 Subscription.unsubscribe 并不会触发 remove listener,不熟悉 RxJS 机制的朋友可以看这个系列:RxJS 系列 – 目录

我们需要使用另一个方法 -- FocusMonitor.stopMonitoring

constructor() {
const focusMonitor = inject(FocusMonitor);
const destroyRef = inject(DestroyRef);
afterNextRender(() => {
const subscription = focusMonitor.monitor(this.button().nativeElement).subscribe(origin => {
console.log(origin);
});
// 1. this won't remove event listeners
subscription.unsubscribe(); // 2. this will remove event listeners
destroyRef.onDestroy(() => focusMonitor.stopMonitoring(this.button().nativeElement));
});
}

这个方法才能 remove listeners

相关源码在 focus-monitor.ts

监听 descendant elements focus 事件

FocusMonitor.monitor 的第二个参数表示是否要监听子孙 elements 的 focus 事件。

默认是 false。

开启后,除了 target element 以外,只要 target element 的子孙 elements 任何一个被 focus / blur,FocusMonitor.monitor 都会监听得到。

App Template

<div #container>
<button #button>click</button>
</div>

用一个 div container 把 button wrap 起来。

App 组件

export class AppComponent {
readonly container = viewChild.required('container', { read: ElementRef }); constructor() {
const focusMonitor = inject(FocusMonitor);
afterNextRender(() => {
// 1. 监听 container
focusMonitor.monitor(this.container().nativeElement, true).subscribe(origin => {
console.log(origin);
});
});
}
}

focus monitor container element 和其子孙 elements。

效果

点击 focus button,container 依然接受到了 focus event。

focusVia

FocusMonitor.focusVia 用来取代 element.focus 方法。

focusMonitor.focusVia(this.button().nativeElement, 'keyboard');
focusMonitor.focusVia(this.button().nativeElement, 'mouse');
focusMonitor.focusVia(this.button().nativeElement, 'touch');
focusMonitor.focusVia(this.button().nativeElement, 'program');

focus 的时候可以设置 origin。

也可以设置要不要 scroll (这个是原生 element.focus 就有的 options,不是 Angular Material 扩展的,只有 origin 是扩展的)。

focusMonitor.focusVia(this.button().nativeElement, 'mouse', { preventScroll: true });

focused class for styling

使用 FocusMonitor.monitor 监听的 element 在 focused 时会附带 2 个 class -- cdk-focuses 和 cdk-{{ origin }}-focused。

像这样

问:假如有一个 button,它没有使用 FocusMonitor.monitor 监听,但使用了 focusVia 去 focus 它,它会有 focused class 吗?

答:不会,只有使用 FocusMonitor.monitor 监听的 element 才会有 focused class。

问:假如有一个 button,它使用 FocusMonitor.monitor 监听,但没有使用 focusVia 去 focus,它会有 focused class 吗?

答:会

CdkMonitorFocus 指令

CdkMonitorFocus 指令是对 FocusMonitor 的上层封装,纯粹为了方便开发者使用而已,没有额外功能。

App Template

<button cdkMonitorElementFocus (cdkFocusChange)="handleFocus($event)">click</button>

指令内部会执行 FocusMonitor.monitor 方法监听 focus,然后通过 @Output (cdkFocusChange) 把接受到的 origin 发布出来。

App 组件

export class AppComponent {
handleFocus(origin: FocusOrigin) {
console.log(origin);
}
}

组件只要接受和处理就可以了。我们不需要去 stopMonitoring 等等,指令都替我们封装了。

监听子孙 elements focus 也是同一个指令,只是 @Input 不同而已。

<div cdkMonitorSubtreeFocus (cdkFocusChange)="handleFocus($event)">
<button>click</button>
</div>

实战例子

这里给一个项目中经常会用到它的案例。

<button>click me</button>

这是一个按钮

它的 Styles

button {
padding: 16px 24px;
background-color: lightblue;
color: blue;
border: 2px solid transparent;
font-size: 24px; &:focus {
border-color: blue;
}
}

效果

当 keyboard focus 到的时候会出现蓝色的框,当 mouse click 的时候也会出现蓝色的框 (因为 click 之后会自动 focus)。

假如我们只希望在 keyboard focus 时才出现蓝色的框 (通常这样的体验比较合理),那我们就可以使用 CdkMonitorFocus 指令了。

首先,添加指令

<button cdkMonitorElementFocus>click me</button>

还记得上面提到的 focused class for styling 吗?

不同 origin 的 focus,element 会被添加不同的 class。

接着把 CSS selector :focus 换成

&.cdk-keyboard-focused {
border-color: blue;
}

效果

只有 keyboard focus 才会出现篮框,click 不会了。

FocusMonitor 原理

FocusMonitor 是如何做到监听 focus 并且得知 origin 的呢?

这可不是原生功能丫。

逛一逛源码

我们直接逛一逛它的源码 (里面有很多奇葩场景的特殊处理,这些我们跳过,只看最简单的部分就好)

FocusMonitor.monitor 方法的源码在 focus-monitor.ts

FocusMonitor.monitor 方法的结尾处会监听 element 的 focus 和 blur 事件。

_registerGlobalListeners 方法

两个知识点:

  1. 它监听的是 focus 而不是 focusin

  2. 它监听的是 capture,而不是冒泡

除此之外,在结尾处它还 subscribe 了 modality detected,并且在触发后做了一个 setOrigin 的动作。

我们先看什么是 modality detected 和 setOrigin。

modality detected 来自一个 InputModalityDetector Root Level Provider

这个 InputModalityDetector 主要是监听了 keydown,mousedown,touchstart 事件

Input Modality 的中文是 "输入方式",我们可以理解为 -- 监听用户的输入方式。也就是 mouse,keyboard,touch screen 的交互方式咯。

监听到后就发布 InputModalityDetector.modalityDetected

回到 FocusMonitor.monitor 监听到这些 modality 之后,它就调用 setOrigin 方法

origin 就是 modality 发布的 keyboard,mouse,touch。

把 origin 存到 _origin 属性里,1 millisecond 以后清除掉。

好,这里告一段咯,我们继续看看 element focused 以后 callback 是什么。

_onFocus 方法

两个知识点

  1. 如果 monitor 没有需要监听子孙,而 focus target 和 currentTarget 不一致 (表示是子孙 focused),那就直接 return skip 掉。

  2. 在执行 _originChanged 方法之前先调用 _getFocusOrigin 方法获取当前 origin。

_getFocusOrigin 方法

原理讲解

看到这里已经有一个大纲:

  1. 监听 document 的 mousedown,keydown,touchstart

    这些代表不同的 origin -- mouse,keyboard,touch

  2. 监听 element focus

  3. 上面 2 个监听的触发顺序是这样的

    点击 button = mousedown -> focus -> mouseup

    再点击另一个 button = mousedown -> blur -> focus -> mouseup

    当 mousedown 的时候,它会赋值 'mouse' 给 _origin 属性。

    在 focus 的时候,它会拿 _origin 属性作为 emit 的 origin。

    如果 focus 是通过 Scripts 触发的,那它的 _origin 就是空值,origin 就应该是 program。

    同样如果 focus 是通过 focusVia 那会先把 focusVia 的参数 origin 赋值给 _origin 属性。

通过上面这一系列的操作,在接收 focus 事件的同时就可以得到其触发的源 origin 了。

大致上是这样啦,有兴趣了解更多的朋友,可以自己逛一逛,它内部还有很多细节的,比如上面我 highlight 到的 touch buffer ms 等等,我是没力逛了。

另外,补充一点,CDK Focus 监听子孙用的不是 focusin 冒泡方案,而是 focus capture 捕获方案。

InteractivityChecker

InteractivityChecker 是一个 Root Level Provider。

它可以用来检测一个 element 是否能被交互。

所谓的交互有 4 种:

  1. isDisabled

    export class AppComponent {
    readonly button = viewChild.required('button', { read: ElementRef }); constructor() {
    const interactivityChecker = inject(InteractivityChecker); afterNextRender(() => {
    console.log(interactivityChecker.isDisabled(this.button().nativeElement)); // false
    });
    }
    }

    它的判断方式很简单,就是看 element 有没有 disabled attribute。

    相关源码在 interactivity-checker.ts

  2. isVisible

    interactivityChecker.isVisible(this.button().nativeElement); // true

    我们不需要知道的太细,反正它们就是一些奇奇怪怪的判断方式就对了。

  3. isTabbable

    interactivityChecker.isTabbable(this.button().nativeElement); // true,button default 的 tabIndex 是 0

    它判断的方式比较杂,但最 common 的方式是查看 tabIndex。
    提醒:tabIndex -1 代表可以 focus,但不代表可以 keyboard tab,只有 tabIndex >= 0 才表示可以 tab。

  4. isFocusable

    interactivityChecker.isFocusable(this.button().nativeElement); // true

    判断 focusable 的方式很原始,就是查看 element 的 nodeName,挨个对

虽然都是一些小功能,但是对 UI Component 开发者来说真的太重要了,

这些很原始很繁琐的功能,往往还需要考虑各种游览器之间的不兼容,所以 CDK 真的是帮了一个大忙。

Strong focus indicators

参考:Docs – Strong focus indicators

Default focused styles

Angular Material 对所有 focsued 组件做了特别的 styles。

当 element 包含 class cdk-program-focused 或者 cdk-keyboard-focused 时 (注:cdk-mouse-focused 不算),会有一个 background-color。

Strong focus styles

如果我们觉得这个 foscued styles 不够明显,那可以依照官网教程,让它变 strong。

styles.scss

@use '@angular/material' as mat;

@include mat.core();
@include mat.strong-focus-indicators(); $pure-material-theme: mat.define-theme((
color: (
theme-type: light,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
density: (
scale: 0,
)
)); :root {
@include mat.all-component-themes($pure-material-theme);
@include mat.strong-focus-indicators-theme($pure-material-theme);
} html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }

加上这两句后,它会多出几个 global variables,如下

html {
--mat-focus-indicator-border-color: black;
--mat-focus-indicator-display: block;
}
html {
--mat-mdc-focus-indicator-border-color: black;
--mat-mdc-focus-indicator-display: block;
}
:root {
--mat-focus-indicator-border-color: #005cbb;
--mat-mdc-focus-indicator-border-color: #005cbb;
}

效果

除了 background-color 还多了一个很深的 border 框。这样就更明显了。

Only when focused by keyboard

一番操作之后,我们会发现它的 focused styles 不仅仅出现在 focused by keyboard / program。

连 focused by mouse 也会出现 border 框。

这完全是错误的体验。相关 Github Issue – Strong focus indicators should only show when the user uses the keyboard to navigate

要知道 focused by mouse 连 background-color 都不会有,怎么可能出现更 strong 的 border 框呢?

它之所以会这样是因为

它的 CSS selector 指定只要是 :focus,也就是说只要 element focused 不管是 by keyboard, program, mouse 一律都出现 border 框。

好,我们修改一下 styles.scss,纠正它的体验

:root {
@include mat.strong-focus-indicators;
@include mat.strong-focus-indicators-theme($pure-material-theme); /* 关掉它的全局 display: block */
--mat-focus-indicator-display: none;
--mat-mdc-focus-indicator-display: none; /* 只有在 cdk-keyboard-focused 时才 display: block */
.cdk-keyboard-focused {
--mat-focus-indicator-display: block;
--mat-mdc-focus-indicator-display: block;
}
}

设置成只有在 .cdk-keyboard-focused (也就是 focused by keyboard) 的情况下才给予 strong focus styles,这样就可以了。

至于 focused by program 要不要使用 strong focus styles 就看个人喜好。参考 Google Ads 会发现,focused by program 只会有 background-color 不会有 border 框,只有 focused by keyboard 才会有 border 框。

但如果你希望 focused by program 也出现 border 框的话,那就加多一个 selector 变成 .cdk-keyboard-focused, .cdk-program-focused 就可以了。

脆弱的 workaround

上面提到的 workaround 相当脆弱,未必适用于每个组件和场景。

比如 checkbox 就不适用

原理是这样的:

  1. 当 input focused (by whatever origin),mat-mdc-focus-indicator 就会 content: '',但它是 display: none,这时还看不到。

  2. 当我们开启 strong focus styles,mat-mdc-focus-indicator 变成 display: block,这时就看见了。

  3. 当我们限制它,只有在 parent 有 .cdk-keyboard-focused 的时候才 display: block,它又消失了。

    因为 tab to checkbox 时,<mat-checkbox> 甚至 input checkbox 都不会有 cdk-keyboard-focused。

要解决这个问题,我们需要自行加上 CdkMonitorFocus 指令,像这样

<mat-checkbox cdkMonitorElementFocus cdkMonitorSubtreeFocus>check me</mat-checkbox>

和 checkbox 类似的组件也都会有相同的问题,唉...没辙,只能期待 Angular Material 团队会 fix 这些 issue 。

题外话:focused & hovered background-color

上一 part 提到了 focused 的 styles,这里顺便提一下 hovered 的 styles。

我们以 raised button 作为例子

<button mat-raised-button>Click me</button>

效果

hovered 和 focused 后的效果

HTML

两个知识点:

  1. ::before

    它不是直接给 button background-color 而是盖了一层 ::before 在上面

  2. alpha

    background-color 是由 + color + opacity (alpha) 完成的

使用 ::before + alpha 的好处是,无论 button 身处任何颜色的背景,当它被 hover 或 focus 时,增加的这个颜色都能被看见,因为 alpha 后就有了叠加的效果。

另外,说一说颜色的细节:

hovered 比较浅 alpha 是 8%

focused 比较深 alpha 是 12%

至于颜色,不同 button 会使用不同的颜色,比如

basic button 用的是 Primary (P-40)

icon button 用的是 On Surface Variant (NV-30)

extended fab button 用的是 On Primary Container (P-10)

注:不熟悉颜色的可以参考 Figma – Material 3

总结:hovered 会有一层浅色的 background-color,focused by (program / keyboard) 会有一层比较深色的 background-color,focused by keyboard 会额外再加上一个 border 框。

CDK Focus Trap

Focus Trap 的作用是让 keyboard tab 不出一个范围。

下图是一个 Angular Material Dialog

两个效果:

  1. autofocus

    当 dialog 打开后,立马自动 focus 到 ok button。

  2. tab / shift + tab

    不管是 keyboard tab 还是 shift + tab,focus element 始终在 dialog 里面打转,无法 focus out of dialog。

CdkTrapFocus 指令

我们若想做出 dialog 的效果,可以借助 CdkTrapFocus 指令。

App Template (先看 before CdkTrapFocus 的效果)

<form>
<input>
<input>
<input>
<button>submit</button>
</form>
<button>outside</button>

一张 form,里面有很多 tabbable element。

效果

我们可以从 form 里面 tab 出来到 outside。

添加  CdkTrapFocus 指令

<form cdkTrapFocus>

提醒:App 组件需要 imports: [A11yModule]

效果

添加 CdkTrapFocus 指令后就无法从 form 里面 tab 出来了。

Focus Trap 的原理

CDK 是如何实现 Focus Trap 的呢?

首先它会在 CdkTrapFocus 指令 element 的上下各插入一个 tabbable div (它叫 Focus Trap Anchor)。

假设我们 tab 到 form 的最后一个 element -- button,再 tab 多一下就会去到下方的 Focus Trap Anchor。

CDK 监听了 Focus Trap Anchor 的 focus 事件,当 focused 它就会 re-focus to form 里面的第一个 tabbale element,所以我们 tab 不出去。

shift + tab 的情况就反着来,当 tab 到上方的 Focus Trap Anchor 时,它就会 re-focus to form 里面的最后一个 tabbale element,这样就循环了,两个方向都出不去。

cdkFocusRegionStart 和 cdkFocusRegionEnd

by default,re-focus to 第一个或最后一个 tabbable element 是通过 InteractivityChecker.isTabbable 一个一个 element 检查出来的。

我们也可以通过 attribute cdkFocusRegionStart/End 去定义哪一个 element 是第一个或最后一个 tabbable element。

效果

第一个 input 始终都 tab 不到了,因为 tab 是往下走,最后会走到下方的 Focus Trap Anchor,然后被 re-focus 到第二个 input (因为第二个 input 是 region start)。

提醒:但是,shift + tab 依然可以走到第一个 input 哦,因为 tab 是依靠下方的 Focus Trap Anchor re-focus 才避开了第一个 input,而 shift + tab 并不会去到下方的 Focus Trap Anchor,也就避不开第一个 input 了。

cdkFocusRegionEnd 的用法就是 cdkFocusRegionStart 反过来

效果

submit button 始终不会被 shift + tab 到,因为上方的 Focus Trap Anchor 会 re-focus 到第三个 input,避开了结尾的 submit button。

提醒:我们一定要确保 cdkFocusRegionStart/End element 是 tabbable。如果它不是 tabbable 就会 focus 不到,focused element 会停留在 Focus Trap Anchor,

如果用户再 tab 一下就会跳出 focus trap element,整个机制就坏掉了。

我个人是觉得 cdkFocusRegionStart 和 cdkFocusRegionEnd 挺难用的,尤其是它只对一个方向起作用,比如上面的例子,

虽然 shift + tab 去不到 submit button,但是 tab 依然能去到 submit button 啊,那意义到底什么呢?

cdkTrapFocusAutoCapture

我们在 form 的上方添加多一个 outside button

先 focus 上方的 outside button 然后 tab 一下。

从 outside tab 进去 form,直接就跳到了结尾的 button 而不是第一个 input,why?

这是因为 outside tab 首先是去到了上方的 Focus Trap Anchor,而它会 re-focus to form 里面的最后一个 tabbale element 也就是 button。

所以,通常使用 Focus Trap 时,我们不会让用户从外面 tab 进去,取而代之的是替它 autofocus to form inside element。

添加 @Input cdkTrapFocusAutoCapture 到 form element 上

<form cdkTrapFocus cdkTrapFocusAutoCapture>

效果

CdkTrapFocus 指令会 autofocus to first tabbable element。

提醒:这个 first tabbable element 是受 cdkFocusRegionStart 影响的哦。

cdkFocusInitial

上面说 autofocus to first tabbable element 是一个不严谨的描述,更严谨的说法是 focus to initial element。

而这个 initial element 是不固定的,它的具体判断过程是这样:

  1. 首先查看 CdkTrapFocus element 内有没有含 attribute cdkFocusInitial 的 element。

    如果有,同时这个 element 可以被 focus 那就 focus 它,

    如果这个 element 不可以被 focus,那就找这个 element 其下第一个 tabbable element (不受 cdkFocusRegion 影响) 来 focus,找不到那就没有任何 autofocus 了。

  2. 如果没有找到 cdkFocusInitial attribute 那就再找看有没有 cdkFocusRegionStart attribute element。

    如果有那就 focus 它,如果它不可被 focus,那就算了,no more autofocus。

    如果没有找到 cdkFocusRegionStart attribute 那就再重新找过第一个 tabbable element (用 InteractivityChecker.isTabbable 一个一个 element 检查) 来 focus。

排除 cdkFocusRegionStart 和 cdkFocusRegionEnd 带来的复杂情况,一般上我们这样用就可以了:

What if no tabbable element

假如 CdkTrapFocus element 内没有任何 tabbable element 会怎样?

当 focus 到 anchor 时,by right 它要 re-focus 到 form 里面的 tabbable element (first or last),但是 form 里面没有任何 tabbable element。

于是它会 stay 在原地 (anchor),此时就已经算是脱离 CdkTrapFocus element 了,再 tab 一下就跑到 outside button 了。

总之,最好不要出现这种特殊场景,就很怪嘛。

FocusTrapFactory and FocusTrap

FocusTrapFactory 是 Root Level Provider。

FocusTrap 是一个普通的 class。

顾名思义,FocusTrapFactory 用于创建 FocusTrap 对象。

绝大部分 Focus Trap 的功能是由 FocusTrap 对象完成的,CdkTrapFocus 指令只是它的上层封装而已。

App Template (没有使用 CdkTrapFocus 指令)

<button>outside</button>
<form #form>
<input>
<input cdkFocusInitial>
<input>
<button>submit</button>
</form>
<button>outside</button>

App 组件

export class AppComponent {
// 1. query form element
readonly formElementRef = viewChild.required('form', { read: ElementRef }); constructor() {
// 2. inject FocusTrapFactory
const focusTrapFactory = inject(FocusTrapFactory);
const destroyRef = inject(DestroyRef); afterNextRender(() => {
// 3. create FocusTrap
// 此时会插入上下 Focus Trap Anchor
const focusTrap = focusTrapFactory.create(this.formElementRef().nativeElement); // 4. 会查找 cdkFocusInitial > cdkFocusRegionStart > InteractivityChecker.isTabbable
focusTrap.focusInitialElement(); // 5. 销毁 focusTrap
destroyRef.onDestroy(() => focusTrap.destroy());
});
}
}

效果

Deprecated FocusTrapFactory and FocusTrap

Angular Material v11.0.0 已经废弃了 FocusTrapFactory 和 FocusTrap,并且推出了替代方案 ConfigurableFocusTrapFactory 和 ConfigurableFocusTrap。

但是!直到今天 11-03-2024,Angular Material v17.3.0 所有内部组件/指令都没有使用新的 ConfigurableFocusTrapFactory 和 ConfigurableFocusTrap,

反而一直用着废弃的 FocusTrapFactory 和 FocusTrap 。

其实也不用太惊讶,因为这是 Angular Team 一贯风格,包括现在 Angular Team 一直在吹的 Signal,Angular Material 源码里连一行 Signal 代码都找不到。

另外,ConfigurableFocusTrapFactory 和 ConfigurableFocusTrap 是有 breaking changes 的,所以要选择用哪一个还得看项目的需求。

总之我想表达的是,对于新东西不要过于乐观,对于旧事物也不要关于悲观。我们可以先观望看一看 Angular Team 的实际行为来决定是否要跟随它们 (不要只看它们的表态)。

更新 2024-07-16:你说巧不巧,我写上述内容时是 Angular Material v17.3.0,然而在 v17.3.2 Angular 竟然偷偷摸摸的把 deprecated 提示给删了

不用惊讶,这就是 Angular 团队会干出来的事。

首先发布一个新功能,告诉你旧的 deprecated 了,叫你用新的。

但是呢,他们自己 (Google) 内部却没有使用新功能。

很长一阵子之后,他们发现换去新功能的弊大于利,于是他们就偷偷把 deprecated 信息给删了。

总结:从这个小小的事件也可以看出 Angular 团队的权衡利弊。他们永远以 Google 内部项目为优先,社区只是他们的白老鼠或者 debuger 而已。(记着我说过的话 -- 了解 Angular 团队才能用好 Angular)

ConfigurableFocusTrapFactory 和 ConfigurableFocusTrap

新旧两个版本有 2 个巨大的区别:

  1. Mouse can‘t focus out of Focus Trap anymore

    下面是旧版本的体验

    Focus Trap 只保护了 tab 和 shift + tab 的操作行为,如果我们用 mouse 点击 outside button,我们就可以脱离 Focus Trap 了。

    在新版本中却不是这样

    const focusTrapFactory = inject(ConfigurableFocusTrapFactory);

    效果

    新版 Focus Trap 不仅仅阻止 tab 还阻止了 mouse。

    用 mouse 点击 outside button 依然脱离不了 Focus Trap,会被拉回去 Focus Trap 里面。

    它的实现手法是这样的,首先监听 document focus

    然后判断 focused element 是否在 focus trap element 里,如果不在就 re-focus to first tabbable element。

    相关源码在 event-listener-inert-strategy.ts

    上面有一段是针对 div.cdk-overlay-pane 的特殊处理,它的意思是如果 focused element 是 under CDK Overlay (以后会教) 那就不需要 re-focus。

  2. Only 1 enabled Trap Focus in the world

    FocusTrap 是可以 disable 和 enable 的。

    focusTrap.enabled = false;
    focusTrap.enabled = true;

    disable 会 remove Focus Trap Anchor 的 tabindex

    这样 Focus Trap Anchor 就没功效了。

    在旧版本中,我们可以同时拥有多个 enabled 的 FocusTrap,因为它们不会互相影响。

    但是在新版本中就不行了,新版本多了一个监听 document focus 然后 re-focus 的机制。

    假如同时有多个 enabled 的 FocusTrap,那要 re-focus 回去哪一个呢?不知道丫。

    所以必须要有一个新机制来管理所有的 FocusTrap,确保知道当前是哪一个,然后才能 re-focus 回到正确的 FocusTrap 里。

    这套机制由 FocusTrapManager 维护

以上就是新旧版本最大的两个区别。

ConfigurableFocusTrapFactory 的 Configurable 到底能 config 什么呢?

答案是 FocusTrapInertStrategy

它就是上面提到的 -- 监听 document focus 然后 re-focus to Focus Trap 机制。

我们可以完全自定义,下面这个是它的接口

默认的实现是 EventListenerFocusTrapInertStrategy

具体源码上面已经讲解过了。

提醒:不要搞混哦,我们可以自定义的只有这个新版本的新机制,这套机制是针对 mouse focus out of Focus Trap 而已,和原本旧版本就有的 tab 机制没有任何关系,tab 机制是不可以自定义的。

总结

FocusMonitor 用来监听 focus 和 blur,它比原生监听 focus 多了一个 origin 概念,可以让我们知道是 mouse, touch, keyboard, program 哪一种方式触发了 focus。

InteractivityChecker 可以检测 element 是否可交互,有四种交互方式:isDisabled,isVisible,isFocusable,isTabbable。

FocusTrap 可以捆着 keyboard tab 走不出一个 element。

目录

上一篇 Angular Material 18+ 高级教程 – Material Ripple

下一篇 Angular Material 18+ 高级教程 – CDK Accessibility の ListKeyManager

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular Material 18+ 高级教程 – CDK Accessibility の Focus的更多相关文章

  1. Angular 学习笔记 ( CDK - Accessibility )

    @angular/ckd 是 ng 对于 ui 组建的基础架构. 是由 material 团队开发与维护的, 之所以会有 cdk 看样子是因为在开发 material 的时候随便抽象一个层次出来给大家 ...

  2. Angular Material 教程之布局篇

    Angular Material 教程之布局篇 (一) : 布局简介https://segmentfault.com/a/1190000007215707 Angular Material 教程之布局 ...

  3. Angular Material TreeTable Component 使用教程

    一. 安装 npm i ng-material-treetable --save npm i @angular/material @angular/cdk @angular/animations -- ...

  4. Angular Material design设计

    官网: https://material.io/design/ https://meterial.io/components 优秀的Meterial design站点: http://material ...

  5. Material使用11 核心模块和共享模块、 如何使用@angular/material

    1 创建项目 1.1 版本说明 1.2 创建模块 1.2.1 核心模块 该模块只加载一次,主要存放一些核心的组件及服务 ng g m core 1.2.1.1 创建一些核心组件 页眉组件:header ...

  6. Siki_Unity_2-9_C#高级教程(未完)

    Unity 2-9 C#高级教程 任务1:字符串和正则表达式任务1-1&1-2:字符串类string System.String类(string为别名) 注:string创建的字符串是不可变的 ...

  7. Angular Material Starter App

      介绍 Material Design反映了Google基于Android 5.0 Lollipop操作系统的原生应用UI开发理念,而AngularJS还发起了一个Angular Material ...

  8. 关于 Angular引用Material出现node_modules/@angular/material/button-toggle/typings/button-toggle.d.ts(154,104): error TS2315: Type 'ElementRef' is not generic.问题

    百度了好久 ,,,最后谷歌出来了.. 该错误可能来自于您将@ angular / material设置为6.0.0, 但所有其他Angular包都是5.x.您应该始终确保Material主要版本与An ...

  9. Angular Material & Hello World

    前言 Angular Material(下称Material)的组件样式至少是可以满足一般的个人开发需求(我真是毫无设计天赋),也是Angular官方推荐的组件.我们通过用这个UI库来快速实现自己的i ...

  10. angular使用@angular/material 出现"export 'ɵɵinject' was not found in '@angular/core'

    WARNING in ./node_modules/@angular/cdk/esm5/a11y.es5.js 2324:206-214 "export 'ɵɵinject' was not ...

随机推荐

  1. 全网最适合入门的面向对象编程教程:13 类和对象的Python实现-可视化阅读代码神器Sourcetrail的安装使用

    全网最适合入门的面向对象编程教程:13 类和对象的 Python 实现-可视化阅读代码神器 Sourcetrail 的安装使用 摘要: 本文主要介绍了可视化阅读代码神器Sourcetrail的安装与使 ...

  2. [oeasy]python0125_汉字打印机_点阵式打字机_汉字字形码

    汉字字形码 回忆上次内容 IBM 将 ASCII 扩展之后 规定了 一个字节的字符集 并制作了 相应的字形库   ​   添加图片注释,不超过 140 字(可选)   这种显示模式和字符大小之下 中文 ...

  3. WRONG(COPY)

    去年总结的列表,欢迎大家补充!! 两个int相乘,50%几率会爆了int.(不开long long见祖宗) 无向图邻接表的边表忘了这是心口永远的痛: 线段树数组开小是4(乘4有时候不够) 调用多个函数 ...

  4. ASP.NET Core 程序集注入(一)

    1.创建[特性]用于标注依赖注入 using Microsoft.Extensions.DependencyInjection; using System; using System.Collecti ...

  5. Python 实现Excel和TXT文本格式之间的相互转换

    Excel是一种具有强大的数据处理和图表制作功能的电子表格文件,而TXT则是一种简单通用.易于编辑的纯文本文件.将Excel转换为TXT可以帮助我们将复杂的数据表格以文本的形式保存,方便其他程序读取和 ...

  6. 周末玩一下云技术,kvm 相关笔记

    由于需要将企业的很贵的显卡和主机装在一个虚拟主机,用来跑  ue5 和 sd3  用来给用户临时使用,但是怎么将主机虚拟出来成多个主机呢,自己没有有钱请不起人,只能自己学一下虚拟化技术,第一步主机开启 ...

  7. Activity的创建

    Activity的创建: 1.layout内写入相关代码 此处为显示的页面 2.Java内创建相关类写入代码 3.在清单内写入 快捷方法:直接完成上面步骤 layout: match_parent// ...

  8. 测试开发jmeter设置线程序号

    测试开发jmeter设置线程序号 ${__threadNum} 需要在请求的名称后面加上${__threadNum} 然后运行结果如下:

  9. python 私有属性的作用

    python 私有属性的作用 class Player(): def __init__(self, name, power, skill): self.name = name self.power = ...

  10. 常用的adb命令(重要)

    常用的adb命令(重要)