🔇 fix: Hide Button Icons from Screen Readers (#10776)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled

If you've got a screen reader that is reading out the whole page,
each icon button (i.e., `<button><SVG></button>`) will have both
the button's aria-label read out as well as the title from the
SVG (which is usually just "image").

Since we are pretty good about setting aria-labels, we should instead
use `aria-hidden="true"` on these images, since they are not useful
to be read out.

I don't consider this a comprehensive review of all icons in the app,
but I knocked out all the low hanging fruit in this commit.
This commit is contained in:
Daniel Lew 2025-12-11 15:35:17 -06:00 committed by GitHub
parent b288d81f5a
commit 1143f73f59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
175 changed files with 340 additions and 183 deletions

View file

@ -93,7 +93,7 @@ function AccountSettings() {
value="logout"
className="select-item text-sm"
>
<LogOut className="icon-md" />
<LogOut className="icon-md" aria-hidden="true" />
{localize('com_nav_log_out')}
</Select.SelectItem>
</Select.SelectPopover>

View file

@ -58,7 +58,7 @@ export default function AgentMarketplaceButton({
className="rounded-full border-none bg-transparent p-2 hover:bg-surface-hover md:rounded-xl"
onClick={handleAgentMarketplace}
>
<LayoutGrid className="icon-lg text-text-primary" />
<LayoutGrid className="icon-lg text-text-primary" aria-hidden="true" />
</Button>
}
/>

View file

@ -144,7 +144,7 @@ const SearchBar = forwardRef((props: SearchBarProps, ref: React.Ref<HTMLDivEleme
tabIndex={showClearIcon ? 0 : -1}
disabled={!showClearIcon}
>
<X className="h-5 w-5 cursor-pointer" />
<X className="h-5 w-5 cursor-pointer" aria-hidden="true" />
</button>
</div>
);

View file

@ -80,17 +80,17 @@ export default function Settings({ open, onOpenChange }: TDialogProps) {
},
{
value: SettingsTabValues.CHAT,
icon: <MessageSquare className="icon-sm" />,
icon: <MessageSquare className="icon-sm" aria-hidden="true" />,
label: 'com_nav_setting_chat',
},
{
value: SettingsTabValues.COMMANDS,
icon: <Command className="icon-sm" />,
icon: <Command className="icon-sm" aria-hidden="true" />,
label: 'com_nav_commands',
},
{
value: SettingsTabValues.SPEECH,
icon: <SpeechIcon className="icon-sm" />,
icon: <SpeechIcon className="icon-sm" aria-hidden="true" />,
label: 'com_nav_setting_speech',
},
...(hasAnyPersonalizationFeature

View file

@ -170,7 +170,7 @@ function Avatar() {
<span>{localize('com_nav_profile_picture')}</span>
<OGDialogTrigger asChild>
<Button variant="outline">
<FileImage className="mr-2 flex w-[22px] items-center" />
<FileImage className="mr-2 flex w-[22px] items-center" aria-hidden="true" />
<span>{localize('com_nav_change_picture')}</span>
</Button>
</OGDialogTrigger>
@ -211,7 +211,7 @@ function Avatar() {
{!isDragging && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center opacity-0 transition-opacity hover:opacity-100">
<div className="rounded-full bg-black/50 p-2">
<Move className="h-6 w-6 text-white" />
<Move className="h-6 w-6 text-white" aria-hidden="true" />
</div>
</div>
)}
@ -236,7 +236,7 @@ function Avatar() {
aria-label={localize('com_ui_zoom_out')}
className="shrink-0"
>
<ZoomOut className="h-4 w-4" />
<ZoomOut className="h-4 w-4" aria-hidden="true" />
</Button>
<Slider
id="zoom-slider"
@ -257,7 +257,7 @@ function Avatar() {
aria-label={localize('com_ui_zoom_in')}
className="shrink-0"
>
<ZoomIn className="h-4 w-4" />
<ZoomIn className="h-4 w-4" aria-hidden="true" />
</Button>
</div>
</div>
@ -270,7 +270,7 @@ function Avatar() {
className="flex items-center space-x-2"
aria-label={localize('com_ui_rotate_90')}
>
<RotateCw className="h-4 w-4" />
<RotateCw className="h-4 w-4" aria-hidden="true" />
<span className="text-sm">{localize('com_ui_rotate')}</span>
</Button>
<Button
@ -280,7 +280,7 @@ function Avatar() {
className="flex items-center space-x-2"
aria-label={localize('com_ui_reset_adjustments')}
>
<X className="h-4 w-4" />
<X className="h-4 w-4" aria-hidden="true" />
<span className="text-sm">{localize('com_ui_reset')}</span>
</Button>
</div>
@ -312,7 +312,7 @@ function Avatar() {
{isUploading ? (
<Spinner className="icon-sm mr-2" />
) : (
<Upload className="mr-2 h-4 w-4" />
<Upload className="mr-2 h-4 w-4" aria-hidden="true" />
)}
{localize('com_ui_upload')}
</Button>

View file

@ -171,7 +171,7 @@ const BackupCodesItem: React.FC = () => {
{isLoading ? (
<Spinner className="mr-2" />
) : (
<RefreshCcw className="mr-2 h-4 w-4" />
<RefreshCcw className="mr-2 h-4 w-4" aria-hidden="true" />
)}
{isLoading
? localize('com_ui_regenerating')

View file

@ -123,12 +123,12 @@ const renderDeleteButton = (
<>
{isLocked ? (
<>
<LockIcon className="size-5" />
<LockIcon className="size-5" aria-hidden="true" />
<span className="ml-2">{localize('com_ui_locked')}</span>
</>
) : (
<>
<Trash className="size-5" />
<Trash className="size-5" aria-hidden="true" />
<span className="ml-2">{localize('com_nav_delete_account_button')}</span>
</>
)}

View file

@ -216,7 +216,7 @@ const TwoFactorAuthentication: React.FC = () => {
>
<OGDialogHeader>
<OGDialogTitle className="mb-2 flex items-center gap-3 text-2xl font-bold">
<SmartphoneIcon className="h-6 w-6 text-primary" />
<SmartphoneIcon className="h-6 w-6 text-primary" aria-hidden="true" />
{user?.twoFactorEnabled
? localize('com_ui_2fa_disable')
: localize('com_ui_2fa_setup')}

View file

@ -48,7 +48,7 @@ export const BackupPhase: React.FC<BackupPhaseProps> = ({
</div>
<div className="flex gap-4">
<Button variant="outline" onClick={onDownload} className="flex-1 gap-2">
<Download className="h-4 w-4" />
<Download className="h-4 w-4" aria-hidden="true" />
<span className="hidden sm:inline">{localize('com_ui_download_backup')}</span>
</Button>
<Button onClick={onNext} disabled={!downloaded} className="flex-1">

View file

@ -53,7 +53,11 @@ export const QRPhase: React.FC<QRPhaseProps> = ({ secret, otpauthUrl, onNext })
onClick={handleCopy}
className={cn('h-auto shrink-0', isCopying ? 'cursor-default' : '')}
>
{isCopying ? <Check className="size-4" /> : <Copy className="size-4" />}
{isCopying ? (
<Check className="size-4" aria-hidden="true" />
) : (
<Copy className="size-4" aria-hidden="true" />
)}
</Button>
</div>
</div>

View file

@ -33,7 +33,11 @@ export const SetupPhase: React.FC<SetupPhaseProps> = ({ isGenerating, onGenerate
className="flex w-full"
disabled={isGenerating}
>
{isGenerating ? <Spinner className="size-5" /> : <QrCode className="size-5" />}
{isGenerating ? (
<Spinner className="size-5" />
) : (
<QrCode className="size-5" aria-hidden="true" />
)}
{isGenerating ? localize('com_ui_generating') : localize('com_ui_generate_qrcode')}
</Button>
</div>

View file

@ -55,7 +55,7 @@ const DangerButton = (props: TDangerButtonProps, ref: ForwardedRef<HTMLButtonEle
id={`${id}-text`}
data-testid={dataTestIdConfirm}
>
{renderMutation(<CheckIcon className="h-5 w-5" />)}
{renderMutation(<CheckIcon className="h-5 w-5" aria-hidden="true" />)}
{mutation && mutation.isLoading ? null : localize(confirmActionTextCode)}
</div>
) : (

View file

@ -120,7 +120,7 @@ function ImportConversations() {
{isUploading ? (
<Spinner className="mr-1 w-4" />
) : (
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" />
<Import className="mr-1 flex h-4 w-4 items-center stroke-1" aria-hidden="true" />
)}
<span>{localize('com_ui_import')}</span>
</Button>

View file

@ -234,7 +234,7 @@ export default function ArchivedChatsTable({
{unarchiveMutation.isLoading ? (
<Spinner />
) : (
<ArchiveRestore className="size-4" />
<ArchiveRestore className="size-4" aria-hidden="true" />
)}
</Button>
}
@ -251,7 +251,7 @@ export default function ArchivedChatsTable({
}}
title={localize('com_ui_delete')}
>
<TrashIcon className="size-4" />
<TrashIcon className="size-4" aria-hidden="true" />
</Button>
}
/>

View file

@ -167,7 +167,7 @@ function Speech() {
value="simple"
style={{ userSelect: 'none' }}
>
<Lightbulb />
<Lightbulb aria-hidden="true" />
{localize('com_ui_simple')}
</Tabs.Trigger>
<Tabs.Trigger
@ -180,7 +180,7 @@ function Speech() {
value="advanced"
style={{ userSelect: 'none' }}
>
<Cog />
<Cog aria-hidden="true" />
{localize('com_ui_advanced')}
</Tabs.Trigger>
</Tabs.List>