4 ポイント 投稿者 GN⁺ 2023-09-30 | 1件のコメント | WhatsAppで共有
  • Red Blob Gamesのインタラクティブなダイアグラムでは、読者がオブジェクトを直接動かすと画面が即座に反応する必要があるため、入力処理を Pointer Events 中心に整理し、マウスとタッチの両方を扱う
  • 以前の mousedown/mouseup/mousemove 方式では、ポインターがオブジェクトの外に出たり、外でボタンを離したりすると ドラッグ状態が壊れることがあり、グローバルハンドラーのような補完が必要だった
  • タッチ入力はイベント伝達方式がマウスと異なり、1本指ドラッグがページスクロールまで引き起こすため、touchstartpreventDefault() を呼び出して衝突を減らす
  • 最終的なレシピは pointerdownpointermovepointeruppointercancelsetPointerCapture() を組み合わせ、左ボタンと開始オフセットを確認してオブジェクトが突然飛ばないようにする
  • テキスト・画像を含むオブジェクト、マルチタッチ、ネストしたドラッグ、<canvas>、hover と drag の組み合わせ、lostpointercapture のような プラットフォームごとの例外 は、実際の UI では別途処理が必要になる

ドラッグ入力ハンドラーの目標

  • 多くのインタラクティブなページには、読者が直接動かす ドラッグ可能なオブジェクト があり、ダイアグラムはオブジェクト位置の変化に反応する必要がある
  • 目標は、マウス入力とタッチ入力の両方で動作する入力イベントハンドラーを作ること
  • 基本の流れは、pointerdown でドラッグを開始し、pointermove で位置を更新し、pointerup またはキャンセルイベントでドラッグを終了する構成である
  • 実際の実装では、マウスボタン、複数指タッチ、イベント伝達先、右クリックのコンテキストメニューのため、単純なモデルより複雑になる
  • 基本レシピは次の要素を組み合わせる
    • pointerdownevent.button !== 0 なら return して 主ボタン のみ処理
    • イベント座標をダイアグラム座標に変換
    • 開始時にオブジェクト位置とポインター位置の差である dxdy を保存
    • el.setPointerCapture(event.pointerId) でポインターキャプチャを設定
    • pointermove で保存したオフセットを加えて state.pos を更新
    • pointeruppointercancelstate.dragging = null
    • touchstartpreventDefault() を呼び出す
  • このレシピは入力イベントハンドラーであり、実際の出力処理は別の状態ハンドラーに接続する必要がある
  • 完全なコードは examples page にあり、オブジェクト移動ではなく数値の左右ドラッグ、キャンバスペインティング、制限付きドラッグの例も含まれる
  • テスト範囲は Gecko/Firefox、Blink/Chrome、WebKit/Safari の Mac、Windows、Linux、Android、iPhone、iPad である
  • hover 可能な stylus、ハイブリッドなタッチ+マウス機器、音声入力はテストしていない
  • この方式は、要素を別の要素の上に置く HTML Drag and Drop API ではなく、ダイアグラムではマウス・タッチイベントを直接読み取る必要がある

マウスイベントだけを使う場合の限界

  • 初期実装は mousedownmouseupmousemove をドラッグ要素に直接付ける方式だった
  • 見た目には動作するが、2つの問題が発生する
    • ポインターを素早く動かすと、もはや円の上にいないためイベントを受け取れない
    • 円の外でボタンを離すと、dragging 状態に閉じ込められることがある
  • 改善策は、mousedown は円で受け、ドラッグ中の mousemovemouseupdocument に一時的に付ける方式である
  • mouseupdocument に付けた mousemovemouseup ハンドラーを削除すると、マウスドラッグはより安定して動作する
  • この方式はマウスだけを処理し、タッチイベント は扱わない

