Skip to content

Commit 3376b8c

Browse files
committed
hasContent checks include accessibleChildren option
For the `heading-has-content` and the `anchor-has-content` rules, add an `accessibleChildren` option that always marks certain children as being accessible.
1 parent 2e2998b commit 3376b8c

9 files changed

+62
-13
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- For the `heading-has-content` and the `anchor-has-content` rules, add an `accessibleChildren` option that always marks certain children as being accessible.
12+
913
### Changed
1014

1115
- Handle non string literal role values and `role-has-required-aria-props` and non string literal value for `kind` attribute in `media-has-caption`.

docs/anchor-has-content.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ This rule takes one optional object argument of type object:
1616
"vuejs-accessibility/anchor-has-content": [
1717
"error",
1818
{
19-
"components": ["Anchor"]
19+
"components": ["Anchor"],
20+
"accessibleChildren": ["MyAccessibleText"]
2021
}
2122
]
2223
}
@@ -25,6 +26,8 @@ This rule takes one optional object argument of type object:
2526

2627
For the `components` option, these strings determine which elements (**always including** `<a>`) should be checked for having content. This is a good use case when you have a wrapper component that simply renders an `a` element.
2728

29+
For the `accessibleChildren` option, these strings determine which elements should be marked as acceptably accessible child elements. For example if you have something like a `<trans tag="hello-world" />` child that you know will translate into accessible text, then you should put the `Trans` component into this array.
30+
2831
### Succeed
2932

