feat: Add keyboard navigation and selection highlighting for AddAssistantPopup (#4022)
* feat(AddAssistantPopup): 添加键盘导航和选中项高亮功能 * feat(AddAssistantPopup): 为所有项添加相同宽度的透明边框,避免布局跳动。
This commit is contained in:
parent
00de616958
commit
9e977f4b35
@ -9,7 +9,7 @@ import { Agent, Assistant } from '@renderer/types'
|
|||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Divider, Input, InputRef, Modal, Tag } from 'antd'
|
import { Divider, Input, InputRef, Modal, Tag } from 'antd'
|
||||||
import { take } from 'lodash'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -30,6 +30,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const inputRef = useRef<InputRef>(null)
|
const inputRef = useRef<InputRef>(null)
|
||||||
const systemAgents = useSystemAgents()
|
const systemAgents = useSystemAgents()
|
||||||
const loadingRef = useRef(false)
|
const loadingRef = useRef(false)
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
const agents = useMemo(() => {
|
const agents = useMemo(() => {
|
||||||
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
const allAgents = [...userAgents, ...systemAgents] as Agent[]
|
||||||
@ -52,7 +54,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
return filtered
|
return filtered
|
||||||
}, [assistants, defaultAssistant, searchText, systemAgents, userAgents])
|
}, [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) {
|
if (loadingRef.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -70,7 +78,56 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||||
resolve(assistant)
|
resolve(assistant)
|
||||||
setOpen(false)
|
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 = () => {
|
const onCancel = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
@ -121,12 +178,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
||||||
<Container>
|
<Container ref={containerRef}>
|
||||||
{take(agents, 100).map((agent) => (
|
{take(agents, 100).map((agent, index) => (
|
||||||
<AgentItem
|
<AgentItem
|
||||||
key={agent.id}
|
key={agent.id}
|
||||||
onClick={() => onCreateAssistant(agent)}
|
onClick={() => onCreateAssistant(agent)}
|
||||||
className={agent.id === 'default' ? 'default' : ''}>
|
className={`agent-item ${agent.id === 'default' ? 'default' : ''} ${index === selectedIndex ? 'keyboard-selected' : ''}`}
|
||||||
|
onMouseEnter={() => setSelectedIndex(index)}>
|
||||||
<HStack
|
<HStack
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
gap={5}
|
gap={5}
|
||||||
@ -161,9 +219,14 @@ const AgentItem = styled.div`
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border: 1px solid transparent;
|
||||||
&.default {
|
&.default {
|
||||||
background-color: var(--color-background-mute);
|
background-color: var(--color-background-mute);
|
||||||
}
|
}
|
||||||
|
&.keyboard-selected {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
}
|
||||||
.anticon {
|
.anticon {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user