建構對話方塊元件

基礎總覽說明如何使用 <dialog> 元素建構可自動調整顏色、回應式,以及可存取的迷你及大型互動視窗。

在這篇文章中,我想分享我對於如何運用 <dialog> 元素建構可自動調整顏色、回應式和無障礙迷你及大型互動視窗的想法。歡迎立即試用查看原始碼

示範在淺色和深色主題中的大型對話方塊和小型對話方塊。

如果您喜歡看影片,請參考這篇文章的 YouTube 版本:

總覽

<dialog> 元素適用於網頁內情境資訊或動作。請想想在哪些情況下,使用者體驗是否可透過同一種網頁動作 (而非多頁動作) 創造效益:表單可能很小,或是使用者只需要確認或取消動作。

<dialog> 元素近期已在所有瀏覽器中保持穩定:

瀏覽器支援

  • 37
  • 79
  • 98
  • 15.4

來源

我發現元素缺少某些元素,因此在這次的 GUI 挑戰中,我加入了預期開發人員體驗項目:額外事件、淺色關閉、自訂動畫,以及小型和大型類型。

標記

<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>

超級對話方塊

大型對話方塊的格式為三個元素:<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 程式碼片段所示,變更顯示屬性值 (如上方的 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,迷你對話方塊會顯示在畫面中央,就像超大型對話方塊一樣。

讓內容脫穎而出

最後,在對話方塊中加入一些功能,看起來就好像位在頁面上方的軟面。只要將對話方塊的角落四捨五入,即可達到柔和度。如要達到此深度,就必須使用一個開放式 Props 精心製作的陰影提案

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

自訂背景虛擬元素

我選擇非常稍微使用背景幕,只為大型對話方塊加上 backdrop-filter 的模糊效果:

瀏覽器支援

  • 76
  • 79
  • 103
  • 9

來源

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

我也選擇在 backdrop-filter 上進行轉換,希望瀏覽器未來能轉換背景元素:

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

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

設定額外項目樣式

我稱之為「extras」,因為比起一般的對話方塊元素,「額外」對於對話方塊元素示範執行更多操作。

捲動隔離設定

對話方塊顯示時,使用者仍能捲動後方的頁面,我不想這麼做:

一般來說,overscroll-behavior 會是我常用的解決方案,但根據規格,這對對話方塊沒有影響,因為這不是捲動通訊埠,也就是說,它不是捲動器,因此沒有可防止的捲軸。我可以使用 JavaScript 監控本指南中的新事件 (例如「已關閉」和「已開啟」),然後切換文件上的 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);
  }
}

在對話方塊標題上重疊顯示 Flexbox 版面配置資訊的 Chrome 開發人員工具螢幕截圖。

設定標題關閉按鈕樣式

由於這個示範使用「開啟 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 的包裝 Flexbox 版面配置,以提供按鈕之間的空間。選單元素具有邊框間距,例如 <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;
}

在頁尾選單元素上疊加 Flexbox 資訊的 Chrome 開發人員工具螢幕截圖。

動畫

對話方塊元素通常是動畫,因為這類元素會進入及離開視窗。為這個進入和離開畫面提供對話方塊可輔助動作,有助於使用者自行操作流程。

一般來說,對話方塊元素只能以動畫形式呈現,不能顯示。這是因為瀏覽器會切換元素的 display 屬性。在先前版本中,指南會將顯示方式設定為格線,一律不設為 none。就能享有動畫進出的功能。

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

  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 事件中,請務必瞭解對話方塊是關閉、取消還是已確認。如果確認無誤,指令碼會擷取表單值並重設表單。重設功能很實用,這樣當對話方塊再次顯示時,就會呈現空白,可供新的提交內容。

結論

現在你知道我怎麼了,這樣會如何 🙂?

讓我們來體驗多元的方法,瞭解透過網路建立內容的所有方式。

建立示範、張貼推文 連結,以便我們將其新增至下方的社群重混專區!

社群重混作品

資源