Repository: vue-a11y/eslint-plugin-vuejs-accessibility
Pull request #196 (2021-07-10 19:35:56)

Adds an `accessibleDirectives` option to rules that use hasContent.
Adds an `accessibleDirectives` option to rules that use hasContent. This covers the use case of a directive that provides accessible content along the lines of `v-html` or `v-text`.

Our specific use case is at Jackbox we have a `v-bb` directive that parses and inserts BBCode syntax. Currently we have this rule turned off because of this limitation. 






Patch:
diff --git a/docs/anchor-has-content.md b/docs/anchor-has-content.md
index ed3f105..cbda29f 100644
--- a/docs/anchor-has-content.md
+++ b/docs/anchor-has-content.md
@@ -17,7 +17,8 @@ This rule takes one optional object argument of type object:
       "error",
       {
         "components": ["Anchor"],
-        "accessibleChildren": ["MyAccessibleText"]
+        "accessibleChildren": ["MyAccessibleText"],
+        "accessibleDirectives": ["myAccessibleDirective"]
       }
     ]
   }
@@ -28,6 +29,8 @@ For the `components` option, these strings determine which elements (**always in
 
 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.
 
+For the `accessibleDirectives` option, these strings declare an element should be marked as acceptably accessible if a directive is present. For example something like `<a href="#" v-bb="myBBCode" />` may provide content in the same way `v-html` would. You would add `bb` into this array. _Notice these strings should not include the `v-` prefix._
+
 ### Succeed
 
 <!-- prettier-ignore -->
@@ -37,6 +40,7 @@ For the `accessibleChildren` option, these strings determine which elements shou
 <a is="TextWrapper" />
 <a v-text="msg" />
 <a v-html="msg" />
+<a v-myAccessibleDirective="msg" />
 <Anchor>Anchor content</!Anchor>
 <a><my-accessible-text /></a>
 ```
diff --git a/docs/heading-has-content.md b/docs/heading-has-content.md
index 59e730f..1e8192c 100644
--- a/docs/heading-has-content.md
+++ b/docs/heading-has-content.md
@@ -17,7 +17,8 @@ This rule takes one optional object argument of type object:
       "error",
       {
         "components": ["MyHeading"],
-        "accessibleChildren": ["MyAccessibleText"]
+        "accessibleChildren": ["MyAccessibleText"],
+        "accessibleDirectives": ["myAccessibleDirective"]
       }
     ]
   }
@@ -28,11 +29,15 @@ For the `components` option, these strings determine which elements (**always in
 
 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.
 
+For the `accessibleDirectives` option, these strings declare an element should be marked as acceptably accessible if a directive is present. For example something like `<h1 v-bb="myBBCode" />` may provide content in the same way `v-html` would. You would add `bb` into this array. _Notice these strings should not include the `v-` prefix._
+
+
 ### Succeed
 
 ```vue
 <h1>Heading Content!</h1>
 <h1 v-html="msg"></h1>
+<h1 v-myAccessibleDirective="msg"></h1>
 <MyHeading>Heading Content!</MyHeading>
 <h1>
   <MyAccessibleText />
diff --git a/src/rules/anchor-has-content.ts b/src/rules/anchor-has-content.ts
index afaeb82..26f9710 100644
--- a/src/rules/anchor-has-content.ts
+++ b/src/rules/anchor-has-content.ts
@@ -27,6 +27,10 @@ const rule: Rule.RuleModule = {
             items: { type: "string" }
           },
           accessibleChildren: {
+            type: "array",
+            items: { type: "string" },
+          },
+          accessibleDirectives: {
             type: "array",
             items: { type: "string" }
           }
@@ -37,7 +41,7 @@ const rule: Rule.RuleModule = {
   create(context) {
     return defineTemplateBodyVisitor(context, {
       VElement(node) {
-        const { components = [], accessibleChildren = [] } =
+        const { components = [], accessibleChildren = [], accessibleDirectives = [] } =
           context.options[0] || {};
 
         const elementTypes = ["a"].concat(components.map(makeKebabCase));
@@ -47,7 +51,7 @@ const rule: Rule.RuleModule = {
 
         if (
           elementTypes.includes(elementType) &&
-          !hasContent(node, accessibleChildTypes) &&
+          !hasContent(node, accessibleChildTypes, accessibleDirectives) &&
           !hasAriaLabel(node)
         ) {
           context.report({ node: node as any, messageId: "default" });
diff --git a/src/rules/heading-has-content.ts b/src/rules/heading-has-content.ts
index 2af69d5..3ac5823 100644
--- a/src/rules/heading-has-content.ts
+++ b/src/rules/heading-has-content.ts
@@ -30,6 +30,10 @@ const rule: Rule.RuleModule = {
           accessibleChildren: {
             type: "array",
             items: { type: "string" }
+          },
+          accessibleDirectives: {
+            type: "array",
+            items: { type: "string" }
           }
         }
       }
@@ -38,7 +42,7 @@ const rule: Rule.RuleModule = {
   create(context) {
     return defineTemplateBodyVisitor(context, {
       VElement(node) {
-        const { components = [], accessibleChildren = [] } =
+        const { components = [], accessibleChildren = [], accessibleDirectives = [] } =
           context.options[0] || {};
 
         const elementTypes = headings.concat(components.map(makeKebabCase));
@@ -48,7 +52,7 @@ const rule: Rule.RuleModule = {
 
         if (
           elementTypes.includes(elementType) &&
-          !hasContent(node, accessibleChildTypes)
+          !hasContent(node, accessibleChildTypes, accessibleDirectives)
         ) {
           context.report({ node: node as any, messageId: "default" });
         }
diff --git a/src/utils/hasContent.ts b/src/utils/hasContent.ts
index 103dd84..802c767 100644
--- a/src/utils/hasContent.ts
+++ b/src/utils/hasContent.ts
@@ -7,7 +7,7 @@ import isHiddenFromScreenReader from "./isHiddenFromScreenReader";
 
 function hasDirective(node: AST.VElement, name: string) {
   return node.startTag.attributes.some(
-    (attribute) => attribute.directive && attribute.key.name.name === name
+    (attribute) => attribute.directive && attribute.key.name.name === name.toLowerCase()
   );
 }
 
@@ -26,9 +26,16 @@ function hasChildImageWithAlt(node: AST.VElement): boolean {
   });
 }
 
-function hasContent(node: AST.VElement, accessibleChildTypes: string[]) {
+function hasAccessibleDirective(node: AST.VElement, accessibleDirectives: string[]): boolean {
+  return accessibleDirectives.some((directive) => {
+    return hasDirective(node, directive)
+  });
+}
+
+function hasContent(node: AST.VElement, accessibleChildTypes: string[], accessibleDirectives: string[]) {
   return (
     hasAccessibleChild(node, accessibleChildTypes) ||
+    hasAccessibleDirective(node, accessibleDirectives) ||
     hasDirective(node, "text") ||
     hasDirective(node, "html") ||
     hasChildImageWithAlt(node)


Test patch:
diff --git a/src/rules/__tests__/anchor-has-content.test.ts b/src/rules/__tests__/anchor-has-content.test.ts
index 8a30897..3f2da03 100644
--- a/src/rules/__tests__/anchor-has-content.test.ts
+++ b/src/rules/__tests__/anchor-has-content.test.ts
@@ -11,6 +11,10 @@ makeRuleTester("anchor-has-content", rule, {
     "<VAnchor  />",
     "<a aria-label='This is my label' />",
     "<a><img alt='foo' /></a>",
+    {
+      code: "<a v-accessibleDirective='msg' />",
+      options: [{ accessibleDirectives: ["accessibleDirective"] }]
+    },
     {
       code: "<a><accessible-child /></a>",
       options: [{ accessibleChildren: ["AccessibleChild"] }]
diff --git a/src/rules/__tests__/heading-has-content.test.ts b/src/rules/__tests__/heading-has-content.test.ts
index 313ce84..fcd48ff 100644
--- a/src/rules/__tests__/heading-has-content.test.ts
+++ b/src/rules/__tests__/heading-has-content.test.ts
@@ -9,6 +9,10 @@ makeRuleTester("heading-has-content", rule, {
     "<h1 v-html='msg'></h1>",
     "<h1>{{ test }}</h1>",
     "<h1><slot /></h1>",
+    {
+      code: "<h1 v-accessibleDirective='msg'></h1>",
+      options: [{ accessibleDirectives: ["accessibleDirective"] }]
+    },
     {
       code: "<h1><accessible-child /></h1>",
       options: [{ accessibleChildren: ["AccessibleChild"] }]