建構對話方塊元件

基礎概念總覽:如何使用 <dialog> 元素建構可適應顏色、具備回應式設計且符合無障礙規範的迷你和巨型模式。

在這篇文章中,我想分享我對如何使用 <dialog> 元素建構可自動調整色彩、具備回應性且無障礙的迷你和大型模式的看法。試用示範模式,並查看來源

示範淺色和深色主題的特大和迷你對話方塊。

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

總覽

<dialog> 元素非常適合用於網頁內文脈絡資訊或動作。請考慮何時可使用單一頁面動作,而非多頁面動作,提升使用者體驗:或許是因為表單很小,或使用者只需要確認或取消。

<dialog> 元素最近在各瀏覽器中已趨於穩定:

Browser Support

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Source

我發現該元素缺少幾項內容,因此在 GUI Challenge 中,我新增了預期的開發人員體驗項目:額外事件、輕觸即可關閉、自訂動畫,以及迷你和巨型類型。

標記

<dialog> 元素的基本要素不多,元素會自動隱藏,並內建樣式來疊加內容。

<dialog>
  …
</dialog>

我們可以改善這個基準。

傳統上,對話方塊元素與模式視窗有許多相似之處,且名稱通常可以互換。我在此擅自使用對話方塊元素,同時處理小型對話方塊彈出視窗 (迷你) 和全頁對話方塊 (特大)。我將這兩個對話方塊分別命名為 mega 和 mini,並根據不同用途稍作調整。我新增了 modal-mode 屬性,可讓您指定類型:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

淺色和深色主題的迷你對話方塊和巨型對話方塊螢幕截圖。

不一定,但一般來說,對話方塊元素會用於收集某些互動資訊。對話方塊元素內的表單會一起運作。 建議您使用表單元素包裝對話方塊內容,讓 JavaScript 存取使用者輸入的資料。此外,使用 method="dialog" 的表單內按鈕可以關閉對話方塊,不必使用 JavaScript 即可傳遞資料。

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Mega 對話方塊

表單內有三個巨型對話方塊元素:<header><article><footer>。這些是語意容器,也是對話方塊呈現方式的樣式目標。標題會顯示在模式視窗中,並提供關閉按鈕。本文適用於表單輸入內容和資訊。頁尾會保留動作按鈕的 <menu>

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    <header>
      <h3>Dialog title</h3>
      <button onclick="this.closest('dialog').close('close')"></button>
    </header>
    <article>...</article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

第一個選單按鈕具有 autofocusonclick 內嵌事件處理常式。對話方塊開啟時,autofocus 屬性會收到焦點,而我認為最佳做法是將焦點放在取消按鈕上,而不是確認按鈕。這樣可確保確認是刻意操作,而非意外。

迷你對話方塊

迷你對話方塊與巨型對話方塊非常相似,只是缺少 <header> 元素。因此可以縮小並內嵌。

<dialog id="MiniDialog" modal-mode="mini">
  <form method="dialog">
    <article>
      <p>Are you sure you want to remove this user?</p>
    </article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

對話方塊元素可做為完整檢視區塊元素的穩固基礎,用於收集資料和使用者互動。這些基本功能可讓您在網站或應用程式中,打造非常有趣且強大的互動體驗。

無障礙設定

對話方塊元素內建的無障礙功能非常完善。我通常會新增這些功能,但這次許多功能都已存在。

還原焦點

建構側邊導覽列元件一節所述,開啟和關閉項目時,請務必將焦點放在相關的開啟和關閉按鈕上。側邊導覽開啟時,焦點會移至關閉按鈕。按下關閉按鈕時,焦點會還原至開啟該按鈕的按鈕。

使用對話方塊元素時,這是內建的預設行為:

很抱歉,如果您想為對話方塊加入進出動畫,這項功能就會失效。我會在 JavaScript 區段中還原這項功能。

鎖定焦點

對話方塊元素會在文件中為您管理 inert。在 inert 之前,JavaScript 會用於監看焦點是否離開元素,並在焦點離開時攔截並放回。

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Source

inert之後,文件中的任何部分都可以「凍結」,不再是焦點目標,也無法透過滑鼠互動。焦點不會受限,而是會導向文件中唯一可互動的部分。

開啟並自動對焦元素

根據預設,對話方塊元素會將焦點指派給對話方塊標記中的第一個可聚焦元素。如果這不是使用者預設的最佳元素,請使用 autofocus 屬性。如先前所述,我認為最佳做法是將此項目放在「取消」按鈕上,而不是「確認」按鈕。這樣才能確保確認是刻意操作,而非意外。

使用 Esc 鍵關閉

