From 23f9446a5666f249066c18cb9f6a7a4e261eb090 Mon Sep 17 00:00:00 2001
From: fkatsuhiro <113022468+fkatsuhiro@users.noreply.github.com>
Date: Tue, 30 Jun 2026 18:50:58 +0900
Subject: [PATCH 01/12] FIx: inverted logic in a11y-no-redundant-roles rule
(#17067)
---
.changeset/loose-chefs-stare.md | 5 ++
packages/astro/e2e/dev-toolbar-audits.test.ts | 47 ++++++++++++
.../pages/a11y-redundant-roles-valid.astro | 21 ++++++
.../src/pages/a11y-redundant-roles.astro | 18 +++++
.../dev-toolbar/apps/audit/rules/a11y.ts | 74 ++++++++++++++++---
5 files changed, 155 insertions(+), 10 deletions(-)
create mode 100644 .changeset/loose-chefs-stare.md
create mode 100644 packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles-valid.astro
create mode 100644 packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles.astro
diff --git a/.changeset/loose-chefs-stare.md b/.changeset/loose-chefs-stare.md
new file mode 100644
index 000000000000..ef1ed387cf9e
--- /dev/null
+++ b/.changeset/loose-chefs-stare.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Fixed a bug where the development toolbar did not output a warning even though the implicit ARIA role and the manually specified role were duplicated.
diff --git a/packages/astro/e2e/dev-toolbar-audits.test.ts b/packages/astro/e2e/dev-toolbar-audits.test.ts
index b8690f0e9ed1..bbd2431193b9 100644
--- a/packages/astro/e2e/dev-toolbar-audits.test.ts
+++ b/packages/astro/e2e/dev-toolbar-audits.test.ts
@@ -277,4 +277,51 @@ test.describe('Dev Toolbar - Audits', () => {
await expect(missingContentHighlights).toHaveCount(1);
});
+
+ test('warns about elements whose explicit role matches their implicit ARIA role', async ({
+ page,
+ astro,
+ }) => {
+ await page.goto(astro.resolveUrl('/a11y-redundant-roles'));
+
+ const toolbar = page.locator('astro-dev-toolbar');
+ const appButton = toolbar.locator('button[data-app-id="astro:audit"]');
+ await appButton.click();
+
+ const auditCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="astro:audit"]');
+ const highlights = auditCanvas.locator(
+ 'astro-dev-toolbar-highlight[data-audit-code="a11y-no-redundant-roles"]',
+ );
+
+ // nav[role=navigation], main[role=main], button[role=button], h1[role=heading],
+ // input[type=checkbox][role=checkbox], input[type=submit][role=button],
+ // a[href][role=link], input[role=textbox],
+ // img[alt="photo"][role=image], aside[role=complementary],
+ // article > aside[aria-label][role=complementary], section[aria-label][role=region]
+ await expect(highlights).toHaveCount(12);
+ });
+
+ test('does not warn about ul/ol/li exceptions, conditional implicit roles, or non-redundant roles', async ({
+ page,
+ astro,
+ }) => {
+ await page.goto(astro.resolveUrl('/a11y-redundant-roles-valid'));
+
+ const toolbar = page.locator('astro-dev-toolbar');
+ const appButton = toolbar.locator('button[data-app-id="astro:audit"]');
+ await appButton.click();
+
+ const auditCanvas = toolbar.locator('astro-dev-toolbar-app-canvas[data-app-id="astro:audit"]');
+ const highlights = auditCanvas.locator(
+ 'astro-dev-toolbar-highlight[data-audit-code="a11y-no-redundant-roles"]',
+ );
+
+ // ul/ol[role=list], li[role=listitem]: exempt (CSS list-style:none workaround)
+ // nav[role=banner], button[role=link], input[type=text][role=combobox]: non-implicit roles
+ // a (no href)[role=link]: no implicit role without href
+ // img[alt=""][role=img]: decorative image has no implicit role
+ // article > aside (no name)[role=complementary]: nested aside without accessible name has no implicit role
+ // section (no name)[role=region]: section without accessible name has no implicit role
+ await expect(highlights).toHaveCount(0);
+ });
});
diff --git a/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles-valid.astro b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles-valid.astro
new file mode 100644
index 000000000000..8322fe7353fc
--- /dev/null
+++ b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles-valid.astro
@@ -0,0 +1,21 @@
+---
+---
+
+
+
+
+ - Item
+
+
+
+
+
+
+
+
+Placeholder Link
+
+
+
diff --git a/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles.astro b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles.astro
new file mode 100644
index 000000000000..9182298dd590
--- /dev/null
+++ b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-redundant-roles.astro
@@ -0,0 +1,18 @@
+---
+---
+
+
+
+Main
+
+Heading
+
+
+Link
+
+
+
+
+
+
+
diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/rules/a11y.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/rules/a11y.ts
index 46a73290ed5a..02837e2277d8 100644
--- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/rules/a11y.ts
+++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/rules/a11y.ts
@@ -226,6 +226,41 @@ function isInteractive(element: Element): boolean {
return true;
}
+function hasAccessibleName(element: Element): boolean {
+ return element.hasAttribute('aria-label') || element.hasAttribute('aria-labelledby');
+}
+
+// Returns the effective implicit role, or null if no implicit role applies.
+const conditional_implicit_roles = new Map string | null>([
+ ['a', (el) => (el.hasAttribute('href') ? 'link' : null)],
+ [
+ 'aside',
+ (el) => {
+ //