タッチイベントの違い

  • タッチ入力では mousedownmouseupmousemove の代わりに touchstarttouchendtouchmove を使う
  • タッチイベントは touchstart の後、自動的に キャプチャ され、すべての touchmove が元の要素に配信される
  • この特性のため、マウスイベントのように document にグローバルイベントハンドラーを一時的に付ける必要がない
  • ブラウザーがタッチシーケンスをキャンセルする必要があるときは touchcancel を送る
  • タッチドラッグ実装では、touchstartpreventDefault() を呼び出し、touchmoveevent.changedTouches[0] の座標を使って位置を更新する
  • この方式はタッチだけを処理し、マウスイベント は処理しない

Pointer Eventsでマウスとタッチを統合する

  • マウスとタッチの両方を処理するには多くのイベントハンドラーが必要で、2021年以前はそのような方式を使っていた
  • 2011〜2014年には d3 プロジェクトで d3-drag を使い、d3 を使わないプロジェクトでは別個のマウス+タッチコードを書いていた
  • Pointer Events は、マウスとタッチの処理を統合して単純化する
    • MS IE は 2012年にサポートを追加
    • Chrome は 2017年、Firefox は 2018年、Safari は 2020年にサポートを追加
  • ブラウザーの動作は時間とともに変化し、2017年の Chrome では一部イベントがデフォルトで passive mode に変わり、オブジェクトをドラッグしようとするとページがスクロールする問題が生じた
  • Safari と Firefox も 2018年に同じ変更を適用した
  • pointer capture を使うと、マウスイベントで必要だった document グローバルハンドラーの追加・削除なしに、より単純なロジックで処理できる

Pointer Captureでポインター追跡を維持する

  • Pointer capture は、ポインターが円、ダイアグラム、ブラウザーウィンドウの外に出ても追跡を継続できるようにする
  • マウスイベントでは同じ効果のために document にイベントハンドラーを付ける必要があったが、Pointer Events では setPointerCapture(event.pointerId) のほうが単純である
  • ポインターキャプチャがないと、素早くドラッグしたりダイアグラムの外に出て戻ったりしたときに、ドラッグが止まることがある
  • ダイアグラムの外でボタンを離した場合や、Alt+Tab で別ウィンドウに移動した場合も、キャプチャの有無によってドラッグ終了処理が変わる
  • タッチデバイスではポインターキャプチャがデフォルト動作だが、マウスでは明示的に設定する必要がある

タッチスクロールと preventDefault()

  • タッチデバイスでの1本指ドラッグは、デフォルトではページスクロールを引き起こす
  • 同じ1本指ドラッグで円も動くと、スクロールとオブジェクトドラッグ が同時に発生する
  • 最も単純な解決策はダイアグラムに CSS touch-action: none を適用することだが、この場合はダイアグラムのどこでもスクロールできなくなる
  • 望ましい動作は、円をドラッグしているときだけスクロールを防ぎ、ダイアグラム自体をドラッグしているときはスクロールを許可すること
  • 比較結果は次のとおり
    • ダイアグラム全体に touch-action: none: 円ドラッグとダイアグラムドラッグの両方でスクロール停止
    • デフォルト値: どちらもスクロールを防げない
    • 円だけに touch-action: none: 期待どおりには動作しない
    • touchstartpreventDefault(): 円ドラッグ中のスクロールだけを防ぎ、ダイアグラムのスクロールは許可
  • preventDefault()pointerstart ではなく touchstart に付ける
  • この方式はほとんどの状況で動作するが、desktop Firefox の “mobile emulation mode” では動作せず、Android の mobile Firefox では動作した

ドラッグ開始オフセットの保存

  • オブジェクトの端をつかんでドラッグするときに、オブジェクトの中心がポインタ位置へ瞬間移動すると操作感が悪くなる
  • 解決策は、ドラッグ開始時にオブジェクト中心とポインタ位置の相対位置を保存すること
  • その後の移動では、現在のポインタ座標に保存した dxdy を足してオブジェクト位置を計算する
  • この方式では、state.dragging を単なる true / false ではなく、{dx, dy} のような相対位置情報に変える
  • マウスでは端をつかんだときのジャンプ差がよく見えるが、タッチでは指で隠れるため目立ちにくいことがある
  • 関連する細かなディテールは little details のページにもまとまっている

