📢 fix: Resolved Screen Reader Issues with TooltipAnchor (#10580)

TooltipAnchor was automatically adding an `aria-describedby`
tag which often duplicated the labeling already present inside
of the anchor. E.g., the screen reader might say
"New Chat, New Chat, button" instead of just "New Chat, button."

I've removed the TooltipAnchor's automatic `aria-describedby` and
worked to make sure that anyone using TooltipAnchor properly defines
its labeling.
This commit is contained in:
Daniel Lew 2025-11-19 16:10:10 -06:00 committed by GitHub
parent 8b9afd5965
commit 014eb10662
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 49 additions and 19 deletions

View file

@ -59,7 +59,12 @@ export default function ArtifactVersion({
<TooltipAnchor <TooltipAnchor
description={localize('com_ui_change_version')} description={localize('com_ui_change_version')}
render={ render={
<Button size="icon" variant="ghost" asChild> <Button
size="icon"
variant="ghost"
asChild
aria-label={localize('com_ui_change_version')}
>
<MenuButton> <MenuButton>
<History <History
size={18} size={18}

View file

@ -166,6 +166,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
<TooltipAnchor <TooltipAnchor
className="absolute bottom-[27px] right-2" className="absolute bottom-[27px] right-2"
description={localize('com_ui_happy_birthday')} description={localize('com_ui_happy_birthday')}
aria-label={localize('com_ui_happy_birthday')}
> >
<BirthdayIcon /> <BirthdayIcon />
</TooltipAnchor> </TooltipAnchor>

View file

@ -180,6 +180,10 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm
} }
}, [isPromptOpen, zoom]); }, [isPromptOpen, zoom]);
const imageDetailsLabel = isPromptOpen
? localize('com_ui_hide_image_details')
: localize('com_ui_show_image_details');
return ( return (
<OGDialog open={isOpen} onOpenChange={onOpenChange}> <OGDialog open={isOpen} onOpenChange={onOpenChange}>
<OGDialogContent <OGDialogContent
@ -198,6 +202,7 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm
onClick={() => onOpenChange(false)} onClick={() => onOpenChange(false)}
variant="ghost" variant="ghost"
className="h-10 w-10 p-0 hover:bg-surface-hover" className="h-10 w-10 p-0 hover:bg-surface-hover"
aria-label={localize('com_ui_close')}
> >
<X className="size-7 sm:size-6" /> <X className="size-7 sm:size-6" />
</Button> </Button>
@ -208,7 +213,12 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm
<TooltipAnchor <TooltipAnchor
description={localize('com_ui_reset_zoom')} description={localize('com_ui_reset_zoom')}
render={ render={
<Button onClick={resetZoom} variant="ghost" className="h-10 w-10 p-0"> <Button
onClick={resetZoom}
variant="ghost"
className="h-10 w-10 p-0"
aria-label={localize('com_ui_reset_zoom')}
>
<RotateCcw className="size-6" /> <RotateCcw className="size-6" />
</Button> </Button>
} }
@ -217,22 +227,24 @@ export default function DialogImage({ isOpen, onOpenChange, src = '', downloadIm
<TooltipAnchor <TooltipAnchor
description={localize('com_ui_download')} description={localize('com_ui_download')}
render={ render={
<Button onClick={() => downloadImage()} variant="ghost" className="h-10 w-10 p-0"> <Button
onClick={() => downloadImage()}
variant="ghost"
className="h-10 w-10 p-0"
aria-label={localize('com_ui_download')}
>
<ArrowDownToLine className="size-6" /> <ArrowDownToLine className="size-6" />
</Button> </Button>
} }
/> />
<TooltipAnchor <TooltipAnchor
description={ description={imageDetailsLabel}
isPromptOpen
? localize('com_ui_hide_image_details')
: localize('com_ui_show_image_details')
}
render={ render={
<Button <Button
onClick={() => setIsPromptOpen(!isPromptOpen)} onClick={() => setIsPromptOpen(!isPromptOpen)}
variant="ghost" variant="ghost"
className="h-10 w-10 p-0" className="h-10 w-10 p-0"
aria-label={imageDetailsLabel}
> >
{isPromptOpen ? ( {isPromptOpen ? (
<PanelLeftOpen className="size-7 sm:size-6" /> <PanelLeftOpen className="size-7 sm:size-6" />

View file

@ -113,6 +113,8 @@ export default function SharedLinkButton({
} }
}; };
const qrCodeLabel = showQR ? localize('com_ui_hide_qr') : localize('com_ui_show_qr');
return ( return (
<> <>
<div className="flex gap-2"> <div className="flex gap-2">
@ -130,6 +132,7 @@ export default function SharedLinkButton({
<Button <Button
{...props} {...props}
onClick={() => updateSharedLink()} onClick={() => updateSharedLink()}
aria-label={localize('com_ui_refresh_link')}
variant="outline" variant="outline"
disabled={isUpdateLoading} disabled={isUpdateLoading}
> >
@ -143,9 +146,14 @@ export default function SharedLinkButton({
/> />
<TooltipAnchor <TooltipAnchor
description={showQR ? localize('com_ui_hide_qr') : localize('com_ui_show_qr')} description={qrCodeLabel}
render={(props) => ( render={(props) => (
<Button {...props} onClick={() => setShowQR(!showQR)} variant="outline"> <Button
{...props}
onClick={() => setShowQR(!showQR)}
variant="outline"
aria-label={qrCodeLabel}
>
<QrCode className="size-4" /> <QrCode className="size-4" />
</Button> </Button>
)} )}
@ -154,7 +162,12 @@ export default function SharedLinkButton({
<TooltipAnchor <TooltipAnchor
description={localize('com_ui_delete')} description={localize('com_ui_delete')}
render={(props) => ( render={(props) => (
<Button {...props} onClick={() => setShowDeleteDialog(true)} variant="destructive"> <Button
{...props}
onClick={() => setShowDeleteDialog(true)}
variant="destructive"
aria-label={localize('com_ui_delete')}
>
<Trash2 className="size-4" /> <Trash2 className="size-4" />
</Button> </Button>
)} )}

View file

@ -59,6 +59,10 @@ const AssistantConversationStarters: React.FC<AssistantConversationStartersProps
const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS; const hasReachedMax = field.value.length >= Constants.MAX_CONVO_STARTERS;
const addConversationStarterLabel = hasReachedMax
? localize('com_assistants_max_starters_reached')
: localize('com_ui_add');
return ( return (
<div className="relative"> <div className="relative">
<label className={labelClass} htmlFor="conversation_starters"> <label className={labelClass} htmlFor="conversation_starters">
@ -108,11 +112,8 @@ const AssistantConversationStarters: React.FC<AssistantConversationStartersProps
> >
<TooltipAnchor <TooltipAnchor
side="top" side="top"
description={ description={addConversationStarterLabel}
hasReachedMax aria-label={addConversationStarterLabel}
? localize('com_assistants_max_starters_reached')
: localize('com_ui_add')
}
className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" className="flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={handleAddStarter} onClick={handleAddStarter}
disabled={hasReachedMax} disabled={hasReachedMax}
@ -140,6 +141,7 @@ const AssistantConversationStarters: React.FC<AssistantConversationStartersProps
<TooltipAnchor <TooltipAnchor
side="top" side="top"
description={localize('com_ui_delete')} description={localize('com_ui_delete')}
aria-label={localize('com_ui_delete')}
className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover" className="absolute right-1 top-1 flex size-7 items-center justify-center rounded-lg transition-colors duration-200 hover:bg-surface-hover"
onClick={() => handleDeleteStarter(index)} onClick={() => handleDeleteStarter(index)}
> >

View file

@ -21,7 +21,6 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted); const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted);
const placement = Ariakit.useStoreState(tooltip, (state) => state.placement); const placement = Ariakit.useStoreState(tooltip, (state) => state.placement);
const id = useId();
const sanitizer = useMemo(() => { const sanitizer = useMemo(() => {
const instance = DOMPurify(); const instance = DOMPurify();
instance.addHook('afterSanitizeAttributes', (node) => { instance.addHook('afterSanitizeAttributes', (node) => {
@ -79,7 +78,6 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
{...props} {...props}
ref={ref} ref={ref}
role={role} role={role}
aria-describedby={id}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn('cursor-pointer', className)} className={cn('cursor-pointer', className)}
/> />
@ -89,7 +87,6 @@ export const TooltipAnchor = forwardRef<HTMLDivElement, TooltipAnchorProps>(func
gutter={4} gutter={4}
alwaysVisible alwaysVisible
className="tooltip" className="tooltip"
id={id}
render={ render={
<motion.div <motion.div
initial={{ opacity: 0, x, y }} initial={{ opacity: 0, x, y }}