🎄Open UI Advent Calendar: Day21 / Customizable Select Element Ep.19
Published on
Customizable Select Elementの関連仕様: `<selectedcontent>` - 子Node変更検知タイミングの仕様決定(現時点で)
Table of Contents
Table of Contents
はじめに
Customizable Select Element Ep.16からは、<selectedcontent>
のクローン実装における、技術的背景をお話ししています。
2024/12/9時点でのselectの各パーツの定義
Ep.19では、CEReactions タイミングで Node 変更の検知をする問題から、同期的なタイミングで Node 変更の検知をする方針に切り替えることが主張された経緯と、cloneNode()
の制限についてお話ししました。
今回は、一連の議論の結果、現状の<selectedcontent>
の仕様がどうなっているのかをみていく、<selectedcontent>
の最終エントリです。
「Node の変更検知タイミング」に関しては、元を辿れば、「<option>
の子 Node が変更された際、検知するか?しないのか?」という Open UI での問いかけから始まりました。
WHATWG での議論が長引き始めた今、そもそも「Node 変更検知のタイミング」を UA 側でコントロールする必要性自体が、疑問視されます。
<option>
変更時のクローンに関する整理と提案
上記コメントを投げかけた Jake は、自身のブログで、<option>
変更時のクローンタイミングに関する整理と提案を行いました。以降の Option1~4 は、彼のブログを参考に筆者がまとめたものです。

