-
Notifications
You must be signed in to change notification settings - Fork 784
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(new-rule): summary elements must have an accessible name (#4511)
This rule checks that summary elements have an accessible name, through text content, aria-label(ledby) or title. It skips summary elements that are not used as controls for `details`, or if its `details` element has no content. Closes: #4510
- Loading branch information
1 parent
0577a74
commit 0d8a99e
Showing
12 changed files
with
383 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
export default function summaryIsInteractiveMatches(_, virtualNode) { | ||
// Summary only interactive if its real DOM parent is a details element | ||
const parent = virtualNode.parent; | ||
if (parent.props.nodeName !== 'details' || isSlottedElm(virtualNode)) { | ||
return false; | ||
} | ||
// Only the first summary element is interactive | ||
const firstSummary = parent.children.find( | ||
child => child.props.nodeName === 'summary' | ||
); | ||
if (firstSummary !== virtualNode) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
function isSlottedElm(vNode) { | ||
// Normally this wouldn't be enough, but since we know parent is a details | ||
// element, we can ignore edge cases like slot being the real parent | ||
const domParent = vNode.actualNode?.parentElement; | ||
return domParent && domParent !== vNode.parent.actualNode; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"id": "summary-name", | ||
"impact": "serious", | ||
"selector": "summary", | ||
"matches": "summary-interactive-matches", | ||
"tags": [ | ||
"cat.name-role-value", | ||
"wcag2a", | ||
"wcag412", | ||
"section508", | ||
"section508.22.a", | ||
"TTv5", | ||
"TT6.a", | ||
"EN-301-549", | ||
"EN-9.4.1.2" | ||
], | ||
"metadata": { | ||
"description": "Ensures summary elements have discernible text", | ||
"help": "Summary elements must have discernible text" | ||
}, | ||
"all": [], | ||
"any": [ | ||
"has-visible-text", | ||
"aria-label", | ||
"aria-labelledby", | ||
"non-empty-title" | ||
], | ||
"none": [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
<details> | ||
<summary id="empty-fail"></summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="text-pass">name</summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="aria-label-pass" aria-label="Name"></summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="aria-label-fail" aria-label=""></summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="aria-labelledby-pass" aria-labelledby="labeldiv"></summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="aria-labelledby-fail" aria-labelledby="nonexistent"></summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="aria-labelledby-empty-fail" aria-labelledby="emptydiv"></summary> | ||
Hello world | ||
</details> | ||
<div id="labeldiv">summary label</div> | ||
<div id="emptydiv"></div> | ||
|
||
<details> | ||
<summary id="combo-pass" aria-label="Aria Name">Name</summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="title-pass" title="Title"></summary> | ||
Hello world | ||
</details> | ||
|
||
<details> | ||
<summary id="presentation-role-fail" role="presentation"></summary> | ||
Conflict resolution gets this to be ignored | ||
</details> | ||
|
||
<details> | ||
<summary id="none-role-fail" role="none"></summary> | ||
Conflict resolution gets this to be ignored | ||
</details> | ||
|
||
<details> | ||
<summary id="heading-role-fail" role="heading"></summary> | ||
Conflict resolution gets this to be ignored | ||
</details> | ||
|
||
<!-- Invalid naming methods --> | ||
|
||
<details> | ||
<summary id="value-attr-fail" value="Button Name"></summary> | ||
Not a valid method for giving a name | ||
</details> | ||
|
||
<details> | ||
<summary id="alt-attr-fail" alt="Button Name"></summary> | ||
Not a valid method for giving a name | ||
</details> | ||
|
||
<label> | ||
<details> | ||
<summary id="label-elm-fail"></summary> | ||
Text here | ||
</details> | ||
Not a valid method for giving a name | ||
</label> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"description": "summary-name test", | ||
"rule": "summary-name", | ||
"violations": [ | ||
["#empty-fail"], | ||
["#aria-label-fail"], | ||
["#aria-labelledby-fail"], | ||
["#aria-labelledby-empty-fail"], | ||
["#presentation-role-fail"], | ||
["#none-role-fail"], | ||
["#heading-role-fail"], | ||
["#value-attr-fail"], | ||
["#alt-attr-fail"], | ||
["#label-elm-fail"] | ||
], | ||
"passes": [ | ||
["#text-pass"], | ||
["#aria-label-pass"], | ||
["#aria-labelledby-pass"], | ||
["#combo-pass"], | ||
["#title-pass"] | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
function appendSerialChild(parent, child) { | ||
if (child instanceof axe.SerialVirtualNode === false) { | ||
child = new axe.SerialVirtualNode(child); | ||
} | ||
child.parent = parent; | ||
parent.children ??= []; | ||
parent.children.push(child); | ||
return child; | ||
} | ||
|
||
describe('summary-name virtual-rule', () => { | ||
let vDetails; | ||
beforeEach(() => { | ||
vDetails = new axe.SerialVirtualNode({ | ||
nodeName: 'details', | ||
attributes: {} | ||
}); | ||
appendSerialChild(vDetails, { nodeName: '#text', nodeValue: 'text' }); | ||
}); | ||
|
||
it('fails without children', () => { | ||
const vSummary = new axe.SerialVirtualNode({ | ||
nodeName: 'summary', | ||
attributes: {} | ||
}); | ||
vSummary.children = []; | ||
appendSerialChild(vDetails, vSummary); | ||
const results = axe.runVirtualRule('summary-name', vSummary); | ||
console.log(results); | ||
assert.lengthOf(results.passes, 0); | ||
assert.lengthOf(results.violations, 1); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('passes with text content', () => { | ||
const vSummary = new axe.SerialVirtualNode({ | ||
nodeName: 'summary', | ||
attributes: {} | ||
}); | ||
appendSerialChild(vSummary, { nodeName: '#text', nodeValue: 'text' }); | ||
appendSerialChild(vDetails, vSummary); | ||
|
||
const results = axe.runVirtualRule('summary-name', vSummary); | ||
assert.lengthOf(results.passes, 1); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('passes with aria-label', () => { | ||
const vSummary = new axe.SerialVirtualNode({ | ||
nodeName: 'summary', | ||
attributes: { 'aria-label': 'foobar' } | ||
}); | ||
appendSerialChild(vDetails, vSummary); | ||
const results = axe.runVirtualRule('summary-name', vSummary); | ||
assert.lengthOf(results.passes, 1); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('passes with title', () => { | ||
const vSummary = new axe.SerialVirtualNode({ | ||
nodeName: 'summary', | ||
attributes: { title: 'foobar' } | ||
}); | ||
appendSerialChild(vDetails, vSummary); | ||
const results = axe.runVirtualRule('summary-name', vSummary); | ||
assert.lengthOf(results.passes, 1); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 0); | ||
}); | ||
|
||
it('incompletes with aria-labelledby', () => { | ||
const vSummary = new axe.SerialVirtualNode({ | ||
nodeName: 'summary', | ||
attributes: { 'aria-labelledby': 'foobar' } | ||
}); | ||
appendSerialChild(vDetails, vSummary); | ||
const results = axe.runVirtualRule('summary-name', vSummary); | ||
assert.lengthOf(results.passes, 0); | ||
assert.lengthOf(results.violations, 0); | ||
assert.lengthOf(results.incomplete, 1); | ||
}); | ||
|
||
it('throws without a parent', () => { | ||
const vSummary = new axe.SerialVirtualNode({ | ||
nodeName: 'summary', | ||
attributes: { 'aria-labelledby': 'foobar' } | ||
}); | ||
vSummary.children = []; | ||
assert.throws(() => { | ||
axe.runVirtualRule('summary-name', vSummary); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.