Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] a11y-no-redundant-roles #7067

Merged
merged 17 commits into from
Jan 3, 2022
Merged
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
4 changes: 4 additions & 0 deletions src/compiler/compile/compiler_warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export default {
code: 'a11y-unknown-role',
message: `A11y: Unknown role '${role}'` + (suggestion ? ` (did you mean '${suggestion}'?)` : '')
}),
a11y_no_redundant_roles: (role: string | boolean) => ({
code: 'a11y-no-redundant-roles',
message: `A11y: Redundant role '${role}'`
}),
a11y_accesskey: {
code: 'a11y-accesskey',
message: 'A11y: Avoid using accesskey'
Expand Down
73 changes: 73 additions & 0 deletions src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,46 @@ const a11y_labelable = new Set([
'textarea'
]);

const a11y_nested_implicit_semantics = new Map([
['header', 'banner'],
['footer', 'contentinfo']
]);

const a11y_implicit_semantics = new Map([
['a', 'link'],
['aside', 'complementary'],
['body', 'document'],
['datalist', 'listbox'],
['dd', 'definition'],
['dfn', 'term'],
['details', 'group'],
['dt', 'term'],
['fieldset', 'group'],
['form', 'form'],
['h1', 'heading'],
['h2', 'heading'],
['h3', 'heading'],
['h4', 'heading'],
['h5', 'heading'],
['h6', 'heading'],
['hr', 'separator'],
['li', 'listitem'],
['menu', 'list'],
['nav', 'navigation'],
['ol', 'list'],
['optgroup', 'group'],
['output', 'status'],
['progress', 'progressbar'],
['section', 'region'],
['summary', 'button'],
['tbody', 'rowgroup'],
['textarea', 'textbox'],
['tfoot', 'rowgroup'],
['thead', 'rowgroup'],
['tr', 'row'],
['ul', 'list']
]);

const invisible_elements = new Set(['meta', 'html', 'script', 'style']);

const valid_modifiers = new Set([
Expand Down Expand Up @@ -98,6 +138,23 @@ const react_attributes = new Map([

const attributes_to_compact_whitespace = ['class', 'style'];

function is_parent(parent: INode, elements: string[]) {
let check = false;

while (parent) {
const parent_name = (parent as Element).name;
if (elements.includes(parent_name)) {
check = true;
break;
}
if (parent.type === 'Element') {
break;
}
parent = parent.parent;
}
return check;
}

function get_namespace(parent: Element, element: Element, explicit_namespace: string) {
const parent_element = parent.find_nearest(/^Element/);

Expand Down Expand Up @@ -351,6 +408,22 @@ export default class Element extends Node {
const match = fuzzymatch(value, aria_roles);
component.warn(attribute, compiler_warnings.a11y_unknown_role(value, match));
}

// no-redundant-roles
const has_redundant_role = value === a11y_implicit_semantics.get(this.name);

if (this.name === value || has_redundant_role) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
}

// Footers and headers are special cases, and should not have redundant roles unless they are the children of sections or articles.
const is_parent_section_or_article = is_parent(this.parent, ['section', 'article']);
if (!is_parent_section_or_article) {
const has_nested_redundant_role = value === a11y_nested_implicit_semantics.get(this.name);
if (has_nested_redundant_role) {
component.warn(attribute, compiler_warnings.a11y_no_redundant_roles(value));
}
}
}

// no-access-key
Expand Down
44 changes: 44 additions & 0 deletions test/validator/samples/a11y-no-redundant-roles/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<a href="/" role="link">a link</a>
<article role="article" />
<aside role="complementary" />
<body role="document" />
<button role="button" />
<datalist role="listbox" />
<dd role="definition" />
<dfn role="term" />
<details role="group" />
<dialog role="dialog" />
<dt role="term" />
<fieldset role="group" />
<figure role="figure" />
<form role="form">foo</form>
<h1 role="heading">heading</h1>
<h2 role="heading">heading</h2>
<h3 role="heading">heading</h3>
<h4 role="heading">heading</h4>
<h5 role="heading">heading</h5>
<h6 role="heading">heading</h6>
<hr role="separator" />
<li role="listitem" />
<link role="link" />
<main role="main"></main>
<menu role="list" />
<nav role="navigation" />
<ol role="list" />
<optgroup role="group" />
<option role="option" />
<output role="status" />
<progress role="progressbar" />
<section role="region" />
<summary role="button" />
<table role="table" />
<tbody role="rowgroup" />
<textarea role="textbox" />
<tfoot role="rowgroup" />
<thead role="rowgroup" />
<tr role="row" />
<ul role="list" />

<!-- Tested header/footer not nested in section/article -->
<header role="banner"></header>
<footer role="contentinfo"></footer>
Loading