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

JavaScript/React键盘导航ul列表时如何实现自适应滚动

列表键盘导航滚动问题修复方案

现有效果

静态效果截图:
效果截图
异常效果演示:
效果演示动图

相关变量说明

  • dataList:ul列表容器的DOM引用
  • selectedCountry:当前选中li元素的DOM引用
  • selectedCountryIndex:当前选中元素的索引
  • autoCompleteSuggestions:输入匹配的国家联想建议列表

核心问题原因

当前实现存在两个核心问题,导致硬编码失效和向上滚动异常:

  1. 硬编码阈值不通用:使用selectedCountryIndex > 3作为滚动触发条件,完全依赖固定容器高度、固定列表项高度的前提,只要样式调整(比如容器变高/变矮、项高度变化)逻辑就会失效
  2. 向上滚动逻辑错误:
    • 触发条件和向下逻辑完全一致,当选中索引回退到3及以下时,即使选中项已经超出容器可视区顶部,也不会触发滚动,导致元素逐步露出、显示不全
    • 滚动量硬编码为单个项高度,没有根据实际位置差计算,容易出现偏移误差
    • React的setSelectedCountryIndex是异步更新,更新后立刻读取selectedCountry.current会拿到上一次渲染的旧DOM,导致滚动计算基准错误

通用滚动实现方案

不需要提前计算可见项总数,直接通过DOM元素的几何位置判断是否需要滚动是最通用的方案,不受容器尺寸、项高度变化影响:

  • 按向下键切换选中项时,如果新选中项的底部超出ul容器可视区底部,就滚动容器让选中项刚好完整显示在可视区底部
  • 按向上键切换选中项时,如果新选中项的顶部超出ul容器可视区顶部,就滚动容器让选中项刚好完整显示在可视区顶部

修复后的完整代码如下:

const arrowNavigation = (
  e: React.KeyboardEvent<HTMLInputElement>
) => {
  const listContainer = dataList.current;
  if (!listContainer) return;
  // 提前获取容器可视区域边界
  const containerRect = listContainer.getBoundingClientRect();
  const maxIndex = autoCompleteSuggestions.length - 1;

  if (e.key === 'ArrowDown') {
    e.preventDefault();
    // 提前计算下一个选中索引,不依赖异步更新的state
    const nextIndex = Math.min(selectedCountryIndex + 1, maxIndex);
    setSelectedCountryIndex(nextIndex);

    // 直接从容器子节点拿目标项,避开ref更新延迟问题
    const targetItem = listContainer.children[nextIndex] as HTMLElement;
    if (!targetItem) return;
    const itemRect = targetItem.getBoundingClientRect();

    // 项超出容器底部时才滚动,滚动量为实际超出距离
    if (itemRect.bottom > containerRect.bottom) {
      listContainer.scrollTop += itemRect.bottom - containerRect.bottom;
    }
  } else if (e.key === 'ArrowUp') {
    e.preventDefault();
    const nextIndex = Math.max(selectedCountryIndex - 1, 0);
    setSelectedCountryIndex(nextIndex);

    const targetItem = listContainer.children[nextIndex] as HTMLElement;
    if (!targetItem) return;
    const itemRect = targetItem.getBoundingClientRect();

    // 项超出容器顶部时才滚动,滚动量为实际超出距离
    if (itemRect.top < containerRect.top) {
      listContainer.scrollTop -= containerRect.top - itemRect.top;
    }
  } else if (e.key === 'Enter') {
    e.preventDefault();
    const selectedValue = autoCompleteSuggestions[selectedCountryIndex];
    if (!selectedValue) return;
    if (textInput.current) {
      textInput.current.value = selectedValue;
    }
    setInputValue(selectedValue);
  }
};

附:获取可见项信息的通用方法

如果确实需要获取ul内可见项总数、首尾可见项,可以通过遍历列表项判断位置实现,不需要依赖硬编码阈值:

const getVisibleListInfo = (container: HTMLUListElement) => {
  const containerRect = container.getBoundingClientRect();
  const allItems = Array.from(container.children) as HTMLElement[];
  
  const visibleItems = allItems.filter(item => {
    const itemRect = item.getBoundingClientRect();
    // 项和容器可视区域有重叠即为可见
    return itemRect.top < containerRect.bottom && itemRect.bottom > containerRect.top;
  });

  return {
    visibleCount: visibleItems.length,
    firstVisibleItem: visibleItems[0],
    lastVisibleItem: visibleItems.at(-1),
    firstVisibleIndex: allItems.indexOf(visibleItems[0]),
    lastVisibleIndex: allItems.indexOf(visibleItems.at(-1) as HTMLElement)
  }
};

对于键盘导航跟随滚动的场景,直接判断当前选中项和容器的位置关系即可,不需要遍历全量列表项,性能更好。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 10:27:41