Skip to content

Commit d753528

Browse files
committed
feat(a11y): add new accessibility rules for nested interactive elements and duplicate accesskeys; enhance lang attribute validation
1 parent 0301182 commit d753528

44 files changed

Lines changed: 714 additions & 193 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/qa.yml

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,40 +52,31 @@ jobs:
5252
run: composer rector -- --dry-run
5353

5454
- name: Run tests with coverage
55+
env:
56+
XDEBUG_MODE: coverage
5557
run: |
5658
./vendor/bin/phpunit \
59+
--coverage-clover=build/coverage/coverage.xml \
5760
--coverage-xml=build/coverage/coverage-xml \
58-
--log-junit=build/coverage/junit.xml \
59-
--coverage-clover=build/coverage/coverage.xml
60-
61-
- name: Upload coverage and test results artifact
62-
uses: actions/upload-artifact@v7
63-
with:
64-
name: test-results-php${{ matrix.php }}
65-
path: |
66-
build/coverage/coverage.xml
67-
build/coverage/junit.xml
68-
if-no-files-found: warn
61+
--log-junit=build/coverage/junit.xml
6962
7063
- name: Upload coverage to Codecov
71-
if: ${{ env.CODECOV_TOKEN != '' }}
64+
if: ${{ matrix.php == '8.3' }}
7265
uses: codecov/codecov-action@v6
73-
env:
74-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
7566
with:
7667
token: ${{ secrets.CODECOV_TOKEN }}
7768
files: build/coverage/coverage.xml
7869
fail_ci_if_error: true
70+
verbose: true
7971

8072
- name: Upload test results to Codecov
81-
if: ${{ !cancelled() }}
73+
if: ${{ matrix.php == '8.3' && !cancelled() }}
8274
uses: codecov/codecov-action@v6
83-
env:
84-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
8575
with:
8676
token: ${{ secrets.CODECOV_TOKEN }}
8777
files: build/coverage/junit.xml
8878
report_type: test_results
79+
fail_ci_if_error: false
8980

9081
infection:
9182
needs: build
@@ -117,6 +108,8 @@ jobs:
117108
run: composer install --prefer-dist --no-progress --no-suggest --optimize-autoloader
118109

119110
- name: Run tests with coverage (for Infection)
111+
env:
112+
XDEBUG_MODE: coverage
120113
run: |
121114
./vendor/bin/phpunit \
122115
--coverage-xml=build/coverage/coverage-xml \

README.md

Lines changed: 83 additions & 71 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

composer.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

phpunit.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Rules/Aria/AriaAllowedAttrRule.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,6 @@ public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
3838
continue;
3939
}
4040

41-
foreach ($this->allowed[$role] as $attr) {
42-
// nothing
43-
}
44-
4541
// naive check: find any aria- attribute not in allowed list
4642
if (preg_match_all('/\baria-[a-z0-9-]+\s*=\s*(?:"|\')[^"\']*(?:"|\')/i', $attrs, $am)) {
4743
foreach ($am[0] as $ariaRaw) {

src/Rules/Aria/AriaRoleRule.php

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,42 @@ public function evaluate(Tokens $tokens, int $tokenIndex, callable $emit): void
1515
return;
1616
}
1717

18-
// Page-level scan across the whole template
1918
$tag = $this->getFullContent($tokens);
2019

21-
if (preg_match_all('/role\s*=\s*(?:"|\')([^"\']+)(?:"|\')/i', $tag, $m)) {
22-
$roles = array_map(strtolower(...), $m[1]);
23-
24-
// expanded list of common ARIA roles
25-
$allowed = [
26-
'alert', 'alertdialog', 'application', 'article', 'banner', 'button', 'checkbox', 'columnheader',
27-
'combobox', 'complementary', 'contentinfo', 'dialog', 'directory', 'document', 'feed', 'figure',
28-
'form', 'grid', 'gridcell', 'group', 'heading', 'img', 'link', 'list', 'listbox', 'listitem', 'log',
29-
'main', 'math', 'menu', 'menubar', 'menuitem', 'menuitemcheckbox', 'menuitemradio',
30-
'navigation', 'none', 'note', 'option', 'presentation', 'progressbar', 'radio', 'radiogroup', 'region',
31-
'row', 'rowgroup', 'rowheader', 'search', 'separator', 'slider', 'spinbutton', 'status', 'switch', 'tab',
32-
'table', 'tablist', 'tabpanel', 'textbox', 'timer', 'toolbar', 'tooltip', 'tree', 'treegrid', 'treeitem',
33-
];
34-
35-
// Collect and report all invalid roles found in the document.
36-
$invalid = [];
37-
foreach ($roles as $role) {
38-
if (!in_array($role, $allowed, true)) {
39-
$invalid[] = $role;
40-
}
20+
if (!preg_match_all('/role\s*=\s*(?:"|\')([^"\']+)(?:"|\')/i', $tag, $m)) {
21+
return;
22+
}
23+
24+
$allowed = RoleCatalog::getAllowedRoles();
25+
$roles = array_map(strtolower(...), $m[1]);
26+
27+
$invalid = [];
28+
foreach ($roles as $role) {
29+
// Skip Twig dynamic expressions
30+
if ($this->containsTwigExpressions($role)) {
31+
continue;
4132
}
4233

43-
if ([] !== $invalid) {
44-
$tokenRef = $tokens->get(0);
45-
$idx = 0;
46-
foreach ($invalid as $role) {
47-
++$idx;
48-
$id = 'AriaRole.InvalidRole';
49-
if ($idx > 1) {
50-
$id .= '#'.$idx;
51-
}
52-
53-
$emit(sprintf('Invalid ARIA role "%s".', $role), $tokenRef, $id);
54-
}
34+
if (!in_array($role, $allowed, true)) {
35+
$invalid[] = $role;
5536
}
5637
}
38+
39+
if ([] === $invalid) {
40+
return;
41+
}
42+
43+
$tokenRef = $tokens->get(0);
44+
$idx = 0;
45+
foreach ($invalid as $role) {
46+
++$idx;
47+
$id = 'AriaRole.InvalidRole';
48+
if ($idx > 1) {
49+
$id .= '#'.$idx;
50+
}
51+
52+
$emit(sprintf('Invalid ARIA role "%s".', $role), $tokenRef, $id);
53+
}
5754
}
5855

5956
protected function evaluateOncePerFile(): bool

0 commit comments

Comments
 (0)