Option1: デフォルトではクローンせず、更新をトリガーする方法を提供する(変更検知でのクローンをOpt-in)
この方法では、<option>
が明示的に選択されたときであれば、<selectedcontent>
にクローンされるようになります。しかし、のちに<option>
の子 Node の DOM やスタイルが変更された際には、再クローンせず、”Out-of sync”な状態になります。
デフォルトでは子 Node の変更を検知してクローンしない上に、”Out-of sync”な状態になったからといって、再クローンする手段がないわけではありません。
<selectedcontent>
に対して、resetContent()
を利用することで、Author 側から再クローンをトリガーすることができます。
つまり、<option>
が明示的に選択されたとき、またはresetContent()
が呼ばれたときに、<selectedcontent>
へのクローンが行われることになります。
Option2: 常に同期的にクローンする(変更検知でのクローンをOpt-out)
この方法では、<option>
の子 Node が変更されるたびに、常に<selectedcontent>
の子 Node を、<option>
最新の子 Node をcloneNode()
した結果に置き換えます。
<option>
内の全ての DOM をクローンするため、例えば、<option>
内部の 1 つの要素の属性を変更しただけでも、<selectedcontent>
内の全ての要素がクローンで置き換えられます。
クローンは同期的に行われるため、予想以上に頻繁に行われる恐れがあります。例えば、上記の React の例で、<option>Loading…</option>
がアイコン付きのオプションに変更される場合、<selectedcontent>
内の変更は3回行われます。
<img>
が挿入される(すでにalt
属性が設定されている)- テキストが更新される
<img>
のsrc
が更新される
例えば、次のような Node の変更があったとしましょう。
// 選択された<option>を取得
const selectedOption = select.selectedOptions[0];
// 選択された<option>最初の子Nodeを、選択された<option>自体にappend
selectedOption.append(selectedOption.firstChild);
この場合、<option>
内の子 Node は 2 回クローンされます。要素を append するには、「削除」&「挿入」という 2 回の Tree 変更が必要だからです。
例えば、<option>
内の要素のスタイルを 10 回変更する場合、それぞれの変更はスタイル属性を更新するため、<selectedcontent>
の子 Node は 10 回クローンで置き換えられることになります。
また、<option>
内の要素で CSS Animations を使用する場合は、フレームごとにelement.style
が変更されます。つまり、<selectedcontent>
の子 Node は、フレームごとに再構築されることになります。
過度な変更検知だけでなく、<option>
内の子 Node の変更が、<selectedcontent>
の子 Node に反映したくない場合も考えられます。
例えば、<option>
内の子 Node と、<selectedcontent>
の子 Node は独立した要素であるため、それぞれに独立したスタイルを当てることができます。
しかし、例えば、JavaScript を使って、mouseenter
時にelement.style
を変更すると、<selectedcontent>
に反映されてしまいます。<option>
内の子 Node と、<selectedcontent>
の子 Node を独立した要素として扱いたい場合、このような挙動は期待しないものとなるでしょう。
加えて、将来的に起こる問題も考えられます。
例えば、<details>
は、開いた状態の時<details open>
となる仕様になっています。つまり、<details>
は自身の属性を変更します。
もし、選択された<option>
内にこうした「自身を変更する要素」ある場合、変更が検知され、クローンが走り、<selectedcontent>
内の<details>
も開くことになります。
加えて、この自動的なクローンは<option>
から<selectedcontent>
への一方通行です。
<selectedcontent>
で子 Node を変更することに意味はなく、<selectedcontent>
での手動の変更は、<option>
の子 Node が次回クローンされるときに上書きされてしまいます。
Option3: debounceさせて、常に同期的にクローンする(変更検知でのクローンをOpt-out)
上記の方法とほとんど同じですが、<option>
の子 Node が変更されると、<selectedcontent>
の子 Node は、非同期のマイクロタスクタイミングでcloneNode()
した結果に置き換えられるところに違いがあります。
同期ではなく、非同期のマイクロタスクタイミングで変更を検知することより、クローンの回数を大幅に減少させることができます。
Option2 で挙げた例を参考にすると、<option>
の子 Node の変更は、マイクロタスクタイミングでまとめることができ、クローンは 1 回のみとすることができます。
しかし、この方法にも問題があります。
// 選択された<option>を取得
const selectedOption = select.selectedOptions[0];
// <selectedoption>を取得
const selectedOptionMirror = select.querySelector('selectedoption');
selectedOption.textContent = 'New text';
// クローンが非同期で遅延されているため、trueにならないかもしれない
console.log(selectedOption.textContent === selectedOptionMirror.textContent);
このほかにも、Option2 で挙げた大半の問題は残存します。次のような問題は、Option3 でも引き続き発生することになります。
<option>
の子 Node が変更されるたびに、<selectedcontent>
の全ての子 Node がクローンで置き換えられることになる<option>
内の子 Node の変更が、<selectedcontent>
の子 Node に反映されないようにしたい場合に対応できない- クローンは
<option>
から<selectedcontent>
への一方通行
Option4: ターゲットのDOM変更を行う
UA が<option>
内の各子Nodeとそれに対応するクローンをリンクしておく方法です。元の要素の属性を変更すると、そのクローンの属性も更新され、該当するクローンの属性のみが更新されます。つまり、<option>
の子 Node がひとつ変更されるたびに、<selectedcontent>
の全ての子 Node がクローンで置き換えられることはなくなります。
挿入も同じで、選択された<option>
に新しい要素が挿入されると、その要素はクローンされ、<selectedcontent>
内の同等の位置に挿入されます。
ただ、この方法でも次の問題は残ります。
<option>
内の子 Node の変更が<selectedcontent>
の子 Node に反映されないようにしたい場合に対応できない- クローンは
<option>
から<selectedcontent>
への一方通行- ※ とはいえ、一方通行のミラーリングの挙動は、②や③とは異なる。クローンオプションでは、選択された
<option>
の変更は、<selectedcontent>
内のコンテンツが「リセット」されるため、コンテンツが完全に新しいクローンで置き換えられます。一方、このオプションでは、DOM の変更が target になっているため、<selectedcontent>
のコンテンツを手動で変更すると、よりフォークのようなものになります。
- ※ とはいえ、一方通行のミラーリングの挙動は、②や③とは異なる。クローンオプションでは、選択された
例えば、クローン要素の挿入は、UA ではelement.insertBefore
を使用して実装されます。参照している特定の Node の前に新たなクローンを挿入するにあたって、<selectedcontent>
の内容が Author 側で変更されてしまうと、UA 側でクローンの挿入が失敗する恐れがあります。
結論
Open UI での議論の結果、最終的には Option1 が採用されることになり、「デフォルトでは子 Node の更新を検知してクローンせず、resetContent()
などの、更新をトリガーする方法を提供する」方針が採用されました。
RESOLVED: dont observe mutations in option elements. only clone into selectedoption during parsing and when a new option becomes selected
つまり、現状の<selectedcontent>
は次のような暫定仕様になっていると言えます。
cloneNode()
の制限の件など、未だに<selectedcontent>
の仕様は策定中です。しかし、「選択された<option>
の子 Node の<selectedcontent>
へのクローンタイミング」に関しては、長い議論を経てようやく落ち着く結論となりました。
次回は、Ep.1~Ep.19 までで追ってきた CSE の現状をまとめます。
それでは、また明日⛄
See you tomorrow!
Appendix
- select: Should
<selectedoption>
respond to mutations in the selected<option>
· Issue #825 · openui/open-ui - Add
<selectedcontent>
element by josepharhar · Pull Request #528 · w3c/html-aria - Define the
<selectedcontent>
element by josepharhar · Pull Request #10633 · whatwg/html - [html-aam] Addition: selectedoption element by scottaohara · Pull Request #2344 · w3c/aria
- 5370555: Implement
for StylableSelect
Standard Positions