Skip to content

Commit

Permalink
add aria-proptype validation to compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
davemo committed May 10, 2021
1 parent a1600ff commit c6d6945
Show file tree
Hide file tree
Showing 11 changed files with 997 additions and 0 deletions.
158 changes: 158 additions & 0 deletions src/compiler/compile/nodes/Element.ts
Expand Up @@ -26,6 +26,154 @@ const aria_attribute_set = new Set(aria_attributes);
const aria_roles = 'alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic graphics-document graphics-object graphics-symbol grid gridcell group heading img link list listbox listitem log main marquee math meter menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status strong subscript superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem'.split(' ');
const aria_role_set = new Set(aria_roles);

const aria_attribute_value_type_map = new Map([
// global attributes
// https://www.w3.org/TR/wai-aria/#global_states
['current', {
type: 'token',
valid_values: 'page step location date time true false'.split(' ')
}],
['disabled', { type: 'boolean'}],
['haspopup', {
type: 'token',
valid_values: 'false true menu listbox tree grid dialog'.split(' ')
}],
['hidden', { type: 'boolean_or_undefined' }],
['invalid', {
type: 'token',
valid_values: 'grammar false spelling true'.split(' ')
}],
['keyshortcuts', { type: 'string' }],
['label', { type: 'string' }],
['roledescription', { type: 'string' }],

// widget attributes
// https://www.w3.org/TR/wai-aria/#attrs_widgets
['autocomplete', {
type: 'token',
valid_values: 'inline list both none'.split(' ')
}],
['checked', { type: 'tristate' }],
['expanded', { type: 'boolean_or_undefined' }],
['level', { type: 'integer' }],
['modal', { type: 'boolean' }],
['multiline', { type: 'boolean' }],
['multiselectable', { type: 'boolean' }],
['orientation', {
type: 'token',
valid_values: 'vertical undefined horizontal'.split(' ')
}],
['placeholder', { type: 'string' }],
['pressed', { type: 'tristate' }],
['readonly', { type: 'boolean' }],
['required', { type: 'boolean' }],
['selected', { type: 'boolean_or_undefined' }],
['sort', {
type: 'token',
valid_values: 'ascending descending none other'.split(' ')
}],
['valuemax', { type: 'number' }],
['valuemin', { type: 'number' }],
['valuenow', { type: 'number' }],
['valuetext', { type: 'string' }],

// live region attributes
// https://www.w3.org/TR/wai-aria/#attrs_liveregions
['atomic', { type: 'boolean' }],
['busy', { type: 'boolean'}],
['live', {
type: 'token',
valid_values: 'assertive off polite'.split(' ')
}],
['relevant', {
type: 'tokenlist',
valid_values: 'additions all removals text'.split(' ')
}],

// drag-and-drop attributes
// https://www.w3.org/TR/wai-aria/#attrs_dragdrop
['dropeffect', {
type: 'tokenlist',
valid_values: 'copy execute link move none popup'.split(' ')
}],
['grabbed', { type: 'boolean_or_undefined'}],

// relationship-attributes
// https://www.w3.org/TR/wai-aria/#attrs_relationships
['activedescendant', { type: 'id'}],
['colcount', { type: 'integer'}],
['colindex', { type: 'integer'}],
['colspan', { type: 'integer'}],
['controls', { type: 'idlist'}],
['describedby', { type: 'idlist'}],
['details', { type: 'id'}],
['errormessage', { type: 'id'}],
['flowto', { type: 'idlist'}],
['labelledby', { type: 'idlist' }],
['owns', { type: 'idlist' }],
['posinset', { type: 'integer' }],
['rowcount', { type: 'integer' }],
['rowindex', { type: 'integer' }],
['rowspan', { type: 'integer' }],
['setsize', { type: 'integer' }]
]);

function invalid_aria_attribute_value_message(mapping: any, attribute: string) {
switch (mapping.type) {
case 'tristate':
return `The value for the aria attribute '${attribute}' must be exactly one of true, false, or mixed`;
case 'token':
return `The value for the aria attribute '${attribute}' must be exactly one of ${mapping.valid_values.join(', ')}`;
case 'tokenlist':
return `The value for the aria attribute '${attribute}' must be a space-separated list of one or more of ${mapping.valid_values.join(', ')}`;
case 'idlist':
// TODO: (smart-idlist) should we make this idlist dynamic?
return `The value for the aria attribute '${attribute}' must be a space-separated list of strings that represent DOM element IDs`;
case 'id':
return `The value for the aria attribute '${attribute}' must be a string that represents a DOM element ID`;
case 'boolean_or_undefined':
return `The value for the aria attribute '${attribute}' must be exactly one of true, false, or undefined`;
case 'boolean':
return `The value for the aria attribute '${attribute}' must be exactly one of true or false`;
case 'string':
case 'integer':
case 'number':
default:
return `The value for the aria attribute '${attribute}' must be of type ${mapping.type}`;
}
}

function is_boolean(value: any): boolean {
return typeof value === 'boolean' || value === 'true' || value === 'false';
}

