ドラッグ可能な要素を作成する

パネル、パレット、ツールバー、インスペクタ、ダイアログなどの既存のフローティング要素にドラッグを追加します。ドラッグ操作は、要素の表示、閉じ、積み重ね、フォーカスを決定するものではありません。

専用のハンドルを使用する

要素に選択可能なテキスト、ボタン、入力、リンクが含まれている場合は、専用のハンドルからドラッグを開始します。これにより、通常のコンテンツは引き続き使用できます。キーボード操作でハンドルにフォーカスできるようにし、矢印キーによる操作を説明するユーザー補助機能用の名前を付けます。

ハンドルに role="application" を塗布しないでください。支援技術の操作が不必要に変更される。

cursor: grab / grabbing、touch-action: none、user-select: none、-webkit-user-select: none を、ドラッグ可能な要素全体ではなく、ハンドルに適用します。これにより、ハンドルの外側のテキストは選択可能なままになり、タップ操作は通常の動作を維持します。

.drag-handle {
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

.drag-handle.dragging {
  cursor: grabbing;
}

ポインタの位置と到達可能性を保持する

ビューポートへの移動をクランプする場合は、position: fixed などのビューポート相対位置モデルを使用します。

  • メインのポインタボタン(event.button === 0)に対してのみドラッグを開始します。
  • pointerdown でポインタのオフセットを記録し、移動のたびにそれを使用することで、要素を角から離してつかんだときに要素がジャンプしないようにします。
  • ポインタがハンドルから離れた後もドラッグを有効なままにします。デフォルトは setPointerCapture() で、lostpointercapture でドラッグ状態をクリアします。ドラッグ中にハンドルが削除または再レンダリングされる可能性がある場合は、ドキュメント レベルの pointermove、pointerup、pointercancel リスナーを使用します。この場合、キャプチャは失われます。
  • 必要に応じて、ドラッグ状態に入る前に約 4 ピクセルの移動を待機します。これにより、ハンドルをクリックしてもドラッグ スタイルがトリガーされなくなります。

ポインタ キャプチャを使用すると、移動リスナーをハンドルに保持できます。

handle.addEventListener('pointerdown', (event) => {
  if (event.button !== 0 || event.target.closest('button, input, select, textarea, a')) return;

  const rect = draggable.getBoundingClientRect();
  offsetX = event.clientX - rect.left;
  offsetY = event.clientY - rect.top;
  handle.setPointerCapture(event.pointerId);
});

handle.addEventListener('pointermove', (event) => {
  if (!handle.hasPointerCapture(event.pointerId)) return;
  handle.classList.add('dragging');
  moveTo(event.clientX - offsetX, event.clientY - offsetY);
});

handle.addEventListener('lostpointercapture', () => {
  handle.classList.remove('dragging');
});

function moveTo(left, top) {
  const maxLeft = Math.max(0, window.innerWidth - draggable.offsetWidth);
  const maxTop = Math.max(0, window.innerHeight - draggable.offsetHeight);

  draggable.style.left = `${Math.max(0, Math.min(left, maxLeft))}px`;
  draggable.style.top = `${Math.max(0, Math.min(top, maxTop))}px`;
}

すべてのポインタとキーボードの位置を同じヘルパーでクランプして、要素にアクセスできるようにします。ビューポートまたは要素サイズの変更後に、現在の位置を再クランプします。レイアウトが重い要素の場合は、ドラッグ中に変換してフレームごとのレイアウトを回避し、リリース時に最終的な位置を left と top に commit することを検討してください。

キーボード操作を提供する

ハンドルにフォーカスがある場合は、矢印キーで要素を移動し、それらのキーに対して preventDefault() を呼び出します。ポインタの移動と同じクランプされた位置指定ヘルパーを使用します。15 ピクセル単位で増やすのが妥当な出発点です。コンポーネントのサイズと精度のニーズに合わせて調整してください。

ダイアログ固有のメモ

<dialog> または [popover] は、ヘッダーをハンドルとしてこのメカニズムを使用できます。left と top を割り当てる前に margin: 0 を設定します。これは、ブラウザのデフォルトのマージンが手動での配置を妨げるためです。ダイアログとポップオーバーの開閉、フォーカス、閉じる動作については、宣言型ダイアログとポップオーバー コントロールとプラットフォーム コントロールの閉じるダイアログをご覧ください。

代替戦略

ドラッグ操作がコンポーネントの位置変更のみを行う場合は、プログレッシブ エンハンスメントとして扱います。JavaScript がなくても、要素、そのコンテンツ、コントロールが、適切なデフォルト位置で表示され、読み取り可能で、機能するようにします。機能しないドラッグ ハンドルを公開しないでください。ハンドルがスタンドアロンのグリップ コントロールである場合は、デフォルトで非表示にし、JavaScript の初期化時に表示します。ヘッダーがハンドルを兼ねている場合は、JavaScript の初期化時にドラッグ アフォーダンスをアタッチします。

Safari でのドラッグ中にテキスト選択が抑制されたままになるように、user-select: none の前に -webkit-user-select: none を含めます。