コンテキストメニューとマウスボタン

  • コンテキストメニューの有効化方法はプラットフォームごとに異なる
    • Windows: 右クリック down+up、Shift+F10
    • Linux: 右ボタン down、Shift+F10
    • Mac: 右ボタン down、Ctrl+左クリック
    • iOS: テキストでのみ long press
    • Android: どこでも long press
  • 問題は、pointerdown は見えても pointerup が常に見えるとは限らないこと
  • 実際にはボタンが離されているのに、コード上ではまだ押された状態だと判断してしまうことがある
  • 対応策は、右ボタンを無視して左マウスボタンでのみ dragging 状態を設定すること
  • Pointer Events 仕様の button 属性では button = 0 が primary button を意味し、これを使って中ボタン・右ボタンを除外する
  • Mac の Chrome と Safari では Ctrl+クリックが左ボタンの pointerdown のように入ってくることがあるため、event.ctrlKey も確認する必要がある
  • 選択肢は次のとおり
    • contextmenupreventDefault() を使ってメニューを防げるが、デフォルトの中クリックやドラッグ動作と衝突する可能性がある
    • 左ボタンのみでドラッグすれば、右クリックまわりの差異の大半を避けられる
    • Mac の Ctrl+クリックまで避けるなら、ctrlKey も除外する必要がある
  • ボタン変化が pointerdownpointerup として伝わらなくても、pointermove のボタン状態から一部を確認できる

テキストや画像を含むオブジェクト

  • ドラッグ可能な要素の中にテキストや画像がある場合は追加処理が必要になる
  • 円をドラッグしている間に内部テキストが選択されることがある
  • 解決策は CSS の user-select: none を適用すること
    • 常に適用すると内部テキストは常時選択できなくなる
    • ドラッグ中だけ適用すれば、通常時はテキスト選択を可能にできる
  • 2023年初頭時点では Safari は unprefixed の user-select をまだサポートしておらず、webkitUserSelect もあわせて設定する必要がある
  • Windows、Linux、Mac はテキストや画像のアプリケーション間 drag and drop をサポートしており、これがオブジェクトのドラッグと衝突する
  • dragstartpreventDefault() を呼び出すと、選択されたテキストや画像のドラッグよりオブジェクトドラッグを優先できる
  • ブラウザや OS によって動作は少しずつ異なる

複数ポインタと同時ドラッグ

  • 複数の指や複数のマウスで複数オブジェクトを同時にドラッグできるかはエッジケースである
  • タッチでは、提示されたコードはすでに複数オブジェクトの同時ドラッグに対応している
  • 2本の指で同じオブジェクトをドラッグする場合は別途処理が必要になる
  • 解決策は、pointerdownevent.pointerIdstate.dragging または状態に保存し、pointermove で同じ pointerId でなければ無視すること
  • この実装例は本文には完全には反映されておらず、canvas dragging test で確認できる
  • マウスについては、Pointer Events 仕様では一般的な OS とユーザーエージェントは通常、複数マウス入力という概念を持たないと述べている
  • トラックパッドと外付けマウスが同時にあっても通常は1つのマウスポインタとして扱われるため、別々のマウスで別々のオブジェクトをドラッグする方法はないと考えられる

複数のマウスボタンを同時に押した場合

  • 複数のマウスボタンを同時に押すと、Pointer Events と Mouse Events では動作が異なる
  • Mouse Events は各ボタン押下ごとに mousedown を、各ボタン解放ごとに mouseup を送る
  • Pointer Events 仕様によれば、最初に押されたボタンで pointerdown が発生し、最後に解放されたボタンで pointerup が発生する
  • このため、pointerdownpointerup が異なるボタン状態で発生することがある
  • たとえば左ボタンを押し、次に右ボタンを押し、その後左ボタンを離すと、オブジェクトがドラッグ状態のまま残ることがある
  • 解決策は、pointermoveevent.buttons & 1 により左ボタン状態を確認し、もう押されていなければ end(event) を呼び出すこと
  • ポインタキャプチャは、すべてのボタンを離すまで継続する可能性があり、明示的にキャプチャを解除しない限り続くことがある
  • この実装は、この問題や複数のエッジケースをすべて処理しているわけではない

