建立浮動式訊息元件

基礎總覽:如何建構自動調整式無障礙訊息方塊元件。

在這篇文章中,我想分享如何建構 Toast 元件的思考過程。試用示範模式

示範

如果比較喜歡看影片,可以觀看這篇貼文的 YouTube 版本:

總覽

Toast 是非互動式、被動式和非同步的簡短訊息,可供使用者參考。 通常用於介面回饋模式,向使用者說明動作結果。

互動

與通知、快訊提示不同,訊息不會互動,也不會顯示或保留。通知適用於較重要的資訊、需要互動的同步訊息,或系統層級訊息 (而非網頁層級)。與其他通知策略相比,Toast 較為被動。

標記

<output> 元素會向螢幕閱讀器播報,因此很適合用於祝 toast。正確的 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;
}

螢幕截圖:在 .gui-toast-container 元素上疊加顯示開發人員工具方塊大小和邊框間距。

除了在可視區域內定位,祝酒詞容器也是格線容器,可對齊及分配祝酒詞。項目會以群組形式置中 (使用 justify-content),並個別置中 (使用 justify-items)。稍微gap一下,避免吐司碰到。

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

螢幕截圖:這次在祝 toast 群組上疊加 CSS 格線,並醒目顯示 toast 子項元素之間的空間和間距。

GUI Toast

個別的祝賀訊息具有一些 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;
}

單一 .gui-toast 元素的螢幕截圖,顯示邊框間距和邊框半徑。

樣式

設定版面配置和定位後,請新增 CSS,協助配合使用者設定和互動。

Toast 容器

輕觸或滑動訊息不會有任何作用,但目前會耗用指標事件。使用下列 CSS,防止祝賀訊息竊取點擊。

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

GUI 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%;
  }
}

動畫

新的祝賀訊息應該會以動畫形式顯示在畫面上。 如要配合減少動作,請將 translate 值預設設為 0,但請在動作偏好設定媒體查詢中,將動作值更新為長度。所有使用者都會看到動畫,但只有部分使用者會看到訊息移動一段距離。

以下是用於訊息方塊動畫的主要畫面格。CSS 會在一個動畫中控制祝 toast 的進入、等待和退出。

@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')

建立訊息方塊群組和訊息方塊

從 JavaScript 載入祝賀訊息模組時,必須建立祝賀訊息容器並新增至網頁。我選擇在 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()

使用 createToast() 函式建立 Toast HTML 元素。這個函式需要一些用於訊息方塊的文字、建立 <output> 元素、以一些類別和屬性裝飾該元素、設定文字,然後傳回節點。

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

  return node
}

管理一或多個祝賀訊息

JavaScript 現在會將容器新增至文件,用於存放訊息,並準備好新增建立的訊息。addToast() 函式會協調處理一或多個祝 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、動畫退出。如果已有現存的訊息方塊,系統會呼叫 flipToast(),並採用 Paul Lewis 所稱的 FLIP 技術。這個概念是要計算加入新祝 toast 前後,容器位置的差異。 這就像標記 Toaster 目前的位置和移動目的地,然後從原位置到新位置製作動畫。

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 動畫 (三個關鍵影格動畫),以解析 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 Promise 是否完成。allSettled 是否適用於我們,在所有承諾都已履行後,自行解決問題並完成。使用 await Promise.allSettled() 表示下一行程式碼可以放心地移除元素,並假設祝賀訊息已完成生命週期。最後,呼叫 resolve() 會完成高階 Toast 承諾,因此開發人員可以在顯示 Toast 後清理或執行其他工作。

export default Toast

最後,Toast 函式會從模組匯出,供其他指令碼匯入及使用。

使用 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')
}

結論

現在你已瞭解我的做法,你會怎麼做呢?🙂

讓我們多元化方法,學習在網路上建構內容的所有方式。 建立試聽版,然後在推特上傳送連結給我,我會將連結加到下方的社群混音區!

社群重混作品