dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

HTMLのフォームコントロールをカスタマイズ可能にするプロポーザル(Enabling Custom Control UI)

Edge の PM (Program Manager) やってる方のツイートが目に留まったので内容をざっくり読んでみた、という話です。

該当のプロポーザルが置かれているリポジトリ https://github.com/MicrosoftEdge/MSEdgeExplainers から、Edge チームが中心になって検討を進めているプロポーザルのようですが、冒頭の Authors を見る限り他にも Google, Salesforce の人が関わってます。

※以下、 https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ControlUICustomization/explainer.md の内容をかいつまんで説明したものであり、また追加で調べたことなど自由に補足を入れているため、完全な翻訳ではありません。

Introduction

  • ブラウザに組み込まれたフォームコントロール<select> やさまざまなタイプの <input>。以下「コントロール」)の問題点として、開発者がサイトのデザインやユーザー体験にフィットするように見た目を柔軟にカスタマイズできないことが挙げられる
  • Survey results for controls & components | Greg Whitworth というサーベイによると、開発者がコントロールをリライトする一番の理由は、ネイティブのコントロールの見た目を十分にカスタマイズすることができないことだったそう
    • リンク先のこのグラフがそれを表してそう

f:id:dackdive:20200817030539p:plain
http://gwhitworth.com/surveys/controls-components/ より引用

  • 開発者がスクラッチでコントロールを再実装すると、web プラットフォームがやってくれているパフォーマンス、信頼性(reliability)、アクセシビリティの最適化の恩恵を受けられない
    • 注: "they're not able to leverage the work done on the Web Platform to optimize performance, ..." なので、「プラットフォームの取り組みを活用してパフォーマンス、... を最適化することができない」と訳した方が適切?
  • このプロポーザルによって、開発者はプラットフォームの恩恵を受けつつ、自分たちのサイトにフィットするようネイティブコントロールをカスタマイズすることが可能になる

Goals

  • 開発者がネイティブコントロールの任意の箇所をスタイリングできる
  • 開発者がネイティブコントロールの好きな部分に任意のコンテンツを挿入できる
  • 複数のパーツからなるコントロールに対し、開発者はすべてのUIをリライトすることなく特定の箇所をスタイリングできる
  • 開発者はデータモデルやユーザー入力にリーチするためのコードを再実装することなく、UIをカスタマイズできる。これはカスタムエレメントとしてスクラッチ実装するような今のアプローチとは対照的である
  • カスタマイズしたコントロールはデフォルトでアクセシブルである

Incremental Approach

  • このドキュメントの目的は上述したゴールを達成でき、かつすべてのコントールに適用できるようなモデルを検討することだが、実際にはモデルの適用はそれぞれのコントロールずつ - つまり各コントロールごとに固有のプロポーザル - になるだろう
    • ロールアウトは開発者の要求(demand)や各コントロールの複雑度合によるし、一部のコントロールには適用されない(実装されない)可能性もある
  • このドキュメントはコンセプトの例として <select><input type="range"> を多用するが、これらのコントロールの詳細なスペックを決めることがゴールではない
    • 全体的なアプローチについて合意がとれたら、これらや他のコントロールの固有なふるまいについて詳細を詰めていく
    • 進捗はすでに https://open-ui.org/ の方で確認できる

Use Cases

  • 主要なユースケースは「コントロールを自分のサイトに配置したいが、コントロールの見た目に高いレベルでの柔軟性を必要としているWeb開発者」
  • 現在、そのようなシチュエーションでは、コントロールのscriptable model(?)とUIをリンクするコードや、アクセシブルにするためのARIA属性などを含めたコードをスクラッチで実装する必要がある
    • このプロポーザルにより開発者はカスタムのマークアップとスタイルのみ提供し、残りはプラットフォームが提供する(というように役割分担がされ、開発者は見た目のカスタマイズに注力できるようになる)

Proposed Solution

