WHATWG 統一 option/optgroup 文字演算法,補上 label 渲染缺口
WHATWG HTML(GitHub commit)· 2026-09-14
<option> 與 <optgroup> 原本各自維護一套文字擷取邏輯,WHATWG HTML 規格用三個連續 commit 把它們合併成同一個「get HTML-aware text content」演算法,同時修掉自訂樣式 <select>(appearance: base-select)算繪時完全忽略 label 屬性的問題。三個 commit 都在 2026 年 9 月 14 日提交:2b62707、d2f752f、8a729be,依序疊加在同一段規格文字上。
背景
customizable <select>(早前由 commit 172cccf47 引入)讓非 base appearance 的 <select> 可以完全用 CSS 客製化外觀。但當時的算繪規則把「顯示 option 內容」直接寫成呼叫 collect option text 並帶入 true,這條路徑完全不會檢查 label 內容屬性。結果是 <option label="foo">bar</option> 在自訂樣式 <select> 底下顯示的是「bar」而不是「foo」,跟瀏覽器原生(base appearance)的行為不一致——Blink、Gecko、WebKit 三個引擎實際上都是「label 非空就顯示 label、否則才退回文字內容」,這正是 WHATWG Issue #10955 要修的落差。
另外兩個問題也同時存在:placeholder label option 的判定過去要求 option 的 parentNode 必須直接就是 <select>(且不能是 <optgroup>),在 customizable select 允許用包裝元素包住 option 之後,只要外層多包一層非 optgroup 元素就會誤判(Issue #12892);而 <optgroup> 的 legend 文字擷取則是另一套手刻邏輯,只判斷「有沒有 child legend」而非「第一個 element child 是不是 legend」,判準本身不夠精確,且當 label 屬性不存在時該回傳什麼也沒有明確定義。
核心改動
最早的 2b62707 把「option 的 label」從一個固定 concept 改寫成吃 includeAltText 參數的演算法「get an option element's label」:先看 label 屬性非空就回傳該值,否則才呼叫 collect option text。IDL 的 label getter 一律帶 false,維持原行為;但非 base appearance 的算繪規則改成呼叫這個新演算法並帶 true,取代舊版直接呼叫 collect option text 的寫法。
| 算繪路徑 | 舊版(172cccf47 之後) | 新版(2b62707 之後) |
|---|---|---|
| 非 base appearance 顯示 option 內容 | 直接呼叫 collect option text(option, true),不看 label 屬性 | 呼叫 get an option element's label(option, true):label 屬性非空就用它,否則才退回文字內容 |
base appearance optgroup legend 顯示文字 | 只讀 optgroup 的 label 屬性值,無視 legend 子元素 | 呼叫「getting an optgroup element's label」:legend 子元素文字優先,其次才是 label 屬性 |
接著 d2f752f 把 placeholder label option 的判定從「parentNode 是 <select>」改成「option 與 <select> 之間沒有 optgroup 祖先」,用祖先檢查取代直接父節點檢查,讓包裝元素不再擋到判定。
最大的一次改動是 8a729be:把 option 專用的 collect option text 重新命名並泛化為 get HTML-aware text content,參數從「option 元素」放寬成「任意 element」,讓 optgroup 的 legend 文字擷取也能共用同一套走訪邏輯(取代舊版手刻的 strip-and-collapse-whitespace 版本)。optgroup 的 label 演算法也改成三步驟:第一個 element child 是 legend 就用它的文字,否則看 label 屬性是否存在,最後回傳空字串——修掉了判準不精確與屬性不存在時行為未定義兩個洞。img alt 文字的處理規則也一併調整:
// 舊版:includeAltText 為 true 且 alt 非空才處理,前後各補一個空白
if (includeAltText && img.alt !== "") {
text += " " + img.alt + " ";
}
if (includeAltText) skipDescendants(img);
// 新版:有 alt 屬性就直接串接,不補空白
if (img.hasAttribute("alt") && includeAltText) {
text += img.alt;
}
skipDescendants(img); // 不論 includeAltText 是否為 true 都跳過取消前後補空白是因為 alt 提供的是替代文字本身,加空白會讓拼接結果多出不該有的字元;無條件跳過 img 子孫則修掉 includeAltText 為 false 時理論上仍可能收集到 img 內部殘留節點資料的漏洞。base appearance 內部 shadow tree 的「optgroup label element」也改成呼叫新的「getting an optgroup element's label」演算法,不再只硬接 label 屬性值,讓 base appearance 與 customizable select 兩條算繪路徑共用同一套 label 決定順序。
影響範圍
自行刻 <select> 替代 UI,或用 appearance: base-select 客製化原生下拉選單樣式的頁面,要重新檢查帶 label 屬性的 <option> 現在顯示的是不是 label 值而非子節點文字——這是這次修正後行為改變最明顯的地方。讀 option.label IDL 屬性或 option.text 的表單函式庫,底層呼叫的演算法已經換掉,混用 <img alt> 與文字節點的 <option> 要重新核對前後是否多了或少了空白字元。
螢幕閱讀器與無障礙工具鏈要留意 <optgroup> 的 legend 子元素內容:先前 base appearance 算繪只認 label 屬性,legend 子元素的文字等於白寫,現在兩種算繪模式都改走同一套決定順序,理論上唸出來的內容會不一樣。用包裝元素實作 customizable select(例如把第一個 <option> 包一層 <div> 或自訂元素)的元件庫,也該重新確認自己的 placeholder-label 邏輯是否符合新的「無 optgroup 祖先即可」規則——舊版用直接父節點比對的自製實作,現在跟規格的判定範圍已經不同。
原始來源:Make option and optgroup share a text content algorithm、Fix placeholder label option to allow wrapper elements、Render an option's label attribute when not using base appearance、MDN:appearance(base-select 背景參考)