mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user