私とIEとフィードバック(IE卒業式)2022年06月19日 12時28分

2022年6月16日に開催された「IE卒業式」というイベントで、「私とIEとフィードバック」という発表(5分間のライトニングトーク)をしてきました。以下に話した内容を掲載します。


私とIEとフィードバック

2022-06-16

nanto_vi (株式会社はてな)

Web開発者によるフィードバック

  • 標準準拠
  • 相互運用性の向上

➜ より開発しやすく

ブラウザベンダが不具合に気づくことを期待するという受身の立場から、積極的に熱心でいる(自らバグを報告する)ことへの移行は、信じられないほど多くの力をあなたにもたらします。

John Resig

[発話] ソフトウェアを作るうえでも使う上でもフィードバックは重要ですね。Webブラウザの場合、Web開発者からのフィードバックによって標準準拠の度合いが進んだり、ブラウザ間の相互運用性が向上したりして、Web開発者にとってはより開発しやすくなります。John Resigさん——jQueryを作った方です——も、バグが直るのを待つのではなく自らバグを報告するとめっちゃええことあるというようなことをおっしゃっています。

個人開発者によるIEへのフィードバック手段

go to http://131.107.85.110/msdn/bugreports/ to report issues

Internet Explorer 5.5 Preview

[発話] 個人がIEにフィードバックする手段は、IE 7の前後で大きく変わっています。IE 7以前はニュースグループ——掲示板やメーリングリストのようなものです——が中心でした。IE 7以降はMicrosoft Connectというサービスを使うようになっています。Microsoft Connectにはバグトラッキングシステムが備わっており、他人の登録したバグを検索したり、自分の登録したバグの状態を知れたりと、バグ報告者からすると使いやすくなっています。ちなみに、IE 5.5 Previewのときにはバグ報告用のURLがIPアドレス丸出しで、のどかな時代だったんだなというのを感じさせます。

IE 8 Betaへのフィードバック

開発者の支持のないブラウザは廃れるとの思いで、標準準拠路線を進めてきた

「ここが変わった! IE8 Beta2」(第13回 Admintech.jp勉強会)

  • 筆者もフィードバックに参加
    • Web Storageがオリジンごとではなくドメインごと
    • Web Storageのイベントが同期的
    • 文書間メッセージング(postMessage)のイベントが同期的

[発話] 私がIEにフィードバックするようになったのはIE 8 Betaが出たあたりからです。このころはFirefoxとSafariがIEを追い上げ、GoogleがChromeを発表し、Microsoftとしては非常に危機感を持っていたのではないかと思います。実際に、当時あったイベントでMicrosoftの方が、開発者の支持のないブラウザは廃れるとの思いで標準準拠路線を進めてきたというようなことをおっしゃっていました。このころはWeb Storage APIや文書間メッセージングが新機能としてもてはやされ、私も標準仕様とIEの実装との差異をいくつか報告しています。Web Storage API——localStorageなど——の範囲がオリジンではなくドメインである、すなわちhttpsのページで書き込んだデータをhttpのページで読み込めるというったことがありました。

フィードバックの返礼品

  • IE 8の工具セット
  • FirefoxとThunderbirdの傘

[発話] そうしたバグ報告をしているとMicrosoftからメールが来て、グッズを送るから住所を登録してくれと言われました。何かしらと思って住所を入力すると後日IE 8の工具セットが届きました。これでIEを直せということでしょうかね。こうしたノベルティの贈呈はMicrosoft以外のブラウザベンダもやっており、MozillaからはFirefoxとThunderbirdの傘をもらったことがあります。

フィードバックしよう!

[発話] 皆さんもどんどんフィードバックしていきましょう。個別のブラウザにフィードバックする以外にも、ブラウザ間の相互運用性を高めるためにweb-platform-testsというものがあります。web-platform-testsにコミットすると各ブラウザで実行され、場合によってはブラウザ側で挙動が変更されることもあります。フィードバックを通じてWebの将来を作っていきましょう。ありがとうございました。


以上が発表内容です。久々の発表、しかもリモートからの参加ということで緊張しましたが、無事終えられてよかったです。

イベント自体は懐かしい機能を思い出したりIEの功績を振り返ったりと、IEの存在感の大きさを改めて感じられるものでした。イベントを企画・運営してくださった方々に感謝します。(IEのケーキが思ったより大きくて、現地で食べてみたかったです。)

IE 6も登場した時点ではそこまで悪いものではなく(個人的にはまだCSS 2ではなくCSS 1なのかと思いましたが……)、開発者の落胆を招いた要因としては、その後5年もメジャーバージョンアップがなかったという部分も大きいかと思います。

発表するにあたって、発表内容で触れた以外にも以下のページを参考にしました。また、窓の杜およびInternet Archiveには当時の記事が多数残っており、調査の大きな助けとなりました。

TypeScript の可変長タプル型における共用体の分配2022年06月09日 01時29分

TypeScript の可変長タプル型 (variadic tuple types) とは、配列型やタプル型を展開して別のタプル型の一部として使える機能のことです (「TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす - Qiita」に詳しいです)。記法としては、展開する型の直前に三連続のドット ... を記述します。例えば、

