toast コンポーネントの作成

アダプティブでアクセスしやすいトースト コンポーネントを作成する方法の基本的な概要。

この投稿では、トースト コンポーネントの作成方法を共有したいと思います。デモを試す。

デモ

動画をご覧になる場合は、この投稿の YouTube バージョンをご覧ください。

概要

トーストは、ユーザーにとって非インタラクティブで受動的な非同期の短いメッセージです。通常は、アクションの結果についてユーザーに知らせるためのインターフェースのフィードバック パターンとして使用されます。

インタラクション数

トーストは、通知、アラートプロンプトとは異なり、インタラクティブではありません。閉じたり永続化したりするためのものではありません。通知は、より重要な情報、操作が必要な同期メッセージ、(ページレベルではなく)システムレベルのメッセージに使用されます。トーストは、他の通知戦略よりも受動的です。

マークアップ

<output> 要素は、スクリーン リーダーで読み上げられるため、トーストに適しています。正しい HTML は、JavaScript や CSS で拡張するための安全な基盤となります。また、多くの JavaScript が存在します。

トースト

<output class="gui-toast">Item added to cart</output>

role="status" を追加することで、よりインクルーシブなものにできます。これにより、ブラウザが <output> 要素に仕様に基づく暗黙的なロールを付与しない場合に、フォールバックが提供されます。

<output role="status" class="gui-toast">Item added to cart</output>

トースト コンテナ

一度に複数のトーストを表示できます。複数のトーストをオーケストレートするために コンテナが使用されますまた、このコンテナは画面上のトーストの位置を処理します。

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

レイアウト

トーストをビューポートの inset-block-end に固定し、さらにトーストを追加すると、その画面の端から積み重ねられます。

GUI コンテナ

トースト コンテナは、トーストを表示するためのすべてのレイアウト作業を行います。これはビューポートに対して fixed であり、論理プロパティ inset を使用して固定するエッジを指定し、同じ block-end エッジからのわずかな padding を指定します。

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

DevTools ボックスのサイズとパディングが .gui-toast-container 要素にオーバーレイされているスクリーンショット。

トースト コンテナは、ビューポート内に配置されるだけでなく、トーストの配置と配置が可能なグリッド コンテナでもあります。アイテムは justify-content でグループとして中央揃えされ、justify-items で個別に中央揃えされます。トーストが触れないように gap を少しスローします。

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

トースト グループ上に CSS グリッド オーバーレイが表示されたスクリーンショット。今回は、トーストの子要素間のスペースとギャップがハイライト表示されています。

GUI トースト

個々のトーストには、paddingborder-radius の柔らかい角、モバイルとパソコンのサイズ設定に役立つ min() 関数があります。次の CSS のレスポンシブ サイズでは、トーストがビューポートまたは 25ch の 90% を超えて拡大するのを防ぎます。

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

パディングと境界の半径が表示された、単一の .gui-toast 要素のスクリーンショット。

スタイル

レイアウトと配置を設定したら、ユーザー設定とユーザー操作への適応に役立つ CSS を追加します。

トースト コンテナ

トーストはインタラクティブではありません。トーストをタップまたはスワイプしても何も起こりませんが、現在はポインタ イベントを消費します。次の CSS を使用して、トーストでクリックが盗まれないようにします。

.gui-toast-group {
  pointer-events: none;
}

GUI トースト

カスタム プロパティ、HSL、優先メディアクエリを使用して、トーストにライトまたはダークのアダプティブ テーマを設定します。

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

アニメーション

新しいトーストが画面に入ると、アニメーションが表示されます。動きの軽減に対応するには、デフォルトでは translate 値を 0 に設定しますが、モーション設定のメディアクエリでモーションの値をある長さに更新します。全員にアニメーションが表示されますが、トーストを長距離移動させるのは一部のユーザーのみです。

トースト アニメーションに使用されるキーフレームは次のとおりです。CSS は、トーストの開始、待機、終了をすべて 1 つのアニメーションで制御します。

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

