diff --git a/.changeset/eight-flowers-remain.md b/.changeset/eight-flowers-remain.md new file mode 100644 index 000000000000..9ef571e10e31 --- /dev/null +++ b/.changeset/eight-flowers-remain.md @@ -0,0 +1,5 @@ +--- +"astro": patch +--- + +Fixes false positives in the dev overlay audit when multiple `role` values exist. diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts index 2b4943908455..48240317e129 100644 --- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts +++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/a11y.ts @@ -29,6 +29,8 @@ import { aria, roles } from 'aria-query'; import { AXObjectRoles, elementAXObjects } from 'axobject-query'; import type { AuditRuleWithSelector } from './index.js'; +const WHITESPACE_REGEX = /\s+/; + const a11y_required_attributes = { a: ['href'], area: ['alt', 'aria-label', 'aria-labelledby'], @@ -496,13 +498,17 @@ export const a11y: AuditRuleWithSelector[] = [ if (is_semantic_role_element(role, element.localName, getAttributeObject(element))) { return; } - const { requiredProps } = roles.get(role)!; - const required_role_props = Object.keys(requiredProps); - const missingProps = required_role_props.filter((prop) => !element.hasAttribute(prop)); - if (missingProps.length > 0) { - (element as any).__astro_role = role; - (element as any).__astro_missing_attributes = missingProps; - return true; + + const elementRoles = role.split(WHITESPACE_REGEX) as ARIARoleDefinitionKey[]; + for(const elementRole of elementRoles) { + const { requiredProps } = roles.get(elementRole)!; + const required_role_props = Object.keys(requiredProps); + const missingProps = required_role_props.filter((prop) => !element.hasAttribute(prop)); + if (missingProps.length > 0) { + (element as any).__astro_role = elementRole; + (element as any).__astro_missing_attributes = missingProps; + return true; + } } }, }, @@ -522,16 +528,20 @@ export const a11y: AuditRuleWithSelector[] = [ match(element) { const role = getRole(element); if (!role) return false; - const { props } = roles.get(role)!; - const attributes = getAttributeObject(element); - const unsupportedAttributes = aria.keys().filter((attribute) => !(attribute in props)); - const invalidAttributes: string[] = Object.keys(attributes).filter( - (key) => key.startsWith('aria-') && unsupportedAttributes.includes(key as any) - ); - if (invalidAttributes.length > 0) { - (element as any).__astro_role = role; - (element as any).__astro_unsupported_attributes = invalidAttributes; - return true; + + const elementRoles = role.split(WHITESPACE_REGEX) as ARIARoleDefinitionKey[]; + for(const elementRole of elementRoles) { + const { props } = roles.get(elementRole)!; + const attributes = getAttributeObject(element); + const unsupportedAttributes = aria.keys().filter((attribute) => !(attribute in props)); + const invalidAttributes: string[] = Object.keys(attributes).filter( + (key) => key.startsWith('aria-') && unsupportedAttributes.includes(key as any) + ); + if (invalidAttributes.length > 0) { + (element as any).__astro_role = elementRole; + (element as any).__astro_unsupported_attributes = invalidAttributes; + return true; + } } }, },