divタグにonClickが設定されるのをESLintでチェックする

## 概要

アクセシビリティのチェックポイントとして、
ユーザーが操作するコンポーネントを適切なタグで実装するか、
適切に属性を設定する必要があるというものがあります。

- [対話的なUIコンポーネントの実装(freeeアクセシビリティー・ガイドライン)](https://a11y-guidelines.freee.co.jp/categories/markup.html#gl-markup-component-implementation)
- [なぜ \<div\> に onClick がダメなのか？](https://zenn.dev/tm35/articles/64eac4c0570c4d)

create-next-appで作成したばかりのプロジェクトでは、
divタグなど非対話型コンテンツにonClickが設定されただけの場合、ESLintで特に指摘されません。
また、StorybookとAccessibility addonでコンポーネントをチェックしても指摘されず、
そのまま実装を進めてしまいがちです。

最近自分もこの事に気付いてコードレビューで指摘される前に検知・改善できないか調査しました。
この記事ではESLintルールを変更して、静的解析で適切なタグが利用できてないことを検知する設定を紹介します。
（[eslint-plugin-jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y)を推奨ルールで利用していると記事内のルールが適用済みのはずです）

ご自身のプロジェクトにてルール設定を見直してみるきっかけになれば幸いです。

## デフォルトの設定を検証する

### 検証に利用したツール

- Next.js: v14.0.3
- React.js: v18.0.2
- Storybook: v7.6.1
- storybook-addon-a11y: v7.6.1

### 検証用のプロジェクトとコンポーネントを用意

まず、以下の流れでプロジェクトを準備します。

- `npx create-next-app@latest` でNext.jsプロジェクトを作成
- `npx storybook@latest init` でStorybookをインストール
- [storybook-addon-a11y](https://storybook.js.org/addons/@storybook/addon-a11y)をインストールして有効化

### コンポーネントをチェックする

Storybookのinitコマンドを実行するとサンプルでボタンコンポーネントが追加されるので
これをbuttonタグからdivタグに変更してみます。

```tsx
"use client";
import React from "react";
import "./button.css";

interface ButtonProps {
  /**
   * Is this the principal call to action on the page?
   */
  primary?: boolean;
  /**
   * What background color to use
   */
  backgroundColor?: string;
  /**
   * How large should the button be?
   */
  size?: "small" | "medium" | "large";
  /**
   * Button contents
   */
  label: string;
  /**
   * Optional click handler
   */
  onClick?: () => void;
}

/**
 * Primary UI component for user interaction
 */
// NOTE: 元のコードではスプレッド構文でpropsを受け取っていましたが変更しています
// スプレッド構文だとonClickなどが設定されているか検知できなくなります
// propsでのスプレッド構文利用の是非はこの記事の範囲外なので詳細は割愛します
export const Button = ({
  primary = false,
  size = "medium",
  backgroundColor,
  label,
  onClick,
}: ButtonProps) => {
  const mode = primary
    ? "storybook-button--primary"
    : "storybook-button--secondary";
  return (
    // NOTE: buttonからdivに変更し、type propsを削除
    <div
      className={["storybook-button", `storybook-button--${size}`, mode].join(
        " "
      )}
      onClick={onClick}
    >
      {label}
      <style jsx>{`
        button {
          background-color: ${backgroundColor};
        }
      `}</style>
    </div>
  );
};
```

VS Code上では特にエラーは出ていません。

![上に記載したButtonコンポーネントをVS Codeで開いてESLintエラーが出てない状態のキャプチャ画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38157/a3a1e4bb-9ebc-57a6-2f3e-9b91f93257ab.png)


StorybookのAccessibility addonでもコントラスト不足の指摘のみとなっています。

![divタグのボタン要素をStorybookで開いてコントラスト不足の指摘のみ表示されている状態のキャプチャ画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38157/adcdb545-43ba-b4ad-d2e9-73bdc30c1fdf.png)


## eslint-plugin-jsx-a11yのルールを変更する

今回のような実装はeslint-plugin-jsx-a11yの[jsx-a11y/no-static-element-interactions](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md)を有効化することで検知可能です。
Next.jsのプロジェクトではeslint-plugin-jsx-a11yのセットアップがされているので
.eslintrc.jsonを編集し、プラグインのREADMEで推奨されている内容でルール追加を行いました。

```json
{
  "extends": ["next/core-web-vitals", "plugin:storybook/recommended"],
  "rules": {
    "jsx-a11y/no-static-element-interactions": [
      "error",
      {
        "handlers": [
          "onClick",
          "onMouseDown",
          "onMouseUp",
          "onKeyPress",
          "onKeyDown",
          "onKeyUp"
        ],
        "allowExpressionValues": true
      }
    ]
  }
}
```

VS Codeで指摘されるようになりました。

![設定を変えたあとESLintエラーがVS Codeで出ている状態でのキャプチャ画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38157/f7fcd6e2-0a15-bb4a-004c-0c7ca414b373.png)



## ちょっとした補足

eslint-plugin-jsx-a11yとStorybookのAccessibility addonはそれぞれチェック対象やルールが異なります。
2つのツールを利用することで、より多くのフィードバックが得られます。
web.devでも同様にeslint-plugin-jsx-a11yとレンダリングされたDOMのテストツールを組み合わせる記事があります。
[Accessibility auditing with react-axe and eslint-plugin-jsx-a11y](https://web.dev/articles/accessibility-auditing-react?hl=en)

Next.jsのデフォルト設定は以下のリンクで確認できますが、
いくつかのjsx-a11yルールが適用されているのでno-static-element-interactionsが有効になっていないのにも理由があるかもしれません。（深掘りはできてないです）
https://github.com/vercel/next.js/blob/canary/packages/eslint-config-next/index.js

StorybookのAccessibility addonはaxe-coreを利用しているので
チェックできるルールは以下のリンクで確認できます。
https://github.com/dequelabs/axe-core/blob/master/doc/rule-descriptions.md

例えばbuttonタグの中にaタグがある場合など、
対話型コンテンツがネストしているとStorybook側で指摘されます。

![Storybookでbuttonタグにaタグをネストしてアクセシビリティアドオンに指摘されているキャプチャ画像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38157/7bd5f269-573f-9661-eaee-079d3fc0ad22.png)



## まとめ

この記事ではeslint-plugin-jsx-a11yとStorybookのAccessibility addonを組み合わせて
ユーザーが操作するコンポーネントを適切なタグでマークアップできるよう静的解析で検知することを紹介しました。

それぞれのプロジェクトで設定したアクセシビリティ目標を達成できるように
コードとともにチェックツールの設定なども改善していけるとよさそうです。