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

实现TagsInput组件支持上下方向键与回车键选择标签功能

修改React TagsInput组件,支持方向键切换与回车选择标签

问题背景

现有TagsInput组件已实现输入字符匹配建议、鼠标选择标签功能,但无法通过上下方向键切换建议选项,也不能用回车键确认选择,需补充该交互功能。

修改后的完整代码

import { useState } from 'react';
import ClearIcon from '@mui/icons-material/Clear';
// 假设classes是导入的样式模块,根据实际项目调整
import classes from './TagsInput.module.css';

export default function TagsInput(props) {
    const { tags, setTags, tagSuggestions } = props;
    const [input, setInput] = useState("");
    const [suggestedTags, setSuggestedTags] = useState([]);
    const [isValid, setIsValid] = useState(true);
    // 新增状态:跟踪当前选中的建议项索引
    const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);

    const onChange = (e) => {
        const { value } = e.target;
        setInput(value);

        if (value.length < 1) {
            setSuggestedTags([]);
            setIsValid(true);
            setSelectedSuggestionIndex(-1);
            return;
        }
        
        const matchedSuggestions = tagSuggestions.filter(s =>
            s.startsWith(value) && 
            !tags.includes(s)                  
        );
        setSuggestedTags(matchedSuggestions);
        // 输入有匹配项时,默认选中第一个
        setSelectedSuggestionIndex(matchedSuggestions.length > 0 ? 0 : -1);
    };

    const onKeyDown = (e) => {
        const { key } = e;
        const trimmedInput = input.trim();

        // 处理上方向键
        if (key === "ArrowUp") {
            e.preventDefault();
            setSelectedSuggestionIndex(prev => {
                const newIndex = prev - 1;
                // 边界循环:到顶部后跳转到最后一项
                return newIndex >= 0 ? newIndex : suggestedTags.length - 1;
            });
            return;
        }

        // 处理下方向键
        if (key === "ArrowDown") {
            e.preventDefault();
            setSelectedSuggestionIndex(prev => {
                const newIndex = prev + 1;
                // 边界循环:到底部后跳转到第一项
                return newIndex < suggestedTags.length ? newIndex : 0;
            });
            return;
        }

        // 处理回车键
        if (key === "Enter") {
            e.preventDefault();
            // 优先选中当前高亮的建议项
            if (selectedSuggestionIndex !== -1 && suggestedTags.length > 0) {
                selectTag(suggestedTags[selectedSuggestionIndex]);
            } else if (trimmedInput.length && !tags.includes(trimmedInput)) {
                // 无选中项时,将输入内容作为新标签添加
                setTags(prevState => [...prevState, trimmedInput]);
                setInput("");
                setSuggestedTags([]);
                setSelectedSuggestionIndex(-1);
            }
            return;
        }

        // 保留原空格添加标签逻辑(按需保留)
        if (key === " ") {
            e.preventDefault();
            if (trimmedInput.length && !tags.includes(trimmedInput)) {
                setTags(prevState => [...prevState, trimmedInput]);
                setInput("");
                setSuggestedTags([]);
                setSelectedSuggestionIndex(-1);
            }
        }
    };

    const deleteTag = (index) => {
        setTags(prevState => prevState.filter((tag, i) => i !== index));
    };

    const selectTag = (tag) => {
        setTags(prevState => [...prevState, tag]);
        setSuggestedTags([]);
        setInput("");
        setSelectedSuggestionIndex(-1);
    };

    return (
        <div className={classes.container}>
            {tags.map((tag, index) => (
                <div className={classes.tag} key={tag}>
                    <ClearIcon
                        className={classes.del}
                        fontSize="big"
                        onClick={() => deleteTag(index)}
                    />
                    {tag}
                </div>
            ))}

            <input
                className={classes.input}
                value={input}
                placeholder={props.inputPlaceholder}
                onKeyDown={onKeyDown}
                onChange={onChange}
            />

            {suggestedTags.length ? (
                <div className={classes.tagSuggestionWrapper}>
                    {suggestedTags.map((tag, index) => (
                        <div
                            key={tag}
                            className={`${classes.tagSuggestion} ${index === selectedSuggestionIndex ? classes.tagSuggestionActive : ''}`}
                            onClick={() => selectTag(tag)}
                        >
                            {tag}
                        </div>
                    ))}
                </div>
            ) : null}
        </div>
    );
}

关键修改说明

  • 新增选中状态:添加selectedSuggestionIndex状态,记录当前通过方向键选中的建议项索引,初始值-1表示未选中任何项。
  • 修正语法错误:原代码中onKeyDown函数嵌套在onChange内,导致逻辑无法正常执行,已将其移出作为独立函数。
  • 方向键交互逻辑:监听ArrowUp和ArrowDown事件,调整选中索引并处理边界循环(比如按上键到第一项后,再按会跳转到最后一项)。
  • 回车键交互逻辑:优先选中当前高亮的建议项;若无选中项且输入内容合法,则将输入内容添加为新标签。
  • 选中项高亮:渲染建议项时,根据selectedSuggestionIndex为当前选中项添加高亮样式类(需在样式文件中定义.tagSuggestionActive,比如设置背景色区分选中状态)。
  • 状态同步重置:在清空输入、选中标签等操作时,同步重置选中索引和建议列表状态,避免残留状态影响交互。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 13:32:59