feat: Add keyboard navigation and selection highlighting for AddAssistantPopup (#4022)

* feat(AddAssistantPopup): 添加键盘导航和选中项高亮功能

* feat(AddAssistantPopup): 为所有项添加相同宽度的透明边框,避免布局跳动。
This commit is contained in:
Hao He 2025-03-30 13:58:52 +08:00 committed by GitHub
parent 00de616958
commit 9e977f4b35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -9,7 +9,7 @@ import { Agent, Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Divider, Input, InputRef, Modal, Tag } from 'antd'
import { take } from 'lodash'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -30,6 +30,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
const inputRef = useRef<InputRef>(null)
const systemAgents = useSystemAgents()
const loadingRef = useRef(false)
const [selectedIndex, setSelectedIndex] = useState(0)
const containerRef = useRef<HTMLDivElement>(null)
const agents = useMemo(() => {
const allAgents = [...userAgents, ...systemAgents] as Agent[]
@ -52,7 +54,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
return filtered
}, [assistants, defaultAssistant, searchText, systemAgents, userAgents])
const onCreateAssistant = async (agent: Agent) => {
// 重置选中索引当搜索或列表内容变更时
useEffect(() => {
setSelectedIndex(0)
}, [agents.length, searchText])
const onCreateAssistant = useCallback(
async (agent: Agent) => {
if (loadingRef.current) {
return
}
@ -70,7 +78,56 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
resolve(assistant)
setOpen(false)
},
[resolve, addAssistant, setOpen]
) // 添加函数内使用的依赖项
// 键盘导航处理
useEffect(() => {
if (!open) return
const handleKeyDown = (e: KeyboardEvent) => {
const displayedAgents = take(agents, 100)
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setSelectedIndex((prev) => (prev >= displayedAgents.length - 1 ? 0 : prev + 1))
break
case 'ArrowUp':
e.preventDefault()
setSelectedIndex((prev) => (prev <= 0 ? displayedAgents.length - 1 : prev - 1))
break
case 'Enter':
// 如果焦点在输入框且有搜索内容,则默认选择第一项
if (document.activeElement === inputRef.current?.input && searchText.trim()) {
e.preventDefault()
onCreateAssistant(displayedAgents[selectedIndex])
}
// 否则选择当前选中项
else if (selectedIndex >= 0 && selectedIndex < displayedAgents.length) {
e.preventDefault()
onCreateAssistant(displayedAgents[selectedIndex])
}
break
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [open, selectedIndex, agents, searchText, onCreateAssistant])
// 确保选中项在可视区域
useEffect(() => {
if (containerRef.current) {
const agentItems = containerRef.current.querySelectorAll('.agent-item')
if (agentItems[selectedIndex]) {
agentItems[selectedIndex].scrollIntoView({
behavior: 'smooth',
block: 'nearest'
})
}
}
}, [selectedIndex])
const onCancel = () => {
setOpen(false)
@ -121,12 +178,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
/>
</HStack>
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
<Container>
{take(agents, 100).map((agent) => (
<Container ref={containerRef}>
{take(agents, 100).map((agent, index) => (
<AgentItem
key={agent.id}
onClick={() => onCreateAssistant(agent)}
className={agent.id === 'default' ? 'default' : ''}>
className={`agent-item ${agent.id === 'default' ? 'default' : ''} ${index === selectedIndex ? 'keyboard-selected' : ''}`}
onMouseEnter={() => setSelectedIndex(index)}>
<HStack
alignItems="center"
gap={5}
@ -161,9 +219,14 @@ const AgentItem = styled.div`
margin-bottom: 8px;
cursor: pointer;
overflow: hidden;
border: 1px solid transparent;
&.default {
background-color: var(--color-background-mute);
}
&.keyboard-selected {
background-color: var(--color-background-mute);
border: 1px solid var(--color-primary);
}
.anticon {
font-size: 16px;
color: var(--color-icon);