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

HTML, JavaScript

はじめに

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

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

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

ソースを見ると、img 要素に dish クラスを設定し、click の listener を追加している。

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 要素はフォーカス不可能なため、キーボード操作でたどり着けない。
また、この img 要素は click イベントしか Listen しない。
つまり、マウスを持ったユーザーが「クリック!」しないと、コンテンツを楽しむことができない。

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

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

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

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 を省略するな」の理由の 1 つはこれ。
(a の href を省略すると非対話型コンテンツになる)。

ユーザーの操作が可能である要素は、対話型コンテンツであるべき。
セマンティックとしての話だけでなく、ブラウザの実装に則って統一された操作性を担保できるため。
従って、非対話型である img 要素に click の Listener を設定すると、悲しみが生まれてしまう。

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

それでは、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
(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.key === 'Enter' || e.key === ' ') {
    handleEvent();
  }
});

実のところ、これでは <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