次に、トースト要素が変数を設定し、キーフレームをオーケストレートします。

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

スタイルとスクリーン リーダーでアクセス可能な HTML の準備が整ったら、ユーザー イベントに基づいてトーストの作成、追加、破棄をオーケストレートするために JavaScript が必要です。トースト コンポーネントのデベロッパーは、次のように最小限の操作で簡単に開始できるようにする必要があります。

import Toast from './toast.js'

Toast('My first toast')

トースト グループとトーストを作成する

トースト モジュールが JavaScript から読み込まれると、トースト コンテナを作成してページに追加する必要があります。body の前に要素を追加することを選択しました。これにより、コンテナがすべての body 要素のコンテナの上にあるため、z-index のスタッキングの問題が生じる可能性が低くなります。

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

head タグと body タグの間のトーストグループのスクリーンショット。

init() 関数はモジュール内部で呼び出され、要素を Toaster として格納します。

const Toaster = init()

トーストの HTML 要素の作成は、createToast() 関数を使用して行います。この関数は、トースト用にテキストを必要とし、<output> 要素を作成していくつかのクラスと属性で装飾し、テキストを設定してノードを返します。

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

1 つまたは複数のトーストを管理する

JavaScript で、トーストを格納するためのコンテナがドキュメントに追加され、作成したトーストを追加できるようになりました。addToast() 関数は、1 つまたは複数のトーストの処理をオーケストレートします。まず、トーストの数と動作に問題がないかどうかをチェックし、この情報を使用してトーストを追加するか、他のトーストで新しいトースト用のスペースが空いたように見せかけます。

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

最初のトーストを追加すると、Toaster.appendChild(toast) によってページにトーストが追加され、CSS アニメーション(開始、待機 3s、終了)がトリガーされます。flipToast() は、既存のトーストがある場合に、Paul Lewis による FLIP という手法を使用して呼び出されます。これは、新しいトーストを追加する前と後のコンテナの位置の差を計算するというものです。トースターの位置をマークし

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

CSS グリッドがレイアウトのリフトを行います。新しいトーストが追加されると、グリッドはそれを先頭に配置し、他のトーストと間隔を空けます。一方、ウェブ アニメーションを使用して、コンテナを古い位置からアニメーション化します。

JavaScript のすべてを 1 つにまとめる

Toast('my first toast') が呼び出されると、トーストが作成されてページに追加されます(新しいトーストに対応するためにコンテナがアニメーション化されている場合もあります)。Promise が返され、作成されたトーストが Promise の解決のために CSS アニメーションの完了(3 つのキーフレーム アニメーション)について監視されます。

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

このコードのわかりにくい部分は、Promise.allSettled() 関数と toast.getAnimations() のマッピングだと感じました。トーストに複数のキーフレーム アニメーションを使用したため、すべてのキーフレーム アニメーションが完了したことを確信を持って確認するために、JavaScript と、それぞれの finished プロミスから完了を確認する必要があります。allSettled は、そのように機能します。すべての Promise が満たされると、それ自体は完了として解決されます。await Promise.allSettled() を使用すると、次のコード行で確実に要素を削除し、トーストのライフサイクルが完了したと見なすことができます。最後に、resolve() を呼び出すと、高レベルの Toast Promise が履行されるため、デベロッパーはトーストが表示された後にクリーンアップやその他の作業を行うことができます。

export default Toast

最後に、他のスクリプトがインポートして使用できるように、Toast 関数がモジュールからエクスポートされます。

Toast コンポーネントの使用

トースト(またはトーストのデベロッパー エクスペリエンス)を使用するには、Toast 関数をインポートし、メッセージ文字列で呼び出します。

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

デベロッパーがクリーンアップ作業などを行う場合、トーストが表示された後に、async と await を使用できます。

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

まとめ

私のやり方がわかったところで、どうしたらいいですか? 🙂?

多様なアプローチと、ウェブでの構築方法を学んでいきましょう。 デモを作成してツイートのリンクをお願いします。下のコミュニティ リミックス セクションに追加します。

コミュニティのリミックス