使用Angular CDK的FocusKeyManager实现列表键盘导航时activeItem为null的问题
我尝试用Angular CDK的ListKeyManager和FocusKeyManager实现列表的键盘导航,编写了子组件MyListItemComponent(实现FocusableOption接口)和父组件ArticlesComponent,但发现键盘操作时keyManager.activeItem始终为null,列表项的focus方法也没被调用——点击列表元素有响应,但键盘完全没反应。
子组件代码:
@Component({ selector: "app-my-list-item", host: { tabindex: "0", role: "list-item" }, template: ` {{ fruit }} `, styles: [ ` :host { display: block; margin: 1rem 0; padding: 1rem; background-color: #0090ff; transition: all 0.3s; outline: none; color: white; } :host(:focus) { background-color: #28bf5f; transform: translateX(1rem); } `, ], }) export class MyListItemComponent implements FocusableOption { @Input() fruit = ""; disabled!: boolean; constructor(private readonly elementRef: ElementRef) {} focus(origin?: FocusOrigin | undefined) { this.elementRef.nativeElement.focus(); } getLabel?() { return this.fruit; } }
父组件代码:
@Component({ selector: "app-articles", templateUrl: "./articles.component.html", styleUrls: ["./articles.component.scss"], }) export class ArticlesComponent implements AfterContentInit, AfterViewInit { @ContentChildren(MyListItemComponent) items!: QueryList<MyListItemComponent>; @HostListener("document:keydown", ["$event"]) onKeyDown( $event: KeyboardEvent, ) { if (this.keyManager) { this.keyManager.onKeydown($event); console.log(this.keyManager.activeItem); // null this.keyManager.activeItem?.focus(); $event.preventDefault(); } } fruits = [ "Apples", "Bananas", "Cherries", "Dewberries", "Blueberries", "Avocados", ]; private keyManager!: FocusKeyManager<MyListItemComponent>; ngAfterContentInit() {} ngAfterViewInit() { this.keyManager = new FocusKeyManager(this.items).withWrap(); } }
父组件模板:
<div style="display: flex; flex-direction: column"> <app-my-list-item *ngFor="let fruit of fruits" [fruit]="fruit" tabindex=0 ></app-my-list-item> </div>
原因及修复方案
1. 错误使用ContentChildren捕获视图子元素
@ContentChildren用于获取内容投影(即通过<ng-content>插入)的子组件,但你的列表项是通过*ngFor直接在父组件模板中渲染的,属于视图子元素,因此QueryList会是空的,导致KeyManager没有可管理的项。
修复:将@ContentChildren(MyListItemComponent)替换为@ViewChildren(MyListItemComponent):
@ViewChildren(MyListItemComponent) items!: QueryList<MyListItemComponent>;
2. KeyManager未设置初始激活项
即使KeyManager初始化成功,若没有设置初始激活项,键盘操作没有起始点,activeItem会一直为null。同时需要监听列表项的变化,确保数据更新时KeyManager同步更新。
修复:在ngAfterViewInit中添加初始激活逻辑和列表变化监听:
ngAfterViewInit() { this.keyManager = new FocusKeyManager(this.items).withWrap(); // 设置第一个项为初始激活项 this.keyManager.setActiveItem(0); // 监听列表项变化,更新KeyManager this.items.changes.subscribe(() => { this.keyManager.update(this.items); }); }
3. 全局键盘监听不合理
使用document:keydown会监听全局所有键盘事件,不仅容易和其他组件冲突,还会在组件未聚焦时触发,不符合预期。
修复:改为监听父组件自身的keydown事件,并让父组件可聚焦:
// 注入ElementRef constructor(private readonly elementRef: ElementRef) {} // 监听组件自身的keydown事件 @HostListener("keydown", ["$event"]) onKeyDown($event: KeyboardEvent) { if (this.keyManager) { this.keyManager.onKeydown($event); $event.preventDefault(); } } ngAfterViewInit() { // ... 其他初始化逻辑 // 让父组件获取焦点,确保键盘事件能触发 (this.elementRef.nativeElement as HTMLElement).focus(); }
4. 重复设置tabindex
子组件的host配置已经设置了tabindex="0",父模板中再次添加tabindex=0属于重复设置,建议移除模板中的该属性。
修复后的父模板:
<div style="display: flex; flex-direction: column"> <app-my-list-item *ngFor="let fruit of fruits" [fruit]="fruit" ></app-my-list-item> </div>
内容的提问来源于stack exchange,提问作者Mario

