前端前線 2026 年 9 月 15 日

2026-09-15 — WHATWG 統一 option/optgroup 文字演算法,補上 label 渲染缺口

primary=https://github.com/whatwg/html/commit/8a729be475929e5448732a8fdb16487a8d4f4c5a primary=https://github.com/whatwg/html/commit/d2f752f663a3576cdd41be98b3476faba643fa65 primary=https://github.com/whatwg/html/commit/2b62707b613fc5a6d6541ed0d281a94990ead21a primary=https://developer.mozilla.org/en-US/docs/Web/CSS/appearance

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 日提交:2b62707d2f752f8a729be,依序疊加在同一段規格文字上。

背景

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 顯示文字只讀 optgrouplabel 屬性值,無視 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」,讓 optgrouplegend 文字擷取也能共用同一套走訪邏輯(取代舊版手刻的 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 子孫則修掉 includeAltTextfalse 時理論上仍可能收集到 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 algorithmFix placeholder label option to allow wrapper elementsRender an option's label attribute when not using base appearanceMDN:appearance(base-select 背景參考)


End of article
0
Would love your thoughts, please comment.x
()
x