ネストしたドラッグ要素

  • ドラッグ可能な要素の中に別のドラッグ可能な要素があると、内側と外側の要素の両方がドラッグを処理してしまう
  • 解決策は、内側のドラッグ要素で event.stopPropagation() を呼び出し、イベントが外側のドラッグ要素へ伝播しないようにすること
  • pointerdownpointermove の両方で伝播を止める方法が使われる
  • デモは本文にはないが、Vue playground example で、赤いドラッグ要素が黄色いドラッグ要素の子になっている例を確認できる

Canvasでのドラッグ

  • 通常はSVGを使うが、<canvas>ではドラッグ可能な要素自体にイベントハンドラーやマウスポインターの形を設定できない
  • 2D CanvasやWebGLでは、イベントハンドラーを<canvas>に付け、座標がドラッグ可能なオブジェクト上にあるかを自分で判定する必要がある
  • 処理方法は次のとおり
    • pointerdown, touchstart, dragstart: ドラッグ可能なオブジェクト上でなければすぐにreturnする
    • pointermove: ドラッグ可能なオブジェクト上かどうかに応じてカーソルを設定する
  • デモはexamples pageにある

マウスHoverとタッチDragの組み合わせ

  • ある種のダイアグラムは、マウスではボタンを押さないhoverで反応し、タッチデバイスではhoverがないためdragで反応する必要がある
  • 基本のレシピでは、マウスとタッチの両方でdragを想定している
  • マウスhoverとタッチdragを併用するには、pointermoveif (!state.dragging)という条件を削除する
  • Responsive Design pageでは、マウスで行の上にhoverするとレイアウトが変わり、タッチではdragでレイアウトが変わる
  • Hexagons Guideでも、多くのダイアグラムがマウスhoverとタッチdragを併用している
  • タッチで項目AからBへドラッグするとき、キャプチャが維持されるとmoveイベントがAに送られるため、Bをハイライトするにはキャプチャの解除が必要になることがある

Toggle Paintとドラッグ状態の拡張

  • Rounded Cell Painterでは、pointerdownが初期のペイント色をキャプチャする
  • その後、pointerupまでのすべてのpointermoveは同じペイント色を使う
  • そのため、state.draggingには初期のx, yだけでなく、初期のペイント色も含める必要がある
  • デモはexamples pageにある

lostpointercaptureのエッジケース

  • lostpointercaptureは、ポインターキャプチャを失ったことを検知するために使える
  • どのような状況で発生し、何をすべきかは、まだ完全には整理されていない
  • コメントで取り上げられていたシナリオは次のとおり
    • 左ボタンで円のドラッグを開始
    • 左ボタンを押したままドラッグ領域の外へ移動
    • 右ボタンを押したあと、左ボタンを離す
    • ドラッグ領域の外のどこかをクリック
  • standardでは、ポインターキャプチャが解除されたあと、ユーザーエージェントはlostpointercaptureイベントを必ず発生させなければならないとしている
  • 実際のブラウザーの挙動はかなり異なり、lostpointercaptureが発生するシステムもあれば、別の場所をクリックするまで発生しないシステムもある