請務必讓使用者能輕鬆關閉這個可能造成干擾的元素。幸好對話方塊元素會為您處理 Esc 鍵,讓您免除協調負擔。

樣式

您可以輕鬆或困難地為對話方塊元素設定樣式。簡單做法是不變更對話方塊的顯示屬性,並接受其限制。我走的是艱難的路,為開啟和關閉對話方塊提供自訂動畫,接管 display 屬性等。

使用 Open Props 設定樣式

為了加快自適應顏色和整體設計一致性,我毫不避諱地導入了 CSS 變數程式庫 Open Props。除了免費提供的變數,我也匯入 normalize 檔案和一些按鈕,這兩者都是 Open Props 提供的選用匯入項目。這些匯入項目可協助我專注於自訂對話方塊和示範,同時不需要大量樣式支援,就能讓對話方塊看起來美觀。

設定 <dialog> 元素的樣式

擁有顯示屬性

對話方塊元素的預設顯示和隱藏行為,會將顯示屬性從 block 切換為 none。很抱歉,這表示無法為其進出動畫,只能為其進入動畫。我想製作進場和退場動畫,第一步是設定自己的 display 屬性:

dialog {
  display: grid;
}

如上述 CSS 程式碼片段所示,變更並因此擁有顯示屬性值後,您需要管理大量樣式,才能提供適當的使用者體驗。首先,對話方塊的預設狀態為關閉。您可以透過下列樣式,以視覺化方式呈現這個狀態,並防止對話方塊接收互動:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

現在對話方塊處於隱藏狀態,未開啟時無法互動。稍後我會新增一些 JavaScript 來管理對話方塊的 inert 屬性,確保鍵盤和螢幕閱讀器使用者也無法存取隱藏的對話方塊。

為對話方塊提供自動調整色彩主題

大型對話方塊顯示淺色和深色主題,示範途徑顏色。

雖然 color-scheme 會根據系統偏好設定,為文件選用瀏覽器提供的適應性色彩主題 (淺色或深色),但我希望進一步自訂對話方塊元素。Open Props 提供幾種介面顏色,可自動配合淺色和深色系統偏好設定調整,類似於使用 color-scheme。這些顏色很適合在設計中建立圖層,我喜歡使用顏色來輔助呈現圖層表面的外觀。背景顏色為 var(--surface-1);如要疊加在該圖層上,請使用 var(--surface-2)

