適応性とユーザー補助機能を備えたトースト コンポーネントを作成する方法の基本的な概要。
この記事では、トースト コンポーネントを作成する方法について考えたいと思います。デモをお試しください。
動画で確認したい場合は、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;
}
トースト コンテナは、ビューポート内に配置されるだけでなく、トーストを配置して分散できるグリッド コンテナです。アイテムは、justify-content
でグループとして中央に配置され、justify-items
で個別に中央に配置されます。トーストが重ならないように、gap
を少し追加します。
.gui-toast-group {
display: grid;
justify-items: center;
justify-content: center;
gap: 1vh;
}
GUI トースト
個々のトーストには padding
があり、角は border-radius
で丸くなっています。また、モバイルとパソコンのサイズ設定に役立つ min()
関数も用意されています。次の CSS のレスポンシブ サイズにより、トーストの幅がビューポートの 90% または 25ch
を超えないようにします。
.gui-toast {
max-inline-size: min(25ch, 90vw);
padding-block: .5ch;
padding-inline: 1ch;
border-radius: 3px;
font-size: 1rem;
}
スタイル
レイアウトと配置を設定したら、ユーザーの設定や操作に適応する 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
}
init()
関数はモジュール内で内部的に呼び出され、要素を Toaster
としてスタッシュします。
const Toaster = init()
Toast 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 をまとめる
Toast('my first toast')
が呼び出されると、トーストが作成され、ページに追加されます(新しいトーストに合わせてコンテナがアニメーション化されることもあります)。Promise が返され、作成されたトーストが CSS アニメーションの完了(3 つのキーフレーム アニメーション)を監視して Promise を解決します。
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')
トーストが表示された後にクリーンアップ処理などを実行する場合は、非同期と await を使用できます。
import Toast from './toast.js'
async function example() {
await Toast('Wizard Rose added to cart')
console.log('toast finished')
}
まとめ
私の方法をご覧になったところで、あなたならどうしますか? 🙂?
アプローチを多様化し、ウェブで構築するすべての方法を学びましょう。デモを作成して、ツイートしてください。リンクを送信していただければ、下のコミュニティ リミックスのセクションに追加します。
コミュニティ リミックス
- @_developit(HTML/CSS/JS): デモとコード
- Joost van der Schee(HTML/CSS/JS): デモとコード