1. はじめに
Shadow DOM により、作者はページを「コンポーネント」、 すなわち、その詳細がコンポーネント自身にのみ関係し、 外部ページには関係しないマークアップの部分木へ分離できます。 これにより、ページのある部分を意図したスタイルが 誤って過剰に適用され、ページの別の部分の見た目を誤らせる可能性が低減されます。 しかし、このスタイル付けの障壁は、ページが実際にそうすることを望む場合にも、 ページがそのコンポーネントと相互作用することを難しくします。
この仕様は、::part() 疑似要素を定義します。 これは、作者が shadow tree 内で、意図的に公開された特定の要素を、外部ページのコンテキストからスタイル付けできるようにします。 custom properties と組み合わせることで、 外部ページは特定の値 (テーマカラーなど)を コンポーネントへ渡し、コンポーネントがそれを自由に扱えるようにできます。 これらの疑似要素により、コンポーネントと外部ページは 安全かつ強力な方法で相互作用でき、 すべての制御を手放すことなく カプセル化を維持できます。
テスト
shadow parts の一般的なテスト
- all-hosts.html (ライブテスト) (ソース)
- animation-part.html (ライブテスト) (ソース)
- chaining-invalid-selector.html (ライブ テスト) (ソース)
- complex-matching.html (ライブテスト) (ソース)
- complex-non-matching.html (ライブ テスト) (ソース)
- different-host.html (ライブテスト) (ソース)
- double-forward.html (ライブテスト) (ソース)
- grouping-with-checked.html (ライブ テスト) (ソース)
- grouping-with-disabled.html (ライブ テスト) (ソース)
- host-stylesheet.html (ライブテスト) (ソース)
- inner-host.html (ライブテスト) (ソース)
- interaction-with-nested-pseudo-class.html (ライブ テスト) (ソース)
- interaction-with-placeholder.html (ライブ テスト) (ソース)
- interaction-with-pseudo-elements.html (ライブ テスト) (ソース)
- invalidation-complex-selector-forward.html (ライブ テスト) (ソース)
- invalidation-complex-selector.html (ライブ テスト) (ソース)
- invalidation-part-pseudo.html (ライブ テスト) (ソース)
- multiple-parts.html (ライブテスト) (ソース)
- part-after-combinator-invalidation.html (ライブ テスト) (ソース)
- part-mutation-pseudo.html (ライブ テスト) (ソース)
- part-nested-pseudo.html (ライブ テスト) (ソース)
- precedence-part-vs-part.html (ライブ テスト) (ソース)
- pseudo-classes-after-part.html (ライブ テスト) (ソース)
- pseudo-elements-after-part.html (ライブ テスト) (ソース)
- serialization.html (ライブテスト) (ソース)
- simple-forward-shorthand.html (ライブ テスト) (ソース)
- simple-forward.html (ライブテスト) (ソース)
- simple-important.html (ライブテスト) (ソース)
- simple-important-important.html (ライブ テスト) (ソース)
- simple-important-inline.html (ライブ テスト) (ソース)
- simple.html (ライブテスト) (ソース)
- simple-important.html (ライブテスト) (ソース)
- simple-inline.html (ライブテスト) (ソース)
- style-sharing.html (ライブテスト) (ソース)
1.1. 動機
カスタム要素が完全に有用で、組み込み要素と同等の能力を持つためには、 その一部を外部からスタイル付けできるべきです。 外部から正確に何をスタイル付けできるかは、要素の作者が制御すべきです。 また、カスタム要素がスタイル付けのための安定した「API」を提示できるべきです。 つまり、カスタム要素の一部をスタイル付けするために使われるセレクターは、 要素の内部詳細を公開したり、その知識を要求したりすべきではありません。 カスタム要素の作者は、セレクターを変更せずに 要素の内部詳細を変更できるべきです。
shadow tree 内をスタイル付けするために以前提案されていた方法である >>> コンビネーターは、 それ自体のためには強力すぎることが判明しました。 それはコンポーネントの内部構造を過度に詳しく調査できるように公開し、 Shadow DOM を使用することで得られるカプセル化の利点の一部を損なっていました。 このため、 またその他のパフォーマンス関連の理由から、 >>> コンビネーターは最終的に削除されました。
これにより、shadow tree 内をスタイル付けする唯一の方法として custom properties を使用することが残されました: コンポーネントは、その内部をスタイル付けするために特定の custom properties を使用することを通知し、 外部ページは必要に応じて、それらのプロパティを shadow host 上に設定できます。 これにより、継承が値を必要な場所まで押し下げます。 これは多くの単純なテーマ設定のユースケースで非常によく機能します。
しかし、これがうまくいかない場合もあります。 コンポーネントが、その shadow tree 内の何かに対する任意のスタイル付けを許可したい場合、 それを行う唯一の方法は、制御を許可したい CSS プロパティ 1 つにつき 1 つ、 何百もの custom properties を定義することです。 これは、使いやすさとパフォーマンスの両方の理由から、 明らかに非現実的です。 作者が、:hover のような疑似クラスに基づいて コンポーネントを異なる方法でスタイル付けしたい場合、 状況はさらに悪化します。 コンポーネントは、各疑似クラス (および :hover:focus のような各組み合わせ)ごとに 使用する custom properties を複製する必要があり、 組み合わせ爆発が生じます。 これにより、使いやすさとパフォーマンスの問題はさらに悪化します。
この場合をはるかにエレガントかつ高性能に処理するために、::part() を導入します。 すべてを custom property 名に詰め込むのではなく、 本来そうであるべきように、 機能はセレクターとスタイル規則の構文の中に存在します。 これは、コンポーネント作者と コンポーネント利用者の両方にとってはるかに使いやすく、 パフォーマンスもはるかに良くなるはずであり、 より良いカプセル化/API サーフェスを可能にします。
::part() は、理論的に新しい能力をまったく提供しないことに注意することが重要です。 これは >>> コンビネーターの焼き直しではなく、 作者がすでに custom properties で行えることのための、 より便利で一貫した構文にすぎません。 要素の明示的に「公開された」部分 (part element map)を、 たまたま含んでいるだけのサブパートから分離することで、 カプセル化にも役立ちます。 作者は、偶発的な過剰スタイル付けを恐れずに ::part() を使用できます。
2. Shadow Element の公開:
shadow tree 内の要素は、 part および exportparts 属性を使用して、 tree の外部のスタイルシートによるスタイル付けのためにエクスポートできます。各要素は、トークンの ordered set である part name list を持ちます。
各要素は、転送される inner part のための string と、 それが公開される名前を与える string を含む tuples の list である forwarded part name list を持ちます。
各 shadow root は、キーが strings であり、値が要素の ordered sets である part element map を持つものと考えることができます。
part element map は、この仕様におけるスタイル計算アルゴリズムの一部としてのみ 記述されます。 これは DOM を通じて公開されません。 その計算は高価である可能性があり、 公開すると closed shadow roots 内の要素へのアクセスを可能にし得るためです。
Part element maps は、要素の追加および削除、 ならびに DOM 内の要素の part name lists および forwarded part name lists の変更の影響を受けます。
-
outerRoot 内の各 descendant el について:
-
el の part name list 内の各 name について、el を outerRoot の part element map[name] に append します。
-
el 自体が shadow host である場合、 innerRoot をその shadow root とします。
-
innerRoot の part element map を 計算します。
-
el の forwarded part name list 内の 各 innerName/outerName について:
-
innerName が ident である場合:
-
innerParts を innerRoot の part element map[innerName] とします
-
innerParts 内の要素を outerRoot の part element map[outerName] に Append します
-
-
innerName が pseudo-element 名である場合:
-
その名前を持つ innerRoot の pseudo-element(s) を outerRoot の part element map[outerName] に Append します。
-
-
-
2.1. Shadow Element の命名:
part
属性
shadow tree 内の任意の要素は、part
属性を持つことができます。
これは、要素を shadow
tree の外部へ公開するために使用されます。
テスト
part 属性は、この要素の part 名を表す、空白区切りのトークンのリストとして構文解析されます。
注記: 1 つの part に複数の名前を与えてもかまいません。 "part name" は、id や tagname ではなく、 class に似たものと考えるべきです。
<style>
c-e::part(textspan) { color: red; }
</style>
<template id="c-e-template">
<span part="textspan">このテキストは赤になります</span>
</template>
<c-e></c-e>
<script>
// テンプレートをカスタム要素 c-e として追加する
...
</script>
2.2. Shadow Element の転送:
exportparts
属性
shadow tree 内の任意の要素は、exportparts 属性を持つことができます。
その要素が shadow host である場合、
これは、shadow
tree 内の host からの part を、この shadow tree の外部の規則で
スタイル付けできるようにするために使用されます
(それらが host と同じ tree 内の要素であり、
part 属性によって名前付けされているかのように)。
テスト
exportparts 属性は、part mappings のコンマ区切りのリストとして構文解析されます。 各 part mapping は次のいずれかです:
innerIdent : outerIdent-
innerIdent/outerIdentを el の forwarded part name list に追加します。 ident-
ident/identを el の forwarded part name list に追加します。注記: これは
ident : identの省略形です。 ::ident : outerIdent-
::identが fully styleable pseudo-element の名前である場合、::ident/outerIdentを el の forward part name list に追加します。 そうでなければ、何もしません。 - その他すべて
-
エラー回復 / 将来の互換性のために無視されます。
注記: sub-part を複数の名前へ map してもかまいません。
<style>
c-e::part(textspan) { color: red; }
</style>
<template id="c-e-outer-template">
<c-e-inner exportparts="innerspan: textspan"></c-e-inner>
</template>
<template id="c-e-inner-template">
<span part="innerspan">
このテキストは赤になります。なぜなら、包含する shadow
host が innerspan を "textspan" として document へ転送し、
document のスタイルがそれに一致するためです。
</span>
<span part="textspan">
このテキストは赤になりません。なぜなら、document スタイル内の textspan は、
inner custom element の内側にある part に対しては、
それが転送されていない場合、一致できないためです。
</span>
</template>
<c-e></c-e>
<script>
// テンプレートをカスタム要素 c-e-inner, c-e-outer として追加する
...
</script>
exportparts
属性内で使用できます:
<template id=custom-element-template>
<p exportparts="::before : preceding-text, ::after : following-text">
Main text.
</template>
そのテンプレートを使用する要素は、 x-component::part(preceding-text) のようなセレクターを使用して、その shadow 内の p::before 疑似要素を対象にできます。 そのため、コンポーネントの利用者は、 preceding text が疑似要素として実装されていることを知る必要がありません。
3. Shadow Element の選択: ::part() 疑似要素
::part()
疑似要素は、
part
属性を介して公開された要素を選択できるようにします。
構文は次のとおりです:
::part() = ::part( <ident>+ )
テスト
::part() 疑似要素は、 originating element が shadow host である場合にのみ、何かに一致します。
tabstrip コントロールは、
part="tab" を持つ複数の要素を持つ場合があり、
それらはすべて ::part(tab) によって選択されます。
一度に 1 つの tab だけが active である場合、
part="tab active" で特別に示すことができ、その後 ::part(tab active)(または順序は重要でないため
::part(active tab))によって選択できます。
::part() 疑似要素は fully styleable pseudo-element です。 originating element の shadow root の part element map が、指定された <ident> を contains する場合、 ::part() はその ident に対応付けられた要素を表します。 複数の ident が与えられ、part element map がそれらすべてを含む場合、 それぞれの ident に対応付けられた要素の積集合を表します。 そうでなければ、何にも一致しません。
::part() 疑似要素は、 originating element の shadow tree 内での位置に従って継承します。
<x-panel> の内部 confirm button が、button の内部 part を panel 自身の
part element map へ転送するために
part="label => confirm-label" のようなものを使用していた場合、
x-panel::part(confirm-label) のようなセレクターは、
他の label を無視して、
その 1 つの button の label だけを選択します。
4. Element
インターフェイスへの拡張
partial interface Element { [SameObject ,PutForwards =value ]readonly attribute DOMTokenList ; };part
テスト
part 属性の getter は、関連付けられた要素がコンテキストオブジェクトであり、 関連付けられた属性のローカル名が part である DOMTokenList オブジェクトを返さなければなりません。 この特定の DOMTokenList オブジェクトのトークン集合は、その要素の parts とも呼ばれます。
これを DOM 仕様の superglobal として定義します。 [w3c/csswg-drafts Issue #3424]
5. 構文解析のためのマイクロ構文
5.1. part mappings を構文解析するための規則
妥当な part mapping は、 U+003A COLON 文字で区切られ、 U+003A COLON の前後に任意個数の空白文字を伴う トークンの tuple です。 トークンは U+003A COLON または U+002C COMMA 文字を含んではなりません。
part mapping を構文解析するための規則は次のとおりです:
-
input を、構文解析される文字列とします。
-
position を、input 内へのポインターとし、初期状態では文字列の先頭を指すものとします。
-
空白文字である code point の列を 収集します
-
空白文字または U+003A COLON 文字でない code point の列を 収集し、 first token をその結果とします。
-
first token が空である場合、error を返します。
-
空白文字である code point の列を 収集します。
-
input の終端に達している場合、tuple (first token, first token) を返します
-
position の文字が U+003A COLON 文字でない場合、error を返します。
-
U+003A COLON 文字を消費します。
-
空白文字である code point の列を 収集します。
-
空白文字または U+003A COLON 文字でない code point の列を 収集します。 そして second token をその結果とします。
-
second token が空である場合、error を返します。
-
空白文字である code point の列を 収集します。
-
position が input の終端を過ぎていない場合、error を返します。
-
tuple (first token, second token) を返します。
5.2. part mappings のリストを構文解析するための規則
妥当な part mappings のリスト は、U+002C COMMA 文字で区切られ、 U+002C COMMA の前後に任意個数の空白文字を伴う、 妥当な part mappings の個数です
part mappings のリストを構文解析するための規則は次のとおりです:
-
input を、構文解析される文字列とします。
-
文字列 input をコンマで分割します。 unparsed mappings を、その結果の文字列のリストとします。
-
mappings を、トークンの tuples の 初期状態で空の list とします。 この list がこのアルゴリズムの結果になります。
-
unparsed mappings 内の各文字列 unparsed mapping について、 次のサブ手順を実行します:
-
unparsed mapping が空であるか、空白文字のみを含む場合、 ループの次の反復へ続行します。
-
mapping を、 part mappings を構文解析するための規則を用いて unparsed mapping を構文解析した結果とします。
-
mapping が error である場合、ループの次の反復へ続行します。 これにより、クライアントは理解されない新しい構文をスキップできます。
-
mapping を mappings に追加します。
-
6. プライバシーに関する考慮事項
この仕様は、ページ上の要素のスタイル付けを対象にする新しい方法を定義しますが、 それらはすでに他の方法で完全にスタイル付け可能です。 そのため、新しいプライバシー上の考慮事項は導入しません。
7. セキュリティに関する考慮事項
shadow trees は意図的にセキュリティ境界ではなく、 ページ作者のための単なる利便性であるため、 この方法でそれらをセレクターに公開しても、新しいセキュリティ上の考慮事項は導入されません。
8. 変更点
2018年11月15日の First Public Working Draft 以降の変更点
-
::part() に複数名のサポートを追加
-
'part name map' を forwarded part name list に改名
-
'part element list' アルゴリズムを再構成
-
さまざまな ::part() の詳細を fully styleable pseudo-element へ移動
-
Web Platform Tests のカバレッジを追加
-
軽微な編集上の改善