🎄Open UI Advent Calendar: Day 17 / Customizable Select Element Ep.15

2024-12-17

目次

目次

  1. はじめに
  2. Customizable Select Elementの関連仕様
    1. ここまでの整理
    2. 新しい<selectlist>のAnatomy
    3. <selectedvalue><selectedoption>
    4. 選択された<option>の子要素を<selectedoption>にクローンする提案の再出発
    5. Appendix

はじめに

🎄 この記事はOpen UI Advent Calendarの15日目の記事です。

Ep.14では、<selectlist>slot属性とbehavior属性の使用が廃止された経緯をお話ししました。slot属性とbehavior属性は「選択された<option><button>にスロットしてカスタマイズできるようにする」ための手段だったのですが、この手段が廃止されたことにより、これからどう話が進むのかをみていきます。

2024/12/9時点でのselectの各パーツの定義 2024/12/9時点でのselectの各パーツの定義

Customizable Select Elementの関連仕様

ここまでの整理

ここまでの経緯を一旦整理しておきます。slot属性とbehavior属性が廃止されるまで、主に4つのIssueが関連しあっていました。

という流れを辿って、<selectedvalue>が提案されるところまで来ました。

新しい<selectlist>のAnatomy

slot属性とbehavior属性の廃止後に出された新しいExplainerでは、Issueでのコメントに基づき、以下のようなAnatomyとなりました。

slot属性とbehavior属性の廃止によりでた差分のみにフォーカスしてみます。

  • button (slot) - The portion of the element which is rendered in the position of the button which opens the listbox. It should contain a button to open the listbox. If this part is not provided by the author, then <selectlist> will automatically create one. All child elements of the <selectlist>, except <listbox>, <option>s, and <optgroup>s will be slotted into this slot.
  • <button type=selectlist> - The button which opens the listbox when clicked. The type=selectlist attribute indicates to the browser that this button should open the listbox.
  • <selectedvalue> - The element which contains the text of the currently selected option. Every time that the user selects an option, the browser will replace the text content of this element with the text content of the selected option.
  • <listbox> - The wrapper that contains the <option>(s) and <optgroup>(s). If this part was not provided by the author, then <selectlist> will automatically create one.

  • button (slot) - listboxが開くbuttonが配置されるスロット部分。このスロットには、listboxを開くためのbuttonが配置される。もしAuthorがこの部分を提供しない場合、<selectlist>が自動的に作成する。<selectlist>の子要素で、<listbox><option><optgroup>以外の全ての要素は、このスロットに配置される
  • <button type=selectlist> - クリックされたときにlistboxを開くbutton。type=selectlist属性値は、このボタンがlistboxを開くことをブラウザに示す。
  • <selectedvalue> - 現在選択されているoptionのテキストを含む要素。ユーザがoptionを選択するたびに、ブラウザはこの要素のテキストコンテンツを選択されたoptionのテキストコンテンツで置き換える。
  • <listbox> - <option><optgroup>を含むラッパー。もしAuthorがこの部分を提供しない場合、<selectlist>が自動的に作成する。

使用例も、以下のようになっています。

<selectlist>
  <button type=selectlist>
    <span>selected option:</span>
    <selectedoption></selectedoption>
  </button>
  <listbox>
    <option>one</option>
    <option>two</option>
  </listbox>
</selectlist>

この差分を見る限り、かなり現在のCSEの仕様に近い形になったように見えます。

しかし、この段階では、<selectedvalue>は選択された<option>の中身をまるっとクローンしてくる現在の仕様とは異なり、まだ単にプレーンテキストで置換するのみというふうに読み取れます。

要素クローンの話はどうなったのかを探る前に、<selectedvalue>は”<selectedvalue>”ではなくなるので、一旦軽く確認しておきます。

<selectedvalue><selectedoption>

そもそも<selectedvalue>で提案されたのは、以前同等の機能を持っていたが、behaviorの廃止とともに消えてしまったbehavior=selected-value属性値に由来するためでした。

上記Issueでは、<selectedoption>が有力候補として挙げられ、特に大きな反対もなく、<selectedoption>が採用されてExplainerに反映されます。

📝 小話: Open UIのIssueで管理されてるselectは、実は2種類ある

  • select: 単一選択の<select>としてShipしようとしているもの。現段階のCSEはこのタグに該当。議論中は「select-v1」とされることもある。
  • select-v2: 複数選択の<select>の意。select-v1のShip後、仕様策定しようとしているもの。まだあまり議論は進んでいない。

ここで再び、元々選択された<option>のクローンが検討された背景のあるIssueに戻ります。

ここでJarharが提案した、「プレーンテキストだけではない、選択された<option>の中身をまるっとクローンする<selectedoption>の実装」が、HTMLで初めて採用される実装の出発点でもあり、現在の<selectedcontent>の元となります。