Custom content and styles via standardized parts, named slots, and shadow DOM replacement

開発者がネイティブコントロールをカスタマイズする方法には3つの選択肢がある:

  1. 標準化されたパーツと状態(standardized parts and states)を使い、擬似クラスと擬似要素でネイティブコントロールのスタイルを上書きする
    • 擬似クラス: :hover:first など (MDN)
    • 擬似要素: ::after など (MDN)
  2. 名前つき <slots> を使い、ネイティブコントロールの一部はそのままに、一部のパーツを開発者が作成したコンポーネントに置き換える
  3. ネイティブコントロールのUI全体を開発者が提供する shadow root で置き換える

下にいくほど自由度が高くなるイメージ。

Standardized control UI anatomy, parts, and behavior

上のいずれの方法を取るにしても、今のコントロールの概念的なパーツやふるまい、ならびに各パーツの名称についての標準化が必要。
このプロセスは現在 https://open-ui.org/ によって進められている。

標準化の例として、たとえば <select> については

  • 以下のパーツで構成される
    • 1つの "button" という名前のパーツ
    • 1つの "selected-value" という名前のパーツ
    • 1つの "listbox" という名前のポップアップパーツ
    • 0〜N 個の "option" という名前のパーツ
  • 期待するふるまいは以下の通り
    • "button" がクリックされるとリストを表示する
    • "selected-value" は内部テキストを更新し現在選択中のオプションの値を表示する
    • 開いているリストの外をクリックすると折りたたむ
    • etc.

開発者はここで定義されたパーツからなるカスタムUIを提供すれば、プラットフォームがよしなにイベントハンドラーやARIA属性を付与して期待するふるまいやアクセシビリティを担保してくれる、というしくみ。

なおネイティブのスタイルは標準化される予定がなく、ブラウザによって今後も差異がある予定。

選択肢1のアプローチ(Styling native parts using pseudo-classes and the part pseudo-element)

先ほどの標準化によって決定された各パーツの名称と、 part という擬似要素を使って、以下のように今より細かい粒度でスタイルを適用できる。

<style>
  .styled-select::part(button) {
    background-color: red
  }
</style>
<select class="styled-select">
  <option>choice 1</option>
  <option>choice 2</option>
</select>

選択肢2のアプローチ(Named slots)

標準化によって決定された各パーツの名称に対応するように slot の name 属性も標準化することで、開発者はカスタマイズしたいパーツを slot name で指定して任意のコンポーネントに置き換えられるようになる。

たとえば <select> であれば "button" や "listbox" という名前が標準化されるので、

<style>
  .custom-button {
    /*...*/ 
  }

  option {
    /*...*/
  }

  .option-text {
    /*...*/
  }
</style>
<select>
  <div slot="button" part="button" class="custom-button">Choose a pet</div>
  <div slot="listbox" part="listbox" class="custom-listbox">
    <option>
      <img src="./cat-icon.jpg"/>
      <div class="option-text">Cat</div>
    </option>
    <option>
      <img src="./dog-icon.jpg"/>
      <div class="option-text">Dog</div>
    </option>
  </div>
</select>

こういったコードを書くことで、↓のようなドロップダウンが作れる。

f:id:dackdive:20200819005527p:plain
https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ControlUICustomization/explainer.md より引用

コンテンツを与えなかった slot についてはデフォルトのUIにフォールバックされる。

選択肢3のアプローチ(Shadow DOM replacement)

注:3つの選択肢のうち、これだけ親となるセクションが異なっていて、文章のつながりにいまいち自信が持てなかった。けど、おそらく上述した選択肢の3番目の説明にあたると思われる。

attachShadow() を呼ぶことで、UIをまるごと開発者が用意したコンポーネントで置き換えちゃうアプローチ。

let customSelect = document.createElement('select');
customSelect.setAttribute("custom", "");
let selectShadow = customSelect.attachShadow({ mode: 'open' });
selectShadow.innerHTML = `My custom select UI`;
document.body.appendChild(customSelect);

