建立浮動式訊息元件

說明如何建構自動調整且可存取的浮動式訊息元件的基本總覽。

在這篇文章中,我想分享如何建構浮動式訊息元件。立即試用示範

示範

如果你偏好使用影片,也可以觀看這篇 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;
}

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

除了定位在可視區域中本身外,浮動式訊息容器是一種格線容器,可對齊及發布浮動式訊息。項目會以 justify-content 群組為中心,並分別以 justify-items 置中。稍微丟棄 gap,這樣浮動式訊息就不會碰到。

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

浮動式訊息群組上的 CSS 格線重疊的螢幕截圖,這次醒目顯示的是浮動式訊息子元素之間的空格和間距。

統一發票

個別的浮動式訊息具有一些 paddingborder-radiusmin() 函式,以配合行動裝置和電腦的大小調整。下列 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,以便配合使用者的設定和互動進行調整。

浮動式訊息容器

浮動式訊息沒有互動,輕觸或滑動浮動式訊息不會執行任何動作,但目前會耗用指標事件。利用下列 CSS 防止浮動式訊息竊取點擊。

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

統一發票

利用自訂屬性、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 會在單一動畫中控制進入、等待及結束浮動式訊息的事宜。

@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 之前新增元素,這會導致 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()

建立浮動式訊息 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
}

管理一或多個浮動式訊息

JavaScript 現在會為包含浮動式訊息的文件新增容器,並準備好新增已建立的浮動式訊息。addToast() 函式會自動化調度管理處理一或多個浮動式訊息。首先,檢查浮動式訊息的數量及運動情況是否合適,然後利用這項資訊附加浮動式訊息或執行一些精美的動畫,讓其他浮動式訊息出現在「釋出空間」來供新浮動式訊息使用。

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') 時,系統會建立浮動式訊息並新增至頁面 (可能甚至會為了配合新浮動式訊息而建立動畫),並傳回「承諾」,且系統會「觀看」建立的浮動式訊息,以實現預期的解析度。

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() 表示下一行程式碼可以放心移除元素,並假設浮動式訊息已完成其生命週期。最後,呼叫 resolve() 可完成高階浮動式訊息的承諾,讓開發人員可在浮動式訊息顯示後清除或執行其他工作。

export default Toast

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

使用浮動式訊息元件

使用浮動式訊息或浮動式訊息的開發人員體驗是透過匯入 Toast 函式,並以訊息字串呼叫的方式完成。

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

如果開發人員想要清除工作或任何事項,在浮動式訊息顯示後,就能使用非同步和「await」await功能。

import Toast from './toast.js'

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

結語

現在既然你已經知道我怎麼做,你會怎麼做‽ 🙂?

讓我們帶您更多元的方法,並瞭解運用網路打造網站的所有方式。 請建立示範並透過 Twitter 推文連結,我就能將這項工具新增至下方的「社群重混」部分!

社群重混作品