You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Angular CDK的FocusKeyManager实现列表键盘导航时activeItem为null的问题

问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 19:32:33