diff --git a/.changeset/loud-items-watch.md b/.changeset/loud-items-watch.md new file mode 100644 index 000000000000..6d179e2a2664 --- /dev/null +++ b/.changeset/loud-items-watch.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fixes a false positive triggered by the Astro Toolbar, which flagged content hidden by the `
` element as non-a11y complaint. diff --git a/packages/astro/e2e/dev-toolbar-audits.test.js b/packages/astro/e2e/dev-toolbar-audits.test.js index 34f3adb19cbd..766857129931 100644 --- a/packages/astro/e2e/dev-toolbar-audits.test.js +++ b/packages/astro/e2e/dev-toolbar-audits.test.js @@ -259,4 +259,26 @@ test.describe('Dev Toolbar - Audits', () => { const count = await auditHighlights.count(); expect(count).toEqual(0); }); + + test('does not warn about content inside closed details elements', async ({ page, astro }) => { + await page.goto(astro.resolveUrl('/a11y-details')); + + 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 auditHighlights = auditCanvas.locator('astro-dev-toolbar-highlight'); + + // Should only flag the 2 empty elements (empty h2 and empty anchor) + // Should NOT flag the headings and anchors inside closed details elements + const count = await auditHighlights.count(); + expect(count).toEqual(2); + + // Verify that both flagged elements have the a11y-missing-content code + for (const auditHighlight of await auditHighlights.all()) { + const auditCode = await auditHighlight.getAttribute('data-audit-code'); + expect(auditCode).toBe('a11y-missing-content'); + } + }); }); diff --git a/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-details.astro b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-details.astro new file mode 100644 index 000000000000..64c0803f4655 --- /dev/null +++ b/packages/astro/e2e/fixtures/dev-toolbar/src/pages/a11y-details.astro @@ -0,0 +1,42 @@ +--- +// Test fixture for issue #15558 +// Content inside closed details elements should not be flagged as missing content +--- + +

Testing Details Element

+ + +
+ Click to expand +

Hidden Heading Inside Details

+

This heading has content but is hidden inside a closed details element.

+
+ + +
+ More information + Hidden Link Inside Details +

This link has content but is hidden inside a closed details element.

+
+ + +
+ Already expanded +

Visible Heading Inside Open Details

+

This is visible content.

+
+ + +
+ Complex content +
+

Nested Heading

+ Nested 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 a8dd54555788..ebe3dbde1c2f 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 @@ -301,9 +301,9 @@ export const a11y: AuditRuleWithSelector[] = [ const nestedLabellableElement = element.querySelector(`${labellableElements.join(', ')}`); if (!hasFor && !nestedLabellableElement) return true; - // Label must have text content, using innerText to ignore hidden text - const innerText = element.innerText.trim(); - if (innerText === '') return true; + // Label must have text content + const textContent = element.textContent?.trim() ?? ''; + if (textContent === '') return true; }, }, { @@ -367,9 +367,10 @@ export const a11y: AuditRuleWithSelector[] = [ 'Headings and anchors must have an accessible name, which can come from: inner text, aria-label, aria-labelledby, an img with alt property, or an svg with a tag .', selector: a11y_required_content.join(','), match(element: HTMLElement) { - // innerText is used to ignore hidden text - const innerText = element.innerText?.trim(); - if (innerText && innerText !== '') return false; + // textContent is used instead of innerText so that content inside + // closed
elements is not falsely reported as missing + const textContent = element.textContent?.trim(); + if (textContent && textContent !== '') return false; // Check for aria-label const ariaLabel = element.getAttribute('aria-label')?.trim(); @@ -381,7 +382,8 @@ export const a11y: AuditRuleWithSelector[] = [ const ids = ariaLabelledby.split(' '); for (const id of ids) { const referencedElement = document.getElementById(id); - if (referencedElement && referencedElement.innerText.trim() !== '') return false; + if (referencedElement && (referencedElement.textContent?.trim() ?? '') !== '') + return false; } } @@ -417,7 +419,8 @@ export const a11y: AuditRuleWithSelector[] = [ const ids = inputAriaLabelledby.split(' '); for (const id of ids) { const referencedElement = document.getElementById(id); - if (referencedElement && referencedElement.innerText.trim() !== '') return false; + if (referencedElement && (referencedElement.textContent?.trim() ?? '') !== '') + return false; } }