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

React如何通过方向键遍历搜索列表项并支持回车触发

问题核心原因

现有代码存在4个直接导致功能失效的问题:

  • 按键判断逻辑错误:e.key返回的是按键名称字符串(如ArrowUp/ArrowDown/Enter),代码里写的38/40是e.keyCode的枚举值,判断条件永远不成立,active状态根本不会触发更新
  • 缺少索引边界处理:按上键到第一项后继续按会出现负数索引,按下键到最后一项继续按会超出数组长度导致报错
  • 仅给激活项加了id标识,没有配套滚动跟随、回车触发逻辑,也没有做索引重置,搜索结果变化/菜单重新打开时激活位置会错乱
  • 列表项没有绑定聚焦相关逻辑,视觉高亮和实际交互逻辑脱节,不符合下拉选择的常规交互

可直接复用的修复方案

完整修复代码

import { useState, useEffect, useRef } from 'react'
import { useSelector } from 'react-redux'
import { NavLink, useNavigate } from 'react-router-dom'
import { BsSearch } from 'react-icons/bs'

// 组件内部逻辑
const [menu, setMenu] = useState(false)
const [searchVal, setSearchVal] = useState('')
const [active, setActive] = useState(0)
const navigate = useNavigate()
// 存储激活项DOM,实现自动滚动跟随
const activeItemRef = useRef(null)
const searchResults = useSelector((state) => Object.values(state.searchReducer.Search))

// 搜索结果更新、菜单重新打开时重置激活索引
useEffect(() => {
  setActive(0)
}, [searchResults, menu])

// 激活项变化时自动滚动到可视区域
useEffect(() => {
  activeItemRef.current?.scrollIntoView({ block: 'nearest' })
}, [active])

const keyDown = (e) => {
  if (!searchResults.length) return

  switch(e.key) {
    case 'ArrowUp':
      e.preventDefault() // 阻止输入框光标默认移动行为
      setActive(prev => prev <= 0 ? searchResults.length - 1 : prev - 1)
      break
    case 'ArrowDown':
      e.preventDefault()
      setActive(prev => prev >= searchResults.length - 1 ? 0 : prev + 1)
      break
    case 'Enter':
      e.preventDefault() // 阻止表单默认提交
      const currentActive = searchResults[active]
      if (currentActive) {
        setSearchVal('')
        setMenu(false)
        navigate(`/users/${currentActive.id}`)
      }
      break
    case 'Escape':
      setMenu(false)
      break
  }
}

const handleSubmit = (e) => {
  e.preventDefault()
  // 原有表单提交逻辑可保留
}

// JSX 渲染部分
<BsSearch className="search-icon" />
<form onSubmit={handleSubmit}>
  <input
    onClick={() => setMenu(true)}
    className="search-input"
    autoFocus
    onKeyDown={keyDown}
    type="search"
    placeholder="Search"
    onChange={(e) => setSearchVal(e.target.value)}
    value={searchVal}
  />
</form>
{menu && (
  <div className="search-cont">
    {searchResults.length > 0 ? (
      searchResults.map((ele) => {
        return (
          <NavLink 
            onClick={() => {
              setSearchVal('')
              setMenu(false)
            }}  
            key={ele.id}
            className="spot-links" 
            to={`/users/${ele.id}`}
            ref={active === i ? activeItemRef : null}
          >
            <div
              className={`search-results-list ${active === i ? 'active' : ''}`}
            >
              {ele.username}
            </div>
          </NavLink>
        );
      })
    ) : (
      <div className="search-empty">无匹配结果</div>
    )}
  </div>
)}

配套CSS调整

把原来绑定给#active的高亮样式,迁移到类选择器上即可,参考样式:

.search-results-list.active {
  background-color: #f0f5ff;
  color: #1677ff;
  cursor: pointer;
}

实现注意事项
  • 列表key不要用遍历索引,优先用数据本身的唯一标识(比如用户id),避免列表动态增删时激活项错位
  • 方向键触发时必须加e.preventDefault(),否则输入框光标会默认移动,打断导航交互
  • 逻辑默认支持首尾循环跳转:到第一项按上键直接跳到最后一项,到最后一项按下键跳回第一项,符合通用下拉组件的交互习惯
  • 可自行补充点击组件外部区域关闭下拉菜单的逻辑,完善交互闭环

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 09:33:18