2022年6月19日

フォーカス: フォーカス/ブラー

要素は、ユーザーがクリックするか、キーボードのTabキーを使用するとフォーカスを受け取ります。また、ページが読み込まれたときにデフォルトで要素にフォーカスを当てる autofocus HTML属性や、フォーカスを取得する他の手段もあります。

一般的に、要素にフォーカスが当たるということは、「ここにデータを受け入れる準備をする」という意味で、必要な機能を初期化するためのコードを実行できる瞬間です。

フォーカスを失う(「ブラー」)瞬間はさらに重要となる場合があります。ユーザーが別の場所をクリックしたり、Tabを押して次のフォームフィールドに移動したり、他の手段がある場合です。

一般的に、フォーカスを失うということは、「データが入力された」という意味なので、それをチェックしたり、サーバーに保存したりするなどのコードを実行できます。

フォーカスイベントを扱う際には重要な特性があります。それらについては後で詳しく説明します。

イベント focus/blur

フォーカス時にfocusイベントが呼び出され、要素がフォーカスを失うとblurイベントが呼び出されます。

入力フィールドの検証に使用してみましょう。

以下の例では、

  • blurハンドラーは、フィールドにメールアドレスが入力されているかどうかをチェックし、入力されていない場合はエラーを表示します。
  • focusハンドラーはエラーメッセージを非表示にします(blur時に再度チェックされます)。
<style>
  .invalid { border-color: red; }
  #error { color: red }
</style>

Your email please: <input type="email" id="input">

<div id="error"></div>

<script>
input.onblur = function() {
  if (!input.value.includes('@')) { // not email
    input.classList.add('invalid');
    error.innerHTML = 'Please enter a correct email.'
  }
};

input.onfocus = function() {
  if (this.classList.contains('invalid')) {
    // remove the "error" indication, because the user wants to re-enter something
    this.classList.remove('invalid');
    error.innerHTML = "";
  }
};
</script>

最新のHTMLでは、requiredpatternなどの入力属性を使用して多くの検証を実行できます。そして、必要なものが得られることもあります。JavaScriptは、より柔軟性が必要な場合に使用できます。また、変更された値が正しい場合は、自動的にサーバーに送信することもできます。

メソッド focus/blur

メソッドelem.focus()elem.blur()は、要素のフォーカスを設定/解除します。

たとえば、値が無効な場合、訪問者が入力を離れることができないようにしてみましょう。

<style>
  .error {
    background: red;
  }
</style>

Your email please: <input type="email" id="input">
<input type="text" style="width:220px" placeholder="make email invalid and try to focus here">

<script>
  input.onblur = function() {
    if (!this.value.includes('@')) { // not email
      // show the error
      this.classList.add("error");
      // ...and put the focus back
      input.focus();
    } else {
      this.classList.remove("error");
    }
  };
</script>

これはFirefox以外のすべてのブラウザで動作します(バグ)。

入力に何かを入力し、Tabキーを使用するか、<input>から離れてクリックしようとすると、onblurがフォーカスを戻します。

onblur内でevent.preventDefault()を呼び出すことによって「フォーカスが失われるのを防ぐ」ことはできません。これは、onblurが要素がフォーカスを失ったに動作するためです。

ただし、実際には、このようなものを実装する前に、よく考える必要があります。一般的にはユーザーにエラーを表示する必要はありますが、フォームの入力を進めることを妨げるべきではありません。最初に他のフィールドを入力したい場合があります。

JavaScriptによって開始されるフォーカスの喪失

フォーカスを失う原因は多数あります。

その1つは、訪問者が別の場所をクリックしたときです。ただし、JavaScript自体が原因となる可能性もあります。例えば、

  • alertはフォーカスを自分自身に移動させるため、要素でフォーカスが失われ(blurイベント)、alertが閉じられると、フォーカスが戻ります(focusイベント)。
  • 要素がDOMから削除されると、フォーカスも失われます。後で再挿入された場合、フォーカスは戻りません。

これらの機能により、focus/blurハンドラーが誤動作し、必要ないときにトリガーされることがあります。

最適な方法は、これらのイベントを使用するときに注意することです。ユーザーによって開始されたフォーカスの喪失を追跡したい場合は、自分でフォーカス喪失を引き起こさないようにする必要があります。

任意の要素でフォーカスを許可する: tabindex

デフォルトでは、多くの要素はフォーカスをサポートしていません。

リストはブラウザ間で多少異なりますが、1つ常に正しいことがあります。focus/blurのサポートは、訪問者が操作できる要素(<button><input><select><a>など)で保証されています。

一方、<div><span><table>など、何かをフォーマットするために存在する要素は、デフォルトではフォーカスできません。メソッドelem.focus()はそれらでは機能せず、focus/blurイベントはトリガーされません。

これは、HTML属性tabindexを使用して変更できます。

tabindexを持つ要素は、フォーカス可能になります。属性の値は、Tab(または同様のもの)を使用して要素間を切り替えるときの要素の順序番号です。

つまり、2つの要素があり、最初の要素にtabindex="1"、2番目の要素にtabindex="2"がある場合、最初の要素でTabを押すと、フォーカスが2番目の要素に移動します。

切り替え順序は次のとおりです。tabindex1以上の要素が最初に表示され(tabindex順)、次にtabindexがない要素(例: 通常の<input>)が表示されます。

一致するtabindexがない要素は、ドキュメントソースの順序(デフォルトの順序)で切り替えられます。

