建構對話方塊元件

基礎概述:如何使用 <dialog> 元素,建立可自適應顏色、回應式且無障礙的迷你和超大型彈出式視窗。

在這篇文章中,我想分享如何使用 <dialog> 元素,建立可自適應顏色、回應式且易於存取的迷你和超大型模式對話方塊。試用示範模式查看來源

展示淺色和深色主題的巨型和迷你對話方塊。

如果你偏好觀看影片,請參閱這篇文章的 YouTube 版本:

總覽

<dialog> 元素非常適合用於頁面內的背景資訊或動作。請考量使用者體驗何時可從同頁動作而非多頁動作中受益:可能是因為表單很小,或是使用者只需確認或取消。

<dialog> 元素最近已在各瀏覽器上穩定運作:

瀏覽器支援

  • Chrome:37。
  • Edge:79。
  • Firefox:98。
  • Safari: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>

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 會監控焦點離開元素的情況,並在該時機攔截並放回焦點。

瀏覽器支援

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

資料來源

inert 之後,文件的任何部分都可以「凍結」,也就是不再是焦點目標,或無法與滑鼠互動。焦點不會被困住,而是會引導至文件中唯一的互動部分。

開啟並自動對焦元素

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

使用 Esc 鍵關閉

請務必讓使用者輕鬆關閉這個可能會中斷的元素。幸運的是,對話方塊元素會為您處理 Escape 鍵,讓您不必負擔協調工作。

樣式

您可以輕鬆為對話方塊元素設定樣式,也可以使用較難的路徑。您可以不變更對話方塊的顯示屬性,並利用其限制來實現簡易路徑。我會採用較困難的做法,為開啟和關閉對話方塊提供自訂動畫,接管 display 屬性等等。

使用 Open Props 設定樣式

為了加快自適應色彩和整體設計的一致性,我毫不客氣地引入了 CSS 變數程式庫 Open Props。除了提供的免費變數,我還匯入了normalize 檔案和一些按鈕,這兩者都是 Open Props 提供的選用匯入項目。這些匯入作業可讓我專注於自訂對話方塊和示範,而不需要許多樣式來支援對話方塊,並讓對話方塊看起來更美觀。

<dialog> 元素設定樣式

擁有顯示資源

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

dialog {
  display: grid;
}

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

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

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

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

Mega 對話方塊:顯示淺色和深色主題,展示途徑顏色。

雖然 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 單元,以便在穩定性提升時使用。

Mega 對話方塊位置

為協助定位對話方塊元素,建議您將其分為兩個部分:全螢幕背景和對話方塊容器。背景必須覆蓋所有內容,提供遮色效果,以便在對話方塊前方顯示,並遮蓋後方無法存取的內容。對話方塊容器可自由在這個背景上居中,並採用內容所需的任何形狀。

下列樣式會將對話方塊元素固定在視窗中,並將其拉伸至各個角落,並使用 margin: auto 將內容置中:

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

在小型檢視區中,我會為這個全頁 mega 模態視窗設定稍有不同的樣式。我將底部邊界設為 0,這樣對話方塊內容就會顯示在可視區域的底部。透過幾項樣式調整,我可以將對話方塊轉換為 actionsheet,讓使用者更容易操作:

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

電腦和行動裝置的 mega 對話方塊開啟時,檢測工具疊加邊距間距的螢幕截圖。

迷你對話方塊位置

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

一秒抓住目光

最後,為對話方塊增添一些風格,讓它看起來像是位於頁面上方柔軟的表面。圓角可讓對話方塊看起來更柔和。您可以使用 Open Props 精心設計的陰影道具來達成深度效果:

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

自訂背景假元素

我選擇以輕鬆的方式處理背景,只在對話方塊中加入 backdrop-filter 的模糊效果:

瀏覽器支援

  • Chrome:76。
  • Edge:79。
  • Firefox:103。
  • Safari:18 歲。

資料來源

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() 在所有瀏覽器中穩定運作:

瀏覽器支援

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

資料來源

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

當 mega 對話方塊開啟時,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> 設定樣式

這個元素的角色是提供對話方塊內容的標題,以及方便找到的關閉按鈕。它也提供表面顏色,讓對話方塊文章內容顯示在後方。這些需求會導致彈性容器容器、與邊緣間隔的垂直對齊項目,以及一些邊框和間距,為標題和關閉按鈕提供一些空間:

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 版面配置資訊的螢幕截圖。

為標頭關閉按鈕設定樣式

由於這個示範使用「開啟道具」按鈕,因此關閉按鈕會自訂為以圓形圖示為中心的按鈕,如下所示:

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> 設定樣式

在這個對話方塊中,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 元素用於包含對話方塊的動作按鈕。它會使用 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;
}

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

動畫

對話方塊元素通常會在進入和離開視窗時顯示動畫效果。為對話方塊提供這類進入和離開動作的支援,有助於使用者在流程中找到方向。

一般來說,對話方塊元素只能以動畫方式顯示,而不能以動畫方式關閉。這是因為瀏覽器會切換元素上的 display 屬性。先前,指南會將顯示畫面設為格狀,但不會設為「無」。這麼做可發揮進出動畫功能。

Open Props 提供許多可用的關鍵影格動畫,可讓您輕鬆編排動畫,並確保動畫易於閱讀。以下是動畫目標和我採用的圖層方法:

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

安全且有意義的預設轉場效果

雖然 Open Props 會提供淡入和淡出的主要影格,但我更偏好將這種分層轉場方法設為預設,並將主要影格動畫設為潛在的升級項目。先前我們已使用不透明度設定對話方塊的顯示設定,並根據 [open] 屬性協調 10。如要進行 0% 到 100% 之間的轉場效果,請告訴瀏覽器您想要的時間長度和緩和效果:

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

為轉場效果加入動態效果

如果使用者允許動態效果,則 mega 和 mini 對話方塊都應在進入時滑動向上,並在離開時縮放。您可以使用 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 屬性進行調整:

@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 元件,會根據動畫和轉場承諾的完成狀態傳回承諾。這就是 dialogClose 為何是非同步函式的原因;接著,它可以await傳回的承諾,並確實繼續執行關閉事件。

新增開啟和已開啟事件

由於內建對話方塊元素不會像關閉事件那樣提供開啟事件,因此這些事件不容易新增。我使用 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 元素。最後,與關閉和已關閉事件類似,請立即調度開啟事件,等待動畫完成,然後調度已開啟事件。

新增已移除的事件

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

您可以使用其他突變觀察器來達成這項目標。這次我們不會觀察對話方塊元素的屬性,而是觀察主體元素的子項,並留意對話方塊元素是否已移除。


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。如果對話方塊已移除,系統會移除點擊和關閉事件,以釋放記憶體,並調度自訂移除事件。

移除 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 事件中,瞭解對話方塊是否已關閉、取消或確認,非常重要。如果確認成功,指令碼就會擷取表單值並重設表單。重設功能很實用,因為當對話方塊再次顯示時,對話方塊會是空白,可供您重新提交。

結論

既然你知道我如何做到,你會怎麼做呢? 🙂?

讓我們多方嘗試,瞭解在網路上建構應用程式的所有方式。

請製作示範作品,並在推特上傳連結,我會將其加入下方的社群重混曲目錄!

社群重混作品

資源