(※ これ以前の時点で、<selectedmenu><selectlist>に変更されているので、この先は<selectlist>と記述します)

選択された<option>の子要素を<selectedoption>にクローンする提案の再出発

Jarjarのコメントが非常によくまとまっているので、この節はコメントを意訳したものです。

この提案は、Mason FreedとDomenicと話し合われた結果、至った結論のまとめです。

TL;DR: ブラウザが、選択された<option>の内容を<selectedoption>cloneNode()して、その子Nodeを置き換えるべきだと考えている。標準化に向けてより良い解決策が見つかれば、それに切り替えることも考えているが、現在は、ChromiumでcloneNode()を用いたプロトタイプを実装している。

次に、上記結論の過程で考慮された、実装方針の比較検討をしています。

1. 選択された<option>の子Nodeを<selectedoption>のShadowRootにクローンする

<selectedoption>はUAのShadowRootを持ち、選択された<option>が変わるたびに<selectedoption>のShadowRootのすべての子を削除し、<option>cloneNode()して、そのクローンの各子Nodeを<selectedoption>ShadowRootに追加します。

また、Author側からUA ShadowRoot内の<selectedoption>のスタイルをする上で、<selectedoption>のShadowRoot内のコンテンツをターゲットに指定するための、::selectedoption()のような疑似要素を提供する必要があります。

これは、SVGの<use>の使い方に似ています。

この手法の問題点:

  • スクリプトがUA ShadowRootである<selectedoption><selectlist>にアクセスできてしまいます。
  • document.querySelector(::selectedoption)は、UA ShadowRoot内のNodeにスクリプトでアクセスする口となり得ます。そこからShadowTreeを遡ることもできます。
  • <script>要素をクローンした場合はどうなるでしょうか?(currentScriptを使用してツリー内のNodeにアクセスできる)
  • <script>をクローンしないようにすることもできますが、<iframe>はどうでしょうか?(frameElementを使用できる)
  • クローン/追加中にスクリプトが同期的に実行されないようにするために、Side effects due to tree insertion or removal (script, iframe) #808を解決する必要があるかもしれません。

2. 選択された<option>の子Nodeを<selectedoption>のLight DOMにクローンする

1のように、クローン先にShadowRootを持つ代わりに、<selectedoption>のLight DOM子NodeをクローンされたNodeで置き換えます。

この手法の問題点:

  • HTMLには現在、Light DOMをこのように変更するものはありません。仕様は<selectedoption>の子Nodeを設定することを、AuthorのConformance Errorとすることができます。
  • クローン/追加中にスクリプトが同期的に実行されないようにするために、Side effects due to tree insertion or removal (script, iframe) #808を解決する必要があるかもしれません。

📝 Light DOM

Shadow RootがアタッチされているHostがLight treeと呼ばれることから、Light treeを構成するNodeは一般的にLight DOMと呼ばれます。

  • The node tree of a shadow root’s host is sometimes referred to as the light tree. DOM - Shadow Tree

📝 Conformance Error

Conformance Errorとは、仕様に従っていない状態を指します。HTMLやCSSなどの仕様には、どのように要素や属性を使用すべきかが定義されていますが、これに従わない場合、Conformance Errorとなります。例えば、以下のようなサイトでチェックすることができます。

The W3C Markup Validation Service

3. CSSでコンテンツを内部的にミラーリングするためのelement()のサポート

FirefoxはCSSでコンテンツをミラーリングする方法を実装しています:

element() - CSS: Cascading Style Sheets | MDN
The element() CSS function defines an <image> value generated from an arbitrary HTML element. This image is live, meaning that if the HTML element is changed, the CSS properties using the resulting value are automatically updated.
element() - CSS: Cascading Style Sheets | MDN favicon https://developer.mozilla.org/en-US/docs/Web/CSS/element
element() - CSS: Cascading Style Sheets | MDN

これと同等の機能を実装し、選択された<option>のレンダリング結果を<selectedoption>にミラーリングすることができます。

この手法の問題点:

  • <selectedoption><option>それぞれに、別のスタイリングを適用できない。
  • Listboxに表示される<option>のボックスサイズは、<selectlist><button>部分のボックスサイズとは通常異なります。そのため、ミラーリングされた画像はcropまたはstretchしてフィットさせる必要があります。
  • <selectedoption>にDOMの実態を提供しないため、<selectedoption>の中身が何であるかを知る術がありません。支援技術はこれを読み取れないか、対処するための修正が必要です。
  • ChromiumとWebKitにはこれの実装がないため、実装コストがかかります。

4. Magical mirroring