2つの特別な値があります。

  • tabindex="0"は、tabindexがない要素の中に要素を配置します。つまり、要素を切り替えるとき、tabindex=0の要素はtabindex ≥ 1の要素の後に移動します。

    通常、これは要素をフォーカス可能にし、デフォルトの切り替え順序を維持するために使用されます。<input>と同等のフォームの一部になるように要素を作成します。

  • tabindex="-1"は、要素に対するプログラムによるフォーカスのみを許可します。Tabキーはこのような要素を無視しますが、メソッドelem.focus()は機能します。

たとえば、ここにリストがあります。最初の項目をクリックして、Tabキーを押します。

Click the first item and press Tab. Keep track of the order. Please note that many subsequent Tabs can move the focus out of the iframe in the example.
<ul>
  <li tabindex="1">One</li>
  <li tabindex="0">Zero</li>
  <li tabindex="2">Two</li>
  <li tabindex="-1">Minus one</li>
</ul>

<style>
  li { cursor: pointer; }
  :focus { outline: 1px dashed green; }
</style>

順序は次のようになります: 1 - 2 - 0。通常、<li>はフォーカスをサポートしませんが、tabindexは、イベントや:focusを使用したスタイル設定とともに、完全に有効にします。

プロパティelem.tabIndexも同様に機能します。

elem.tabIndexプロパティを使用して、JavaScriptからtabindexを追加できます。これは同じ効果があります。

委譲: focusin/focusout

イベントfocusblurはバブリングしません。

たとえば、<form>onfocusを配置して、次のように強調表示することはできません。

<!-- on focusing in the form -- add the class -->
<form onfocus="this.className='focused'">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

上記の例は機能しません。これは、ユーザーが<input>にフォーカスすると、focusイベントはその入力でのみトリガーされるためです。これはバブリングされません。したがって、form.onfocusはトリガーされません。

2つの解決策があります。

1つ目は、おかしな歴史的な機能です。focus/blurはバブリングしませんが、キャプチャフェーズで下方向に伝播します。

これは機能します。

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  // put the handler on capturing phase (last argument true)
  form.addEventListener("focus", () => form.classList.add('focused'), true);
  form.addEventListener("blur", () => form.classList.remove('focused'), true);
</script>

2つ目は、focusinおよびfocusoutイベントです。これらは、focus/blurとまったく同じですが、バブリングします。

これらはon<event>ではなく、elem.addEventListenerを使用して割り当てる必要があることに注意してください。

したがって、これは別の動作するバリアントです。

<form id="form">
  <input type="text" name="name" value="Name">
  <input type="text" name="surname" value="Surname">
</form>

<style> .focused { outline: 1px solid red; } </style>

<script>
  form.addEventListener("focusin", () => form.classList.add('focused'));
  form.addEventListener("focusout", () => form.classList.remove('focused'));
</script>

まとめ

イベントfocusblurは、要素のフォーカスがオン/オフになるとトリガーされます。

それらの特別な点は、

  • バブリングしません。代わりにキャプチャ状態またはfocusin/focusoutを使用できます。
  • ほとんどの要素は、デフォルトではフォーカスをサポートしていません。tabindexを使用して、あらゆるものをフォーカス可能にします。

現在フォーカスされている要素は、document.activeElementとして使用できます。

タスク

重要度: 5

クリックすると<textarea>に変わる<div>を作成します。

テキストエリアを使用すると、<div>内のHTMLを編集できます。

ユーザーがEnterキーを押すか、フォーカスを失うと、<textarea><div>に戻り、そのコンテンツが<div>のHTMLになります。

新しいウィンドウでデモ

タスクのサンドボックスを開きます。

重要度: 5

クリック時に表のセルを編集可能にします。

  • クリック時 - セルは「編集可能」になり(テキストエリアが内側に表示される)、HTMLを変更できます。サイズ変更はなく、すべての形状は同じままにする必要があります。
  • 編集を終了/キャンセルするために、セルにOKボタンとキャンセルボタンが表示されます。
  • 一度に編集できるセルは1つのみです。<td>が「編集モード」の場合、他のセルへのクリックは無視されます。
  • テーブルには多くのセルがある場合があります。イベント委譲を使用してください。

デモ

タスクのサンドボックスを開きます。

  1. クリック時 - セルのinnerHTMLを、同じサイズでボーダーのない<textarea>で置き換えます。適切なサイズを設定するには、JavaScriptまたはCSSを使用できます。
  2. textarea.valuetd.innerHTMLに設定します。
  3. テキストエリアにフォーカスします。
  4. セルの下にOK/キャンセルボタンを表示し、クリックを処理します。

サンドボックスでソリューションを開きます。

重要度: 4

マウスにフォーカスしてください。その後、矢印キーで移動させます。

新しいウィンドウでデモ

追伸:イベントハンドラーは、#mouse要素以外にはどこにも記述しないでください。

追々伸:HTML/CSSを修正しないでください。このアプローチは汎用的で、どんな要素でも動作するようにする必要があります。

タスクのサンドボックスを開きます。

mouse.onclickを使用してクリックを処理し、position:fixedでマウスを「移動可能」にすることができます。その後、mouse.onkeydownで矢印キーを処理します。

唯一の落とし穴は、keydownがフォーカスのある要素でのみトリガーされることです。そのため、要素にtabindexを追加する必要があります。HTMLを変更することは禁止されているため、mouse.tabIndexプロパティを使用できます。

追伸:mouse.onclickmouse.onfocusに置き換えることもできます。

サンドボックスでソリューションを開きます。

チュートリアルマップ

コメント

コメントする前にこちらをお読みください…
  • 改善するための提案がある場合は、コメントする代わりにGitHub issueを送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、具体的に説明してください。
  • コードを数語挿入するには、<code>タグを使用してください。数行の場合は<pre>タグで囲み、10行以上になる場合はサンドボックス(plnkrjsbincodepen…)を使用してください。