建構對話方塊元件

基本介紹如何使用 <dialog> 元素建構自動調整亮度、回應式,以及方便存取的迷你和大型模組。

在這篇文章中,我想分享您使用 <dialog> 元素建構自動調整色彩、回應式,以及無障礙迷你和大型視窗的想法。試用示範查看原始碼

以淺色和深色主題顯示大型與迷你對話方塊。

如果你偏好使用影片,也可以觀看這篇 YouTube 文章:

總覽

<dialog> 元素適用於網頁內內容資訊或動作。請考慮在何時可對單一網頁動作 (而非多頁動作) 提供良好的使用者體驗:也許是因為表單過小,或者使用者只需要確認或取消。

<dialog> 元素最近在各瀏覽器上變得穩定:

瀏覽器支援

  • 37
  • 79
  • 98
  • 15.4

資料來源

我發現該元素缺少部分內容,因此在 GUI 挑戰中,我新增了預期的開發人員體驗項目:額外事件、輕關閉、自訂動畫,以及迷你和大型相片類型。

標記

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

<dialog>
  …
</dialog>

我們可以改善這個基準。

一般來說,對話方塊元素會與強制回應模組共用大量,且名稱經常可互換。我決定將對話方塊元素用於小型對話方塊 (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>

超級對話

大型對話方塊包含以下三個元素:<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 是用來監控焦點離開元素,此時它會攔截並傳回元素。

瀏覽器支援

  • 102
  • 102
  • 112
  • 15.5

資料來源

inert 之後,文件的任何部分可能會「凍結」,因為這些部分已不再是焦點目標,或無法與滑鼠互動。焦點會引導使用者前往文件中唯一的互動部分,而不是裁剪焦點。

開啟並自動聚焦元素

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

使用 Esc 鍵結束

請務必輕易關閉這個可能會造成乾擾的元素。幸好,對話方塊元素會為您處理逸出鍵,讓您免於自動化調度管理的負擔。

風格

要設定對話方塊元素樣式和固定路徑樣式,方法也有很多。使用簡易路徑的方法是不變更對話方塊的顯示屬性,並遵守其限制。我會使用最困難的路徑提供自訂動畫,用於開啟和關閉對話方塊、接管 display 屬性等。

使用開放屬性設定樣式

為了加速自動調整色彩和整體設計一致性,我對於 CSS 變數程式庫的「Open Props」很瞭解。除了免費提供的變數以外,我也會匯入正規化檔案和一些按鈕,這兩個按鈕都會以選用匯入的形式提供。這些匯入項目可讓我專注於自訂對話方塊和示範,而不需要大量樣式來支援樣式,讓外觀看起來更美觀。

設定 <dialog> 元素的樣式

擁有多媒體廣告屬性

對話方塊元素的預設顯示和隱藏行為會將顯示屬性從 block 切換為 none。很遺憾,這表示不能在內外動畫。我想要同時為內外的使用者加上動畫效果,第一步是設定自己的 display 屬性:

dialog {
  display: grid;
}

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

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

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

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

顯示淺色和深色主題的大型對話方塊,示範表面顏色。

雖然 color-scheme 會將文件設為瀏覽器提供的自動調整色彩主題,以符合淺色和深色的系統偏好設定,但我想自訂對話方塊元素。開啟 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 的模糊效果:

瀏覽器支援

  • 76
  • 17
  • 103
  • 9

資料來源

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

我還選擇在 backdrop-filter 上加入轉場效果,希望瀏覽器日後能夠轉換背景元素:

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

大型對話方塊的螢幕截圖,與彩色顯示圖片的模糊背景重疊。

其他樣式設定

我將這個部分稱為「額外項目」,因為比起一般的對話方塊元素,在對話方塊元素示範中,需要進行更多操作。

捲動隔離

顯示對話方塊時,使用者仍可捲動後面的頁面,但請勿執行以下動作:

通常,overscroll-behavior 是我常用的解決方案,但根據規格,它在對話方塊上不會產生任何作用,因為這不是捲動連接埠,因此不是捲動器,因此沒有可防止的。我可以使用 JavaScript 監控本指南中的新事件 (例如「closed」和「opened」),並在文件中切換 overflow: hidden;或者,我也可以等待 :has() 在所有瀏覽器中穩定運作:

瀏覽器支援

  • 105
  • 105
  • 121
  • 15.4

資料來源

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 開發人員工具螢幕截圖,在對話方塊標頭上疊加 Flexbox 版面配置資訊。

設定標頭關閉按鈕樣式

由於示範使用的是「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 開發人員工具螢幕截圖:將 Flexbox 版面配置資訊疊加在頁尾元素上。

menu 元素是用來包含對話方塊的動作按鈕。它使用包含 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 開發人員工具螢幕截圖:在頁尾選單元素上疊加 Flexbox 資訊。

動畫

對話方塊元素通常會以動畫呈現,因為會進入並離開視窗。為對話方塊提供一些輔助動作,幫助使用者瞭解該進入流程。

一般來說,對話方塊元素只能以動畫形式呈現,無法發揮動畫效果。這是因為瀏覽器會切換元素的 display 屬性。之前,本指南會將顯示畫面設為格線,然後永不設為無。如此一來,你就可以加入動畫加入和跳出動畫效果。

開放式屬性提供許多主要畫面格動畫,讓自動化調度管理作業變得簡單又清晰。以下是我採取的動畫目標和分層做法:

  1. 慢動作為預設轉場效果,簡單的不透明度淡入/淡出。
  2. 如果動作沒有問題,就會新增滑動和縮放動畫。
  3. 大型對話方塊的回應式行動裝置版面配置會經過調整,以滑出。

安全且有意義的預設轉換作業

雖然公開屬性具有淡入和淡出的主要畫面格,但我偏好使用這個分層轉場效果,並以主要畫面格動畫做為預設升級效果。我們稍早已將對話方塊的顯示設定設為不透明度的樣式,並根據 [open] 屬性自動化調度管理 10。如要在 0% 和 100% 之間轉換,請告知瀏覽器您需要的時間以及您希望的加/減速設定:

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

為轉場效果新增動態效果

如果使用者能接受動作,大型對話方塊和迷你對話方塊就應當進入時滑行,並在使用者離開時向外擴充。您可以利用 prefers-reduced-motion 媒體查詢和一些公開屬性來實現:

@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;
  }
}

