Repository: ymrl/a11y-visualizer (ts)

Issue:
disabled, checkedを普通のinputやselectやtextarea要素につけた場合に、aria-disabled, aria-checkedと同等の表示をする
#106 で追加したもののうち、HTMLのネイティブセマンティクスで同じことができるものを同じ表示にしたい

Pull request:
:+1: add aria-checked/disabled on normal checked/disabled
close #112 

Patch:
diff --git a/src/content/dom/tips/globalTips.ts b/src/content/dom/tips/globalTips.ts
index 8c8876c..3f65cc5 100644
--- a/src/content/dom/tips/globalTips.ts
+++ b/src/content/dom/tips/globalTips.ts
@@ -9,6 +9,17 @@ const globalAriaStates = [
   // "aria-hidden",
 ] as const;
 
+const ariaStateValues: { [ariaState: string]: (string | null)[] } = {
+  "aria-busy": ["true", "false"],
+  "aria-current": ["true", "false", "page", "step", "location", "date", "time"],
+  "aria-checked": ["true", "false", "mixed", "undefined"],
+  "aria-disabled": ["true", "false"],
+  "aria-expanded": ["true", "false", "undefined"],
+  "aria-invalid": ["true", "false", "grammar", "spelling"],
+  "aria-pressed": ["true", "false", "mixed", "undefined"],
+  "aria-selected": ["true", "false", "undefined"],
+};
+
 const ariaStateRoles: { [ariaState: string]: KnownRole[] } = {
   "aria-checked": [
     "checkbox",
@@ -108,11 +119,11 @@ const ariaStateRoles: { [ariaState: string]: KnownRole[] } = {
 export const globalTips = (el: Element): ElementTip[] => {
   const result: ElementTip[] = [];
   const description = computeAccessibleDescription(el);
-  const roleAttr = el.getAttribute("role") || "";
+  const roleAttr = el.getAttribute("role");
   const role = getKnownRole(el);
+  const tagName = el.tagName.toLowerCase();
 
   if (roleAttr) {
-    const tagName = el.tagName.toLowerCase();
     result.push({ type: "tagName", content: tagName });
     result.push({ type: "role", content: roleAttr });
   }
@@ -120,10 +131,17 @@ export const globalTips = (el: Element): ElementTip[] => {
   for (const state of globalAriaStates) {
     const value = el.getAttribute(state);
     if (value) {
-      result.push({
-        type: "ariaStatus",
-        content: `ariaStatus.${state}.${value}`,
-      });
+      if (ariaStateValues[state].includes(value)) {
+        result.push({
+          type: "ariaStatus",
+          content: `ariaStatus.${state}.${value}`,
+        });
+      } else {
+        result.push({
+          type: "error",
+          content: `messages.invalidAriaValue`,
+        });
+      }
     }
   }
 
@@ -131,14 +149,58 @@ export const globalTips = (el: Element): ElementTip[] => {
     if (role && roles.includes(role)) {
       const ariaStateValue = el.getAttribute(ariaState);
       if (ariaStateValue) {
-        result.push({
-          type: "ariaStatus",
-          content: `ariaStatus.${ariaState}.${ariaStateValue}`,
-        });
+        if (ariaStateValues[ariaState].includes(ariaStateValue)) {
+          result.push({
+            type: "ariaStatus",
+            content: `ariaStatus.${ariaState}.${ariaStateValue}`,
+          });
+        } else {
+          result.push({
+            type: "error",
+            content: `messages.invalidAriaValue`,
+          });
+        }
       }
     }
   }
 
+  if (
+    tagName === "input" &&
+    (["checkbox", "radio"] as (string | null)[]).includes(
+      el.getAttribute("type"),
+    ) &&
+    !el.hasAttribute("aria-checked")
+  ) {
+    result.push({
+      type: "ariaStatus",
+      content: `ariaStatus.aria-checked.${
+        (el as HTMLInputElement).checked ? "true" : "false"
+      }`,
+    });
+  }
+
+  if (
+    [
+      "button",
+      "fieldset",
+      "optgroup",
+      "option",
+      "select",
+      "textarea",
+      "input",
+    ].includes(tagName)
+  ) {
+    if (
+      el.hasAttribute("disabled") ||
+      (el.closest("fieldset[disabled]") && !el.closest("legend"))
+    ) {
+      result.push({
+        type: "ariaStatus",
+        content: "ariaStatus.aria-disabled.true",
+      });
+    }
+  }
+
   if (description) {
     result.push({ type: "description", content: description });
   }
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 783100d..1bfcd8a 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -95,7 +95,8 @@
     "noControlForLabel": "No control for label",
     "noNameAttr": "No name attribute",
     "noRadioGroup": "No radiobutton group",
-    "noNameSection": "No name <section>"
+    "noNameSection": "No name <section>",
+    "invalidAriaValue": "Invalid WAI-ARIA Attribute Value"
   },
   "optionsPage": {
     "description": "Settings changed from the popup are saved per host of the web page, and will be applied to other pages on the same host. Changing settings here does not affect those values."
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index 2b9b119..a10e4ce 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -101,7 +101,8 @@
     "noControlForLabel": "フォームコントロールなしのラベル",
     "noNameAttr": "name属性なし",
     "noRadioGroup": "ラジオボタングループなし",
-    "noNameSection": "名前なしの <section>"
+    "noNameSection": "名前なしの <section>",
+    "invalidAriaValue": "WAI-ARIA属性値が不正"
   },
   "optionsPage": {
     "description": "拡張機能のポップアップから設定を変更すると、閲覧中のWebページのホストごとに設定が保存され、同じホストのWebページに対して適用されます。この画面で設定を変更しても、それらの設定値は変更されません。"


Test patch:
diff --git a/src/content/dom/tips/globalTips.test.ts b/src/content/dom/tips/globalTips.test.ts
index 8c85112..8e05206 100644
--- a/src/content/dom/tips/globalTips.test.ts
+++ b/src/content/dom/tips/globalTips.test.ts
@@ -86,4 +86,30 @@ describe("globalTips()", () => {
     expect(result.find((t) => t.type === "role")).toBeDefined();
     expect(result.find((t) => t.type === "tagName")).toBeDefined();
   });
+
+  test("checked checkbox", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "checkbox");
+    element.checked = true;
+    expect(globalTips(element)).toEqual([
+      { type: "ariaStatus", content: "ariaStatus.aria-checked.true" },
+    ]);
+  });
+
+  test("unchecked checkbox", () => {
+    const element = document.createElement("input");
+    element.setAttribute("type", "checkbox");
+    element.checked = false;
+    expect(globalTips(element)).toEqual([
+      { type: "ariaStatus", content: "ariaStatus.aria-checked.false" },
+    ]);
+  });
+
+  test("disabled input", () => {
+    const element = document.createElement("input");
+    element.setAttribute("disabled", "");
+    expect(globalTips(element)).toEqual([
+      { type: "ariaStatus", content: "ariaStatus.aria-disabled.true" },
+    ]);
+  });
 });
diff --git a/src/test/categories/FormControl.tsx b/src/test/categories/FormControl.tsx
index a95c1a5..b7bca67 100644
--- a/src/test/categories/FormControl.tsx
+++ b/src/test/categories/FormControl.tsx
@@ -192,6 +192,19 @@ export const FormControl = () => (
           title: "Single Label",
           body: <label className="intentional-violation">Single Label</label>,
         },
+        {
+          title: "wrong aria-checked",
+          body: (
+            <label>
+              <input
+                className="intentional-violation"
+                type="checkbox"
+                aria-checked="hoge"
+              />{" "}
+              wrong aria-checked
+            </label>
+          ),
+        },
       ]}
     />
   </>