Skip to content

Commit

Permalink
Merge pull request #1 from NielsdeBlaauw/aria-roles
Browse files Browse the repository at this point in the history
Aria roles
  • Loading branch information
NielsdeBlaauw committed Oct 11, 2020
2 parents 07532b8 + 4a408a3 commit 8bf75f2
Show file tree
Hide file tree
Showing 8 changed files with 153 additions and 23 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,21 @@ The following rules are implemented as part of this ruleset.

Using a non `0` or `-1` value for tabindex results in unexpected behaviour for keyboard users. Variables in the tabindex property of an element are considered invalid.


### BannedHTMLTags

**[Axe Blink rule description.](https://dequeuniversity.com/rules/axe/3.5/blink)**

**[Axe Marquee rule description.](https://dequeuniversity.com/rules/axe/3.5/marquee)**


The `blink` and `marquee` tags are disallowed from use. These elements can cause issues for users with cognitive disabilities.

### AriaRoles

**[Axe aria-roles rule description.](https://dequeuniversity.com/rules/axe/3.5/aria-roles)**

Catches invalid Aria role values. Typo's, non-standard and dynamic roles are not allowed.

Invalid roles can not be correctly interpreted by assistive technology.

## Roadmap
The idea is to implement as many rules as possible from the [Axe Linter](https://axe-linter.deque.com/docs/ruleset/) ruleset.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"composer validate --strict",
"twigcs tests/test.twig --ruleset \\\\NdB\\\\TwigCSA11Y\\\\Ruleset --reporter checkstyle > tests/snapshot.compare.xml || true",
"diff tests/snapshot.xml tests/snapshot.compare.xml",
"psalm"
"psalm",
"phpcs"
],
"test:update-snapshot": [
"twigcs tests/test.twig --ruleset \\\\NdB\\\\TwigCSA11Y\\\\Ruleset --reporter checkstyle > tests/snapshot.xml || true"
Expand Down
112 changes: 112 additions & 0 deletions src/Rules/AriaRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

namespace NdB\TwigCSA11Y\Rules;

use Allocine\Twigcs\Rule\AbstractRule;
use Allocine\Twigcs\Rule\RuleInterface;
use Twig\Token as TwigToken;
use Twig\TokenStream;

class AriaRoles extends AbstractRule implements RuleInterface
{
const VALID_ROLES = [
"article", "banner", "complementary", "main", "navigation", "region",
"search", "contentinfo", "alert", "alertdialog", "application",
"dialog", "group", "log", "marquee", "menu", "menubar", "menuitem",
"menuitemcheckbox", "menuitemradio", "progressbar", "separator",
"slider", "spinbutton", "status", "tab", "tablist", "tabpanel",
"timer", "toolbar", "tooltip", "tree", "treegrid", "treeitem",
"button", "button", "checkbox", "columnheader", "combobox",
"contentinfo", "form", "grid", "gridcell", "heading", "img", "link",
"listbox", "listitem", "option", "radio", "radiogroup", "row",
"rowgroup", "rowheader", "scrollbar", "textbox", "document",
"application", "presentation", "math", "definition", "note", "directory"
];

const ABSTRACT_ROLES = [
"command", "composite", "input", "landmark", "range", "section",
"sectionhead", "select", "structure", "widget"
];

/**
* @var \Allocine\Twigcs\Validator\Violation[]
*/
protected $violations = [];

/**
* @param int $severity
*/
public function __construct($severity)
{
parent::__construct($severity);
$this->violations = [];
}

public function check(TokenStream $tokens)
{
while (!$tokens->isEOF()) {
$token = $tokens->getCurrent();

if ($token->getType() === TwigToken::TEXT_TYPE) {
$matches = [];
$textToAnalyse = (string) $token->getValue();
$terminated = false;
$tokenIndex = 1;
while (!$terminated) {
$nextToken = $tokens->look($tokenIndex);
if ($nextToken->getType() !== TwigToken::ARROW_TYPE) {
$textToAnalyse .= (string) $nextToken->getValue();
}
if ($nextToken->getType() === TwigToken::TEXT_TYPE
|| $nextToken->getType() === TwigToken::EOF_TYPE
) {
$terminated = true;
}
$tokenIndex++;
}
if (preg_match(
"/role=((.)+)([>'\" ]+)/U",
$textToAnalyse,
$matches
)
) {
$value = preg_replace('/[^\da-z]/i', '', $matches[1]);
if (! in_array($value, self::VALID_ROLES, true)) {
if (in_array($value, self::ABSTRACT_ROLES, true)) {
/**
* @psalm-suppress InternalMethod
* @psalm-suppress UndefinedPropertyFetch
*/
$this->addViolation(
(string) $tokens->getSourceContext()->getPath(),
$token->getLine(),
$token->columnno,
sprintf(
'[A11Y.AriaRoles] Invalid abstract \'role\' value. Found `%1$s`.',
trim($matches[0])
)
);
} else {
/**
* @psalm-suppress InternalMethod
* @psalm-suppress UndefinedPropertyFetch
*/
$this->addViolation(
(string) $tokens->getSourceContext()->getPath(),
$token->getLine(),
$token->columnno,
sprintf(
'[A11Y.AriaRoles] Invalid \'role\'. Role must have a valid value. Found `%1$s`.',
trim($matches[0])
)
);
}
}
}
}

$tokens->next();
}
return $this->violations;
}
}
2 changes: 1 addition & 1 deletion src/Rules/BannedHTMLTags.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function check(TokenStream $tokens)
(string) $tokens->getSourceContext()->getPath(),
$token->getLine(),
$token->columnno,
sprintf('[A11Y.BannedHTMLTags] Tag \'%1$s\' is dissallowed. Found `%2$s`.', $tag, $matches[0])
sprintf('[A11Y.BannedHTMLTags] Invalid tag \'%1$s\'. Found `%2$s`.', $tag, $matches[0])
);
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/Rules/TabIndex.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ public function check(TokenStream $tokens)
$tokenIndex = 1;
while (!$terminated) {
$nextToken = $tokens->look($tokenIndex);
$textToAnalyse .= (string) $nextToken->getValue();
if ($nextToken->getType() !== TwigToken::ARROW_TYPE) {
$textToAnalyse .= (string) $nextToken->getValue();
}
if ($nextToken->getType() === TwigToken::TEXT_TYPE
|| $nextToken->getType() === TwigToken::EOF_TYPE
) {
Expand All @@ -58,7 +60,7 @@ public function check(TokenStream $tokens)
$token->getLine(),
$token->columnno,
sprintf(
'[A11Y.TabIndex] Invalid \'tabindex\'. Tabindex must be 0 or -1. Found `%1$s.`',
'[A11Y.TabIndex] Invalid \'tabindex\'. Tabindex must be 0 or -1. Found `%1$s`.',
trim($matches[0])
)
);
Expand Down
4 changes: 3 additions & 1 deletion src/Ruleset.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Allocine\Twigcs\Ruleset\RulesetInterface;
use Allocine\Twigcs\Validator\Violation;
use NdB\TwigCSA11Y\Rules\AriaRoles;
use NdB\TwigCSA11Y\Rules\BannedHTMLTags;
use NdB\TwigCSA11Y\Rules\TabIndex;

Expand All @@ -13,7 +14,8 @@ public function getRules()
{
return [
new BannedHTMLTags(Violation::SEVERITY_ERROR),
new TabIndex(Violation::SEVERITY_ERROR)
new TabIndex(Violation::SEVERITY_ERROR),
new AriaRoles(Violation::SEVERITY_ERROR)
];
}
}
2 changes: 1 addition & 1 deletion tests/snapshot.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?xml version="1.0"?>
<checkstyle version="1.0.0"><file name="tests/test.twig"><error column="0" line="2" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex=1&gt;.`" source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="20" line="12" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex=test.`" source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="53" line="12" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex='test'.`" source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="0" line="16" severity="error" message="[A11Y.BannedHTMLTags] Tag 'marquee' is dissallowed. Found `&lt;marquee&gt;`." source="NdB\TwigCSA11Y\Rules\BannedHTMLTags"/><error column="0" line="18" severity="error" message="[A11Y.BannedHTMLTags] Tag 'blink' is dissallowed. Found `&lt;blink&gt;`." source="NdB\TwigCSA11Y\Rules\BannedHTMLTags"/></file></checkstyle>
<checkstyle version="1.0.0"><file name="tests/test.twig"><error column="0" line="2" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex=1&gt;`." source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="0" line="3" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex=&quot;1&quot;`." source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="0" line="4" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex='1'`." source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="22" line="10" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex=test`." source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="0" line="11" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex='test'`." source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="0" line="12" severity="error" message="[A11Y.TabIndex] Invalid 'tabindex'. Tabindex must be 0 or -1. Found `tabindex='test'`." source="NdB\TwigCSA11Y\Rules\TabIndex"/><error column="0" line="15" severity="error" message="[A11Y.BannedHTMLTags] Invalid tag 'marquee'. Found `&lt;marquee&gt;`." source="NdB\TwigCSA11Y\Rules\BannedHTMLTags"/><error column="0" line="16" severity="error" message="[A11Y.BannedHTMLTags] Invalid tag 'blink'. Found `&lt;blink&gt;`." source="NdB\TwigCSA11Y\Rules\BannedHTMLTags"/><error column="0" line="20" severity="error" message="[A11Y.AriaRoles] Invalid 'role'. Role must have a valid value. Found `role=&quot;invalid_role&quot;`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/><error column="0" line="21" severity="error" message="[A11Y.AriaRoles] Invalid 'role'. Role must have a valid value. Found `role='invalid_role'`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/><error column="0" line="22" severity="error" message="[A11Y.AriaRoles] Invalid 'role'. Role must have a valid value. Found `role=invalid&gt;`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/><error column="0" line="23" severity="error" message="[A11Y.AriaRoles] Invalid abstract 'role' value. Found `role=command&gt;`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/><error column="0" line="24" severity="error" message="[A11Y.AriaRoles] Invalid 'role'. Role must have a valid value. Found `role=invalid&gt;`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/><error column="26" line="24" severity="error" message="[A11Y.AriaRoles] Invalid 'role'. Role must have a valid value. Found `role=invalid&gt;`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/><error column="0" line="25" severity="error" message="[A11Y.AriaRoles] Invalid 'role'. Role must have a valid value. Found `role='variable'`." source="NdB\TwigCSA11Y\Rules\AriaRoles"/></file></checkstyle>

37 changes: 22 additions & 15 deletions tests/test.twig
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
{# TabIndex #}
<div tabindex=1></div>
<div tabindex="1"></div>
<div tabindex='1'></div>
<div tabindex="0"></div>
<div tabindex='0'></div>
<div tabindex='-1'></div>
<div tabindex=-1></div>
{{test}}
<div tabindex='-1'></div>
{{test}}
<div class="{{test}}" tabindex={{test}} id="{{blala}}"></div>
<div tabindex='{{test}}'></div>
<div tabindex=1></div> {# BAD #}
<div tabindex="1"></div> {# BAD #}
<div tabindex='1'></div> {# BAD #}
<div tabindex="0"></div> {# GOOD #}
<div tabindex='0'></div> {# GOOD #}
<div tabindex='-1'></div> {# GOOD #}
<div tabindex=-1></div> {# GOOD #}
<div tabindex='-1'></div> {# GOOD #}
<div class="{{ test }}" tabindex={{ test }} id="{{ blala }}"></div> {# BAD #}
<div tabindex='{{ test }}'></div> {# BAD #}
<div tabindex='{{test}}'></div> {# BAD #}

{# BannedHTMLTags #}
<marquee>This is a marquee</marquee>
{# - #}
<p><blink>This blinks</blink></p>
<marquee>This is a marquee</marquee> {# BAD #}
<p><blink>This blinks</blink></p> {# BAD #}

{# AriaRoles #}
<div role="article"></div> {# GOOD #}
<div role="invalid_role"></div> {# BAD #}
<div role='invalid_role'></div> {# BAD #}
<div role=invalid></div> {# BAD #}
<div role=command></div> {# BAD - abstract role #}
<div class='{{ variable }}' role=invalid></div> {# BAD #}
<div role='{{ variable }}' /> {# BAD #}

0 comments on commit 8bf75f2

Please sign in to comment.