3033
<!-- prettier-ignore -->
@@ -34,11 +37,14 @@ For the `components` option, these strings determine which elements (**always in
3437
<a is="TextWrapper" />
3538
<a v-text="msg" />
3639
<a v-html="msg" />
40+
<Anchor>Anchor content</!Anchor>
41+
<a><my-accessible-text /></a>
3742
```
3843

3944
### Fail
4045

4146
```vue
4247
<a />
4348
<a><TextWrapper aria-hidden /></a>
49+
<Anchor></Anchor>
4450
```

docs/heading-has-content.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ This rule takes one optional object argument of type object:
1616
"vuejs-accessibility/heading-has-content": [
1717
"error",
1818
{
19-
"components": ["MyHeading"]
19+
"components": ["MyHeading"],
20+
"accessibleChildren": ["MyAccessibleText"]
2021
}
2122
]
2223
}
@@ -25,15 +26,22 @@ This rule takes one optional object argument of type object:
2526

2627
For the `components` option, these strings determine which elements (**always including** `<h1>` thru `<h6>`) should be checked for having content. This is a good use case when you have a wrapper component that simply renders an `h1` element.
2728

29+
For the `accessibleChildren` option, these strings determine which elements should be marked as acceptably accessible child elements. For example if you have something like a `<trans tag="hello-world" />` child that you know will translate into accessible text, then you should put the `Trans` component into this array.
30+
2831
### Succeed
2932

3033
```vue
3134
<h1>Heading Content!</h1>
3235
<h1 v-html="msg"></h1>
36+
<MyHeading>Heading Content!</MyHeading>
37+
<h1>
38+
<MyAccessibleText />
39+
</h1>
3340
```
3441

3542
### Fail
3643

3744
```vue
3845
<h1></h1>
46+
<MyHeading></MyHeading>
3947
```

src/rules/__tests__/anchor-has-content.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ makeRuleTester("anchor-has-content", rule, {
1010
"<a><slot /></a>",
1111
"<VAnchor />",
1212
"<a aria-label='This is my label' />",
13-
"<a><img alt='foo' /></a>"
13+
"<a><img alt='foo' /></a>",
14+
{
15+
code: "<a><accessible-child /></a>",
16+
options: [{ accessibleChildren: ["AccessibleChild"] }]
17+
}
1418
],
1519
invalid: [
1620
"<a />",

src/rules/__tests__/heading-has-content.test.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ makeRuleTester("heading-has-content", rule, {
88
"<h1 v-text='msg'></h1>",
99
"<h1 v-html='msg'></h1>",
1010
"<h1>{{ test }}</h1>",
11-
"<h1><slot /></h1>"
11+
"<h1><slot /></h1>",
12+
{
13+
code: "<h1><accessible-child /></h1>",
14+
options: [{ accessibleChildren: ["AccessibleChild"] }]
15+
}
1216
],
1317
invalid: [
1418
"<h1 />",

src/rules/anchor-has-content.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ module.exports = {
2323
components: {
2424
type: "array",
2525
items: { type: "string" }
26+
},
27+
accessibleChildren: {
28+
type: "array",
29+
items: { type: "string" }
2630
}
2731
}
2832
}
@@ -31,14 +35,17 @@ module.exports = {
3135
create(context) {
3236
return defineTemplateBodyVisitor(context, {
3337
VElement(node) {
34-
const { components = [] } = context.options[0] || {};
38+
const { components = [], accessibleChildren = [] } =
39+
context.options[0] || {};
3540

3641
const elementTypes = ["a"].concat(components.map(makeKebabCase));
42+
const accessibleChildTypes = accessibleChildren.map(makeKebabCase);
43+
3744
const elementType = getElementType(node);
3845

3946
if (
4047
elementTypes.includes(elementType) &&
41-
!hasContent(node) &&
48+
!hasContent(node, accessibleChildTypes) &&
4249
!hasAriaLabel(node)
4350
) {
4451
context.report({ node, messageId: "default" });

src/rules/heading-has-content.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ module.exports = {
2424
components: {
2525
type: "array",
2626
items: { type: "string" }
27+
},
28+
accessibleChildren: {
29+
type: "array",
30+
items: { type: "string" }
2731
}
2832
}
2933
}
@@ -32,12 +36,18 @@ module.exports = {
3236
create(context) {
3337
return defineTemplateBodyVisitor(context, {
3438
VElement(node) {
35-
const { components = [] } = context.options[0] || {};
39+
const { components = [], accessibleChildren = [] } =
40+
context.options[0] || {};
3641

3742
const elementTypes = headings.concat(components.map(makeKebabCase));
43+
const accessibleChildTypes = accessibleChildren.map(makeKebabCase);
44+
3845
const elementType = getElementType(node);
3946

40-
if (elementTypes.includes(elementType) && !hasContent(node)) {
47+
if (
48+
elementTypes.includes(elementType) &&
49+
!hasContent(node, accessibleChildTypes)
50+
) {
4151
context.report({ node, messageId: "default" });
4252
}
4353
}

src/utils/hasAccessibleChild.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
const getElementType = require("./getElementType");
12
const isHiddenFromScreenReader = require("./isHiddenFromScreenReader");
23

3-
const hasAccessibleChild = (node) =>
4+
const hasAccessibleChild = (node, accessibleChildTypes = []) =>
45
node.children.some((child) => {
56
switch (child.type) {
67
case "VText":
78
return child.value.trim().length > 0;
8-
case "VElement":
9+
case "VElement": {
10+
const elementType = getElementType(child);
11+
912
return (
13+
accessibleChildTypes.includes(elementType) ||
1014
child.rawName === "slot" ||
11-
(!isHiddenFromScreenReader(child) && hasAccessibleChild(child))
15+
(!isHiddenFromScreenReader(child) &&
16+
hasAccessibleChild(child, accessibleChildTypes))
1217
);
18+
}
1319
case "VExpressionContainer":
1420
if (child.expression && child.expression.type === "Identifier") {
1521
return child.expression.name !== "undefined";

src/utils/hasContent.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ const hasChildImageWithAlt = (node) =>
2222
}
2323
});
2424

25-
const hasContent = (node) =>
26-
hasAccessibleChild(node) ||
25+
const hasContent = (node, accessibleChildTypes) =>
26+
hasAccessibleChild(node, accessibleChildTypes) ||
2727
hasDirective(node, "text") ||
2828
hasDirective(node, "html") ||
2929
hasChildImageWithAlt(node);

0 commit comments

Comments
 (0)