jQuery / メソッド / .hover() - 進入・退出イベントとonへの置き換え

jQueryの.hover()は、マウスポインターが要素に入るときと出るときの関数を登録する省略メソッドです。jQuery 3.3から非推奨で、新しいコードには.on("mouseenter", ...)と.on("mouseleave", ...)を使います。非推奨と削除は異なり、以下の両方式は3.7.1と4.0.0で実行しました。

二つの関数と一つの共通関数

.hover()の二関数形式は、最初を進入時、二つ目を退出時に呼びます。hoverというイベントを登録するのではなく、mouseenterとmouseleaveをまとめて扱います。

従来の形式 置き換え 意味
.hover(enter, leave) .on("mouseenter", enter).on("mouseleave", leave) 進入と退出を別関数で処理
.hover(handler) .on("mouseenter mouseleave", handler) 両イベントを同じ関数で処理

mouseenterは要素の境界への進入です。既に親の中にあるポインターが子のspanへ移っても、親への再進入にはなりません。子への移動でも伝播するmouseoverとの違いを理解すると、不要な繰り返しを防げます。

hoverとonを比較する完全な例

全コードをUTF-8のhover-demo.htmlとして保存し、ブラウザーで開きます。コンテンツ配信ネットワーク(CDN)から公式jQueryファイル4.0.0を読むためインターネット接続が必要ですが、インストールは不要です。両例、子への移動、動的要素、キーボード、解除を3.7.1と4.0.0で実行しました。

Alphaは従来の.hover()、Betaは#cardsに委任した.on()を使います。カードの内容は常に表示し、強調だけを変えます。Betaにはfocusin・focusoutも登録し、キーボードフォーカスでも強調します。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Hover events and keyboard focus</title>
  <script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
  <style>
    body { font: 16px/1.6 sans-serif; max-width: 760px; margin: 24px auto; padding: 16px; }
    button { font: inherit; margin: 6px; }
    .card { display: block; padding: 20px; border: 2px solid teal; background: white; }
    .active { background: #d9f4ef; }
    .card:focus-visible { outline: 3px solid #af5f00; outline-offset: 3px; }
  </style>
</head>
<body>
  <h1>Compare legacy hover and on</h1>
  <button type="button" id="add">Add modern card</button>
  <button type="button" id="bind">Bind modern</button>
  <button type="button" id="unbind">Unbind modern</button>
  <button type="button" id="reset">Reset</button>
  <div id="cards">
    <button type="button" class="card legacy">Legacy <span>Alpha</span></button>
    <button type="button" class="card modern">Modern <span>Beta</span></button>
  </div>
  <p id="result" aria-live="polite"></p>
  <script>
    $(function () {
      const $cards = $("#cards");
      let legacyEntries = 0;
      let modernEntries = 0;
      function report() {
        $("#result").text(`legacy enters=${legacyEntries} | modern enters=${modernEntries}`);
      }
      $(".legacy").hover(function () {
        legacyEntries++;
        $(this).addClass("active");
        report();
      }, function () { $(this).removeClass("active"); });
      function bindModern() {
        $cards.off(".hoverLesson").on(
          "mouseenter.hoverLesson mouseleave.hoverLesson focusin.hoverLesson focusout.hoverLesson",
          ".modern", function (event) {
            const $card = $(this);
            if (event.type === "mouseenter") { $card.data("inside", true); modernEntries++; }
            if (event.type === "mouseleave") $card.data("inside", false);
            if (event.type === "focusin") $card.data("focused", true);
            if (event.type === "focusout") $card.data("focused", false);
            $card.toggleClass("active", Boolean($card.data("inside") || $card.data("focused")));
            report();
          }
        );
      }
      $("#bind").on("click", bindModern);
      $("#unbind").on("click", function () {
        $cards.off(".hoverLesson");
        $(".modern").removeClass("active").removeData("inside focused");
      });
      $("#add").on("click", function () {
        if (!$("#gamma").length) $('<button type="button" id="gamma" class="card modern">Modern <span>Gamma</span></button>').appendTo($cards);
      });
      $("#reset").on("click", function () {
        $("#gamma").remove();
        $(".card").removeClass("active").removeData("inside focused");
        legacyEntries = 0;
        modernEntries = 0;
        bindModern();
        report();
      });
      bindModern();
      report();
    });
  </script>
</body>
</html>

初期の進入回数はどちらも0です。

legacy enters=0 | modern enters=0
  1. Legacy Alphaに入ると背景が変わり、legacy entersが一度増えます。内部のAlphaのspanへ動いても増えません。
  2. 外へ出ると強調が消えます。Modern Betaも進入ごとにmodern entersが増えます。
  3. Add modern cardでGammaを追加します。親の委任処理が新しいカードも扱い、個別登録なしで動作します。
  4. TabでBetaにフォーカスすると背景と外枠が現れます。ポインターだけ外へ動かしてもフォーカスが残る間は背景を維持し、両方が外れると解除します。
  5. Unbind modernはModernだけを解除し、Alphaのhoverは動き続けます。
  6. Bind modernを繰り返しても重複呼び出しになりません。ResetはGammaと強調を消し、回数を0にし、Modernを再登録します。

ポインターとフォーカスをinside・focusedとして別々に保持し、片方でも有効なら強調します。mouseleaveで無条件にクラスを消すと、キーボードフォーカスが残っていても視覚的な状態が失われます。

動的要素と解除範囲

.on()の委任形式は残り続ける親に登録し、子孫セレクターを指定します。ここでは#cardsが残り、.modernボタンだけを扱います。従来の.hover()は登録時の選択要素に直接接続するため、後から作った要素が自動的に同じ処理を得るとは限りません。

hoverLessonはイベント名前空間です。bindModern()は.off(".hoverLesson")で自分の処理だけを外してから再登録します。引数なしの.off()や広範囲の.off("mouseenter mouseleave")は他のコードの処理も消しかねないため、範囲を限定します。

通常のfunction内のthisは処理対象のModernカードです。イベントの発生元となった内部spanかもしれないevent.targetとは区別します。このthisを必要とするなら、安易にアロー関数へ変更しません。

単一関数形式と入力方法を確認する

開発者ツールのConsoleで次の完全な部分コードを実行すると、一つの関数に両イベントが渡ることを確認できます。一時ボタンを挿入せずにイベントを発生させ、最後に整理します。実際のポインター移動は上の完全な例で確認します。

const events = [];
const $button = $("<button>Temporary</button>");
$button.hover(function (event) { events.push(event.type); });
$button.trigger("mouseenter").trigger("mouseleave");
console.log(events.join(", "));
$button.remove();
mouseenter, mouseleave

これは進入・退出を受けるという意味で、マウスの動きを常時受けるわけではありません。共通関数でもevent.typeに応じて状態を明示すると、他の変更で単純なtoggleの状態が逆になる問題を避けやすくなります。

タッチ画面ではhoverだけで必須内容を開く設計に頼りません。重要な機能はクリック・タップやキーボードでも使えるようにし、装飾だけならCSSの:hoverと:focus-visibleで十分かも検討します。この例は内容を常に表示し、強調だけを変えます。