リンクのようなボタンを作る2010年12月15日 23時52分

こんばんは、JavaScript Advent Calendar 2010、15 日目担当の nanto_vi (なんと) です。12 月 15 日が何の日か調べてみると東北本線が宮城県に到達した日とのこと。当時は上野から仙台まで 12 時間 20 分かかったそうです。それから 123 年を経た現在では同じ時間で鹿児島中央から新青森まで行けるようになり、鉄道の速度にも JavaScript の実行速度にも日進月歩を感じる今日この頃です。

さて、アプリケーションを作っていると、見た目はリンクのようだがリンクでない UI 部品を使いたくなるときがあります。ここで「リンクでない」とは、クリックしてもページ遷移が発生しないということです。このような UI 部品は、ページ遷移の代わりにメニューの表示といった何らかのアクションを引き起こす、すなわちボタンとして振舞います。

ユーザーインターフェース記述言語として HTML を使っているとき、この「リンクのようなボタン」をどのように実現すればいいのでしょうか。

input、button 要素

ボタンとして振舞うものはボタンとして記述すべきです。HTML では汎用的なボタンとして <input type="button"> 及び <button type="button"> が用意されています。スタイルシートを使えば見た目をリンクのようにもできるでしょうし、画像を使いたければ <input type="image"> もあります。

<input type="button" value="リンクのようなボタン" onclick="...">
<button type="button" onclick="...">リンクのようなボタン</button>

しかし、フォームコントロールに対してはスタイルシートが期待通り適用されないこともあり、ボタンをどうしてもインライン要素にしたいときにこの方法は取れません。

a 要素

リンクのように見えるならリンクにすればいいということで a 要素が使われることもあります。click イベントを処理するときにデフォルトアクションをキャンセルすれば、もともと指定してあったリンク先に飛ぶことはありません。あるいはリンク先に javascript:void(0); と指定することでページ遷移が発生しないようにします。

<a href="#" onclick="...; return false;">リンクのようなボタン</a>

しかし、a 要素はあくまでハイパーリンク、すなわち他のリソースへ移動するためのものです。外見がどうあろうとリンクでないものの記述に使うの好ましくありません。

span 要素

click イベントに対する処理はどんな要素にも付加できるので、a 要素にこだわる必要はありません。「リンクのような」の部分はスタイルシートで実現できます。

<style>
.trigger {
  color: #00f;
  text-decoration: underline;
  cursor: pointer;
}
</style>

<span class="trigger" onclick="...">リンクのようなボタン</span>

しかし、これではキーボードを用いてボタンにアクセスすることができません。多くのブラウザでは Tab キーでリンクやフォームコントロールへ移動できますが、span 要素はその対象から外れています。

span 要素 + tabindex 属性

tabindex 属性はフォーカス順を変更するものだと思っていたあなた、それはこの属性が持つパワーのほんの一部でしかありません。HTML5 において tabindex 属性は要素をフォーカス可能にする属性として生まれ変わったのです。この属性はどんな要素にもつけられ、値に 0 を指定すればその要素がキーボードアクセス可能になります。

<span class="trigger" tabindex="0" onclick="...">
  リンクのようなボタン
</span>

しかし、Tab キーでこのボタンにフォーカスし、Enter キーを押しても何もおきません。a要素によるリンクなら Enter キーを押すと click イベントが発生するのにも関わらずです。なお、Opera なら span 要素でも Enter キーにより click イベントが発生し、指定したアクションが実行されます。

span 要素 + tabindex 属性 + onkeypress 属性

click イベントが発生しないなら自分で click イベントに対する処理を呼び出せばいいのです。onclick 属性に指定したコードは onclick プロパティから関数として取得できます。Enter キーを表すキーコードは 13 なので、そのときのみ処理を実行します。

<span class="trigger" tabindex="0" onclick="..."
      onkeypress="if (event.keyCode === 13) this.onclick(event);">
  リンクのようなボタン
</span>

しかし、click イベントに対する処理が onclick 属性に書かれているとは限りません。addEventListener や attachEvent メソッドでイベントリスナが追加されていることもあれば、祖先要素、文書ノードで click イベントが処理されていることもあります。また、Opera ではこの場合 Enter キーを押すと onclick 属性の内容が2回実行されてしまいます。

span 要素 + tabindex 属性 + キーイベント処理

click イベントに対する処理を直接実行できなくとも、click イベントを発生させれば自然とそれらが実行されます。イベントを発生させるのに、DOM イベントモデルでは dispatchEvent、IE のイベントモデルでは fireEvent メソッドを用います。Opera 対策に keypress イベントのデフォルトアクションをキャンセルしておきましょう。

<script>
function activate(event) {
    event = event || window.event;
    if (event.keyCode !== 13) return;
    if (document.createEvent) {
        var e = document.createEvent('MouseEvent');
        e.initMouseEvent(
            'click', true, true, event.view, 1,
            event.screenX, event.screenY, event.clientX, event.clientY,
            event.ctrlKey, event.altKey, event.shiftKey, event.metaKey, 0, null
        );
        event.target.dispatchEvent(e);
        event.preventDefault();
    } else if (document.createEventObject) {
        var e = document.createEventObject(event);
        event.srcElement.fireEvent('onclick', e);
        event.returnValue = false;
    }
}
</script>

<span class="trigger" tabindex="0"
      onclick="..." onkeypress="activate(event);">
  リンクのようなボタン
</span>

しかし、視覚的なブラウザであれば見た目からこの要素が何らかのアクションを引き起こすことがわかりますが、環境によってもそもそもこれが「押せる」ということすら伝わらないかもしれません。

span 要素 + tabindex 属性 + キーイベント処理 + role 属性

視覚によらず、機械的に UI 部品を認識するための仕様として WAI-ARIA があります。要素がボタンの「役割」を果たすことを表すには、role 属性の値に "button" を指定します。これで人の目からだけでなく、機械から見たときもボタンとして認識・操作できるようになりました。

<span class="trigger" role="button" tabindex="0"
      onclick="..." onkeypress="activate(event);">
  リンクのようなボタン
</span>

サンプル

リンクのようなボタンのサンプルで実際の挙動を確認できます。

終わりに

以上はあくまで JavaScript の使える環境が前提です。スクリプトが動かなくとも最低限の機能は利用できるよう気をつけましょう。スクリプトが動く場合でも、本当にリンクのようなボタンでなければいけないのか、通常のボタン、またはハイパーリンクでは実現できないのかよく検討した上で使うようにしてください。

参考資料