3のような、単なる画像としてのミラーリングではなく、選択された<option>の子Nodeが<selectedoption>Flat treeまたはLayout treeにも現れるようにミラーリングする方法を見つけることができるかもしれません。

📝 Flat tree, Layout tree, etc

  • Node tree: DOMツリーの基本的な構造で、Nodeクラスから構成されるツリー。
  • Shadow tree: Shadow Rootがルートのツリー。hostを通じて、必ず別のNode treeと接続されている。
  • Light tree: Shadow Rootのhostとなるツリー
  • Flat tree: Shadow treeを含む、複合的なツリーをフラット化して、単一のNode Treeにしたもの。 Tree Flattening Tree Flattening - 出典: chromium.googlesource.com /DOM
  • Layout tree: LayoutObjectをNodeとして構成されるNode tree。Viewport内でのNodeの正確な位置やサイズなどが計算された、Paintのinputとなるツリー。つまり、ブラウザレンダリングフェーズの中でも、Layoutフェーズ時に構築される。

類似のアイデアが提案されていますが、同じものを意図するかは不明です: <mirror> element, like <slot>, but not limited to ShadowDOM, elements from anywhere can be assigned to it · Issue #6507 · whatwg/html

この手法の問題点:

  • 実装が難しいか不可能かもしれません。
  • NodeがDOMツリー、Flat tree、Layout treeに一度だけ現れるという不変条件を達成する必要があります。
  • ミラーリングでは、「同じNode」が<selectedoption><option>の両方の子として現れます。CSSセレクタではどうなるでしょうか?例えば、selectedoption > .foo { ... }option > .foo { ... }は、異なるスタイリングの目標を達成するために異なる動作を与えることが期待されていますが、CSSセレクタは「Node」を選択するため、「同じNode」をターゲットにします。

5. あきらめて全員にスクリプトを追加させる

「Light DOMへのクローン」を実現するために必要な、以下のようなスクリプトを開発者に提供します:

selectlist.addEventListener('change', () => {
  while (selectedoption.firstChild) {
    selectedoption.firstChild.remove();
  }
  for (const newChild of selectmenu.selectedOption.cloneNode(true)) {
    selectedoption.appendChild(newChild);
  }
  selectedoption.className = selectmenu.selectedOption.className;
});

この手法の問題点:

  • 開発者の80%が、<selectlist>のユースケースがこの動作をすることを期待しているとされています。そんな中、このスクリプトをコピー&ペーストして動作させる必要があるならば、<selectlist>の持つ価値が失われてしまいます。宣言的な解決策の提供は重要で、もしできなければ、開発者は<selectlist>を使用しないかもしれません。

このように、一口にクローンすると言ってもさまざまな手法が考えられ、それぞれにpros/consがあることがわかります。単にクローンするにしてもLight DOMにクローンするのか、Shadow DOMにクローンするのかに判断の余地があったり、ミラーリングは実装やDOM的な懸念があったりします。

これに対して、Domenicは、クローンによるLight DOMの変更というのは前例のないことではあるが、このユースケースを達成するための唯一の合理的な選択肢であると考えている、とフィードバックします。

Light DOMに別の要素のLight DOMをクローンすると、<selectedoption>は具体的には以下のような挙動をすることになります。

以下の図では、選択された<option>の子Nodeが、<selectedoption>Light DOMにクローンされる様子を示しています。 Light DOMに直接挿入されるため、クローンされた要素は、Author Style Sheetの適用が可能となります。

UAによってLight DOMにクローンされたNodeが直接挿入される様子 UAによってLight DOMにクローンされたNodeが直接挿入される様子

こうした、要素が自身のLight DOMを変更するという挙動は、他の長年のHTML機能に対するリクエストを解決するためにも必要かもしれないとDomenicは述べます。

例えば、上記のように、UA が Light DOM を変更して良いのであれば、これまで実現できなかった以下のようなユースケースもカバーできるようになるかもしれません:

Domenicは、この実装によって、プラットフォームが長年苦しんできた「Light DOMは完全にAuthorの領域であり、UAスクリプトによって変更されるべきではない」という制約に対して、今回意識的にその境界を越えることができれば、今後の新しい選択肢が開けるかもしれないという証拠を提供したい、と述べています。


こうして、<selectedoption>の提案が再スタートを切り、選択された<option><selectedcontent>のLight DOMにクローンする仕様が策定&実装されていくことになります。

HTML史上初となる、UAからLight DOMへのクローン追加実装。CSEのみならず、HTMLの新たな可能性を切り開くきっかけと言え、非常に興味深いものとなっていきそうです!

次回からは、この<selectedoption>について、どのような実装上の課題があり、どう解決されていくのかを見ていきます。

それでは、また明日⛄

See you tomorrow!

Appendix

Copyright © 2024 saku 🌸 All rights reserved.