不可視状態からフェードインする要素を即座にフォーカスする方法

CSSのdisplay: nonevisibility: hiddenによって不可視状態になっている要素を、表示して即座にフォーカスしたいということがある。たとえば、初期状態では非表示になっている検索ボックスを、ユーザーのインタラクションに応じて表示するような場合。そうしたとき、通常では、スタイルを切り替えてすぐにfocusメソッドを呼び出すだけで良い。

<input type="search" />
input[type='search'] {
  display: none;

  &.open {
    display: revert;
  }
}
const searchBoxElement = document.querySelector('input[type="search"]');

function open() {
  searchBoxElement.classList.add('open');
  searchBoxElement.focus();
}

function close() {
  searchBoxElement.classList.remove('open');
}

しかし場合によっては、表示する際にフェードインなどのアニメーションを伴わせたいこともある。これを簡単に実現するには、CSSのtransitionプロパティを使いつつ、opacityプロパティとvisibilityプロパティを操作するとよい。visibilityプロパティはアニメーション可能(discrete)であるため、transitionプロパティで制御することができる。

input[type='search'] {
  visibility: hidden;
  opacity: 0;
  transition:
    visibility 300ms,
    opacity 300ms;

  &.open {
    visibility: revert;
    opacity: revert;
  }
}
const searchBoxElement = document.querySelector('input[type="search"]');

function open() {
  searchBoxElement.classList.add('open');
  searchBoxElement.focus();
}

function close() {
  searchBoxElement.classList.remove('open');
}

ただし、このような実装ではフォーカスができずに失敗してしまう。なぜなら、スタイルを書き換えた直後にはまだ不可視状態(visibility: hidden)のままだからだ。フォーカスさせるには、アニメーションが開始してvisibility: visibleの状態になるまで待機しなければならない。そのために対処として、requestAnimationFrameメソッドが使用されがちである。

function open() {
  searchBoxElement.classList.remove('open');

  requestAnimationFrame(() => {
    searchBoxElement.focus();
  });
}

残念ながら、これは必ずしもうまくいくわけではない。requestAnimationFrameメソッドが呼び出される時点ですでにアニメーションが開始しているかどうかは保証されていないからだ。そこで苦肉の策として、requestAnimationFrameメソッドを二重に入れ子にすることで成功率を上げるというやり方もあるが、やはり確実ではない。

requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    searchBoxElement.focus();
  });
});

ここで重要なのは、アニメーションの開始タイミングに合わせて処理をすることである。そのためには、transitionstartイベントを使用するのが適切だ。

function open() {
  searchBoxElement.classList.remove('open');

  searchBoxElement.addEventListener(
    'transitionstart',
    () => {
      searchBoxElement.focus();
    },
    { once: true },
  );
}

visibilityプロパティをvisibleに書き換えると、アニメーションの開始タイミングですぐにvisibleに切り替わる(Animation of visibility)。そのため、トランジションの開始をフックにすればうまくいく。

さらには、transition-behaviorプロパティと@starting-styleを活用すればより簡単に実現できそうだ。visibilityプロパティをアニメーションさせるのと違って、displayプロパティにallow-discreteを適用すると、表示の際にはトランジションの開始を待たずに切り替わるらしい。この性質を利用すれば、focusメソッドは単に同期的に呼び出すだけでよい。

input[type='search'] {
  display: none;
  opacity: 0;
  transition:
    opacity 300ms,
    display 300ms allow-discrete;

  &.open {
    @starting-style {
      opacity: 0;
    }
    display: revert;
    opacity: revert;
  }
}
const searchBoxElement = document.querySelector('input[type="search"]');

function open() {
  searchBoxElement.classList.add('open');
  searchBoxElement.focus();
}

function close() {
  searchBoxElement.classList.remove('open');
}

参考文献