調整行動裝置的結束動畫

先前在樣式一節中,大型對話方塊樣式經過調整,適合行動裝置使用,就像動作表一樣,將一小片從螢幕底部滑入並仍連接至底部。向外擴充動畫並不適合這個新設計,我們可透過以下幾個媒體查詢和一些公開屬性來調整這項設定:

@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 函式也用於建構浮動式訊息元件,會根據動畫是否完成和轉換承諾傳回承諾。這就是為何 dialogClose非同步函式的原因,因此可以接著 await 傳回的承諾,並放心移至關閉的事件。

新增開啟和已開啟的活動

這些事件並不容易新增,因為內建的對話方塊元素不會像關閉功能一樣提供開啟事件。我使用 MutationObserver 深入分析對話方塊的屬性變更。在這個觀察器中,我會注意開啟屬性的變更,並據此管理自訂事件。

類似於我們開始和關閉事件的方式,建立兩個名為 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 元素。最後,與關閉和關閉事件類似,請立即傳送開啟事件,等待動畫完成,然後分派開啟的事件。

新增已移除的事件

在單一頁面應用程式中,系統通常會根據路徑或其他應用程式需求和狀態,新增及移除對話方塊。移除對話方塊時,建議您清除事件或資料。

您可以使用其他異動觀察器達成此目的。這次不會觀察對話方塊元素的屬性,而是會觀察主體元素的子項,並留意對話方塊元素遭到移除的情形。

…
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)
      }
    })
  })
})

每當在文件內文中新增或移除子項時,就會呼叫異動觀察器回呼。監控的特定異動適用於具有對話方塊 nodeNameremovedNodes。如果對話方塊已移除,系統會移除點擊和關閉事件來釋放記憶體,並分派自訂的移除事件。

移除載入屬性

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

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 事件中,請務必瞭解對話方塊是否已關閉、取消或已確認。如果已經確認,指令碼就會擷取表單值並重設表單。重設會很實用,因此在再次顯示對話方塊時,該對話方塊會是空白的,可以準備新的提交內容。

結語

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

讓我們帶您更多元的方法,並瞭解運用網路打造網站的所有方式。

請建立示範並透過 Twitter 推文連結,我就能將這項工具新增至下方的「社群重混」部分!

社群重混作品

資源