非対話型コンテンツにclickイベントを設定してはいけない理由

追記(2019/09/24): 「付与」という表現が適切でないと思ったので修正しました

non-interactive content(非対話型コンテンツ)上のclickイベントのことを書きます。

はじめに

無限 無添 くら寿司| 採用サイト

くら寿司採用サイトのページ。画面下に皿があり、クリックすると何かが起こるらしい

皿を「クリック!」すると、なんか演出がある、というページです。

ソースを見ると、img要素にdishクラスを設定し、clickイベントを登録しています。

html
<img src="images/dish.png" alt="" class="dish" />
common.js
$("#system .dish").on("click.dish",function(){

  dish();

});

また、スマートフォン向けにスワイプ操作まで実装されています。

common.js
$("#system .dish").touchwipe({

  /* 省略 */

  wipeDown: function() {
    dish();
  },

  /* 省略 */

});

このimg要素はフォーカス不可能なため、tab移動でたどり着くことができません。
また、このimg要素にはclickイベントしか存在しないので、キーボード操作には反応しません。
つまり、マウスを持ったユーザーが「クリック!」しないと、コンテンツを楽しむことができなくなっています。

このimg要素は、non-interactive content(非対話型コンテンツ)というものです。
非対話型コンテンツについては後述しますが、ユーザーからの操作を想定していない要素です。
そのため、JavaScriptで記述された「クリック!」「スワイプ!」以外の操作を行うことができません。

初期む◯ん丸のカードをコンプリートした経験のあるファンでも「クリック!」しなければ皿を投げられない仕様は、UXにプラスとして働くことは決してありません。
img要素の操作性を向上させるためにはどうすれば良いのか、非対話型コンテンツのclickイベントがなぜ良くないのかについて学びます。

(この記事は、該当サイトの操作性を批判するものではありません。)
(また、むてん○のカードの話は本題とは全く関係ありません。)

img要素と非対話型コンテンツ

HTML LSにはflow contentphrasing contentなどのContent Model(コンテンツモデル)というグループのようなものが存在します。
各要素は、コンテキストに応じたコンテンツモデルに属し、子要素に入れて良い要素などが定められています。
https://html.spec.whatwg.org/multipage/dom.html#content-models

その中にinteractive content(対話型コンテンツ)というものが存在します。
対話型コンテンツは、ユーザーの操作のやり取りを期待するコンテンツモデルです。
<button><input>などが該当します。
コンテンツカテゴリー - 開発者ガイド | MDN より 対話型コンテンツ

本件のimg要素は操作をやり取りするものでないので、対話型コンテンツには属しません。
useMap属性があると対話型コンテンツとなるのですが、<map>を持たない限りは適切ではありません。
useMapを使わない限りは、操作の対象にはならないからです。
<input>についても、hidden属性がついているものは対話型コンテンツではなくなります。
あくまでも「ユーザーの操作」があることを期待しているようです。

要するに、ユーザーの操作を前提としない要素はnon-interactive content(対話型コンテンツ)です。
具体的には、下記のものが対話型で、それ以外は非対話型となります。

  • a(href属性があるもの)
  • audio(controls属性があるもの)
  • button
  • details
  • embed
  • iframe
  • img(usemap属性があるもの)
  • input(hiddenでないもの)
  • label
  • object(usemap属性があるもの)
  • select
  • textarea
  • video(controls属性があるもの)
  • tabindex="0"以上の全ての要素

対話型の中でも特に汎用的なのが<button><a href="###">になります。
これらはブラウザ側でEnterキー(buttonの場合はスペースキーも含む)を押すと、clickイベントとして発火させる役割を持ちます。
「ボタンには<button>を使うべき」や、「<a>hrefを省略するな」の理由の一つはこれです。
(aのhrefを省略すると非対話型コンテンツになります)。

ユーザーの操作が可能である要素は、対話型コンテンツであるべきです。
セマンティックとしての話だけでなく、webの標準に則って統一された操作性を担保できるためです。
従って、非対話型であるimg要素にclickイベントを設定するのは、(本当は)正しくありません。

対話型コンテンツへ近づける

それでは、useMapでないimg要素を対話型コンテンツにすることができるのか、試してみます。
結論から言えば、近づけることはできますが、やめたほうがいいです。

