🦍

Shiftキーを押しながらだとdraggableが効かない?!

2023/12/12に公開

はじめに

「らくしふ」のシフト表には、シフトのドラッグ&ドロップによるシフトの移動機能があります。

この機能をリリースして以降、せっかくなら「Shiftキーを押しながらドラッグ&ドロップしたらコピーされてほしい」という声も聞こえてきました。

「おう、そんなの簡単やぞ。やったろやないか」 ということで実装してみたら、ブラウザの仕様?影響によりちょっと問題にぶち当たったので、その問題と解決方法のご紹介です。

ドロップ時の判定実装

そもそも「コピー」か「移動」はどう判定すれば良いのでしょうか。
それはdropイベント時のshiftKeyフラグを使えば簡単です。

https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/drop_event

Shiftキー押してるとドラッグできない?!

しかし動作確認してると次の問題に直面しました。

「ドラッグを開始してからShiftキーを押してドロップすれば問題なくコピーできるが、最初からShiftキーを押していると、そもそもdraggable要素を選択できずドラッグができない

ググってみるとChromeのバグなんじゃないかというStackOverFlowの記事がヒットします。

https://stackoverflow.com/questions/40184454/draggable-with-shift-key-doesnt-work-in-chrome

かなり前から遭遇しうる事象のようです。ただそこからより深く調べ進めていると、どうやら「子要素があるDOMに対してのみ起きる事象」ということがわかってきました。

手元で検証してみたら確かに子要素がなければ問題ありません。
わかりやすいようにサンプルを用意しました。

<div :draggable="true">子要素なし</div>
<div :draggable="true">
  <div>子要素あり(div)</div>
</div>

解決策

ブラウザの問題も絡んでくると悩ましいなとなってきます。
しかし、ここでピンと思いつきました。
「Shiftキーを押している間、ドラッグ対象の子要素をポインターイベントから外せば行けるのでは?」
擬似的にShift Keydown時にポインターイベント子要素がない状態にするということです。

つまり先ほどのサンプルを以下のようにしてみます。

<div :draggable="true">子要素なし</div>
<div :draggable="true">
  <div style="pointer-events: none">子要素あり(div)</div> // ← ポインターイベントから除外
</div>

うまくいきそうです!

回避策を実装してみる

ではアプリケーションではどうするかというと以下のようにします。

  1. ページコンポーネントでKeyDownイベントが配信されるたびに呼び出すメソッドを用意
  2. そのメソッド内でドラッグ対象の子要素のスタイルを変更
  3. KeyUpイベント時も1と同様にして、2で変更を加えたスタイルを戻す

コードは以下のようにしました。

const DOM_CLASS_NAME = '.hogehoge'
// keydownおよびkeyup共通のメソッド
const addPointerEventStyle = (style: "none" | "auto") => {
  const targetDoms = document.querySelectorAll(DOM_CLASS_NAME);
  shiftDoms.forEach((d) =>
    Array.from(d.children).forEach((childDom: any) => (childDom.style.pointerEvents = style))
}

const onKeyDown = (e: KeyboardEvent) => {
  if (!e.shiftKey) return;
  addPointerEventStyle("none");
};

const onKeyUp = (e: KeyboardEvent) => {
  // keyup時はshiftKeyだとしても、e.shiftKeyがfalseになるため`e.key`を利用して判定
  if (e.key !== "Shift") return;
  addPointerEventStyle("auto");
};

onMounted(() => {
  document.addEventListener("keydown", onKeyDown);
  document.addEventListener("keyup", onKeyUp);
});

onBeforeUnmount(() => {
  document.removeEventListener("keydown", onKeyDown);
  document.removeEventListener("keyup", onKeyUp);
});

*記事用に実際のコードに少し変更を加えています。
*型注釈は省略しています。

2つのポイントがあります。

  1. keyup時のshiftKey
    コード上にもコメントで補足していますが、keyup時にはe.shiftKeyfalseとなります。個人的にはShiftキーを上げたらそのままtrueになって欲しいところですが現状の仕様として仕方ありません。e.key === 'Shift'を使うことにしました。

  2. ドラッグ対象要素は1つとは限らない
    これは要素設計によって違いますが、今回の場合はドラッグ対象要素の中に子要素が1つではなく複数存在しました。1つでもポインター要素が残っているとドラッグできないので全てポインターイベント対象から外すため、対象要素の子要素の配列に対してループを回して適用しています。

さいごに

これにより無事「Shiftキーを押しながらドラッグ&ドロップしたらコピーされてほしい」という要望に応えることができました

どんどん使い勝手の良いシフト表に進化していっていますが、その裏では意外とpointer-events: noneが役に立っていたりします。

今回のShiftキー+ドラッグの問題は、本日2023年末時点だとネット上にあまり解決策があがっておりませんでした。もし他にも良い対応方法などご存知の方いらっしゃいましたら気軽にコメントくださいませ〜

クロスビットテックブログ

Discussion