今後の方向性を示す

Sérgio Gomes

ウェブ上のものを指す操作は、以前は簡単でした。マウスを動かしたり、ボタンを押したりするだけでした。マウス以外のものはすべてマウスとしてエミュレートされ、デベロッパーは信頼できるものを正確に把握していました。

ただし、シンプルだからといって必ずしも良いとは限りません。時間の経過とともに、すべてがマウス(またはマウスのふりをするデバイス)である必要がなくなってきました。圧力感知や傾斜検知に対応したペンを使用すれば、驚くほど自由に創作できます。指を使えば、デバイスと手さえあれば作業できます。それに、複数の指を使うと作業がはかどります。

そのために、タッチイベントが用意されていますが、これはタッチ専用のまったく別の API であり、マウスとタッチの両方をサポートするには、2 つの別々のイベントモデルをコーディングする必要があります。Chrome 55 では、両方のモデルを統合した新しい標準であるポインタ イベントが搭載されています。

単一イベントモデル

ポインタ イベントは、ブラウザのポインタ入力モデルを統合し、タップ、ペン、マウスを 1 つのイベントセットにまとめます。次に例を示します。

document.addEventListener('pointermove',
    ev => console.log('The pointer moved.'));
foo.addEventListener('pointerover',
    ev => console.log('The pointer is now over foo.'));

使用可能なすべてのイベントのリストは次のとおりです。マウス イベントに精通している方には、よく見慣れたものと思われます。

pointerover ポインタが要素の境界ボックスに入った。 これは、ホバーをサポートしているデバイスでは直ちに、サポートしていないデバイスでは pointerdown イベントの前に発生します。
pointerenter pointerover に似ていますが、バブルアップせず、子孫を異なる方法で処理します。仕様の詳細
pointerdown ポインタがアクティブなボタン状態になりました。入力デバイスのセマンティクスに応じて、ボタンが押されたか、接触が確立されています。
pointermove ポインタの位置が変更されました。
pointerup ポインタがアクティブなボタンの状態から離れた。
pointercancel ポインタがイベントを送信する可能性が低いことを意味する何かが発生しました。つまり、進行中のアクションをキャンセルして、ニュートラルな入力状態に戻す必要があります。
pointerout ポインタが要素または画面の境界ボックスから外れた。また、デバイスがホバーをサポートしていない場合は、pointerup の後にも発生します。
pointerleave pointerout に似ていますが、バブルアップせず、子孫を異なる方法で処理します。仕様の詳細
gotpointercapture 要素がポインタ キャプチャを受け取った。
lostpointercapture キャプチャ中だったポインタが解放されました。

さまざまな入力タイプ

一般的に、ポインタ イベントを使用すると、入力に依存しない方法でコードを記述できます。異なる入力デバイスに個別のイベント ハンドラを登録する必要はありません。もちろん、ホバーの概念が適用されるかどうかなど、入力タイプの違いに注意する必要があります。異なる入力デバイスの種類を区別する場合(異なる入力に別々のコードや機能を提供する場合など)は、PointerEvent インターフェースの pointerType プロパティを使用して、同じイベント ハンドラ内で区別できます。たとえば、サイド ナビゲーション ドロワーをコーディングする場合、pointermove イベントに次のロジックを設定できます。

switch(ev.pointerType) {
    case 'mouse':
    // Do nothing.
    break;
    case 'touch':
    // Allow drag gesture.
    break;
    case 'pen':
    // Also allow drag gesture.
    break;
    default:
    // Getting an empty string means the browser doesn't know
    // what device type it is. Let's assume mouse and do nothing.
    break;
}

デフォルトのアクション

タッチ対応のブラウザでは、特定の操作でページのスクロール、ズーム、更新を行います。タッチイベントの場合、これらのデフォルト アクションが行われている間もイベントを受信します。たとえば、ユーザーがスクロールしている間も touchmove は引き続き発生します。

ポインタ イベントでは、スクロールやズームなどのデフォルト アクションがトリガーされるたびに pointercancel イベントが送信され、ブラウザがポインタを制御していることが通知されます。次に例を示します。

document.addEventListener('pointercancel',
    ev => console.log('Go home, the browser is in charge now.'));

組み込みの速度: このモデルでは、タッチイベントと比較して、デフォルトでパフォーマンスが向上します。タッチイベントでは、同じレベルの応答性を実現するためにパッシブ イベント リスナーを使用する必要があります。

ブラウザが制御を取得しないようにするには、touch-action CSS プロパティを使用します。要素で none に設定すると、その要素で開始されたブラウザ定義のアクションがすべて無効になります。ただし、pan-x など、よりきめ細かい制御が可能な値も多数あります。これにより、ブラウザが x 軸の動きには反応し、y 軸の動きには反応しないようにできます。Chrome 55 では、次の値がサポートされています。

auto デフォルト: ブラウザはデフォルトのアクションを実行できます。
none ブラウザはデフォルトのアクションを実行できません。
pan-x ブラウザは、横方向のスクロールのデフォルト アクションのみを実行できます。
pan-y ブラウザは、垂直スクロールのデフォルト アクションのみを実行できます。
pan-left ブラウザは、水平スクロールのデフォルト アクションのみを実行でき、ページを左にパンするのみです。
pan-right ブラウザは、横方向のスクロールのデフォルト アクションのみを実行でき、ページを右にパンすることのみが許可されます。
pan-up ブラウザは、垂直スクロールのデフォルト アクションと、ページを上方向にパンする操作のみを実行できます。
pan-down ブラウザは、垂直スクロールのデフォルト アクションと、ページを下にパンする操作のみを実行できます。
manipulation ブラウザはスクロールとズームのアクションのみを実行できます。

ポインタ キャプチャ

mouseup イベントが機能しない問題をデバッグして 1 時間も費やした後、ユーザーがクリック ターゲットの外側のボタンを離していることに気づいたことはありませんか?思いませんか?私だけかもしれませんね。

しかし、これまでは、この問題に対処する良い方法がありませんでした。ドキュメントに mouseup ハンドラを設定し、アプリケーションに状態を保存して、状態を追跡することは可能です。ただし、これはきれいなソリューションではありません。特に、ウェブ コンポーネントを構築していて、すべてをきれいに分離しようとしている場合はなおさらです。

ポインタ イベントを使用すると、はるかに優れたソリューションを実現できます。ポインタをキャプチャできるため、pointerup イベント(またはその他の捉えにくいイベント)を確実に取得できます。

const foo = document.querySelector('#foo');
foo.addEventListener('pointerdown', ev => {
    console.log('Button down, capturing!');
    // Every pointer has an ID, which you can read from the event.
    foo.setPointerCapture(ev.pointerId);
});

foo.addEventListener('pointerup', 
    ev => console.log('Button up. Every time!'));

ブラウザ サポート

執筆時点では、ポインタ イベントは Internet Explorer 11、Microsoft Edge、Chrome、Opera でサポートされており、Firefox では部分的にサポートされています。最新のリストは caniuse.com で確認できます。

ギャップを埋めるには、ポインタ イベント ポリフィルを使用できます。または、ランタイムでブラウザのサポートを確認することも簡単です。

if (window.PointerEvent) {
    // Yay, we can use pointer events!
} else {
    // Back to mouse and touch events, I guess.
}

ポインタ イベントは、段階的な拡張に最適です。初期化メソッドを変更して上記のチェックを行い、if ブロックにポインタ イベント ハンドラを追加し、マウス/タップ イベント ハンドラを else ブロックに移動するだけです。

ぜひお試しいただき、ご感想をお聞かせください。