type Sandwich<Fillings extends unknown[]> = ['bread', ...Fillings, 'bread'];

のように具材 (filling) をパン (bread) で挟む Sandwich 型があったとき、Sandwich<['ham']> 型は ['bread', 'ham', 'bread'] 型に展開されます。

type HamSandwich = Sandwich<['ham']>;
// → ['bread', 'ham', 'bread']

type BLTSandwich = Sandwich<['bacon', 'lettuce', 'tomato']>;
// → ['bread', 'bacon', 'lettuce', 'tomato', 'bread']

type RichHamSandwich = Sandwich<'ham'[]>;
// → ['bread', ...'ham'[], 'bread']

type TwoSlicesOfBread = Sandwich<[]>;
// → ['bread', 'bread']

ここで型引数 Fillings に共用体型 (縦線 | で区切った複数の型のうちのいずれかを表す型) を渡すとどうなるでしょうか。具材がハムか卵なら、できあがるのはハムサンドか卵サンドになります。つまり、Sandwich<['ham'] | ['egg']> 型は Sandwich<['ham']> | Sandwich<['egg']> 型と同等に扱われます。

type HamOrEggSandwich = Sandwich<['ham'] | ['egg']>;
// → ['bread', 'ham', 'bread'] | ['bread', 'egg', 'bread']

このように、T<A | B | C | (略)> 型が T<A> | T<B> | T<C> | (略) 型として扱われる挙動を「共用体の分配 (union distribution)」と呼びます。 共用体の分配は条件型 P extends Q ? R : S でも発生しますが (「TypeScriptの型初級 - Qiita」に詳しいです)、可変長タプル型でも発生するのです。

条件型における共用体の分配では、never 型を分配しようとすると展開結果も never 型になります。これは可変長タプル型においても同じです。

type ImpossibleSandwich = Sandwich<never>
// → never

なお、可変長タプル型において any 型は any[] 型に展開されます。unknown 型を展開することはできません。

type AnySandwich = Sandwich<any>;
// → ['bread', ...any[], 'bread']

type UnknownSandwich = Sandwich<unknown>;
// → Error: Type 'unknown' does not satisfy the constraint 'unknown[]'.

可変長タプル型で共用体の分配が発生するというのは、可変長タプル型を導入した pull request の説明に書かれています。私はこの挙動を、type-challenges の回答のひとつをきっかけに知りました (その回答は誤答なのですが)。

  • When the type argument for T is a union type, the union is spread over the tuple type. For example, [A, ...T, B] instantiated with X | Y | Z as the type argument for T yields a union of instantiations of [A, ...T, B] with X, Y and Z as the type argument for T respectively.

Variadic tuple types by ahejlsberg · Pull Request #39094 · microsoft/TypeScript

HTML のフォームコントロール要素と label 要素の紐づけ2021年12月24日 21時11分

この記事は HTML アドベントカレンダーの 24 日目の分、兼 JavaScript アドベントカレンダーの 24 日目の分です。


HTML のフォームコントロール要素 (inputtextareaselectbutton 要素など) には、label 要素を使ってラベルを指定できます。ここでいうラベルとは、そのフォームコントロールに何を入力するか・そのフォームコントロールで何ができるのかの簡単な説明であり、人間が読んで理解できるようなフォームコントロールの名前です。

ある label 要素の子孫にフォームコントロール要素が存在すれば、その label 要素の内容が、そのフォームコントロール要素のラベルとなります。そうでない場合、label 要素の for 属性にフォームコントロール要素の ID (id 属性の値) を指定する必要があり、その label 要素の内容が、その ID を持つフォームコントロール要素のラベルとなります。

このフォームコントロール要素と label 要素との紐づきは JavaScript を使って参照できます。フォームコントロール要素オブジェクトの labels プロパティはそのフォームコントロール要素と紐づく label 要素の一覧 (NodeList オブジェクト) を返し、label 要素オブジェクト (HTMLLabelElement オブジェクト) の control プロパティはその label 要素に紐づくフォームコントロール要素を返します。

labels プロパティの名前が複数形なのは、ひとつのフォームコントロール要素に対して複数の label 要素を紐づけられるからですね。

<label id="query-label-1" for="query-field">キーワード</label>
<label id="query-label-2" for="query-field">URL</label>
<input id="query-field" type="search" name="q">
const label1 = document.getElementById('query-label-1');
const label2 = document.getElementById('query-label-2');
const field = document.getElementById('query-field');

console.assert(label1.control === field, 'control プロパティでフォームコントロールを参照できる (1)');
console.assert(label2.control === field, 'control プロパティでフォームコントロールを参照できる (2)');
console.assert(field.labels[0] === label1, 'labels プロパティで label 要素を参照できる (1)');
console.assert(field.labels[1] === label2, 'labels プロパティで label 要素を参照できる (2)');

あるフォームコントロールにおいて、labels プロパティの返す NodeList オブジェクトは常に同一です。紐づく label 要素に変更があれば、その NodeList オブジェクトの内容が動的に変化します。

const oldLabels = field.labels;
label1.remove();
const newLabels = field.labels;