dialog {
  
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

日後會為子項元素 (例如頁首和頁尾) 新增更多適應性色彩。我認為這些是對話方塊元素的額外功能,但對於設計引人入勝且設計良好的對話方塊而言,這些功能非常重要。

回應式對話方塊大小

對話方塊預設會將大小委派給內容,這通常很棒。我的目標是將 max-inline-size 限制在可讀取的大小 (--size-content-3 = 60ch) 或可視區域寬度的 90%。這樣可確保對話方塊不會在行動裝置上從一側延伸到另一側,也不會在電腦螢幕上過寬而難以閱讀。然後我會新增 max-block-size,這樣對話方塊就不會超過網頁高度。這也表示我們需要指定對話方塊的可捲動區域,以免對話方塊元素過高。

dialog {
  
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

請注意,我重複了 max-block-size 兩次。第一個使用 80vh,這是實體檢視區塊單位。我真正想做的是讓對話方塊保持在相對流程中,以供國際使用者使用,因此我在第二個宣告中使用了邏輯上較新的 dvb 單元 (僅部分支援),等這個單元更穩定後再使用。

大型對話方塊位置

如要協助放置對話方塊元素,建議將其拆解為兩部分:全螢幕背景和對話方塊容器。背景必須遮蓋所有內容,提供陰影效果,以支援這個對話方塊位於前方,且無法存取後方內容。對話方塊容器可自由將自身置中於這個背景上,並根據內容需求採取任何形狀。

下列樣式會將對話方塊元素固定在視窗中,並延展至每個角落,然後使用 margin: auto 將內容置中:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
行動裝置超大對話方塊樣式

在小型檢視區塊中,我會以稍微不同的方式設定這個全頁大型模式的樣式。我將底部邊界設為 0,將對話方塊內容帶到可視區域底部。只要稍微調整樣式,就能將對話方塊變成動作表,更貼近使用者的拇指:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

螢幕截圖:開發人員工具在開啟時,將邊界間距疊加在電腦和行動裝置的巨型對話方塊上。

迷你對話方塊的位置

使用較大的可視區域 (例如在桌上型電腦上) 時,我選擇將迷你對話方塊放置在呼叫這些對話方塊的元素上方。我需要 JavaScript 才能執行這項操作。您可以在這裡找到我使用的技術,但我覺得這超出本文範圍。如果沒有 JavaScript,迷你對話方塊會顯示在畫面中央,就像大型對話方塊一樣。

讓圖片更生動

最後,為對話方塊增添一些風格,讓它看起來像遠高於頁面的柔軟表面。只要將對話方塊的邊角設為圓角,即可達到柔和效果。 深度是透過 Open Props 精心製作的陰影屬性達成:

dialog {
  
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

自訂背景虛擬元素

我選擇以非常輕微的方式處理背景,只使用 backdrop-filter 在巨型對話方塊中新增模糊效果:

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

Source

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

我也選擇在 backdrop-filter 上放置轉場效果,希望瀏覽器日後能允許轉場背景元素:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

螢幕截圖:大型對話方塊疊加在模糊背景上,背景是色彩繽紛的個人資料相片。

額外樣式

我將這個部分稱為「extras」,因為它與對話方塊元素的一般用途相比,更與對話方塊元素示範有關。

捲動範圍限制

顯示對話方塊時,使用者仍可捲動後方的網頁,這並非我所要的結果:

通常 overscroll-behavior 是我的常用解決方案,但根據規格,這對話方塊並非捲動埠,也就是說,這不是捲動器,因此無法防止任何項目。我可以使用 JavaScript 監看本指南中的新事件 (例如「closed」和「opened」),並在文件中切換 overflow: hidden,也可以等待 :has() 在所有瀏覽器中穩定運作:

Browser Support

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Source

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

現在開啟大型對話方塊時,HTML 文件會包含 overflow: hidden

<form>」版面配置

除了是收集使用者互動資訊的重要元素外,我還會使用它來配置標題、頁尾和文章元素。我打算使用這個版面配置,將文章子項設為可捲動區域。我使用 grid-template-rows 達成這個目標。 文章元素會取得 1fr,而表單本身的高度上限與對話方塊元素相同。設定這個固定高度和固定列大小,可讓文章元素受到限制,並在溢位時捲動:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

螢幕截圖:開發人員工具在列上方重疊顯示格線版面配置資訊。

設定對話方塊 <header> 的樣式

這個元素的作用是為對話方塊內容提供標題,並提供容易找到的關閉按鈕。此外,也提供表面顏色,讓對話方塊文章內容顯示在後方。這些需求會產生 flexbox 容器、垂直對齊的項目 (間距會延伸至邊緣),以及一些邊框間距和間隙,為標題和關閉按鈕預留空間:

dialog > form > header {
  display: flex;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  background: var(--surface-2);
  padding-block: var(--size-3);
  padding-inline: var(--size-5);
}

@media (prefers-color-scheme: dark) {
  dialog > form > header {
    background: var(--surface-1);
  }
}

螢幕截圖:Chrome 開發人員工具在對話方塊標題上疊加彈性方塊版面配置資訊。

設定標題關閉按鈕的樣式

由於這個範例使用 Open Props 按鈕,關閉按鈕會自訂為圓形圖示的置中按鈕,如下所示:

dialog > form > header > button {
  border-radius: var(--radius-round);
  padding: .75ch;
  aspect-ratio: 1;
  flex-shrink: 0;
  place-items: center;
  stroke: currentColor;
  stroke-width: 3px;
}

螢幕截圖:Chrome 開發人員工具疊加顯示標題關閉按鈕的大小和邊框間距資訊。

設定對話方塊 <article> 的樣式

文章元素在這個對話方塊中扮演特殊角色:如果對話方塊較高或較長,這個空間就會用於捲動。

為達成此目的,父項表單元素已為自身設定一些上限,如果這個文章元素過高,就會受到這些限制。設定 overflow-y: auto,只在需要時顯示捲軸,並使用 overscroll-behavior: contain 在其中包含捲動功能,其餘則為自訂呈現樣式:

dialog > form > article {
  overflow-y: auto; 
  max-block-size: 100%; /* safari */
  overscroll-behavior-y: contain;
  display: grid;
  justify-items: flex-start;
  gap: var(--size-3);
  box-shadow: var(--shadow-2);
  z-index: var(--layer-1);
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: light) {
  dialog > form > article {
    background: var(--surface-1);
  }
}

頁尾的角色是包含動作按鈕選單。Flexbox 用於將內容對齊頁尾的內嵌軸尾端,然後提供一些間距,讓按鈕有空間。

dialog > form > footer {
  background: var(--surface-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: dark) {
  dialog > form > footer {
    background: var(--surface-1);
  }
}

螢幕截圖:Chrome 開發人員工具在頁尾元素上疊加彈性方塊版面配置資訊。

menu 元素用於包含對話方塊的動作按鈕。它使用換行 flexbox 版面配置搭配 gap,在按鈕之間提供空間。選單元素有邊框間距,例如 <ul>。我也不需要這個樣式,因此一併移除。

dialog > form > footer > menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  padding-inline-start: 0;
}

dialog > form > footer > menu:only-child {
  margin-inline-start: auto;
}

螢幕截圖:Chrome 開發人員工具在頁尾選單元素上疊加彈性方塊資訊。

動畫

對話方塊元素通常會加入動畫效果,因為它們會進入和離開視窗。 為對話方塊的進入和退出動作提供一些輔助動作,有助於使用者在流程中瞭解自己的位置。

一般來說,對話方塊元素只能以動畫效果顯示,無法隱藏。這是因為瀏覽器會切換元素中的 display 屬性。先前,指南會將顯示畫面設為格線,但絕不會設為無。這樣就能製作進出動畫。

Open Props 隨附許多可用的影格動畫,方便您輕鬆編排及解讀動畫。以下是我採取的動畫目標和分層做法:

  1. 「減少動態效果」是預設轉場效果,可簡單地淡入和淡出不透明度。
  2. 如果動作沒問題,系統會新增滑動和縮放動畫。
  3. 大型對話方塊的回應式行動版面配置已調整為滑出。

安全且有意義的預設轉換

雖然 Open Props 隨附淡入和淡出的關鍵影格,但我偏好這種分層式轉場效果,並將關鍵影格動畫視為潛在升級項目。我們稍早已使用不透明度設定對話方塊的顯示設定,並根據 [open] 屬性協調 10。如要在 0% 和 100% 之間轉換,請告知瀏覽器您要的時間長度和緩和類型:

dialog {
  transition: opacity .5s var(--ease-3);
}

為轉場效果新增動態效果

如果使用者接受動態效果,大型和迷你對話方塊都應向上滑動做為進入動畫,並縮放做為退出動畫。您可以使用 prefers-reduced-motion 媒體查詢和幾個 Open Props 達成此目的:

@media (prefers-reduced-motion: no-preference) {
  dialog {
    animation: var(--animation-scale-down) forwards;
    animation-timing-function: var(--ease-squish-3);
  }

  dialog[open] {
    animation: var(--animation-slide-in-up) forwards;
  }
}

調整行動裝置的退場動畫

在樣式設定一節中,我們已將大型對話方塊樣式調整為更適合行動裝置,使其更像動作功能表,彷彿一小張紙從畫面底部向上滑動,並仍附著在底部。縮放退出動畫不太適合這個新設計,我們可以透過幾個媒體查詢和一些 Open Props 調整:

@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    animation: var(--animation-slide-out-down) forwards;
    animation-timing-function: var(--ease-squish-2);
  }
}

JavaScript

您可以使用 JavaScript 新增許多項目:

// dialog.js
export default async function (dialog) {
  // add light dismiss
  // add closing and closed events
  // add opening and opened events
  // add removed event
  // removing loading attribute
}

這些新增項目是為了實現輕觸即關閉 (點按對話方塊背景)、動畫,以及一些額外事件,以便更準確地取得表單資料。

新增輕觸即關閉功能

這項工作很簡單,非常適合加到未顯示動畫的對話方塊元素。互動方式是監看對話方塊元素上的點擊,並利用事件冒泡評估點擊的項目,且只有在點擊最上層元素時才會close()

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

請注意 dialog.close('dismiss')。系統會呼叫事件並提供字串。 其他 JavaScript 可以擷取這個字串,深入瞭解對話方塊的關閉方式。您會發現,每次從各種按鈕呼叫函式時,我也提供了近似字串,為應用程式提供使用者互動的背景資訊。

新增結案和已結案事件

對話方塊元素會隨附關閉事件:呼叫對話方塊 close() 函式時,系統會立即發出該事件。由於我們要為這個元素製作動畫,因此最好在動畫前後都有事件,以便擷取資料或重設對話方塊表單。我在這裡使用它來管理已關閉對話方塊中 inert 屬性的新增作業,而在示範中,我使用這些屬性來修改顯示圖片清單 (如果使用者已提交新圖片)。

如要達成這個目標,請建立兩個名為 closingclosed 的新事件。然後監聽對話方塊的內建關閉事件。接著,將對話方塊設為 inert,並傳送 closing 事件。下一個工作是等待對話方塊上的動畫和轉場效果執行完畢,然後傳送 closed 事件。

const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')

export default async function (dialog) {
  
  dialog.addEventListener('close', dialogClose)
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

animationsComplete 函式 (也用於「建構 Toast 元件」) 會根據動畫和轉場效果 Promise 的完成情況,傳回 Promise。因此 dialogClose非同步函式,可以 await 傳回的 Promise,並放心地繼續處理關閉事件。

新增開幕和已開放活動

由於內建對話方塊元素不會提供開啟事件 (如同關閉事件),因此這類事件較難新增。我使用 MutationObserver,深入瞭解對話方塊屬性的變化。在這個觀察器中,我會監看 open 屬性的變更,並據此管理自訂事件。

與開始和結束事件的建立方式類似,請建立兩個名為 openingopened 的新事件。先前我們監聽對話方塊關閉事件,這次則使用建立的變異觀察器監看對話方塊的屬性。


const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

對話方塊屬性變更時,系統會呼叫變動觀察器回呼函式,並以陣列形式提供變更清單。疊代屬性變更,尋找要開啟的 attributeName。接著,請檢查元素是否具有屬性,這會告知對話方塊是否已開啟。如果已開啟,請移除 inert 屬性,並將焦點設為要求 autofocus 的元素,或對話方塊中找到的第一個 button 元素。最後,與 closing 和 closed 事件類似,請立即傳送 opening 事件,等待動畫完成,然後傳送 opened 事件。

新增已移除的活動

在單頁應用程式中,對話方塊通常會根據路徑或其他應用程式需求和狀態新增及移除。移除對話方塊時,清除事件或資料可能很有用。

你可以使用另一個變動觀察器達成這個目標。這次我們要觀察的不是對話方塊元素上的屬性,而是 body 元素的子項,並監看對話方塊元素是否遭到移除。


const dialogRemovedEvent = new Event('removed')

export default async function (dialog) {
  
  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })
}