下記のimg要素は、クリックすると、動作はしますが正しくありません。
もちろんフォーカスすることも、Enterキーで動作させることもできません。

html
<img id="imgButton" src="foo.jpg" alt="クリックすると何かが起こる…?">
javascript
imgButton.addEventListener('click', () => {
  alert('何かが起こった');
});

HTML LSによると、すべての要素は、tabindex="0"を設定すると対話型コンテンツになるようです。
https://html.spec.whatwg.org/multipage/dom.html#interactive-content

早速img要素にtabindex="0"を設定してみます。
フォーカスもできるようになったし、いい感じです。

html
<img id="imgButton" src="foo.jpg" alt="クリックすると何かが起こる…?" tabindex="0">

しかし、実はこれもダメなパターンです。
このtabindex属性は、そもそも対話型コンテンツにつけるべき属性になっています。

その理由はアクセシビリティにあります。
元々フォーカスできない非対話型コンテンツが混ざると、アクセシビリティツリーに追加されなかったり、余計なタブオーダーを量産して操作上の混乱を招く可能性があるようです。
tabindex - HTML: HyperText Markup Language | MDN
(Auditなどで使われるaXeなどは、この問題を検知できませんが、FirefoxのInspectorは検知します。)

ではimg要素をbuttonとして認識させるために、roleを設定します。
この要素はbuttonの役割を持っているので、tabindexでの問題は消えました。

html
<img role="button" id="imgButton" src="foo.jpg" alt="クリックすると何かが起こる" tabindex="0">

しかし、まだダメなパターンです。
Enterキーの押下を拾えていません。

ロールはネイティブな要素の振る舞いを再現しません。
先述した通り、<a>または<button>の場合はキーボードでの発火が保証されますが、
自分を<button>だと思い込んでいる<img>に対しては、その効力を発揮しないようです。

しょうがないのでキーボードイベントを作成します。

javascript
function handleEvent() {
  alert('アラート出た');
}

img.addEventListener('click', () => {
  handleEvent();
});

img.addEventListener('keydown', (e: KeyboardEvent) => {
  if (e.keyCode === 13 || e.keyCode === 32) {
    handleEvent();
  }
});

実のところ、これでは<button>の挙動を再現できていません。

実際に操作するとわかるのですが、buttonのclickイベントを発火させる時の挙動として、Enterキーはkeydown、スペースキーはkeyupをシミュレートしているようです。
ところが、スペースキーには元々ページスクロールの役割があります。buttonロールとtabindex="0"をつけた非対話型コンテンツでは、これを抑制することができず、スクロール抑制の処理が必要になります。
また、IE, Chromiumベースのブラウザは、button上でスペースキーを押している間:active状態となり、これの再現も必要です。
どう考えても現実的ではないので、やりませんが、スペースキーで操作するユーザーのために、せめてEnterキーの挙動として再現させる必要はあります。

とにかく、こうしてimg要素を対話型のボタンとして扱うことに成功しました。

…というのは嘘で、まだダメなボタンです。
Firefoxでは、ネイティブでフォーカス可能な要素が優先されるので、例えばtabindex="0"のimg要素の後に<button>があると、そちらが優先されて実質フォーカスが飛びます。

このように、無限の苦しみを味うことになるので、<button>を使うのが手っ取り早いです。

html
<button id="imgButton" aria-label="実行すると何かが起こるボタン" type="button">
  <img role="presentation" src="foo.jpg" alt="">
</button>

これによりネイティブレベルのフォーカスと、キーボード操作を保証します。

まとめ

  • ユーザーの操作を期待しない要素はnon-interactive content(非対話型コンテンツ)である
  • img要素のような非対話型コンテンツに、clickイベントを設定するのはオススメしない
  • 非対話型コンテンツをフォーカス可能にする場合は、roleの指定も必要
  • 非対話型コンテンツにclickイベントを設定する時は、キーボード操作の実装も必要
  • roleはネイティブな要素の振る舞いを再現しない、自分で再現するのはなかなか難しい

参考

eslint-plugin-jsx-a11yのルールには、今回の記事のことが含まれています。

eslint-plugin-jsx-a11y: no-noninteractive-element-interactions
eslint-plugin-jsx-a11y: no-noninteractive-tabindex
eslint-plugin-jsx-a11y: click-events-have-key-events