console.assert(oldLabels === newLabels, 'labels プロパティの値は何度参照しても同一のオブジェクトである');
console.assert(oldLabels.length === 1, 'labels プロパティの値は動的に変化する');

ただし、実際のところひとつのフォームコントロール要素に複数の label 要素を紐づけるような場面はほとんどないと思います。

button 要素にも label 要素を紐づけられます。しかしながら、button 要素の場合は自身の内容がラベルとして扱われるので (<button type="submit">検索する</button> なら「検索する」がそのボタンのラベルになります)、実際のところ button 要素に label 要素を紐づけるような場面はほとんどないと思います。

ラベルは aria-label 属性aria-labelledby 属性を使って指定することもできます。

<form action="/search">
  <p>
    <input type="search" name="q" aria-label="キーワード">
    <button type="submit" aria-label="検索する">🔍</button>
  </p>
</form>

HTML のテキスト入力欄の入力値の一部を置換する2021年12月23日 23時48分

この記事は HTML アドベントカレンダーの 23 日目の分、兼 JavaScript アドベントカレンダーの 23 日目の分です。


HTML のテキスト入力欄 (<input type="text"> 要素や textarea 要素など) で選択範囲の文字列を置換したいとき、一昔前は JavaScript で以下のように書く必要がありました。

// 引数 field には HTMLInputElement オブジェクトまたは HTMLTextAreaElement オブジェクトを、
// 引数 newText には文字列を、それぞれ受け取る。
function replaceSelectionText(field, newText) {
  var start = field.selectionStart;
  var end = field.selectionEnd;
  var text = field.value;
  field.value = text.substring(0, start) + newText + text.substring(end);
  field.setSelectionRange(start, start + newText.length);
}

(さらに大昔は IE 向けに TextRange オブジェクトを使う必要がありました。)

今は HTML 標準で setRangeText メソッドが定義されているため、以下のように書けます。

function replaceSelectionText(field, newText) {
  field.setRangeText(newText);
}

setRangeText メソッドには置換する範囲を指定することもできます。テキスト入力欄 field の入力値が abcde のとき、以下のコードを実行すると入力値が axde になります。

field.setRangeText('x', 1, 3);

このとき、元の入力値において改行は \n に正規化され、1 文字として数えられます。また、「1 文字」というのが Unicode の符号位置ではなく UTF-16 の符号単位を表すことに注意が必要です。

置換する範囲を指定して setRangeText メソッドを呼び出したとき、選択範囲はメソッド呼び出しの前後であまり変化しないように調整されます。この挙動は、第 4 引数に selectstartendpreserve のいずれかの値を指定することで変更できます (省略時は preserve)。以下の例ではメソッド呼び出し後に部分文字列 x が選択されます。

field.setRangeText('x', 1, 3, 'select');

Web Platform Tests での HTML 標準のテストに触れる2021年12月22日 23時50分

この記事は HTML アドベントカレンダーの 22 日目の分、兼 JavaScript アドベントカレンダーの 22 日目の分です。


Web Platform Tests をご存じでしょうか。Web ブラウザ間の相互運用性を高めるための、様々な Web 標準技術に関するテストスイートです。主要 Web ブラウザは Web Platform Tests を開発プロセスに取り込み、互換性の向上やリグレッションの防止を図っています。

Web Platform Tests はあなたが今閲覧に使っているブラウザで実行できます。試しに HTML 標準に関するテストを実行してみましょう。https://wpt.live/ からディレクトリをたどって https://wpt.live/html/syntax/parsing/ に行くと、数多くのテストファイルが並んでいます。

ファイルリストの先頭にある DOMContentLoaded-defer.html を開いてみてください。あら、テストが失敗してしまいました!

DOMContentLoaded-defer.html を開くと 1 Fail と表示されます。

気を取り直して次の Document.getElementsByTagName-foreign-01.html を開いてみると……今度は成功しています! JavaScript から document.getElementsByTagName メソッドを呼び出して SVG の要素を取得できることを確かめられました。

Document.getElementsByTagName-foreign-01.html を開くと 37 Pass と表示されます。

ブラウザによっては失敗したテストをバグトラッキングシステムで管理しています。先ほど失敗した DOMContentLoaded-defer というファイル名を Mozilla Bugzilla (Firefox のバグ管理にも使われています) で検索してみると、1242128 - Frequent Win8 TEST-UNEXPECTED-PASS | /html/syntax/parsing/DOMContentLoaded-defer.html | The end: DOMContentLoaded and defer scripts - expected FAIL が出てきました。

議論の場はそこから DOMContentLoaded-defer.html is likely wrong · Issue #4267 · web-platform-tests/wpt に移っています。どうもこれはテストのほうがおかしいのではないかという話が出ていますね。

Web Platfrom Tests は絶対的なものではなく、日々追加・変更されています。開発は GitHub で開かれており、開発への貢献は各 Web ブラウザの品質向上につながります。筆者もかつて縦書きの文書におけるスクロール位置に関するテストを追加したところ、それに関する Chrome の挙動が修正されました

あなたも Web Platform Tests に参加して Web プラットフォームを進化させていきませんか!