1件のコメント

 
GN⁺ 2023-09-30
Hacker News の意見
  • ドラッグを扱った記事ですが、Amit が挙げている落とし穴はほぼ全部経験していて、同じ解決策にたどり着きました。素晴らしい記事です。
    ユーザーの立場では、ドラッグによる並べ替えはとても自然に見えますが、サイズがばらばらの項目、項目間のプレースホルダー生成、エッジ検出、ボックスフィット・アルゴリズムの沼まで踏み込むと、自前で実装するのはかなり難しい部類に入ります。

    • こういうときに気に入っている、かなり一般的なコツがあります。ユーザーがドラッグを開始したら、あり得るすべてのドロップ先、または現在見えている対象のレイアウトを計算し、それぞれのレイアウトでドラッグ中のオブジェクトが最終的に置かれる位置を記録します。
      マウスが動くたびに、それらの位置のうち現在のドラッグオブジェクトの位置に最も近いものを選び、選ばれたレイアウトをレンダリングすればよいのです。複雑なレイアウトやリフローがあっても、感触よく動作します。
    • 私のインデックスカードアプリ Card Buddy でこの問題を解く必要がありました。[1] 面白い挑戦で、後にはもっと良い方法も見つけました。
      最終的には、カードを持ち上げるとそのカードがボードから削除されたかのようにレイアウトを計算するようにしました。そうすると簡単になります。マウスを乗せた位置にあったものを押しのけるだけでよいからです。
      それでも例外処理はものすごく多く、特にまだデータモデルに「コミット」されていない新しいカードを編集し始めるときは厄介でした。既存のカードを押しのけてゴーストカードの場所を作れるオプションも追加する必要がありました。
      手作業で処理しなければならない例外が多いと認めることが助けになります。もっと一般的で均質な解決策があるはずだ、という姿勢で臨むと設計で堂々巡りになります。いつかさまざまな例外ケースをブログ記事にまとめるべきかもしれません。
      さらに良いレイアウト方式も見つけました。不要な計算を減らし、レイアウトエンジンをより柔軟でユーザーフレンドリーにしてくれます。データモデルの表現をどう選ぶかが、解決策に非常に大きな違いを生むのは驚きです。
      [1] https://www.ussherpress.com/cardbuddy/
    • ここにネストまで対応するドラッグ並べ替えとなると、さらに大変になります。
    • 同感です。アイデアは単純でも、実装は残酷なほど大変になり得ます。ドラッグ選択も何年もの間、あまりに当たり前のものだと思っていました。
  • 記事ではキャンセル処理は扱っていないようです。たとえば Windows では、Escape を押すとドラッグがキャンセルされる慣例があります。
    場合によっては、定義された領域の外でマウスを離したらドラッグをキャンセルしたいこともあります。キャンセルはユーザーに、より素早い取り消し、あるいは「やっぱりドラッグするつもりじゃなかった」といった逃げ道を与えます。
    そのため、別の取り消し機能がなくても、ドラッグ開始時点の元の状態を保存しておき、キャンセルされたら復元する必要があります。
    領域外ドラッグキャンセルの場合には、キャンセル解除もあります。ポインタが領域外にあるときは元の状態に戻ったかのように視覚的に見せ、ポインタが再び領域内に入るとドラッグを再開する方式です。実際のキャンセルはマウスを離したときに起こりますが、ドラッグ中にもキャンセルされることを事前に視覚的に知らせるわけです。

    • Windows では、何かを遠くまでドラッグしすぎると、ドラッグ中のオブジェクトとポインタが突然消え、警告もなく元の位置へ跳ね返る動作があります。
      Microsoft がいまだにこれを良いUI設計だと信じているのが信じられません。ユーザーを完全に混乱させ、何が起きているのか分からず左マウスボタンを離すのを怖がったり、ヨーヨーのように繰り返したりする状況も見られます。
      キャンセルはしばしば見過ごされる大きな問題で、単純ではないという点には同意します。一般ユーザーは ESC が役に立つことすら知りません。
    • ほかのプラットフォームで、ESC 以外に処理すべきキャンセルキーが別にあるのか気になります。
  • Ardour [0] の GUI でさまざまなオブジェクトをドラッグする作業を20年やってきて学んだ追加の細部があります。すべてのボタン押下・解放イベントを、動きのないドラッグイベントとして処理すべきです。
    つまり、押下は常にドラッグを開始し、解放で終了する。そして解放を処理するコードが、動きなしという条件を特別扱いする、という方式です。
    [0] https://ardour.org/

    • その方式でも動くでしょうが、私はそうしていません。基本をきちんと押さえた後は問題ありませんでした。
      通常の mousedownmouseup のハンドラを置き、クリックなのかドラッグ開始なのか、あるいは別の動作なのかを判断するために、約150msの設定可能なタイムアウトを使います。mouseup が発生するか、マウスが画面外に出ていて進行中のドラッグがあれば stopdrag します。私にとってドラッグは、いくつもの特殊ケースの一つにすぎません。
    • そうする理由が気になります。
  • はるか昔、ミネアポリスに NSF が支援する geometry center がありました。計算群論の学会が開かれ、周辺分野からマスコット的な人たちが何人か招かれました。
    私は代数幾何学システムを書いていたおかげで招待され、-20°F の寒さの中、非常に早く起きてその日に使う Silicon Graphics ワークステーションを確保し、群の生成元と関係をよりよく理解するためのゲームをコーディングしました。
    そのゲームにはドラッグが入っていました。その結果、午前2時まで続いた会話が楽しいものになりました。私の考えでは、ドラッグが現実世界の物理法則に従う必要はなく、ドラッグは非常に強い LSD のタブのような感覚であるべきでした。皆がこれに夢中になり、それぞれアイデアを出しました。

  • この記事は、Web ブラウザで発生するさまざまな例外を脇に置けば、比較的基本的なドラッグを実装している。ガイドラインにスナップする、境界や他のオブジェクトとの衝突を防ぐ、ドラッグ操作に反応してサイズや位置が変わるアニメーション付きのドロップ対象など、制約のあるドラッグの資料があるのか気になる。
    以前、円形の時計を長さの異なる扇形のピースに分けて、集中/休憩の区間を定義するカスタム Pomodoro タイマー UI を作りたいと思っていた。その扇形ピースのドラッグによる並べ替えの実装は、あまり進まなかった。

    • このコードの方式では、ドラッグの動きが何らかの状態を更新する。状態を設定するときに制約条件を適用でき、その状態がまた表示を駆動する。
      制約システムをイベント処理システムから切り離したかった。jquery-ui のようなライブラリは両者を結び付けてしまうため、イベント処理システムが可能なすべての制約を知っている必要がある。jquery-ui は境界ボックス、軸、正方形グリッド、DOM 要素へのスナップをサポートしている。だが、六角形グリッド、対数スケールのグリッド、円形境界にスナップしたい場合はサポートされていない。
      コード内に state.pos = … が見えるが、そこが状態を設定している場所だ。制約では pos を setter の背後に隠す。そうすれば、ドラッグイベント処理コードがどの制約が必要かを知らなくても、setter が制約を適用できる。
      ページを更新して制約の例を見せるべきだと思う。コードレシピでこの側面に触れるのを完全に忘れていた。
      古い制約の例は https://www.redblobgames.com/articles/curved-paths/making-of... にあり、衝突回避の例は https://redblobgames.github.io/circular-obstacle-pathfinding... にある。ただし、ドラッグによる並べ替えやアニメーション付きドロップ対象はまだ試していない。
  • このサイトには私も感謝している。Uber で地理分析用の六角形システムを設計したとき、https://www.redblobgames.com/grids/hexagons/ を大いに参考にした。

  • きちんとしたインタラクションを扱うときに出てくる落とし穴や注意点をよく整理した記事だ。
    より「すぐ使える」ものとしては、複数のプロジェクトで interactjs をかなり長く使ってきた。

  • みなさんありがとう。今日 HN に載って驚いた。質問には答えられる。

    • HTML 組み込みの draggable="true" 属性とドラッグイベントを見たことがあるのか気になる。
      組み込み機能がこのコードと比べて足りない点があるのかも気になる。
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...
    • JavaScript のドラッグに関するすばらしい議論で、情報が本当に役に立った。
      簡単な質問がある。dragstart のような組み込み DOM イベントで、ドラッグの動きを 1 つの軸に制限するにはどうすればよいのか気になる。dragstart/dragenter/dragover/drop イベントでドラッグ&ドロップ機能を実装したが、動きを x 軸に制限する手早い方法を見つけられなかった。JQuery のドラッグ&ドロップ API はこれをサポートしていたと記憶している。ネイティブの DOM イベント/API だけを使おうとしているので、関連情報や手がかりがあればありがたい。
    • 以前はブラウザ UI コードをいつも書いていたが、数年離れていた。誰かがポインターイベントを使うという話は初めて聞いた。
      Safari が 2020 年にサポートを追加したことを考えると、ブラウザがこれをサポートしていないのではと心配しないのか気になる。macOS Mojave ユーザーも Safari 13 や 14 を使っているはずなので、Safari 12 は実際にはもう使われていないことを願うが、一般の Web ユーザー向けのプロダクションアプリで、ドラッグのような単純な機能が動かないとかなりまずい。
      ドラッグ中に document にイベントハンドラーを追加するのは昔からの慣行であり、ブラウザは特定のユースケースを単純化しようとする新機能を追加し続けているが、それぞれ例外や落とし穴がある。記事でも、まだ完全には扱っていないと言っている。最終結果にもポインターイベントとタッチイベントの組み合わせが残っている。
      コードが減るという意味での「単純さ」が、追加の例外、低いブラウザサポート、そして開発者がマウスイベントよりも理解や文書化が進んでいないかもしれないポインターイベントの詳細やブラウザ差異を知る必要があるというコストを払うほど価値があるのか気になる。
  • 抜けているものが1つある:アクセシビリティ。キーボード操作でドラッグをどう可能にすべきだろうか?

    • ここでは少し的外れな質問のように思う。ペイントソフトで、キーボード利用者がキャンバスをパンしようとしてクリックしたままにする動作をどうするのか、と聞くのに似ている。そんなことはしない。マウスでパンしたりオブジェクトをドラッグしたりするのはインターフェースの1つにすぎず、課題はキャンバスのパンであり、それを達成する方法はいろいろある。
      私の例でアクセシビリティの答えは、その作業を完了するためのキーボード操作を追加で提供することだ。可能なら、キーを押し続けなくてもよい方式が望ましい。たとえばショートカットキーでパンモードに入り、矢印キーでビューポートをキャンバス上で移動させれば解決する。
      ドラッグ可能なオブジェクトの場合、作業は順序変更だ。これをアクセシブルにするには、順序を変える代替手段を提供すればよい。たとえば TAB でオブジェクト間のフォーカスを巡回し、キーでフォーカスされたオブジェクトを選択したあと、矢印キーでドラッグのプレビューをその方向の最も近い有効位置へ移動する、といった具合だ。
    • おおむね答えは「そうはしない」に近い。UI は、ポインターやタッチデバイスを使えない人でもページとやり取りできるよう、ドラッグの代替手段を提供すべきだ。
      つまり、ドラッグ実装の中にキーボード対応を無理に押し込むのではなく、ドラッグ機能とは別に意味のあるキーボード機能を追加すべきだ。
    • それは移動可能なオブジェクトになるだろうし、この実装をベースにすれば簡単に作れそうだ。
    • TAB / Space / Enter で項目を選択し、Ctrl + 矢印キーで「ドラッグ中」の要素を動かせばよい。当然、キーを1回押すごとに1ブロックずつしか移動できない。
  • いいね。今週の初めに Vue アプリで SVG 要素のドラッグを実装していたのだが、筆者が説明したほぼすべてを同じ順序で自分で発見する必要があり、最終的にもほぼ同じコンポーネントになった。
    ただし私は関数コンポーネントではなく、useDragging という composable ユーティリティとして書いた。

    • すばらしい。directive とスロットを持つコンポーネントは試してみたが、composable はまだやっていない。その方式でイベントハンドラをどう設定すべきか確信が持てなかったからだ。composable の経験はそれほど多くない。