fix(site): add aria-label to icon-only Back navigation links (#26222)

## Summary

Two icon-only "Back" navigation links in the topbars have no accessible
name. The visible tooltip text is wired via Radix's `aria-describedby`,
which provides a *description*, not an accessible **name**, so screen
readers announce them as just "link" with no purpose.

This adds an `aria-label` to each link matching the visible tooltip
text, following the existing `aria-label` convention already used in
these same files (e.g. `aria-label="Create File"`, `aria-label="Daily
usage"`).

## WCAG 2.1 criteria addressed

- **SC 4.1.2 Name, Role, Value** (Level A)
- **SC 2.4.4 Link Purpose (In Context)** (Level A)

## Changes

| File | Element | Added |
|---|---|---|
| `site/src/pages/WorkspacePage/WorkspaceTopbar.tsx` |
Back-to-workspaces chevron link | `aria-label="Back to workspaces"` |
| `site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx` |
Back-to-template chevron link | `aria-label="Back to the template"` |

No visual change. No behavior change for sighted users. `aria-label` is
safe here because the elements have no visible text content (only an
icon), so it is not overriding a visible name.

<details>
<summary>Why not rely on the existing Tooltip?</summary>

Radix UI's `Tooltip` wires `aria-describedby` from the trigger to the
tooltip content. `aria-describedby` provides an *accessible
description*, not an *accessible name*. WCAG 4.1.2 requires interactive
controls to have a programmatically determinable name, and screen
readers do not consistently announce descriptions, especially when no
name is present. Adding `aria-label` gives the link a stable,
programmatic name that exactly matches the visible tooltip text.

</details>

---

_Created by Coder Agents on behalf of @tracyjohnsonux._
This commit is contained in:
TJ
2026-08-12 17:23:37 +00:00
committed by GitHub
parent 52bd05adb4
commit b145142404
4 changed files with 24 additions and 4 deletions
@@ -47,7 +47,17 @@ const meta: Meta<typeof TemplateVersionEditor> = {
export default meta;
type Story = StoryObj<typeof TemplateVersionEditor>;
export const Example: Story = {};
export const Example: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const backLink = canvas.getByRole("link", { name: "Back to the template" });
await expect(backLink).toBeVisible();
await expect(backLink).toHaveAttribute(
"aria-label",
"Back to the template",
);
},
};
export const UndefinedLogs: Story = {
args: {
@@ -223,7 +223,10 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
<Tooltip>
<TooltipTrigger asChild>
<TopbarIconButton asChild>
<RouterLink to={templateLink}>
<RouterLink
to={templateLink}
aria-label="Back to the template"
>
<ChevronLeftIcon />
</RouterLink>
</TopbarIconButton>
@@ -54,7 +54,14 @@ const meta: Meta<typeof WorkspaceTopbar> = {
export default meta;
type Story = StoryObj<typeof WorkspaceTopbar>;
export const Example: Story = {};
export const Example: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const backLink = canvas.getByRole("link", { name: "Back to workspaces" });
await expect(backLink).toBeVisible();
await expect(backLink).toHaveAttribute("aria-label", "Back to workspaces");
},
};
export const Outdated: Story = {
args: {
@@ -121,7 +121,7 @@ export const WorkspaceTopbar: FC<WorkspaceTopbarProps> = ({
<Tooltip>
<TooltipTrigger asChild>
<TopbarIconButton asChild>
<RouterLink to="/workspaces">
<RouterLink to="/workspaces" aria-label="Back to workspaces">
<ChevronLeftIcon className="size-icon-sm" />
</RouterLink>
</TopbarIconButton>