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

如何在Lit Web Component的Shadow DOM中手动设置元素焦点

解决Lit自定义下拉组件的焦点控制与焦点陷阱问题

一、手动切换焦点到首个下拉项

要让元素获得焦点,核心是确保元素可聚焦后调用focus()方法,具体步骤:

  1. 让下拉项具备可聚焦特性
    优先用<button>作为下拉项(自带可聚焦特性+语义化,更符合无障碍要求);如果坚持用<div>,必须给元素添加tabindex="0",示例:

    <div id="dropdown-panel">
      <!-- 用button的方式 -->
      <button id="first-dropdown-item" class="dropdown-item">选项1</button>
      <!-- 用div的方式 -->
      <div id="first-dropdown-item" class="dropdown-item" tabindex="0">选项1</div>
    </div>
    
  2. 打开面板后设置焦点
    由于Lit更新DOM后需要等待渲染完成才能获取到元素,建议用requestAnimationFrame延迟执行焦点设置,避免元素未挂载导致的报错:

    openDropdown() {
      this.isOpen = true;
      // 等待DOM渲染完成后聚焦首个选项
      requestAnimationFrame(() => {
        const firstItem = this.renderRoot.querySelector('#first-dropdown-item');
        firstItem?.focus(); // 可选链防止元素不存在时报错
      });
    }
    

二、实现下拉面板内的焦点陷阱

焦点陷阱的核心是让焦点在面板内循环,无法跳出到外部元素,同时支持Esc键关闭面板并返回焦点到触发元素,实现逻辑如下:

  1. 监听面板的键盘事件
    在组件的firstUpdated生命周期中绑定键盘事件(确保DOM已渲染完成):

    firstUpdated() {
      const panel = this.renderRoot.querySelector('#dropdown-panel');
      panel.addEventListener('keydown', this.handlePanelKeydown.bind(this));
    }
    
  2. 处理键盘交互逻辑
    针对Tab/Shift+Tab、上下箭头、Esc键分别处理:

    handlePanelKeydown(e) {
      const items = Array.from(this.renderRoot.querySelectorAll('.dropdown-item'));
      const currentIndex = items.findIndex(item => item === document.activeElement);
    
      switch(e.key) {
        // 处理Tab键循环
        case 'Tab':
          if (e.shiftKey) {
            // Shift+Tab:当前是首个元素时,跳转到最后一个
            if (currentIndex === 0) {
              e.preventDefault();
              items[items.length - 1].focus();
            }
          } else {
            // 正常Tab:当前是最后一个元素时,跳转到首个
            if (currentIndex === items.length - 1) {
              e.preventDefault();
              items[0].focus();
            }
          }
          break;
        // Esc键关闭面板,焦点返回触发元素
        case 'Escape':
          this.closeDropdown();
          this.renderRoot.querySelector('#trigger-div').focus();
          break;
        // 上下箭头导航选项
        case 'ArrowDown':
          e.preventDefault();
          const nextIndex = (currentIndex + 1) % items.length;
          items[nextIndex].focus();
          break;
        case 'ArrowUp':
          e.preventDefault();
          const prevIndex = (currentIndex - 1 + items.length) % items.length;
          items[prevIndex].focus();
          break;
      }
    }
    
    closeDropdown() {
      this.isOpen = false;
    }
    

三、完善触发元素的无障碍属性

为了让屏幕阅读器正确识别组件,给触发<div>添加必要的ARIA属性:

<div 
  id="trigger-div"
  tabindex="0"
  role="button"
  aria-haspopup="listbox"
  aria-expanded="${this.isOpen}"
  @click=${this.openDropdown}
  @keydown=${this.handleTriggerKeydown}
>
  选择选项
</div>

触发元素的键盘事件处理(支持回车/空格打开面板):

handleTriggerKeydown(e) {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault(); // 阻止默认滚动行为
    this.openDropdown();
  }
}

内容的提问来源于stack exchange,提问作者ineedtoknow

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 19:30:05