非対話型コンテンツに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 content
や phrasing 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