function valid_aria_attribute_value(mapping: any, value: any): boolean {
switch (mapping.type) {
case 'boolean':
return is_boolean(value);
case 'boolean_or_undefined':
return is_boolean(value) || value == null;
case 'string':
case 'id':
return typeof value === 'string';
case 'tristate':
return value === 'true' || value === 'false' || value === 'mixed';
case 'integer':
case 'number':
return typeof value !== 'boolean' && isNaN(Number(value)) === false;
case 'token':
return mapping.valid_values.indexOf(typeof value === 'string' ? value.toLowerCase() : value) > -1;
case 'idlist':
// TODO: (smart-idlist) should we make this idlist verification dynamic based on the ids present
// in the direct parent component that this element is contained within?
return typeof value === 'string' && value.split(' ').every((id) => typeof id === 'string');
case 'tokenlist':
return typeof value === 'string' && value.split(' ').every((token) => mapping.valid_values.indexOf(token.toLowerCase()) > -1);
default:
return false;
}
}

const a11y_required_attributes = {
a: ['href'],
area: ['alt', 'aria-label', 'aria-labelledby'],
Expand Down Expand Up @@ -367,6 +515,16 @@ export default class Element extends Node {
message: `A11y: <${this.name}> element should not be hidden`
});
}

// aria-proptypes
const value = attribute.get_static_value();
const aria_attribute_type_mapping = aria_attribute_value_type_map.get(type);
if (aria_attribute_type_mapping && !valid_aria_attribute_value(aria_attribute_type_mapping, value)) {
component.warn(attribute, {
code: 'a11y-invalid-aria-attribute-value',
message: `A11y: ${invalid_aria_attribute_value_message(aria_attribute_type_mapping, name)}`
});
}
}

// aria-role
Expand Down
@@ -0,0 +1,2 @@
<span aria-dropeffect="copy execute badvalue">foo</span>
<span aria-grabbed="yes">foo</span>
@@ -0,0 +1,33 @@
[
{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 45,
"column": 45,
"line": 1
},
"message": "A11y: The value for the aria attribute 'aria-dropeffect' must be a space-separated list of one or more of copy, execute, link, move, none, popup",
"pos": 6,
"start": {
"character": 6,
"column": 6,
"line": 1
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 81,
"column": 24,
"line": 2
},
"message": "A11y: The value for the aria attribute 'aria-grabbed' must be exactly one of true, false, or undefined",
"pos": 63,
"start": {
"character": 63,
"column": 6,
"line": 2
}
}
]
@@ -0,0 +1,9 @@
<span aria-hidden="yes">foo</span>
<span aria-current="some wrong values">foo</span>
<span aria-details>foo</span>
<span aria-disabled="yes">foo</span>
<span aria-haspopup="listbox tree">foo</span>
<span aria-invalid="grammar spelling">foo</span>
<span aria-keyshortcuts>foo</span>
<span aria-label>foo</span>
<span aria-roledescription>foo</span>
145 changes: 145 additions & 0 deletions test/validator/samples/a11y-aria-proptypes-global/warnings.json
@@ -0,0 +1,145 @@
[
{
"code": "a11y-invalid-aria-attribute-value",
"message": "A11y: The value for the aria attribute 'aria-hidden' must be exactly one of true, false, or undefined",
"start": {
"line": 1,
"column": 6,
"character": 6
},
"end": {
"line": 1,
"column": 23,
"character": 23
},
"pos": 6
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 73,
"column": 38,
"line": 2
},
"message": "A11y: The value for the aria attribute 'aria-current' must be exactly one of page, step, location, date, time, true, false",
"pos": 41,
"start": {
"character": 41,
"column": 6,
"line": 2
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"message": "A11y: The value for the aria attribute 'aria-details' must be a string that represents a DOM element ID",
"start": {
"line": 3,
"column": 6,
"character": 91
},
"end": {
"line": 3,
"column": 18,
"character": 103
},
"pos": 91
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 140,
"column": 25,
"line": 4
},
"message": "A11y: The value for the aria attribute 'aria-disabled' must be exactly one of true or false",
"pos": 121,
"start": {
"character": 121,
"column": 6,
"line": 4
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 186,
"column": 34,
"line": 5
},
"message": "A11y: The value for the aria attribute 'aria-haspopup' must be exactly one of false, true, menu, listbox, tree, grid, dialog",
"pos": 158,
"start": {
"character": 158,
"column": 6,
"line": 5
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 235,
"column": 37,
"line": 6
},
"message": "A11y: The value for the aria attribute 'aria-invalid' must be exactly one of grammar, false, spelling, true",
"pos": 204,
"start": {
"character": 204,
"column": 6,
"line": 6
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 270,
"column": 23,
"line": 7
},
"message": "A11y: The value for the aria attribute 'aria-keyshortcuts' must be of type string",
"pos": 253,
"start": {
"character": 253,
"column": 6,
"line": 7
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 298,
"column": 16,
"line": 8
},
"message": "A11y: The value for the aria attribute 'aria-label' must be of type string",
"pos": 288,
"start": {
"character": 288,
"column": 6,
"line": 8
}
},

{
"code": "a11y-invalid-aria-attribute-value",
"end": {
"character": 336,
"column": 26,
"line": 9
},
"message": "A11y: The value for the aria attribute 'aria-roledescription' must be of type string",
"pos": 316,
"start": {
"character": 316,
"column": 6,
"line": 9
}
}
]
@@ -0,0 +1,4 @@
<span aria-atomic="yes">foo</span>
<span aria-busy="yes">foo</span>
<span aria-live="assertive polite">foo</span>
<span aria-relevant="additions removals badvalue">foo</span>

0 comments on commit c6d6945

Please sign in to comment.