const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

每當子項新增至文件主體或從中移除時,系統就會呼叫變動觀察工具回呼。要監看的特定突變是針對具有對話方塊 removedNodesnodeName。如果對話方塊已移除,系統會移除點擊和關閉事件,以釋放記憶體,並傳送自訂移除事件。

移除 loading 屬性

為避免對話方塊動畫在新增至網頁或網頁載入時播放結束動畫,對話方塊已新增載入屬性。下列指令碼會等待對話方塊動畫執行完畢,然後移除屬性。現在對話方塊可以自由進出動畫,我們也有效隱藏了原本會造成干擾的動畫。

export default async function (dialog) {
  
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

進一步瞭解如何防止網頁載入時出現關鍵影格動畫

全部

現在我們已逐一說明各個部分,以下是完整的 dialog.js

// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')
const dialogRemovedEvent = new Event('removed')

// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

// wait for all dialog animations to complete their promises
const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

// page load dialogs setup
export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
  dialog.addEventListener('close', dialogClose)

  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })

  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })

  // remove loading attribute
  // prevent page load @keyframes playing
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

使用 dialog.js 模組

模組匯出的函式預期會被呼叫,並傳遞要新增這些新事件和功能的對話方塊元素:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

就這樣,這兩個對話方塊都已升級,可輕觸關閉、修正動畫載入問題,並提供更多事件供您使用。

