如何为React Select组件添加键盘导航功能(上下箭头、Tab、Enter)
实现React Select组件的键盘导航功能
先修正基础问题:你的状态初始值类型不符合需求,isOpen应该是布尔值,focusedIndex作为选项索引应该用数字(初始设为-1表示未选中任何选项),先把这部分调整正确:
const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1);
接下来完善键盘导航逻辑,处理所有需要的按键行为:
const keyboardNavigation = (event: React.KeyboardEvent<HTMLDivElement>) => { // 阻止默认行为,避免页面滚动、表单提交等干扰 const preventDefault = () => { event.preventDefault(); event.stopPropagation(); }; switch (event.key) { case "ArrowDown": preventDefault(); setIsOpen(true); // 循环切换到下一个选项,到末尾后回到开头 setFocusedIndex(prev => { const nextIndex = prev + 1; return nextIndex >= searchResults.length ? 0 : nextIndex; }); break; case "ArrowUp": preventDefault(); setIsOpen(true); // 循环切换到上一个选项,到开头后回到末尾 setFocusedIndex(prev => { const prevIndex = prev - 1; return prevIndex < 0 ? searchResults.length - 1 : prevIndex; }); break; case "Escape": preventDefault(); setIsOpen(false); setFocusedIndex(-1); // 把焦点放回输入框 (event.currentTarget.querySelector("input") as HTMLInputElement)?.focus(); break; case "Enter": preventDefault(); // 有选中选项时触发选择逻辑 if (focusedIndex >= 0 && focusedIndex < searchResults.length) { const selectedOption = searchResults[focusedIndex]; setSearch(selectedOption.value.toString()); setIsOpen(false); setFocusedIndex(-1); } break; case "Tab": // Tab键默认切换焦点,这里仅关闭下拉框 setIsOpen(false); setFocusedIndex(-1); break; } };
然后优化选项渲染,添加焦点样式、处理选项自身的键盘事件,同时在选中索引变化时自动滚动到对应选项提升体验:
首先添加自动滚动的副作用:
useEffect(() => { if (isOpen && focusedIndex >= 0) { const optionElement = document.querySelector(`[data-index="${focusedIndex}"]`); optionElement?.scrollIntoView({ block: "nearest" }); } }, [isOpen, focusedIndex]);
修改选项渲染代码:
{searchResults.map((option, index) => { const isFocused = index === focusedIndex; return ( <div key={option.label} data-index={index} tabIndex={0} className={`option-item ${isFocused ? "focused" : ""}`} onClick={() => { setIsOpen(false); setSearch(option.value.toString()); setFocusedIndex(-1); }} onKeyDown={(e) => { // 选项上按Enter也触发选择 if (e.key === "Enter") { setIsOpen(false); setSearch(option.value.toString()); setFocusedIndex(-1); } }} > {option.value} </div> ); })}
可以补充简单的CSS样式区分选中状态:
.option-item { padding: 8px; cursor: pointer; } .option-item.focused { background-color: #e6f7ff; outline: none; }
最后整合完整的组件代码:
import { useState, useMemo, useEffect } from "react"; type OptionType = { label: string; value: string | number; }; type SelectProps = { options: OptionType[]; // 其他props按需补充 }; export function SelectComponent({ options }: SelectProps) { const [isOpen, setIsOpen] = useState(false); const [search, setSearch] = useState(""); const [focusedIndex, setFocusedIndex] = useState(-1); const searchResults = useMemo(() => { if (!search) return options; return options.filter(item => item.value.toString().toLowerCase().includes(search.toLowerCase()) ); }, [options, search]); useEffect(() => { if (isOpen && focusedIndex >= 0) { const optionElement = document.querySelector(`[data-index="${focusedIndex}"]`); optionElement?.scrollIntoView({ block: "nearest" }); } }, [isOpen, focusedIndex]); const keyboardNavigation = (event: React.KeyboardEvent<HTMLDivElement>) => { const preventDefault = () => { event.preventDefault(); event.stopPropagation(); }; switch (event.key) { case "ArrowDown": preventDefault(); setIsOpen(true); setFocusedIndex(prev => { const nextIndex = prev + 1; return nextIndex >= searchResults.length ? 0 : nextIndex; }); break; case "ArrowUp": preventDefault(); setIsOpen(true); setFocusedIndex(prev => { const prevIndex = prev - 1; return prevIndex < 0 ? searchResults.length - 1 : prevIndex; }); break; case "Escape": preventDefault(); setIsOpen(false); setFocusedIndex(-1); (event.currentTarget.querySelector("input") as HTMLInputElement)?.focus(); break; case "Enter": preventDefault(); if (focusedIndex >= 0 && focusedIndex < searchResults.length) { const selectedOption = searchResults[focusedIndex]; setSearch(selectedOption.value.toString()); setIsOpen(false); setFocusedIndex(-1); } break; case "Tab": setIsOpen(false); setFocusedIndex(-1); break; } }; return ( <div onKeyDown={keyboardNavigation} style={{ position: "relative", width: "200px" }}> <input onClick={() => setIsOpen(!isOpen)} value={search} onChange={event => setSearch(event.target.value)} style={{ width: "100%", padding: "8px" }} /> {isOpen && ( <div style={{ position: "absolute", top: "100%", left: 0, right: 0, border: "1px solid #ddd", maxHeight: "200px", overflowY: "auto", backgroundColor: "#fff" }}> {searchResults.map((option, index) => { const isFocused = index === focusedIndex; return ( <div key={option.label} data-index={index} tabIndex={0} className={`option-item ${isFocused ? "focused" : ""}`} onClick={() => { setIsOpen(false); setSearch(option.value.toString()); setFocusedIndex(-1); }} onKeyDown={(e) => { if (e.key === "Enter") { setIsOpen(false); setSearch(option.value.toString()); setFocusedIndex(-1); } }} > {option.value} </div> ); })} </div> )} </div> ); }
这样就完整实现了箭头上下导航、Enter选中、Escape关闭、Tab切换时关闭下拉框的功能,同时包含焦点样式和自动滚动的体验优化。
内容的提问来源于stack exchange,提问作者TSDev
相关产品推荐
相关产品推荐

