建構對話方塊元件

基礎總覽說明如何使用 <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 屬性等等。

使用開放式問題設定樣式

為了加快自適應色彩和整體設計的一致性,我毫不客氣地引入了 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;
}

現在當開啟大型對話方塊時,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 版面配置資訊的螢幕截圖。

為標題關閉按鈕設定樣式

由於這個示範使用「開啟 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> 設定樣式

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

結論

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

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

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

社群重混作品

資源