題幹與適用場景
這是現代 Web 平台與可存取互動題。目標是實作選單、篩選面板或說明氣泡等非模態浮層;頁面其餘內容仍可互動,浮層需要由觸發器開啟並支援 light-dismiss。HTML Popover API 透過 popover 屬性、popovertarget 與 top layer 提供瀏覽器管理的顯示生命週期。題目不要求把所有浮層都遷移到原生 API,也不把定位演算法本身當作重點。
面試官考察點
- 能否區分
popover="auto"、popover="manual"與 modal dialog 的互動語義。 - 能否讓觸發按鈕、浮層與關閉動作形成可理解的語義關係。
- 能否處理 Escape、外部點擊、多個浮層、焦點回退與事件生命週期。
- 能否說明漸進增強、瀏覽器支援與不破壞頁面的降級策略。
回答前需要釐清的問題
先確認浮層是否允許使用者繼續操作背景;若背景必須阻斷,應改用 <dialog> 的 showModal(),而不是 popover。再確認同一時間只允許一個,還是允許多個手動浮層並存;是否需要點擊觸發器切換、表單提交後關閉、動畫與定位;以及產品的最小瀏覽器範圍。篩選面板通常是非模態,但鍵盤使用者仍需要明確的焦點與關閉路徑。
30 秒回答框架
我會優先使用語義 HTML:觸發按鈕寫 popovertarget,浮層寫 popover="auto",需要自訂關閉編排時才使用 manual。瀏覽器負責 top layer、Escape 與外部 light-dismiss;我補充焦點進入、關閉後回退、狀態文字與 beforetoggle/toggle 生命週期。若瀏覽器不支援,就用相同 DOM 結構和最小 JavaScript polyfill,避免透過全域點擊監聽誤關其他浮層。
分步驟深入解答
1. 選擇 popover 或 modal dialog
Popover 適合選單、篩選器、非阻塞說明與臨時操作;它不會把頁面變成模態背景。<dialog> 的 showModal() 適合確認、付款或必須先處理的警告,因為它提供模態語義與背景阻斷。兩者都可放入 top layer,但可存取名稱、焦點策略與關閉原因不同,不能只把 dialog 換成 popover 屬性。
2. 建立觸發器與浮層關係
最小語義結構可以讓瀏覽器維護目標關係:
<button type="button" popovertarget="filters" aria-controls="filters">
篩選
</button>
<div id="filters" popover="auto">
<form method="get">
<label>狀態 <select name="status"><option>全部</option></select></label>
<button type="submit">套用</button>
</form>
</div>popovertarget 讓按鈕成為宣告式觸發器,瀏覽器會在開啟與關閉時維護關聯狀態。aria-controls 可協助輔助技術理解關係,但不能取代可見名稱和正確焦點行為。複雜元件應在腳本中保存觸發器參照,關閉時把焦點還給仍存在且可聚焦的按鈕。
3. 理解 auto、manual 與 light-dismiss
auto 浮層支援由瀏覽器管理的 light-dismiss:按 Escape 或點擊浮層外部可關閉,並參與自動關閉鏈。manual 浮層不會被這些機制自動關閉,必須呼叫 showPopover()、hidePopover() 或 togglePopover();適合需要多個浮層並存或自訂生命週期的情境。不要在 auto 模式下再註冊 document 層級點擊關閉,否則巢狀浮層與觸發器點擊容易產生競態。
4. 處理 top layer、定位與遮罩
Popover 開啟後進入 top layer,不再受普通祖先的 overflow 或 z-index 堆疊上下文限制。可用 CSS Anchor Positioning 或普通版面做定位,但定位失敗時應提供靜態回退。非模態 popover 不應預設加入全螢幕 backdrop;若視覺上需要遮罩,應重新確認這是否已是 dialog 的模態需求。top layer 改變繪製順序,卻不會自動解決窄視口、滾動容器與碰撞定位。
5. 建立焦點與鍵盤策略
開啟後把焦點移到浮層內第一個可操作控制項,或保留瀏覽器預設行為並在實測中確認。Escape 關閉後把焦點還給觸發按鈕;若觸發器已被刪除,回退到最近的可見上下文。Tab 順序應自然穿過浮層內容,不能用 tabindex="-1" 把所有控制項隱藏。選單型 popover 還需遵循選單項的方向鍵與選取語義,篩選表單則使用普通表單鍵盤行為。
6. 監聽生命週期而不是全域猜測狀態
用 beforetoggle 檢查是否允許切換、記錄關閉原因或同步應用狀態;用 toggle 在實際狀態變化後更新按鈕文字與焦點。事件處理器應按元素註冊並在元件銷毀時清理。若多個 auto 浮層互斥,依賴瀏覽器的自動關閉堆疊,避免維護一份可能過期的全域 openId。
7. 處理表單提交與狀態同步
篩選表單成功提交後可以顯式呼叫 hidePopover(),但不要把關閉當作儲存成功的證據。請求失敗時保持浮層開啟並把錯誤與表單關聯。開啟狀態是暫時 UI 狀態,篩選條件是業務狀態,兩者應分開管理;路由更新或返回歷史記錄時按產品需求重新計算,而不是把 :popover-open 當作唯一資料來源。
8. 漸進增強與測試
能力檢測應針對 HTMLElement.prototype.showPopover 等實際 API。支援時使用原生行為;不支援時保留相同按鈕、內容與可存取名稱,以小型 polyfill 補齊顯示、Escape、外部點擊與焦點回退。測試鍵盤、觸控、縮放、滾動、巢狀浮層、觸發器被刪除、表單失敗與 prefers-reduced-motion。還要用螢幕閱讀器確認名稱、狀態與錯誤被讀出,而不是只檢查像素位置。
高品質示範回答
我會把它定義為非模態篩選 popover,而不是 dialog。按鈕使用 popovertarget,面板使用 popover="auto",由瀏覽器管理 top layer、Escape 與外部 light-dismiss;腳本只負責表單狀態、焦點回退和生命週期事件。開啟時焦點進入第一個控制項,關閉後回到觸發按鈕;提交失敗保持開啟並關聯錯誤,成功後才關閉。多個手動浮層才使用 manual,不在 auto 模式下重複註冊全域點擊監聽。定位使用 CSS Anchor Positioning 時提供靜態回退,能力檢測失敗則用相同語義結構的最小 polyfill,並用鍵盤、觸控、滾動和輔助技術測試。
常見錯誤
- 用 popover 承載必須阻斷背景的確認操作,導致模態語義缺失。
auto模式再加 document 點擊監聽,誤關閉巢狀浮層或提交按鈕。- 只改變
display,不維護觸發器關係、焦點回退與錯誤狀態。 - 用全螢幕 backdrop 模擬 dialog,卻仍把元件當非模態處理。
- 把 top layer 當作自動定位,忽略滾動、窄視口和碰撞回退。
- 失敗時無條件關閉表單,使用者看不到錯誤,也無法繼續修正。
- 只在滑鼠點擊下驗證,遺漏 Escape、Tab、觸控和螢幕閱讀器。
追問及應對
什麼時候必須改用 <dialog>?
當背景內容必須不可互動、使用者必須先確認,或需要模態焦點循環時,使用 <dialog> 的 showModal()。Popover 的 light-dismiss 與非模態語義無法取代阻斷背景、模態說明和明確的取消/確認動作。
如何讓多個篩選浮層互斥?
優先讓互斥浮層使用 auto,由瀏覽器處理自動關閉鏈。若業務要求多個 manual 浮層並存,維護明確的實例集合,關閉時只操作目標實例,並為每個實例保存自己的觸發器和焦點回退。
如何處理不支援 Popover API 的瀏覽器?
能力檢測後使用相同 DOM 和語義屬性,polyfill 只補顯示、關閉、Escape、外部點擊與焦點回退。不要把不支援環境降級成沒有名稱、沒有鍵盤路徑的純絕對定位 div;也要記錄相容矩陣並在發布前測試。
如何測試 light-dismiss 的邊界?
測試點擊觸發器、浮層內部、外部兄弟元素、滾動容器與另一個 popover;驗證內部點擊不會關閉,外部點擊只關閉符合規則的 auto 浮層,觸發器再次點擊不會產生雙重狀態。補充鍵盤 Escape 與觸控事件測試。
動畫會影響可存取性嗎?
根據 prefers-reduced-motion 縮短或移除過渡,確保動畫期間焦點與可讀狀態仍一致。不要用透明度隱藏仍可聚焦的控制項;關閉動畫結束前就應阻止重複提交,並讓螢幕閱讀器取得穩定的狀態變化。
如何處理浮層定位失敗?
使用 anchor positioning 時提供預設區塊流位置或視口內固定回退,並監聽尺寸與滾動變化。定位失敗不應讓內容溢出螢幕或被裁切;核心表單功能仍需在沒有定位增強時可操作。