🔇 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

@ -92,6 +92,7 @@ export default function Badge({
'@container-[600px]:h-4 @container-[600px]:w-4 relative h-5 w-5',
!label && 'mx-auto',
)}
aria-hidden="true"
/>
)}
<span className="@container-[600px]:inline relative hidden">{label}</span>

View file

@ -68,7 +68,7 @@ const DialogContent = React.forwardRef<
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-6 top-[1.6rem] rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-gray-100 dark:focus:ring-white dark:focus:ring-offset-gray-700 dark:data-[state=open]:bg-gray-800">
<X className="h-5 w-5 text-black dark:text-white" />
<X className="h-5 w-5 text-black dark:text-white" aria-hidden="true" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}

View file

@ -80,7 +80,7 @@ const DialogContent = React.forwardRef<
{children}
{showCloseButton && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-ring-primary ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<X className="h-4 w-4" aria-hidden="true" />
{/* eslint-disable-next-line i18next/no-literal-string */}
<span className="sr-only">Close</span>
</DialogPrimitive.Close>

View file

@ -63,7 +63,7 @@ const PaginationPrevious = ({
className={cn('gap-1 pl-2.5', className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<ChevronLeft className="h-4 w-4" aria-hidden="true" />
<span>Previous</span>
</PaginationLink>
);
@ -77,7 +77,7 @@ const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof Pag
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
<ChevronRight className="h-4 w-4" aria-hidden="true" />
</PaginationLink>
);
PaginationNext.displayName = 'PaginationNext';
@ -88,7 +88,7 @@ const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'
className={cn('flex h-9 w-9 items-center justify-center', className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<MoreHorizontal className="h-4 w-4" aria-hidden="true" />
<span className="sr-only">More pages</span>
</span>
);

View file

@ -102,7 +102,11 @@ const Radio = memo(function Radio({
currentValue === option.value ? 'text-foreground' : 'text-foreground'
} ${disabled ? 'cursor-not-allowed opacity-50' : ''} ${fullWidth ? 'flex-1' : ''}`}
>
{option.icon && <span className="flex-shrink-0">{option.icon}</span>}
{option.icon && (
<span className="flex-shrink-0" aria-hidden="true">
{option.icon}
</span>
)}
<span className="whitespace-nowrap">{option.label}</span>
</button>
))}

View file

@ -38,7 +38,7 @@ const TagPrimitiveRoot = React.forwardRef<HTMLDivElement, TagProps>(
className="rounded-full bg-green-600/50"
aria-label={`Remove ${label}`}
>
<X className="m-[1.5px] p-1" />
<X className="m-[1.5px] p-1" aria-hidden="true" />
</button>
)}
</div>

View file

@ -12,9 +12,9 @@ type ThemeType = 'system' | 'dark' | 'light';
const Theme = ({ theme, onChange }: { theme: string; onChange: (value: string) => void }) => {
const themeIcons: Record<ThemeType, JSX.Element> = {
system: <Monitor />,
dark: <Moon color="white" />,
light: <Sun />,
system: <Monitor aria-hidden="true" />,
dark: <Moon color="white" aria-hidden="true" />,
light: <Sun aria-hidden="true" />,
};
const nextTheme = isDark(theme) ? 'light' : 'dark';