監聽新的自訂事件

每個升級的對話方塊元素現在都可以監聽五個新事件,如下所示:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

以下是處理這些事件的兩個範例:

const dialogOpening = ({target:dialog}) => {
  console.log('Dialog opening', dialog)
}

const dialogClosed = ({target:dialog}) => {
  console.log('Dialog closed', dialog)
  console.info('Dialog user action:', dialog.returnValue)

  if (dialog.returnValue === 'confirm') {
    // do stuff with the form values
    const dialogFormData = new FormData(dialog.querySelector('form'))
    console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))

    // then reset the form
    dialog.querySelector('form')?.reset()
  }
}

在以對話方塊元素建構的示範中,我使用該關閉事件和表單資料,將新的虛擬人偶元素新增至清單。時機恰到好處,因為對話方塊已完成結束動畫,然後一些指令碼會為新虛擬人偶製作動畫。有了這些新事件,就能更順暢地安排使用者體驗。

注意 dialog.returnValue:這包含呼叫對話方塊 close() 事件時傳遞的關閉字串。在 dialogClosed 事件中,瞭解對話方塊是否已關閉、取消或確認至關重要。如果確認無誤,指令碼就會抓取表單值並重設表單。重設後,對話方塊再次顯示時就會空白,方便您重新提交。

結論

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

讓我們多元化地運用各種方法,學習在網路上建構內容。

建立試聽版,然後在推特上傳送連結給我,我會將連結加到下方的社群混音區!

社群重混作品

資源