diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index 735562d627..f9dad93a75 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -45,6 +45,20 @@ const a11y_required_attributes = { object: ['title', 'aria-label', 'aria-labelledby'] }; +const a11y_required_role_props = { + checkbox: ['aria-checked'], + combobox: ['aria-controls', 'aria-expanded'], + heading: ['aria-level'], + menuitemcheckbox: ['aria-checked'], + menuitemradio: ['aria-checked'], + meter: ['aria-valuemax', 'aria-valuemin', 'aria-valuenow'], + option: ['aria-selected'], + radio: ['aria-checked'], + scrollbar: ['aria-controls', 'aria-valuenow'], + slider: ['aria-valuenow'], + switch: ['aria-checked'] +}; + const a11y_distracting_elements = new Set([ 'blink', 'marquee' @@ -407,9 +421,9 @@ export default class Element extends Node { } validate_attributes_a11y() { - const { component } = this; + const { component, attributes } = this; - this.attributes.forEach(attribute => { + attributes.forEach(attribute => { if (attribute.is_spread) return; const name = attribute.name.toLowerCase(); @@ -462,6 +476,21 @@ export default class Element extends Node { component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value)); } } + + // @ts-ignore + const required_role_props = a11y_required_role_props[value]; + + // role-has-required-aria-props + if (required_role_props) { + const has_missing_props = required_role_props.some(prop => !attributes.find(a => a.name === prop)); + + if (has_missing_props) { + component.warn(attribute, { + code: 'a11y-role-has-required-aria-props', + message: `A11y: Elements with the ARIA role "${value}" must have the following attributes defined: ${String(required_role_props)}` + }); + } + } } // no-access-key