diff --git a/.changeset/thirty-panthers-repeat.md b/.changeset/thirty-panthers-repeat.md new file mode 100644 index 000000000000..58227df25978 --- /dev/null +++ b/.changeset/thirty-panthers-repeat.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix some false positive in the audit logic of the dev toolbar diff --git a/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts b/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts index a5e6e5073ad5..15432e910538 100644 --- a/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts +++ b/packages/astro/src/runtime/client/dev-overlay/plugins/audit/a11y.ts @@ -38,6 +38,8 @@ const a11y_required_attributes = { const interactiveElements = ['button', 'details', 'embed', 'iframe', 'label', 'select', 'textarea']; +const labellableElements = ['input', 'meter', 'output', 'progress', 'select', 'textarea']; + const aria_non_interactive_roles = [ 'alert', 'alertdialog', @@ -217,7 +219,7 @@ export const a11y: AuditRuleWithSelector[] = [ code: 'a11y-aria-activedescendant-has-tabindex', title: 'Elements with attribute `aria-activedescendant` must be tabbable', message: - 'This element must either have an inherent `tabindex` or declare `tabindex` as an attribute.', + 'Element with the `aria-activedescendant` attribute must either have an inherent `tabindex` or declare `tabindex` as an attribute.', selector: '[aria-activedescendant]', match(element) { if (!(element as HTMLElement).tabIndex && !element.hasAttribute('tabindex')) return true; @@ -280,14 +282,20 @@ export const a11y: AuditRuleWithSelector[] = [ selector: 'a[href]:is([href=""], [href="#"], [href^="javascript:" i])', }, { - code: 'a11y-label-has-associated-control', - title: '`label` tag should have an associated control and a text content.', + code: 'a11y-invalid-label', + title: '`label` element should have an associated control and a text content.', message: - 'The `label` tag must be associated with a control using either `for` or having a nested input. Additionally, the `label` tag must have text content.', - selector: 'label:not([for])', - match(element) { - const inputChild = element.querySelector('input'); - if (!inputChild?.textContent) return true; + 'The `label` element must be associated with a control either by using the `for` attribute or by containing a nested form element. Additionally, the `label` element must have text content.', + selector: 'label', + match(element: HTMLLabelElement) { + // Label must be associated with a control, either using `for` or having a nested valid element + const hasFor = element.hasAttribute('for'); + 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; }, }, { @@ -347,8 +355,10 @@ export const a11y: AuditRuleWithSelector[] = [ title: 'Missing content on element important for accessibility', message: 'Headings and anchors must have content to be accessible.', selector: a11y_required_content.join(','), - match(element) { - if (!element.textContent) return true; + match(element: HTMLElement) { + // innerText is used to ignore hidden text + const innerText = element.innerText.trim(); + if (innerText === '') return true; }, }, {