概略說明如何建構可自動調整且符合無障礙設計的 Toast 元件。
在本篇文章中,我想分享如何建構 Toast 元件的想法。試用示範模式。
如果你偏好觀看影片,請參閱這篇文章的 YouTube 版本:
總覽
Toast 是一種非互動、非同步的短訊息,可供使用者參考。一般來說,這類訊息會做為介面回饋模式使用,用於通知使用者操作結果。
互動
與通知、快訊和提示不同,彈出式通知並非互動式,因此無法關閉或保留。通知適用於更重要的資訊、需要互動的同步訊息,或系統層級訊息 (而非網頁層級)。與其他通知策略相比,浮動式通知較為被動。
標記
<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>
顯示訊息容器
一次最多可顯示多個浮動式通知。為了協調多個 Toast,我們使用了容器。這個容器也會處理 Toast 在螢幕上的位置。
<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 Toast
個別彈出式通知含有 padding
,部分圓角使用 border-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;
}
樣式
設定版面配置和定位後,請新增 CSS,以便配合使用者設定和互動。
Toast 容器
Toast 並非互動式,輕觸或滑動不會有任何效果,但目前會使用指標事件。使用下列 CSS 避免彈出式通知竊取點擊。
.gui-toast-group {
pointer-events: none;
}
GUI Toast
為 Toast 提供淺色或深色自適應主題,並使用自訂屬性、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%;
}
}
動畫
新的 Toast 應在進入畫面時,以動畫呈現。為了配合減少動作的做法,預設會將 translate
值設為 0
,但在動作偏好設定媒體查詢中,會將動作值更新為長度。每位使用者都會看到一些動畫,但只有部分使用者會看到彈出式通知移動一段距離。
以下是用於顯示訊息動畫的主要畫面格。CSS 會控制彈出式通知的顯示、等待和結束,全部都在一個動畫中。
@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 來根據使用者事件,協調建立、新增和刪除 Toast。彈出式訊息元件的開發人員體驗應盡可能簡單,且容易上手,例如:
import Toast from './toast.js'
Toast('My first toast')
建立 Toast 群組和 Toast
當 Toast 模組從 JavaScript 載入時,必須建立 Toast 容器並將其新增至頁面。我選擇在 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()
您可以使用 createToast()
函式建立 Toast HTML 元素。這個函式需要 Toast 的文字,並建立 <output>
元素,以便透過一些類別和屬性裝飾,設定文字,然後傳回節點。
const createToast = text => {
const node = document.createElement('output')
node.innerText = text
node.classList.add('gui-toast')
node.setAttribute('role', 'status')
return node
}
管理一個或多個 Toast
JavaScript 現在會在文件中新增容器,用於容納 Toast,並準備新增已建立的 Toast。addToast()
函式會協調處理一個或多個 Toast。首先檢查 Toast 數量,以及動畫是否正常運作,然後使用這項資訊附加 Toast,或執行一些精美的動畫,讓其他 Toast 出現以便為新 Toast 騰出空間。
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
、動畫離開。當有現有的 Toast 時,系統會呼叫 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')
時,系統會建立並新增 Toast 至頁面 (甚至可能會為容器提供動畫,以便容納新的 Toast),並傳回 promise,且會為 CSS 動畫完成 (三個關鍵影格動畫) 監控 Toast,以便解析 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
會為我們執行這項工作,一旦所有承諾都已兌現,就會將自身視為已完成。使用 await Promise.allSettled()
表示下一個程式碼行可以放心移除元素,並假設 Toast 已完成其生命週期。最後,呼叫 resolve()
可履行高層級 Toast 承諾,讓開發人員在 Toast 顯示後進行清理或執行其他工作。
export default Toast
最後,Toast
函式會從模組匯出,供其他指令碼匯入及使用。
使用 Toast 元件
如要使用 Toast 或 Toast 的開發人員體驗,請匯入 Toast
函式,並使用訊息字串呼叫該函式。
import Toast from './toast.js'
Toast('Wizard Rose added to cart')
如果開發人員想要在顯示 Toast 後執行清除工作或其他作業,可以使用 async 和 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:示範與程式碼