mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 23:22:05 +08:00
fix(a11y): improve screen reader (NVDA) support with aria-label attributes (#11678)
* fix(a11y): improve screen reader support with aria-label attributes Add aria-label attributes to all interactive buttons and toolbar elements to improve accessibility for screen reader users (NVDA, etc.). Changes: - Add aria-label with i18n translations to all ActionIconButton components - Add role="button", tabIndex, and keyboard handlers for non-semantic elements - Fix hardcoded English aria-labels in WindowControls to use i18n - Add aria-pressed for toggle buttons to indicate state - Add aria-expanded for expandable menus - Add aria-disabled for disabled buttons Components updated: - SendMessageButton, CopyButton, SelectionToolbar - CodeToolbar, RichEditor toolbar, MinimalToolbar - WindowControls - 12 Inputbar tool buttons (WebSearch, Attachment, KnowledgeBase, etc.) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(a11y): enhance accessibility in CodeToolbar snapshot Added aria-label, role, and tabindex attributes to improve screen reader support for interactive elements in the CodeToolbar component. This change aligns with ongoing efforts to enhance accessibility across the application. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
fb20173194
commit
9637fb8a43
@ -64,7 +64,11 @@ exports[`CodeToolbar > basic rendering > should match snapshot with mixed tools
|
|||||||
data-title="code_block.more"
|
data-title="code_block.more"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="code_block.more"
|
||||||
class="c2"
|
class="c2"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="tool-icon"
|
class="tool-icon"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ActionTool } from '@renderer/components/ActionTools'
|
import type { ActionTool } from '@renderer/components/ActionTools'
|
||||||
import { Dropdown, Tooltip } from 'antd'
|
import { Dropdown, Tooltip } from 'antd'
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
import { ToolWrapper } from './styles'
|
import { ToolWrapper } from './styles'
|
||||||
|
|
||||||
@ -9,13 +9,30 @@ interface CodeToolButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CodeToolButton = ({ tool }: CodeToolButtonProps) => {
|
const CodeToolButton = ({ tool }: CodeToolButtonProps) => {
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
tool.onClick?.()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[tool]
|
||||||
|
)
|
||||||
|
|
||||||
const mainTool = useMemo(
|
const mainTool = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Tooltip key={tool.id} title={tool.tooltip} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
|
<Tooltip key={tool.id} title={tool.tooltip} mouseEnterDelay={0.5} mouseLeaveDelay={0}>
|
||||||
<ToolWrapper onClick={tool.onClick}>{tool.icon}</ToolWrapper>
|
<ToolWrapper
|
||||||
|
onClick={tool.onClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
role="button"
|
||||||
|
aria-label={tool.tooltip}
|
||||||
|
tabIndex={0}>
|
||||||
|
{tool.icon}
|
||||||
|
</ToolWrapper>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
[tool]
|
[tool, handleKeyDown]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (tool.children?.length && tool.children.length > 0) {
|
if (tool.children?.length && tool.children.length > 0) {
|
||||||
|
|||||||
@ -40,7 +40,19 @@ const CodeToolbar = ({ tools }: { tools: ActionTool[] }) => {
|
|||||||
{quickToolButtons}
|
{quickToolButtons}
|
||||||
{quickTools.length > 1 && (
|
{quickTools.length > 1 && (
|
||||||
<Tooltip title={t('code_block.more')} mouseEnterDelay={0.5}>
|
<Tooltip title={t('code_block.more')} mouseEnterDelay={0.5}>
|
||||||
<ToolWrapper onClick={() => setShowQuickTools(!showQuickTools)} className={showQuickTools ? 'active' : ''}>
|
<ToolWrapper
|
||||||
|
onClick={() => setShowQuickTools(!showQuickTools)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
setShowQuickTools(!showQuickTools)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={showQuickTools ? 'active' : ''}
|
||||||
|
role="button"
|
||||||
|
aria-label={t('code_block.more')}
|
||||||
|
aria-expanded={showQuickTools}
|
||||||
|
tabIndex={0}>
|
||||||
<EllipsisVertical className="tool-icon" />
|
<EllipsisVertical className="tool-icon" />
|
||||||
</ToolWrapper>
|
</ToolWrapper>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import { Copy } from 'lucide-react'
|
import { Copy } from 'lucide-react'
|
||||||
import type { FC } from 'react'
|
import type { FC, KeyboardEvent } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@ -39,8 +39,24 @@ const CopyButton: FC<CopyButtonProps> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleCopy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ariaLabel = tooltip || t('common.copy')
|
||||||
|
|
||||||
const button = (
|
const button = (
|
||||||
<ButtonContainer $color={color} $hoverColor={hoverColor} onClick={handleCopy}>
|
<ButtonContainer
|
||||||
|
$color={color}
|
||||||
|
$hoverColor={hoverColor}
|
||||||
|
onClick={handleCopy}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
role="button"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
tabIndex={0}>
|
||||||
<Copy size={size} className="copy-icon" />
|
<Copy size={size} className="copy-icon" />
|
||||||
{label && <RightText size={size}>{label}</RightText>}
|
{label && <RightText size={size}>{label}</RightText>}
|
||||||
</ButtonContainer>
|
</ButtonContainer>
|
||||||
|
|||||||
@ -171,7 +171,9 @@ export const Toolbar: React.FC<ToolbarProps> = ({ editor, formattingState, onCom
|
|||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
onClick={() => handleCommand(command)}
|
onClick={() => handleCommand(command)}
|
||||||
data-testid={`toolbar-${command}`}>
|
data-testid={`toolbar-${command}`}
|
||||||
|
aria-label={tooltipText}
|
||||||
|
aria-pressed={isActive}>
|
||||||
<Icon color={isActive ? 'var(--color-primary)' : 'var(--color-text)'} />
|
<Icon color={isActive ? 'var(--color-primary)' : 'var(--color-text)'} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -86,7 +86,7 @@ const WindowControls: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<WindowControlsContainer>
|
<WindowControlsContainer>
|
||||||
<Tooltip title={t('navbar.window.minimize')} placement="bottom" mouseEnterDelay={DEFAULT_DELAY}>
|
<Tooltip title={t('navbar.window.minimize')} placement="bottom" mouseEnterDelay={DEFAULT_DELAY}>
|
||||||
<ControlButton onClick={handleMinimize} aria-label="Minimize">
|
<ControlButton onClick={handleMinimize} aria-label={t('navbar.window.minimize')}>
|
||||||
<Minus size={14} />
|
<Minus size={14} />
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -94,12 +94,14 @@ const WindowControls: React.FC = () => {
|
|||||||
title={isMaximized ? t('navbar.window.restore') : t('navbar.window.maximize')}
|
title={isMaximized ? t('navbar.window.restore') : t('navbar.window.maximize')}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
mouseEnterDelay={DEFAULT_DELAY}>
|
mouseEnterDelay={DEFAULT_DELAY}>
|
||||||
<ControlButton onClick={handleMaximize} aria-label={isMaximized ? 'Restore' : 'Maximize'}>
|
<ControlButton
|
||||||
|
onClick={handleMaximize}
|
||||||
|
aria-label={isMaximized ? t('navbar.window.restore') : t('navbar.window.maximize')}>
|
||||||
{isMaximized ? <WindowRestoreIcon size={14} /> : <Square size={14} />}
|
{isMaximized ? <WindowRestoreIcon size={14} /> : <Square size={14} />}
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('navbar.window.close')} placement="bottom" mouseEnterDelay={DEFAULT_DELAY}>
|
<Tooltip title={t('navbar.window.close')} placement="bottom" mouseEnterDelay={DEFAULT_DELAY}>
|
||||||
<ControlButton $isClose onClick={handleClose} aria-label="Close">
|
<ControlButton $isClose onClick={handleClose} aria-label={t('navbar.window.close')}>
|
||||||
<X size={17} />
|
<X size={17} />
|
||||||
</ControlButton>
|
</ControlButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC, KeyboardEvent } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
@ -6,10 +7,24 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
|
const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {
|
||||||
|
if (!disabled && (e.key === 'Enter' || e.key === ' ')) {
|
||||||
|
e.preventDefault()
|
||||||
|
sendMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<i
|
<i
|
||||||
className="iconfont icon-ic_send"
|
className="iconfont icon-ic_send"
|
||||||
onClick={sendMessage}
|
onClick={disabled ? undefined : sendMessage}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
role="button"
|
||||||
|
aria-label={t('chat.input.send')}
|
||||||
|
aria-disabled={disabled}
|
||||||
|
tabIndex={disabled ? -1 : 0}
|
||||||
style={{
|
style={{
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
|
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const ActivityDirectoryButton: FC<Props> = ({ quickPanel, quickPanelController,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.activity_directory.title')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('chat.input.activity_directory.title')} mouseLeaveDelay={0} arrow>
|
||||||
<ActionIconButton onClick={handleOpenQuickPanel}>
|
<ActionIconButton onClick={handleOpenQuickPanel} aria-label={t('chat.input.activity_directory.title')}>
|
||||||
<FolderOpen size={18} />
|
<FolderOpen size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -152,13 +152,15 @@ const AttachmentButton: FC<Props> = ({ quickPanel, couldAddImageFile, extensions
|
|||||||
}
|
}
|
||||||
}, [couldAddImageFile, openQuickPanel, quickPanel, t])
|
}, [couldAddImageFile, openQuickPanel, quickPanel, t])
|
||||||
|
|
||||||
|
const ariaLabel = couldAddImageFile ? t('chat.input.upload.image_or_document') : t('chat.input.upload.document')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip placement="top" title={ariaLabel} mouseLeaveDelay={0} arrow>
|
||||||
placement="top"
|
<ActionIconButton
|
||||||
title={couldAddImageFile ? t('chat.input.upload.image_or_document') : t('chat.input.upload.document')}
|
onClick={openFileSelectDialog}
|
||||||
mouseLeaveDelay={0}
|
active={files.length > 0}
|
||||||
arrow>
|
disabled={disabled}
|
||||||
<ActionIconButton onClick={openFileSelectDialog} active={files.length > 0} disabled={disabled}>
|
aria-label={ariaLabel}>
|
||||||
<Paperclip size={18} />
|
<Paperclip size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -15,18 +15,18 @@ interface Props {
|
|||||||
const GenerateImageButton: FC<Props> = ({ model, assistant, onEnableGenerateImage }) => {
|
const GenerateImageButton: FC<Props> = ({ model, assistant, onEnableGenerateImage }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const ariaLabel = isGenerateImageModel(model)
|
||||||
|
? t('chat.input.generate_image')
|
||||||
|
: t('chat.input.generate_image_not_supported')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip placement="top" title={ariaLabel} mouseLeaveDelay={0} arrow>
|
||||||
placement="top"
|
|
||||||
title={
|
|
||||||
isGenerateImageModel(model) ? t('chat.input.generate_image') : t('chat.input.generate_image_not_supported')
|
|
||||||
}
|
|
||||||
mouseLeaveDelay={0}
|
|
||||||
arrow>
|
|
||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
onClick={onEnableGenerateImage}
|
onClick={onEnableGenerateImage}
|
||||||
active={assistant.enableGenerateImage}
|
active={assistant.enableGenerateImage}
|
||||||
disabled={!isGenerateImageModel(model)}>
|
disabled={!isGenerateImageModel(model)}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-pressed={assistant.enableGenerateImage}>
|
||||||
<Image size={18} />
|
<Image size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -124,7 +124,8 @@ const KnowledgeBaseButton: FC<Props> = ({ quickPanel, selectedBases, onSelect, d
|
|||||||
<ActionIconButton
|
<ActionIconButton
|
||||||
onClick={handleOpenQuickPanel}
|
onClick={handleOpenQuickPanel}
|
||||||
active={selectedBases && selectedBases.length > 0}
|
active={selectedBases && selectedBases.length > 0}
|
||||||
disabled={disabled}>
|
disabled={disabled}
|
||||||
|
aria-label={t('chat.input.knowledge_base')}>
|
||||||
<FileSearch size={18} />
|
<FileSearch size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -516,7 +516,10 @@ const MCPToolsButton: FC<Props> = ({ quickPanel, setInputValue, resizeTextArea,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('settings.mcp.title')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('settings.mcp.title')} mouseLeaveDelay={0} arrow>
|
||||||
<ActionIconButton onClick={handleOpenQuickPanel} active={assistant.mcpServers && assistant.mcpServers.length > 0}>
|
<ActionIconButton
|
||||||
|
onClick={handleOpenQuickPanel}
|
||||||
|
active={assistant.mcpServers && assistant.mcpServers.length > 0}
|
||||||
|
aria-label={t('settings.mcp.title')}>
|
||||||
<Hammer size={18} />
|
<Hammer size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -46,7 +46,10 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('assistants.presets.edit.model.select.title')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('assistants.presets.edit.model.select.title')} mouseLeaveDelay={0} arrow>
|
||||||
<ActionIconButton onClick={handleOpenQuickPanel} active={mentionedModels.length > 0}>
|
<ActionIconButton
|
||||||
|
onClick={handleOpenQuickPanel}
|
||||||
|
active={mentionedModels.length > 0}
|
||||||
|
aria-label={t('assistants.presets.edit.model.select.title')}>
|
||||||
<AtSign size={18} />
|
<AtSign size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -20,7 +20,9 @@ const NewContextButton: FC<Props> = ({ onNewContext }) => {
|
|||||||
title={t('chat.input.new.context', { Command: newContextShortcut })}
|
title={t('chat.input.new.context', { Command: newContextShortcut })}
|
||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
arrow>
|
arrow>
|
||||||
<ActionIconButton onClick={onNewContext}>
|
<ActionIconButton
|
||||||
|
onClick={onNewContext}
|
||||||
|
aria-label={t('chat.input.new.context', { Command: newContextShortcut })}>
|
||||||
<Eraser size={18} />
|
<Eraser size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -250,7 +250,7 @@ const QuickPhrasesButton = ({ quickPanel, setInputValue, resizeTextArea, assista
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip placement="top" title={t('settings.quickPhrase.title')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('settings.quickPhrase.title')} mouseLeaveDelay={0} arrow>
|
||||||
<ActionIconButton onClick={handleOpenQuickPanel}>
|
<ActionIconButton onClick={handleOpenQuickPanel} aria-label={t('settings.quickPhrase.title')}>
|
||||||
<Zap size={18} />
|
<Zap size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -37,7 +37,11 @@ const SlashCommandsButton: FC<Props> = ({ quickPanelController, session, openPan
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.slash_commands.title')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('chat.input.slash_commands.title')} mouseLeaveDelay={0} arrow>
|
||||||
<ActionIconButton onClick={handleOpenQuickPanel} active={isActive} disabled={!hasCommands}>
|
<ActionIconButton
|
||||||
|
onClick={handleOpenQuickPanel}
|
||||||
|
active={isActive}
|
||||||
|
disabled={!hasCommands}
|
||||||
|
aria-label={t('chat.input.slash_commands.title')}>
|
||||||
<Terminal size={18} />
|
<Terminal size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -142,17 +142,18 @@ const ThinkingButton: FC<Props> = ({ quickPanel, model, assistantId }): ReactEle
|
|||||||
}
|
}
|
||||||
}, [currentReasoningEffort, openQuickPanel, quickPanel, t])
|
}, [currentReasoningEffort, openQuickPanel, quickPanel, t])
|
||||||
|
|
||||||
return (
|
const ariaLabel =
|
||||||
<Tooltip
|
|
||||||
placement="top"
|
|
||||||
title={
|
|
||||||
isThinkingEnabled && supportedOptions.includes('none')
|
isThinkingEnabled && supportedOptions.includes('none')
|
||||||
? t('common.close')
|
? t('common.close')
|
||||||
: t('assistants.settings.reasoning_effort.label')
|
: t('assistants.settings.reasoning_effort.label')
|
||||||
}
|
|
||||||
mouseLeaveDelay={0}
|
return (
|
||||||
arrow>
|
<Tooltip placement="top" title={ariaLabel} mouseLeaveDelay={0} arrow>
|
||||||
<ActionIconButton onClick={handleOpenQuickPanel} active={currentReasoningEffort !== 'none'}>
|
<ActionIconButton
|
||||||
|
onClick={handleOpenQuickPanel}
|
||||||
|
active={currentReasoningEffort !== 'none'}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
aria-pressed={currentReasoningEffort !== 'none'}>
|
||||||
{ThinkingIcon(currentReasoningEffort)}
|
{ThinkingIcon(currentReasoningEffort)}
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -48,7 +48,11 @@ const UrlContextButton: FC<Props> = ({ assistantId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip placement="top" title={t('chat.input.url_context')} arrow>
|
<Tooltip placement="top" title={t('chat.input.url_context')} arrow>
|
||||||
<ActionIconButton onClick={handleToggle} active={assistant.enableUrlContext}>
|
<ActionIconButton
|
||||||
|
onClick={handleToggle}
|
||||||
|
active={assistant.enableUrlContext}
|
||||||
|
aria-label={t('chat.input.url_context')}
|
||||||
|
aria-pressed={assistant.enableUrlContext}>
|
||||||
<Link size={18} />
|
<Link size={18} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -25,13 +25,15 @@ const WebSearchButton: FC<Props> = ({ quickPanelController, assistantId }) => {
|
|||||||
}
|
}
|
||||||
}, [enableWebSearch, toggleQuickPanel, updateWebSearchProvider])
|
}, [enableWebSearch, toggleQuickPanel, updateWebSearchProvider])
|
||||||
|
|
||||||
|
const ariaLabel = enableWebSearch ? t('common.close') : t('chat.input.web_search.label')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip placement="top" title={ariaLabel} mouseLeaveDelay={0} arrow>
|
||||||
placement="top"
|
<ActionIconButton
|
||||||
title={enableWebSearch ? t('common.close') : t('chat.input.web_search.label')}
|
onClick={onClick}
|
||||||
mouseLeaveDelay={0}
|
active={!!enableWebSearch}
|
||||||
arrow>
|
aria-label={ariaLabel}
|
||||||
<ActionIconButton onClick={onClick} active={!!enableWebSearch}>
|
aria-pressed={!!enableWebSearch}>
|
||||||
<WebSearchProviderIcon pid={selectedProviderId} />
|
<WebSearchProviderIcon pid={selectedProviderId} />
|
||||||
</ActionIconButton>
|
</ActionIconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -238,19 +238,27 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
|
|||||||
<LeftSection>
|
<LeftSection>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Tooltip title={t('minapp.popup.goBack')} placement="bottom">
|
<Tooltip title={t('minapp.popup.goBack')} placement="bottom">
|
||||||
<ToolbarButton onClick={handleGoBack} $disabled={!canGoBack}>
|
<ToolbarButton
|
||||||
|
onClick={handleGoBack}
|
||||||
|
$disabled={!canGoBack}
|
||||||
|
aria-label={t('minapp.popup.goBack')}
|
||||||
|
aria-disabled={!canGoBack}>
|
||||||
<ArrowLeftOutlined />
|
<ArrowLeftOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title={t('minapp.popup.goForward')} placement="bottom">
|
<Tooltip title={t('minapp.popup.goForward')} placement="bottom">
|
||||||
<ToolbarButton onClick={handleGoForward} $disabled={!canGoForward}>
|
<ToolbarButton
|
||||||
|
onClick={handleGoForward}
|
||||||
|
$disabled={!canGoForward}
|
||||||
|
aria-label={t('minapp.popup.goForward')}
|
||||||
|
aria-disabled={!canGoForward}>
|
||||||
<ArrowRightOutlined />
|
<ArrowRightOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title={t('minapp.popup.refresh')} placement="bottom">
|
<Tooltip title={t('minapp.popup.refresh')} placement="bottom">
|
||||||
<ToolbarButton onClick={onReload}>
|
<ToolbarButton onClick={onReload} aria-label={t('minapp.popup.refresh')}>
|
||||||
<ReloadOutlined />
|
<ReloadOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -261,7 +269,7 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
|
|||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{canOpenExternalLink && (
|
{canOpenExternalLink && (
|
||||||
<Tooltip title={t('minapp.popup.openExternal')} placement="bottom">
|
<Tooltip title={t('minapp.popup.openExternal')} placement="bottom">
|
||||||
<ToolbarButton onClick={handleOpenLink}>
|
<ToolbarButton onClick={handleOpenLink} aria-label={t('minapp.popup.openExternal')}>
|
||||||
<ExportOutlined />
|
<ExportOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -271,7 +279,11 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={isPinned ? t('minapp.remove_from_launchpad') : t('minapp.add_to_launchpad')}
|
title={isPinned ? t('minapp.remove_from_launchpad') : t('minapp.add_to_launchpad')}
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<ToolbarButton onClick={handleTogglePin} $active={isPinned}>
|
<ToolbarButton
|
||||||
|
onClick={handleTogglePin}
|
||||||
|
$active={isPinned}
|
||||||
|
aria-label={isPinned ? t('minapp.remove_from_launchpad') : t('minapp.add_to_launchpad')}
|
||||||
|
aria-pressed={isPinned}>
|
||||||
<PushpinOutlined />
|
<PushpinOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -284,21 +296,29 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
|
|||||||
: t('minapp.popup.open_link_external_off')
|
: t('minapp.popup.open_link_external_off')
|
||||||
}
|
}
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<ToolbarButton onClick={handleToggleOpenExternal} $active={minappsOpenLinkExternal}>
|
<ToolbarButton
|
||||||
|
onClick={handleToggleOpenExternal}
|
||||||
|
$active={minappsOpenLinkExternal}
|
||||||
|
aria-label={
|
||||||
|
minappsOpenLinkExternal
|
||||||
|
? t('minapp.popup.open_link_external_on')
|
||||||
|
: t('minapp.popup.open_link_external_off')
|
||||||
|
}
|
||||||
|
aria-pressed={minappsOpenLinkExternal}>
|
||||||
<LinkOutlined />
|
<LinkOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{isDev && (
|
{isDev && (
|
||||||
<Tooltip title={t('minapp.popup.devtools')} placement="bottom">
|
<Tooltip title={t('minapp.popup.devtools')} placement="bottom">
|
||||||
<ToolbarButton onClick={onOpenDevTools}>
|
<ToolbarButton onClick={onOpenDevTools} aria-label={t('minapp.popup.devtools')}>
|
||||||
<CodeOutlined />
|
<CodeOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Tooltip title={t('minapp.popup.minimize')} placement="bottom">
|
<Tooltip title={t('minapp.popup.minimize')} placement="bottom">
|
||||||
<ToolbarButton onClick={handleMinimize}>
|
<ToolbarButton onClick={handleMinimize} aria-label={t('minapp.popup.minimize')}>
|
||||||
<MinusOutlined />
|
<MinusOutlined />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@ -72,8 +72,22 @@ const ActionIcons: FC<{
|
|||||||
(action: ActionItem) => {
|
(action: ActionItem) => {
|
||||||
const displayName = action.isBuiltIn ? t(action.name) : action.name
|
const displayName = action.isBuiltIn ? t(action.name) : action.name
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleAction(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionButton key={action.id} onClick={() => handleAction(action)} title={isCompact ? displayName : undefined}>
|
<ActionButton
|
||||||
|
key={action.id}
|
||||||
|
onClick={() => handleAction(action)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
title={isCompact ? displayName : undefined}
|
||||||
|
role="button"
|
||||||
|
aria-label={displayName}
|
||||||
|
tabIndex={0}>
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
{action.id === 'copy' ? (
|
{action.id === 'copy' ? (
|
||||||
renderCopyIcon()
|
renderCopyIcon()
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user