今はコントロールに対して attachShadow() を実行すると例外を throw するが、カスタマイズができるようになるとこのメソッドが実行可能になる。

この場合も、プラットフォーム側は開発者が渡した shadow DOM にコアとなるパーツ(<select> なら part="button"part="listbox")が含まれているかどうかチェックする。

もし必須のパーツが渡されなかった場合は、shadow DOMはレンダーされない。


その他

ここまでで背景やどういう解決方法になりそうなのかはなんとなく把握できましたが、元のドキュメントの内容的には半分ぐらいです。
残りはざっと読んでいくつか気になった部分をメモします。

Light-DOM content under <input>

先ほどの選択肢2のようにカスタマイズしたい内容をコントロールの子要素として配置していくアプローチの問題点として、 <input> には子要素を配置できないという問題がある。

この問題に対するワークアラウンドとして、 <input> の各 type に対応する新しい HTMLElement を標準化することを提案している。
新しい element は対応する <input> element と等価だが、その type と無関係なメソッドやプロパティは取り除かれたものになる。 例: <input type="range"> に対して HTMLRangeElement を新しく導入する。これは <input type="range"> と同じメソッド、プロパティ、ふるまいを持つが、その他の input type にのみ関係するメソッド・プロパティは除外されている。この element は以下のように使うことができる。

<range>
  <div slot="thumb" part="thumb"><svg><!-- Use SVG to draw the thumb icon... --></svg></div>
</range>

Ensuring accessibility by default

このプロポーザルの key goal に「カスタムコントロールはデフォルトでアクセシブルである」というのがあった。
これを達成するために、part 属性の値に応じて ARIA 属性などをプラットフォーム(ブラウザー)側が暗黙的に適用する。

プロポーザル中では implicit accessibility semantics という用語を使っており、これは HTML AAM というドキュメントに定義されている implicit semantics と同じ意味合いだそう。(該当のドキュメントを読んでいないので詳しいことはわかりません)

ARIA 属性は HTML に実際に追加されるわけではないが、プラットフォームは適切な ARIA 属性が付与されているかのようにみなしてアクセシビリティツールに伝える。

たとえば、先ほどの

<range>
  <div slot="thumb" part="thumb"><svg><!-- Use SVG to draw the thumb icon... --></svg></div>
</range>

の場合、プラットフォームは <div part="thumb"> について以下のような implicit accessibility semantics をアクセシビリティツールに伝える。

  • slider という ARIA role
  • aria-valuenow
  • aria-valuemax
  • aria-valuemin

Feature detection

ブラウザ側がこの機能に対応しているかどうかを判定する方法について。

新しい HTMLElement が導入されている要素(先ほどの、<input type="range"> に対する <range> のように)であれば、以下のようにその要素が存在するかどうかでチェックできる。

if (!window.hasOwnProperty("HTMLRangeElement")) {
  /* apply polyfill/fallback */
}

そうでない場合、 attachShadow() が例外を投げるかどうかで判断できる。

function hasCustomSelectFeature() {
  try {
    document.createElement("select").attachShadow({mode: "open"});
    return true;
  } catch (e) {
    return false;
  }
}

Privacy and Security Considerations > Security

コントロールの中には、サードパーティのコードにそのまま提供されていると危険な、特権的な振る舞いを持つものがある。

  • たとえば <select> のドロップダウンリストはブラウザのウィンドウや iframe の外に飛び出すことが可能

f:id:dackdive:20200819014740p:plain:w480

  • このプロポーザルによって任意のコンテンツを <select> に渡せるようになると、OSのUIになりすましたり、iframeから外のコンテンツになりすますといったことが可能
  • すべてのコントロールタイプは、このような潜在的に悪用される可能性のある特権的な振る舞いのケースを慎重に調査する必要がある