Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/loud-items-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes a false positive triggered by the Astro Toolbar, which flagged content hidden by the `<details>` element as non-a11y complaint.
22 changes: 22 additions & 0 deletions packages/astro/e2e/dev-toolbar-audits.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
// Test fixture for issue #15558
// Content inside closed details elements should not be flagged as missing content
---

<h1>Testing Details Element</h1>

<!-- Closed details with heading - should NOT be flagged -->
<details>
<summary>Click to expand</summary>
<h2>Hidden Heading Inside Details</h2>
<p>This heading has content but is hidden inside a closed details element.</p>
</details>

<!-- Closed details with anchor - should NOT be flagged -->
<details>
<summary>More information</summary>
<a href="#test">Hidden Link Inside Details</a>
<p>This link has content but is hidden inside a closed details element.</p>
</details>

<!-- Open details with heading - should NOT be flagged -->
<details open>
<summary>Already expanded</summary>
<h2>Visible Heading Inside Open Details</h2>
<p>This is visible content.</p>
</details>

<!-- Multiple nested elements in closed details - should NOT be flagged -->
<details>
<summary>Complex content</summary>
<div>
<h3>Nested Heading</h3>
<a href="#nested">Nested Link</a>
</div>
</details>

<!-- Empty heading - SHOULD be flagged -->
<h2></h2>

<!-- Empty link - SHOULD be flagged -->
<a href="#empty"></a>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
},
{
Expand Down Expand Up @@ -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 <title></title>.',
selector: a11y_required_content.join(','),
match(element: HTMLElement) {
// innerText is used to ignore hidden text
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we purposely used innerText for this reason and it used to be textContent before?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is "hidden text"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, it was something with if you had elements nested in a certain common way, you can see the original PR that changed this here: #9483

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately there aren't tests for possible regressions :/

Copy link
Contributor

@matthewp matthewp Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, textContent collects children element text too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ematipico use const text = element.firstChild?.nodeType === 3 ? element.firstChild.nodeValue.trim() : ""; instead

const innerText = element.innerText?.trim();
if (innerText && innerText !== '') return false;
// textContent is used instead of innerText so that content inside
// closed <details> 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();
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -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;
}
}

Expand Down
Loading