Days on the Moon http://nanto.asablo.jp/blog/ ja mc 0.0 Fri, 07 Jun 2024 10:43:17 +0900 JavaScriptの識別子に中黒が使えるようになった http://nanto.asablo.jp/blog/2024/06/07/9690975 http://nanto.asablo.jp/blog/2024/06/07/9690975 Fri, 07 Jun 2024 10:21:45 +0900 2024-06-07T10:43:17+09:00 2024-06-07T10:24:44+09:00 <p>JavaScriptの識別子(変数名、関数名、プロパティ名など)の2文字目以降に中黒「・」(U+30FB KATAKANA MIDDLE DOT)が使えるようになりました。以下のコードはChrome 124では構文エラーになりますが、Chrome 125では問題なく実行できます。</p> <pre><code>const シン・ゴジラ = 2016;</code></pre> <h3>JavaScriptの識別子</h3> <p>中黒が使えるようになったのは、JavaScript(ECMAScript)の仕様が変わったからではありません。変わったのはUnicodeの仕様のほうです。Unicode 15.1.0(2023年9月)において<code>Other_ID_Continue</code>プロパティ(を持つ文字の集まり)に中黒が追加されました。</p> <p>そもそもJavaScriptの識別子に使える文字は、Unicodeを参照して定義されています。ECMAScript 2023(2023年6月)では以下のようになっています。</p> <dl> <dt>識別子の1文字目に使える文字</dt> <dd> <ul> <li>Unicodeの<code>ID_Start</code>プロパティを持つ文字</li> <li><code>$</code>(U+0024 DOLLAR SIGN)</li> <li><code>_</code>(U+005F LOW LINE)</li> </ul> </dd> <dt>識別子の2文字目以降に使える文字</dt> <dd> <ul> <li>Unicodeの<code>ID_Continue</code>プロパティを持つ文字</li> <li><code>$</code>(U+0024 DOLLAR SIGN)</li> <li>ゼロ幅非接合子(U+200C ZERO WIDTH NON-JOINER)</li> <li>ゼロ幅接合子(U+200D ZERO WIDTH JOINER)</li> </ul> </dd> </dl> <p>(いわゆるアンダースコア「_」は<code>ID_Continue</code>プロパティを持つため、2文字目以降にも使えます。)</p> <p>Unicodeの仕様も毎年のように改定されますが、ECMAScript仕様ではUnicodeの「最新バージョン」が参照されています。</p> <h3>Unicodeの識別子</h3> <p>Unicodeの<code>ID_Start</code>、<code>ID_Continue</code>プロパティは、各種の「識別子」に使える文字として推奨されるものを表しており、<a href="https://unicode.org/reports/tr31/" hreflang="en" lang="en">UAX #31 Unicode Identifiers and Syntax</a>で以下の文字を含むものとして定義されています。</p> <dl> <dt><code>ID_Start</code>プロパティ</dt> <dd> <ul> <li>一般カテゴリが<code>Letter</code>である文字</li> <li>一般カテゴリが<code>Letter_Number</code>である文字</li> <li><code>Other_ID_Start</code>プロパティを持つ文字</li> <li>ただし<code>Pattern_Syntax</code>プロパティまたは<code>Pattern_White_Space</code>プロパティを持つ文字を除く(具体的には<code>ⸯ</code>(U+2E2F VERTICAL TILDE)が除かれる)</li> </ul> </dd> <dt><code>ID_Continue</code>プロパティ</dt> <dd> <ul> <li><code>ID_Start</code>プロパティを持つ文字</li> <li>一般カテゴリが<code>Nonspacing_Mark</code>である文字</li> <li>一般カテゴリが<code>Spacing_Mark</code>である文字</li> <li>一般カテゴリが<code>Decimal_Number</code>である文字</li> <li>一般カテゴリが<code>Connector_Punctuation</code>である文字</li> <li><code>Other_ID_Continue</code>プロパティを持つ文字</li> <li>ただし<code>Pattern_Syntax</code>プロパティまたは<code>Pattern_White_Space</code>プロパティを持つ文字を除く</li> </ul> </dd> </dl> <p>ここで<code>Other_ID_Start</code>プロパティと<code>Other_ID_Continue</code>プロパティというのは、後方互換性のためにそれぞれ<code>ID_Start</code>プロパティと<code>ID_Continue</code>プロパティに含めるべき文字を表しています。<code>Other_ID_Continue</code>プロパティに中黒(U+30FB)が追加されたことで、巡り巡ってJavaScriptの識別子に中黒が使えるようになったのです。</p> <p>なお、中黒だけでなくゼロ幅非接合子(U+200C)とゼロ幅接合子(U+200D)も追加されたため、ECMAScript 2024以降では識別子の2文字目以降に使える文字の定義が「Unicodeの<code>ID_Continue</code>プロパティを持つ文字または<code>$</code>(U+0024 DOLLAR SIGN)」と簡潔になる予定です。</p> <h3>過去にも識別子に中黒が使えた</h3> <p><code>ID_Continue</code>プロパティの後方互換性のために中黒が追加されたということは、さらに以前は中黒が<code>ID_Continue</code>プロパティに含まれていたのでしょうか?</p> <p>まさにその通りで、Unicode 4.0.1(2004年5月)以前は中黒の一般カテゴリが<code>Connector_Punctuation</code>になっており、結果として<code>ID_Continue</code>プロパティに含まれていました。Unicode 4.1.0(2005年3月)で中黒の一般カテゴリが<code>Other_Punctuation</code>に変更され、<code>ID_Continue</code>プロパティに含まれなくなっていたのです。</p> <p>ECMAScript 5.1ではUnicode 3.0以上への適合が求められていたため、ECMAScript 3(1999年12月)~5.1(2011年6月)の間は中黒を識別子に使えた可能性があります。(ECMAScript 2(1998年8月)以前はASCIIの範囲内の文字のみ識別子に使用可能、ECMAScript 2015(ES6、2015年6月)以降はUnicode 5.1.0以上への適合が求められる。)</p> <h3>他のプログラミング言語の識別子</h3> <p>識別子の定義にUAX #31を参照しているのはJavaScriptだけではありません。<a href="https://ja.wikibooks.org/wiki/C%E8%A8%80%E8%AA%9E/C23%E3%81%AE%E5%A4%89%E6%9B%B4%E7%82%B9#Unicode_%E8%AD%98%E5%88%A5%E5%AD%90%E6%9B%B4%E6%96%B0">C</a>、<a href="https://open-std.org/JTC1/SC22/WG21/docs/papers/2021/p1949r7.html" hreflang="en">C++</a>、<a href="https://perldoc.perl.org/perldata#Identifier-parsing" hreflang="en">Perl</a>、<a href="https://peps.python.org/pep-3131/" hreflang="en">Python</a>、<a href="https://doc.rust-lang.org/reference/identifiers.html" hreflang="en">Rust</a>など多くのプログラミング言語がUAX #31を参照しています。これらの言語でもそのうち識別子に中黒が使えるようになるでしょう(<code>\p{Word}</code>との共通部分を採用しているPerlを除く)。</p> <p>なお、JavaScript以外では<code>ID_Start</code>、<code>ID_Continue</code>プロパティではなく、<code>XID_Start</code>、<code>XID_Continue</code>プロパティを参照していることが多いです。これらのプロパティは、ある識別子にUnicode正規化を適用しても識別子として有効であり続けることを保証するため、それぞれ<code>ID_Start</code>、<code>ID_Continue</code>プロパティからいくつかの文字を除外しています。</p> <h3>参考文献</h3> <ul> <li><a href="https://ufcpp.net/blog/2021/2/uax31/">UAX31: Unicode Identifier の話 | ++C++; // 未確認飛行 C ブログ</a></li> <li><a href="https://www.unicode.org/L2/L2023/23160-utc176-properties-recs.pdf" hreflang="en" lang="en">UTC #176 properties feedback &amp; recommendations (PDF)</a>——「<span lang="en">2.2 Katakana middle dots in XID_Continue</span>」において、中黒が識別子に使えなくなったのは「うっかり(<span lang="en">accidentaly</span>)」だったと述べられている。</li> <li><a href="https://www.unicode.org/Public/4.0-Update1/PropList-4.0.1.txt">PropList-4.0.1.txt</a>——中黒(U+30FB)の一般カテゴリが<code>Connector_Punctuation</code>(<code>Pc</code>)であることを確認</li> <li><a href="https://www.unicode.org/Public/4.1.0/ucd/PropList.txt">PropList-4.1.0.txt</a>——中黒(U+30FB)の一般カテゴリが<code>Other_Punctuation</code>(<code>Po</code>)であることを確認</li> <li><a href="https://www.unicode.org/Public/15.0.0/ucd/PropList.txt">PropList-15.0.0.txt</a>——<code>Other_ID_Continue</code>プロパティに中黒(U+30FB)が含まれないことを確認</li> <li><a href="https://www.unicode.org/Public/15.1.0/ucd/PropList.txt">PropList-15.1.0.txt</a>——<code>Other_ID_Continue</code>プロパティに中黒(U+30FB)が含まれることを確認</li> </ul> コンピュータ一般 JavaScript CSSでチェックボックスやラジオボタンをカスタマイズする 2024年版 http://nanto.asablo.jp/blog/2024/05/24/9686885 http://nanto.asablo.jp/blog/2024/05/24/9686885 Fri, 24 May 2024 09:05:10 +0900 2024-05-24T09:21:29+09:00 2024-05-24T09:10:59+09:00 <p>HTMLのチェックボックス(<code>&lt;input type="checkbox"&gt;</code>)やラジオボタン(<code>&lt;input type="radio"&gt;</code>)をCSSで装飾したいというのはよく聞く話です。2024年現在は、HTMLの記述は簡単なまま、CSSで自由度の高い装飾も実現できるようになっています。</p> <ol class="toc"> <li><a href="#customcheckradio-conclusions">結論</a></li> <li><a href="#customcheckradio-traditional-way">従来の手法</a></li> <li><a href="#customcheckradio-appearance"><code>appearance</code>プロパティを使う手法</a></li> <li><a href="#customcheckradio-frame">外枠の配置</a></li> <li><a href="#customcheckradio-checked-state">未チェックとチェック済みの切り替え</a></li> <li><a href="#customcheckradio-forced-colors">強制カラーモードへの対応</a> <ol> <li><a href="#customcheckradio-border-or-outline">透明なボーダーやアウトライン</a></li> <li><a href="#customcheckradio-shadow-or-gradient">内向きの影や背景グラデーション</a></li> <li><a href="#customcheckradio-image-or-text">画像やテキスト</a></li> <li><a href="#customcheckradio-built-in-appearance">ブラウザ組み込みの外観</a></li> </ol> </li> <li><a href="#customcheckradio-additional-states">状態に応じたスタイルの指定</a></li> <li><a href="#customcheckradio-references">参考文献</a></li> </ol> <h3 id="customcheckradio-conclusions">結論</h3> <p>単に色調を整えられればよいという場合は、<a href="https://developer.mozilla.org/ja/docs/Web/CSS/accent-color"><code>accent-color</code>プロパティ</a>を使います。</p> <pre><code>input[type="checkbox"], input[type="radio"] { accent-color: #d31; }</code></pre> <div class="customcheckradio-example customcheckradio-example-1"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-1" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-1"> みかん</label> </p> </div> <figure> <p class="figure"> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20240524/20240524014833.png" alt="accent-colorプロパティを用いてチェックボックスとラジオボタンを装飾した図" width="243" height="115"> </p> <figcaption>Windows 11 Firefox Nightly 128.0a1での表示</figcaption> </figure> <p><code>accent-color</code>プロパティは継承するので、<code>body</code>要素などに指定してその子孫のフォームコントロールに一律に適用することもできます。</p> <p>自由に装飾したい場合は、<code>input</code>要素に<a href="https://developer.mozilla.org/ja/docs/Web/CSS/appearance"><code>appearance: none</code></a>を指定したうえで、未チェック状態とチェック済み状態のスタイルを指定していきます。<code>::before</code>、<code>::after</code>疑似要素も使えます。</p> <pre><code>input[type="checkbox"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; border: thin solid; } input[type="checkbox"]::before { content: '✔'; visibility: hidden; } input[type="checkbox"]:checked::before { visibility: visible; }</code></pre> <div class="customcheckradio-example customcheckradio-example-2"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> </div> <figure> <p class="figure"> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20240524/20240524014836.png" alt="appearanceプロパティを用いてチェックボックスを装飾した図" width="237" height="72"> </p> <figcaption>Windows 11 Firefox Nightly 128.0a1での表示</figcaption> </figure> <h3 id="customcheckradio-traditional-way">従来の手法</h3> <p>これまでチェックボックスやラジオボタンをCSSで装飾したい場合は、<code>input</code>要素自身ではなくその直後に置いた<code>span</code>要素や<code>label</code>要素を装飾するのが一般的でした。</p> <pre><code>&lt;label&gt; &lt;input type="checkbox"&gt; &lt;span&gt;...&lt;/span&gt; &lt;/label&gt; &lt;input id="the-radio-1" type="radio"&gt; &lt;label for="the-radio-1"&gt;...&lt;/label&gt;</code></pre> <pre><code>/* 組み込みのチェックボックスとラジオボタンを見えなくする */ input[type="checkbox"], input[type="radio"] { position: absolute; clip: rect(0, 0, 0, 0); } input[type="checkbox"] + span { /* 未チェック状態のスタイル */ } input[type="checkbox"]:checked + span { /* チェック済み状態のスタイル */ } input[type="radio"] + label { /* 未チェック状態のスタイル */ } input[type="radio"]:checked + label { /* チェック済み状態のスタイル */ }</code></pre> <p>この手法は、スタイル指定のためだけの<code>span</code>要素が必要となる、あるいは<code>input</code>要素に<code>id</code>属性を指定して明示的に<code>label</code>要素と紐づける必要があるという点で、ひと手間かかります。また、アクセシビリティを確保するため、<code>input</code>要素を見えなくするのには<code>display: none</code>以外の手段を使う必要があります(<code>display: none</code>を使うと、チェックボックスやラジオボタンをキーボードで操作できなくなります)。</p> <h3 id="customcheckradio-appearance"><code>appearance</code>プロパティを使う手法</h3> <p><code>appearance</code>プロパティを使う場合、HTML側では<code>label</code>要素と<code>input</code>要素の暗黙的な紐づけを使えます(明示的な紐づけを使っても構いません)。</p> <pre><code>&lt;label&gt; &lt;input type="checkbox"&gt; ... &lt;/label&gt;</code></pre> <p>CSS側では<code>appearance: none</code>を指定します。これにより、<code>input</code>要素が組み込みのチェックボックスやラジオボタンではなく、単なる「内容が空の要素」として描画されることになります。<code>&lt;span&gt;&lt;/span&gt;</code>と同じ扱いができるので、背景、ボーダー、影、アニメーション、それに<code>::before</code>、<code>::after</code>疑似要素といったCSSの各種機能をふんだんに使って装飾できます。</p> <h3 id="customcheckradio-frame">外枠の配置</h3> <p><code>appearance: none</code>を指定しただけでは内容が空なので何も表示されません。まずは幅と高さを指定してチェックボックスまたはラジオボタンの領域を確保しましょう。幅と高さの指定が確実に効くようにするため、<code>display</code>プロパティに<code>inline-block</code>、<code>inline-grid</code>、<code>inline-flex</code>などの値を指定する必要があります。</p> <pre><code>input[type="checkbox"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-block; width: 1.4em; height: 1.4em; }</code></pre> <p>そのままだとブラウザ組み込み(ユーザーエージェントスタイルシート)のスタイルが残ることがあるので、適宜初期化していきます。ブラウザごとの差異を吸収するため、マージンと背景色は常に指定したほうがよいでしょう。<code>em</code>単位を使うのなら<code>font-size</code>プロパティを、親要素と同じフォントや文字色(あるいは<a href="https://developer.mozilla.org/ja/docs/Web/CSS/color_value#currentcolor_%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89"><code>currentcolor</code>キーワード</a>)を使うのなら<code>font-family</code>プロパティや<code>color</code>プロパティを、それぞれ継承させます。</p> <pre><code>input[type="checkbox"] { ... margin: 0; background-color: transparent; color: inherit; font: inherit; }</code></pre> <p>また、気をつけておきたいのが行内での縦位置です。個人的には、チェックボックスおよびラジオボタンの縦中央と、ラベル文字列の縦中央とがそろったほうが収まりがよく感じられます(<a href="https://developer.mozilla.org/ja/docs/Web/CSS/vertical-align"><code>vertical-align: middle</code></a>は英小文字の縦中央とそろえるので、日本語文字の縦中央とは少しずれますが……)。</p> <pre><code>input[type="checkbox"] { ... vertical-align: middle; }</code></pre> <p>外枠の仕上げとしてボーダーを指定します。ラジオボタンの場合は<code>border-radius</code>プロパティで円形にするのがわかりやすいでしょう。ボーダーではなく背景画像やグラデーションで外枠を表現することもできます。後述する強制カラーモードへの対応のため、背景画像を使わないときはボーダーを指定することをお勧めします(<code>border: thin solid transparent</code>といった透明のボーダーでも構いません)。</p> <pre><code>input[type="checkbox"], input[type="radio"] { ... border: thin solid; } input[type="checkbox"], input[type="radio"] { ... border-radius: 0.2em; } input[type="radio"] { ... border-radius: 50%; }</code></pre> <p>ここまでの指定を適用したのが以下の例です。</p> <div class="customcheckradio-example customcheckradio-example-3"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-3" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-3"> みかん</label> </p> </div> <h3 id="customcheckradio-checked-state">未チェックとチェック済みの切り替え</h3> <p>外枠ができたら次は内側、チェック状態の切り替えを表現していきましょう。<code>::before</code>疑似要素でチェック済みを表す内容を指定しつつ最初は隠しておき、チェック済み状態(<code>:checked</code>疑似クラス)になったらその内容を表示するというパターンが多いです。</p> <p>以下の例では、ボーダーを使ってチェックボックスのチェックマークとラジオボタンの点を描画しています。ボーダーを使っているのは、後述する強制カラーモードでもチェックマークや点が表示されるようにするためです。また、内容を上下左右とも中央ぞろえするためにグリッドレイアウトを使っています。</p> <pre><code>input[type="checkbox"], input[type="radio"] { ... display: inline-grid; place-content: center; } input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } input[type="checkbox"]:checked::before { visibility: visible; } input[type="radio"]::before { content: ''; border: 0.4em solid; border-radius: 50%; visibility: hidden; } input[type="radio"]:checked::before { visibility: visible; } </code></pre> <div class="customcheckradio-example customcheckradio-example-4"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-4" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-4"> みかん</label> </p> </div> <p><a href="https://developer.mozilla.org/ja/docs/Web/CSS/clip-path"><code>clip-path</code>プロパティ</a>を使って好きな形に切り抜いたり、背景画像やボーダー画像にグラデーションを使ったりと、自由度の高い表現が可能です。</p> <pre><code>input[type="checkbox"]::before { content: ''; border: 0.5em solid; clip-path: polygon(0% 60%, 0% 45%, 35% 65%, 100% 0%, 100% 15%, 45% 100%, 30% 100%); visibility: hidden; } input[type="radio"]::before { content: ''; width: 1em; height: 1em; border: 0.2em solid transparent; border-radius: 50%; padding: 0.2em; background: repeating-conic-gradient(from 45deg, #f33 0deg 90deg, #fc6 90deg 180deg) content-box, repeating-conic-gradient(from 45deg, #fc6 0deg 90deg, #f33 90deg 180deg) padding-box, repeating-conic-gradient(from 45deg, #f33 0deg 90deg, #fc6 90deg 180deg) border-box, CanvasText; visibility: hidden; }</code></pre> <div class="customcheckradio-example customcheckradio-example-5"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-5" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-5"> みかん</label> </p> </div> <p>チェック状態の切り替え時に、フェードイン・フェードアウトや拡大・縮小といったアニメーション効果をつけるのもよいでしょう。</p> <pre><code>input[type="checkbox"]::before { ... opacity: 0; transition: opacity 0.5s ease-out; } input[type="checkbox"]:checked::before { opacity: 1; } input[type="radio"]::before { ... scale: 0; transition: scale 0.5s ease-out; } input[type="radio"]:checked::before { ... scale: 1; }</code></pre> <div class="customcheckradio-example customcheckradio-example-6"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-6" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-6"> みかん</label> </p> </div> <h3 id="customcheckradio-forced-colors">強制カラーモードへの対応</h3> <p>強制カラーモードとは、テキストを読みやすくするために、背景色や文字色をコントラストのはっきりした色にする機能のことです。OSのアクセシビリティ機能の一環として提供されることが多く、Windowsなら「ハイコントラスト(Windows 10)」「コントラストテーマ(Windows 11)」という設定を有効にすることで、ブラウザで表示中の文書でも強制カラーモードが有効化されます。</p> <p class="figure"> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20240524/20240524014839_original.png" alt="Windows 11のコントラストテーマ「夕暮れ」を適用した図" width="1163" height="907"> </p> <p>強制カラーモードの主な影響は以下の通りです。詳しくは<a href="https://developer.mozilla.org/ja/docs/Web/CSS/@media/forced-colors"><code>forced-colors</code>メディア特性</a>を参照してください。</p> <ul> <li><code>color</code>、<code>background-color</code>、<code>border-color</code>、<code>outline-color</code>、<code>accent-color</code>などは、OSやブラウザ側で決められた色に変更されます。</li> <li><code>text-shadow</code>、<code>box-shadow</code>は無視されます。</li> <li><code>background-image: linear-gradient(...)</code>などのグラデーションの指定は無視されます。</li> </ul> <p>一般的なテキストと画像に関しては、強制カラーモードを意識せずにスタイルを指定していても、最低限テキストが読める状態になることが多いです。しかし、カスタマイズしたチェックボックスやラジオボタンに関しては、気をつけないと全く表示されず、ユーザーがそれらフォームコントールの存在に気づかないという事態が起こりかねません。</p> <p>チェック状態に関しても、チェックマークや点を単純に<code>background-color: #d51</code>のような背景色だけで表現してしまうと、強制カラーモードではチェック済みかどうか視認できなくなってしまいます。前述のボーダーを使った表現や、または以下に挙げるような表現手法をとる必要があります。</p> <h4 id="customcheckradio-border-or-outline">透明なボーダーやアウトライン</h4> <p>背景部分に透明なボーダーやアウトラインを重ねる手法です。強制カラーモードが無効なときは背景がそのまま表示されますし、有効なときはボーダーやアウトラインが塗りつぶされて視認できます。背景部分にアウトラインを重ねるためには、<code>outline-offset</code>プロパティの値に負数を指定してボックスの内側にアウトラインを配置します。</p> <pre><code>input[type="checkbox"]::before { content: ''; border: 0.4em solid transparent; background-color: #d31; visibility: hidden; } input[type="radio"]::before { content: ''; width: 0.8em; height: 0.8em; border-radius: 50%; background-color: #d31; outline: 0.4em solid transparent; outline-offset: -0.4em; visibility: hidden; }</code></pre> <div class="customcheckradio-example customcheckradio-example-7"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-7" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-7"> みかん</label> </p> </div> <h4 id="customcheckradio-shadow-or-gradient">内向きの影や背景グラデーション</h4> <p><a href="https://developer.mozilla.org/ja/docs/Web/CSS/system-color"><code>CanvasText</code>(文書の文字色を示す)といったシステム色</a>は、強制カラーモードでも視認できる背景色として使えます。内向きの影(<code>box-shadow: inset ...</code>)は背景の前面に描画されるため、強制カラーモードが無効なら影の色が表示されますが、有効だと背景色が表示されることになります。背景画像としてグラデーションを使った場合も、そのグラデーションは背景色の前面に描画されます。</p> <pre><code>input[type="checkbox"]::before { content: ''; width: 0.8em; height: 0.8em; background-color: CanvasText; box-shadow: inset 0 0 0 0.4em #d31; visibility: hidden; } input[type="radio"]::before { content: ''; width: 0.8em; height: 0.8em; border-radius: 50%; background: linear-gradient(#d31 0% 100%), CanvasText; visibility: hidden; }</code></pre> <div class="customcheckradio-example customcheckradio-example-8"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-8" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-8"> みかん</label> </p> </div> <h4 id="customcheckradio-image-or-text">画像やテキスト</h4> <p><code>background-image: url(...)</code>や<code>content: url(...)</code>で読み込んだ画像、および<code>content</code>プロパティで指定したテキストは、強制カラーモードでもそのまま表示されます。</p> <pre><code>input[type="checkbox"]::before { content: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2010%2010%22%3E%3Cpath%20d%3D%22M%205%2C1%20L%209%2C5%205%2C9%201%2C5%20Z%22%20fill%3D%22%23fda%22%20stroke%3D%22%23f81%22%2F%3E%3C%2Fsvg%3E"); width: 1.2em; height: 1.2em; visibility: hidden; } input[type="radio"] { ... display: inline-block; text-align: center; } input[type="radio"]::before { content: ''; display: block; margin-top: -0.2em; } input[type="radio"]:checked::before { content: '👉'; }</code></pre> <div class="customcheckradio-example customcheckradio-example-9"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-9" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-9"> みかん</label> </p> </div> <h4 id="customcheckradio-built-in-appearance">ブラウザ組み込みの外観</h4> <p>強制カラーモードではカスタマイズせずに、ブラウザ組み込みの外観を使うのもひとつの手です。<code>forced-colors</code>メディア特性で強制カラーモードが有効かどうかを判別でき、<a href="https://developer.mozilla.org/ja/docs/Web/CSS/all"><code>all</code>プロパティ</a>と<a href="https://developer.mozilla.org/ja/docs/Web/CSS/revert"><code>revert</code>キーワード</a>の組み合わせでスタイル指定を撤回できます。</p> <pre><code>@media (forced-colors: active) { input[type="checkbox"], input[type="checkbox"]::before, input[type="radio"], input[type="radio"]::before { /* 疑似クラスに指定されたスタイル(後述)も撤回したいので!importを指定する。 */ all: revert !important; } }</code></pre> <div class="customcheckradio-example customcheckradio-example-10"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-10" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-10"> みかん</label> </p> </div> <p>ここまで書いておいてなんですが、筆者は強制カラーモードを常用しておらず、強制カラーモードでチェックボックスやラジオボタンをカスタマイズしてもよいのかしないほうがよいのかのベストプラクティスを持ち合わせていません。ご存じの方は教えていただけると幸いです。</p> <h3 id="customcheckradio-additional-states">状態に応じたスタイルの指定</h3> <p>以上で最低限の表示切替ができましたが、ほかにも状態に応じたスタイルを指定できます。</p> <p>よく使うのはマウスホバー時を表す<code>:hover</code>疑似クラスでしょう。フォームコントロールに対する<code>:hover</code>疑似クラスは、そのフォームコントロールに紐づけられた<code>label</code>要素にマウスホバーしているときにも適用されます。</p> <p>次の例では<code>:hover</code>疑似クラスと<a href="https://developer.mozilla.org/ja/docs/Web/CSS/:enabled"><code>:enabled</code>疑似クラス</a>を組み合わせています。<code>:hover</code>疑似クラスを使うのは「今まさにユーザーが操作できる対象を強調したい」という意図であり、無効状態の(操作できない)フォームコントロールに適用してしまうとその意図から外れてしまうからです。</p> <pre><code>input[type="checkbox"]:enabled:hover, input[type="radio"]:enabled:hover { box-shadow: 0 0 0.3em 0.1em #d31; }</code></pre> <div class="customcheckradio-example customcheckradio-example-11"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-11" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-11"> みかん</label> </p> </div> <p>多くのブラウザでは、チェックボックスやラジオボタンにキーボード(Tabキー)でフォーカスしたときに、フォーカスリングが表示されます。このフォーカスリングの表示を変更したいときには<a href="https://developer.mozilla.org/ja/docs/Web/CSS/:focus-visible"><code>:focus-visible</code>疑似クラス</a>を使います。その際には<a href="https://developer.mozilla.org/ja/docs/Web/CSS/:focus-visible#%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B7%E3%83%93%E3%83%AA%E3%83%86%E3%82%A3%E3%81%AE%E8%80%83%E6%85%AE">「アクセシビリティの考慮」の項</a>や<a href="https://waic.jp/translations/WCAG21/Understanding/non-text-contrast.html"><abbr title="Web Content Accessibility Guidelines">WCAG</abbr> 2.1の達成基準1.4.11「非テキストのコントラスト」の解説</a>も参考に、「今まさにユーザーが操作できる対象」が伝わるようにしてください。</p> <p>以下の例では、影を使ってフォーカスリングを表現しつつ、強制カラーモードに備えて透明のアウトラインを設定しています。単に<code>outline-color: transparent</code>を指定するだけ(<code>outline-style: auto</code>のまま)だとブラウザによってはアウトラインが見えてしまうことがあるので、<code>outline-style: solid</code>にしています。</p> <pre><code>input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible { box-shadow: 0 0 0.3em 0.1em #d31; outline: medium solid transparent; } </code></pre> <div class="customcheckradio-example customcheckradio-example-12"> <p> <label><input type="checkbox" checked> くだもの</label> <label><input type="checkbox"> やさい</label> </p> <p> <label><input type="radio" name="customcheckradio-example-12" checked> りんご</label> <label><input type="radio" name="customcheckradio-example-12"> みかん</label> </p> </div> <p>無効状態の(<code>disabled</code>属性が付与された)チェックボックスやラジオボタンを提供するのなら、<code>:disabled</code>疑似クラスを使います。次の例では<a href="https://developer.mozilla.org/ja/docs/Web/CSS/filter"><code>filter</code>プロパティ</a>を使って無彩色にしたうえで、<code>opacity</code>プロパティで色を薄くしています。強制カラーモードで背景が明色になろうと暗色になろうと、半透明なら色が薄くなることに変わりありません。</p> <pre><code>input[type="checkbox"]:disabled, input[type="radio"]:disabled { border-style: dashed; filter: grayscale(1); opacity: 0.6; }</code></pre> <div class="customcheckradio-example customcheckradio-example-13"> <p> <label><input type="checkbox" checked disabled> くだもの</label> <label><input type="checkbox" disabled> やさい</label> <label><input type="checkbox" checked> にく</label> <label><input type="checkbox"> さかな</label> </p> <p> <label><input type="radio" name="customcheckradio-example-13" checked disabled> りんご</label> <label><input type="radio" name="customcheckradio-example-13" disabled> みかん</label> <label><input type="radio" name="customcheckradio-example-13-enabled" checked> とうふ</label> <label><input type="radio" name="customcheckradio-example-13-enabled"> おあげ</label> </p> </div> <p>チェックボックスには未チェックともチェック済みとも言えない<a href="https://developer.mozilla.org/ja/docs/Web/HTML/Element/input/checkbox#%E6%9C%AA%E6%B1%BA%E5%AE%9A%E7%8A%B6%E6%85%8B%E3%81%AE%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9">未決定状態</a>が存在します。ただし、未決定状態になるのはJavaScriptで<code>input</code>要素オブジェクトの<code>indeterminate</code>プロパティを設定したときだけです。そのようなJavaScriptの記述がなければ、未決定状態に対するスタイルを指定する必要はありません。そのような記述があるのなら、<a href="https://developer.mozilla.org/ja/docs/Web/CSS/:indeterminate"><code>:indeterminate</code>疑似クラス</a>を使ってスタイルを指定します。</p> <p>以下の例では「くだもの」グループに属すチェックボックスの一部のみがチェックされているときに、「くだもの」チェックボックスを未決定状態にしています。</p> <pre><code>input[type="checkbox"]:indeterminate::before { width: 0.5em; height: 0.5em; border: 0.2em dotted; transform: none; visibility: visible; }</code></pre> <div class="customcheckradio-example customcheckradio-example-14"> <fieldset> <legend><label><input type="checkbox"> くだもの</label></legend> <p> <label><input type="checkbox"> りんご</label> <label><input type="checkbox"> みかん</label> <label><input type="checkbox"> いちご</label> </p> <script> document.currentScript.closest('fieldset').addEventListener('click', (event) => { const target = event.target; if (!(target instanceof HTMLInputElement)) return; const parentField = event.currentTarget.querySelector(':scope > legend input[type="checkbox"]'); const childFields = event.currentTarget.querySelectorAll(':scope > :not(legend) input[type="checkbox"]'); if (target === parentField) { childFields.forEach((field) => { field.checked = target.checked; }); } else { const checkedCount = Array.from(childFields).filter((field) => field.checked).length; parentField.checked = checkedCount === childFields.length; parentField.indeterminate = 0 < checkedCount && checkedCount < childFields.length; } }); </script> </fieldset> </div> <h3 id="customcheckradio-references">参考文献</h3> <p>チェックボックスとラジボタンのカスタマイズについて:</p> <ul> <li> <a href="https://twitter.com/arktds/status/1548976622011043840">Xユーザーのしゃろさん: 「&lt;input type="radio"&gt; や &lt;input type="radio"&gt; を独自にスタイリングするために、次の3つを使用していた。 - いわゆる visually hidden なスタイル - 隣接する label/div/span 要素、または、それらの疑似要素 ::before/::after - 隣接セレクタ + と擬似クラス :checked」 / X</a><br> この記事を書くきっかけとなったツイート。返信中に記載された例では、行内での縦位置を揃えるため<code>label</code>要素に<code>display: inline-flex</code>を指定している。 </li> <li> <a href="https://moderncss.dev/pure-css-custom-styled-radio-buttons/" hreflang="en" lang="en">Pure CSS Custom Styled Radio Buttons | Modern CSS Solutions</a><br> ラジオボタンの装飾方法について。強制カラーモードへの対応や<code>:focus-within</code>疑似クラスを用いたラベルのスタイル指定など。 </li> <li> <a href="https://moderncss.dev/pure-css-custom-checkbox-style/" hreflang="en" lang="en">Pure CSS Custom Checkbox Style | Modern CSS Solutions</a><br> チェックボックスの装飾方法について。<code>clip-path</code>プロパティを用いたチェックマークの作成など。 </li> <li> <a href="https://www.scottohara.me/blog/2021/09/24/custom-radio-checkbox-again.html" hreflang="en" lang="en">One last time: custom styling radio buttons and checkboxes | scottohara.me</a><br> チェックボックスとラジオボタンの装飾方法について。双方向書字やダークモードなどでの確認を勧める。 </li> <li> <a href="https://css-tricks.com/custom-styling-form-inputs-with-modern-css-features/" hreflang="en" lang="en">Custom Styling Form Inputs With Modern CSS Features | CSS-Tricks - CSS-Tricks</a><br> チェックボックスとラジオボタンの装飾方法について。トグルスイッチの例など。 </li> </ul> <p>強制カラーモードについて:</p> <ul> <li> <a href="https://nulab.com/ja/blog/nulab/accessibility-and-contrast-themes/">アクセシビリティを高めるための、コントラストテーマの基本対応 | 株式会社ヌーラボ(Nulab inc.)</a><br> WindowsのコントラストテーマとBacklogでの対応事例。 </li> </ul> <template> <style> .customcheckradio-example { border: medium solid #ddd; padding: 0.7em; background-color: #fff; color: #000; } .customcheckradio-example p { margin: 0; } .customcheckradio-example p + p { margin-top: 0.7em; } .customcheckradio-example-1 input[type="checkbox"], .customcheckradio-example-1 input[type="radio"] { accent-color: #d31; } .customcheckradio-example-2 input[type="checkbox"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; border: thin solid; } .customcheckradio-example-2 input[type="checkbox"]::before { content: '✔'; visibility: hidden; } .customcheckradio-example-2 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-3 input[type="checkbox"], .customcheckradio-example-3 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-block; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-3 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-3 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-4 input[type="checkbox"], .customcheckradio-example-4 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-4 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-4 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } .customcheckradio-example-4 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-4 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-4 input[type="radio"]::before { content: ''; border: 0.4em solid; border-radius: 50%; visibility: hidden; } .customcheckradio-example-4 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-5 input[type="checkbox"], .customcheckradio-example-5 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-5 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-5 input[type="checkbox"]::before { content: ''; border: 0.5em solid; clip-path: polygon(0% 60%, 0% 45%, 35% 65%, 100% 0%, 100% 15%, 45% 100%, 30% 100%); visibility: hidden; } .customcheckradio-example-5 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-5 input[type="radio"] { width: 2.4em; height: 2.4em; border-radius: 50%; } .customcheckradio-example-5 input[type="radio"]::before { content: ''; width: 1em; height: 1em; border: 0.2em solid transparent; border-radius: 50%; padding: 0.2em; background: repeating-conic-gradient(from 45deg, #f33 0deg 90deg, #fc6 90deg 180deg) content-box, repeating-conic-gradient(from 45deg, #fc6 0deg 90deg, #f33 90deg 180deg) padding-box, repeating-conic-gradient(from 45deg, #f33 0deg 90deg, #fc6 90deg 180deg) border-box, CanvasText; visibility: hidden; } .customcheckradio-example-5 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-6 input[type="checkbox"], .customcheckradio-example-6 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-6 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-6 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); opacity: 0; transition: opacity 0.5s ease-out; } .customcheckradio-example-6 input[type="checkbox"]:checked::before { opacity: 1; } .customcheckradio-example-6 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-6 input[type="radio"]::before { content: ''; border: 0.4em solid; border-radius: 50%; scale: 0; transition: scale 0.5s ease-out; } .customcheckradio-example-6 input[type="radio"]:checked::before { scale: 1; } .customcheckradio-example-7 input[type="checkbox"], .customcheckradio-example-7 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-7 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-7 input[type="checkbox"]::before { content: ''; border: 0.4em solid transparent; background-color: #d31; visibility: hidden; } .customcheckradio-example-7 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-7 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-7 input[type="radio"]::before { content: ''; width: 0.8em; height: 0.8em; border-radius: 50%; background-color: #d31; outline: 0.4em solid transparent; outline-offset: -0.4em; visibility: hidden; } .customcheckradio-example-7 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-8 input[type="checkbox"], .customcheckradio-example-8 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-8 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-8 input[type="checkbox"]::before { content: ''; width: 0.8em; height: 0.8em; background-color: CanvasText; box-shadow: inset 0 0 0 0.4em #d31; visibility: hidden; } .customcheckradio-example-8 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-8 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-8 input[type="radio"]::before { content: ''; width: 0.8em; height: 0.8em; border-radius: 50%; background: linear-gradient(#d31 0% 100%), CanvasText; visibility: hidden; } .customcheckradio-example-8 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-9 input[type="checkbox"], .customcheckradio-example-9 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-9 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-9 input[type="checkbox"]::before { content: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2010%2010%22%3E%3Cpath%20d%3D%22M%205%2C1%20L%209%2C5%205%2C9%201%2C5%20Z%22%20fill%3D%22%23fda%22%20stroke%3D%22%23f81%22%2F%3E%3C%2Fsvg%3E"); width: 1.2em; height: 1.2em; visibility: hidden; } .customcheckradio-example-9 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-9 input[type="radio"] { display: inline-block; width: 1.8em; height: 1.8em; text-align: center; border-radius: 50%; } .customcheckradio-example-9 input[type="radio"]::before { content: ''; display: block; margin-top: -0.2em; } .customcheckradio-example-9 input[type="radio"]:checked::before { content: '👉'; } .customcheckradio-example-10 input[type="checkbox"], .customcheckradio-example-10 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-10 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-10 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } .customcheckradio-example-10 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-10 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-10 input[type="radio"]::before { content: ''; border: 0.4em solid; border-radius: 50%; visibility: hidden; } .customcheckradio-example-10 input[type="radio"]:checked::before { visibility: visible; } @media (forced-colors: active) { .customcheckradio-example-10 input[type="checkbox"], .customcheckradio-example-10 input[type="checkbox"]::before, .customcheckradio-example-10 input[type="radio"], .customcheckradio-example-10 input[type="radio"]::before { all: revert !important; } } .customcheckradio-example-11 input[type="checkbox"], .customcheckradio-example-11 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-11 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-11 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } .customcheckradio-example-11 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-11 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-11 input[type="radio"]::before { content: ''; border: 0.4em solid; border-radius: 50%; visibility: hidden; } .customcheckradio-example-11 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-11 input[type="checkbox"]:enabled:hover, .customcheckradio-example-11 input[type="radio"]:enabled:hover { box-shadow: 0 0 0.3em 0.1em #d31; } .customcheckradio-example-12 input[type="checkbox"], .customcheckradio-example-12 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-12 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-12 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } .customcheckradio-example-12 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-12 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-12 input[type="radio"]::before { content: ''; border: 0.4em solid; border-radius: 50%; visibility: hidden; } .customcheckradio-example-12 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-12 input[type="checkbox"]:focus-visible, .customcheckradio-example-12 input[type="radio"]:focus-visible { box-shadow: 0 0 0.3em 0.1em #d31; outline: medium solid transparent; } .customcheckradio-example-13 input[type="checkbox"], .customcheckradio-example-13 input[type="radio"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-13 input[type="checkbox"] { border-radius: 0.2em; } .customcheckradio-example-13 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid #d31; border-bottom: 0.2em solid #d31; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } .customcheckradio-example-13 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-13 input[type="radio"] { border-radius: 50%; } .customcheckradio-example-13 input[type="radio"]::before { content: ''; border: 0.4em solid #d31; border-radius: 50%; visibility: hidden; } .customcheckradio-example-13 input[type="radio"]:checked::before { visibility: visible; } .customcheckradio-example-13 input[type="checkbox"]:disabled, .customcheckradio-example-13 input[type="radio"]:disabled { border-style: dashed; filter: grayscale(1); opacity: 0.6; } .customcheckradio-example-14 fieldset { padding: 0.7em; } .customcheckradio-example-14 legend { margin: 0 0.5em; padding: 0 0.5em; } .customcheckradio-example-14 input[type="checkbox"] { -webkit-appearance: none; /* Safari 15.3以下のため */ appearance: none; display: inline-grid; place-content: center; width: 1.4em; height: 1.4em; margin: 0; border-radius: 0.2em; background-color: transparent; color: inherit; font: inherit; vertical-align: middle; border: thin solid; } .customcheckradio-example-14 input[type="checkbox"]::before { content: ''; width: 0.7em; height: 0.3em; border-left: 0.3em solid; border-bottom: 0.2em solid; transform: translateY(-0.1em) rotate(-40deg); visibility: hidden; } .customcheckradio-example-14 input[type="checkbox"]:checked::before { visibility: visible; } .customcheckradio-example-14 input[type="checkbox"]:indeterminate::before { width: 0.5em; height: 0.5em; border: 0.2em dotted; transform: none; visibility: visible; } </style> </template> <script> document.head.appendChild(document.currentScript.previousElementSibling.content.cloneNode(true)); </script> Web 関連技術 Perlのレキシカルサブルーチンとperlcritic http://nanto.asablo.jp/blog/2022/12/21/9549384 http://nanto.asablo.jp/blog/2022/12/21/9549384 Wed, 21 Dec 2022 02:48:02 +0900 2022-12-22T10:07:22+09:00 2022-12-21T02:49:39+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/perl">Perl Advent Calendar 2022</a>の21日目の分です。</p> <hr> <p>Perlでは、関数内で定義した関数も外部から見えてしまいます。</p> <pre><code>use feature 'say'; sub foo { sub bar { say 'bar'; } bar(); } # foo関数の外でもbar関数を呼び出せる。 bar();</code></pre> <p>特定のスコープでのみ参照できる関数を定義したいときは、関数定義を<code>sub</code>ではなく<code>my sub</code>(または<code>state sub</code>)から始めます。この機能は<a href="https://perldoc.perl.org/perlsub#Lexical-Subroutines" hreflang="en">レキシカルサブルーチン(<span lang="en">lexical subroutines</span>)</a>と呼ばれます。</p> <pre><code>use feature 'say'; sub foo { my sub bar { say 'bar'; } bar(); } # 未定義の関数呼び出しによる例外が発生する。 bar();</code></pre> <p>ちょっとした処理をまとめるのに便利なレキシカルサブルーチンですが、<a href="https://metacpan.org/dist/Perl-Critic/view/bin/perlcritic" hreflang="en"><code>perlcritic</code></a>との組み合わせに難がありました。レキシカルサブルーチンを使ったコードを<code>perlcritic</code>にかけると、<a href="https://metacpan.org/pod/Perl::Critic::Policy::Subroutines::ProhibitNestedSubs" hreflang="en"><code>Subroutines::ProhibitNestedSubs</code>ポリシー</a>と<a href="https://metacpan.org/pod/Perl::Critic::Policy::Subroutines::ProhibitBuiltinHomonyms" hreflang="en"><code>Subroutines::ProhibitBuiltinHomonyms</code>ポリシー</a>のエラーが出てしまうのです。(<code>perlcritic</code>はPerl向けのリンターです。詳しくは「<a href="https://blog.utgw.net/entry/2020/12/04/perlcritic">perlcriticとのつきあい方 - 私が歌川です</a>」などを参照してください。)</p> <p><code>Subroutines::ProhibitNestedSubs</code>ポリシーは関数の入れ子を禁止します。入れ子の内側の関数が意図せず外部に公開されるのを避けるためのものなので、もともと外部に公開されないレキシカルサブルーチンに対しては禁止する意味がありません。</p> <p><code>Subroutines::ProhibitBuiltinHomonyms</code>ポリシーは組み込み関数と同名の関数を禁止します。<code>perlcritic</code>の内部で使われるで使われる<a href="https://metacpan.org/pod/PPI" hreflang="en"><code>PPI</code>モジュール</a>の不具合により、レキシカルサブルーチンの名前は常に<code>sub</code>であるとみなされていました。</p> <p>これらの問題を解決するため、昨年<code>perlcritic</code>と<code>PPI</code>に以下のプルリクエストを提出しました。</p> <ul> <li><a href="https://github.com/Perl-Critic/Perl-Critic/pull/971" hreflang="en" lang="en">Allow lexical subroutines to be inside subroutines by nanto · Pull Request #971 · Perl-Critic/Perl-Critic</a></li> <li><a href="https://github.com/Perl-Critic/Perl-Critic/pull/973" hreflang="en" lang="en">Stop improper violation for lexical subroutines in Subroutines::ProhibitBuiltinHomonyms by nanto · Pull Request #973 · Perl-Critic/Perl-Critic</a></li> <li><a href="https://github.com/Perl-Critic/PPI/pull/261" hreflang="en" lang="en">Return correct name for lexical subroutines by nanto · Pull Request #261 · Perl-Critic/PPI</a></li> </ul> <p>この年末までにこれらの変更がすべて取り込まれたので、<code>perlcritic</code>(と<code>PPI</code>)のバージョンを最新にすれば、心置きなくレキシカルサブルーチンを使えます。</p> Perl CSSのconic-gradientで直線的な模様を作る http://nanto.asablo.jp/blog/2022/12/19/9549005 http://nanto.asablo.jp/blog/2022/12/19/9549005 Mon, 19 Dec 2022 11:11:30 +0900 2022-12-19T11:13:05+09:00 2022-12-19T11:13:05+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/cascading_style_sheets">CSS Advent Calendar 2022</a>の19日目の分です。</p> <hr> <p>CSSの<a href="https://developer.mozilla.org/ja/docs/Web/CSS/gradient/conic-gradient"><code>conic-gradient</code>(扇形グラデーション)関数</a>を使うと、円グラフや集中線のような表現ができます。ここであえて扇形の中心角の部分に注目し、直線的な模様を作ってみることはできないでしょうか? いくつか試してみました。</p> <h3>例</h3> <h4>例1. 二重のボーダー</h4> <p><a href="https://jsfiddle.net/2ywdcxmr/">「二重のボーダー」のデモ</a></p> <details> <summary>「二重のボーダー」のCSSコード</summary> <pre><code>p { --outer-border-width: 1em; --inner-border-width: 1em; --outer-border-top-left-color: #1bf; --inner-border-top-left-color: #35b; --inner-border-bottom-right-color: #dc6; --outer-border-bottom-right-color: #3a8; margin: 0; padding: 1em; border: calc(var(--outer-border-width) + var(--inner-border-width)) solid transparent; background: conic-gradient(from 180deg at var(--outer-border-width) var(--outer-border-width), var(--outer-border-top-left-color) 270deg, transparent 270deg) no-repeat border-box 0 0 / calc(100% - var(--outer-border-width)) 100%, conic-gradient(from 180deg at var(--inner-border-width) var(--inner-border-width), var(--inner-border-top-left-color) 270deg, transparent 270deg) no-repeat border-box var(--outer-border-width) var(--outer-border-width) / calc(100% - 2 * var(--outer-border-width) - var(--inner-border-width)) calc(100% - 2 * var(--outer-border-width)), conic-gradient(from 0deg at calc(100% - var(--inner-border-width)) calc(100% - var(--inner-border-width)), var(--inner-border-bottom-right-color) 270deg, transparent 270deg) no-repeat border-box calc(var(--outer-border-width) + var(--inner-border-width)) var(--outer-border-width) / calc(100% - 2 * var(--outer-border-width) - var(--inner-border-width)) calc(100% - 2 * var(--outer-border-width)), conic-gradient(from 0deg at calc(100% - var(--outer-border-width)) calc(100% - var(--outer-border-width)), var(--outer-border-bottom-right-color) 270deg, transparent 270deg) no-repeat border-box var(--outer-border-width) 0 / calc(100% - var(--outer-border-width)) 100%; }</code></pre> </details> <p class="figure"><img src=" https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20221219/20221219105859.png" alt="「二重のボーダー」の画像" width="520" height="310"></p> <h4>例2. 矢印</h4> <p><a href="https://jsfiddle.net/Lmwdtzuh/">「矢印」のデモ</a></p> <details> <summary>「矢印」のCSSコード</summary> <pre><code>p { --arrow-bg-color: #aaa; --arrow-color: #ff5; --arrow-angle: 120deg; --arrow-from-angle: calc(180deg + var(--arrow-angle) / 2); --arrow-outer-angle: calc(360deg - var(--arrow-angle)); --arrow-margin-left: 1em; --arrow-width: 2em; --arrow-center: calc(var(--arrow-width) / 2); --arrow-offset: 1em; margin: 0; padding: 1em; border: solid transparent; border-width: 0 0 0 calc(var(--arrow-margin-left) + var(--arrow-width)); border-radius: 0.5em; background: #ddd no-repeat border-box var(--arrow-margin-left) 0 / var(--arrow-width) 100%; background-image: conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(1 * var(--arrow-offset)), var(--arrow-bg-color) var(--arrow-outer-angle), transparent var(--arrow-outer-angle)), conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(2 * var(--arrow-offset)), var(--arrow-color) var(--arrow-outer-angle), transparent var(--arrow-outer-angle)), conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(3 * var(--arrow-offset)), var(--arrow-bg-color) var(--arrow-outer-angle), transparent var(--arrow-outer-angle)), conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(4 * var(--arrow-offset)), var(--arrow-color) var(--arrow-outer-angle), var(--arrow-bg-color) var(--arrow-outer-angle)); }</code></pre> </details> <p class="figure"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20221219/20221219105903.png" alt="「矢印」の画像" width="520" height="230"></p> <h4>例3. 星形</h4> <p><a href="https://jsfiddle.net/ns1rv26q/">「星形」のデモ</a></p> <details> <summary>「星形」のCSSコード</summary> <pre><code>p { --outer-block-angle: 6deg; --outer-inline-angle: 22deg; --cone-angle: calc(90deg - var(--outer-block-angle) - var(--outer-inline-angle)); --cone-top-left-color: #bde; --cone-top-right-color: #cae; --cone-bottom-right-color: #ecc; --cone-bottom-left-color: #ffb; margin: 0; padding: 2em 3em; background: conic-gradient(from calc(90deg + var(--outer-block-angle)) at 0 0, var(--cone-top-left-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 0 0 / 50% 50%, conic-gradient(from calc(180deg + var(--outer-inline-angle)) at 100% 0, var(--cone-top-right-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 100% 0 / 50% 50%, conic-gradient(from calc(270deg + var(--outer-block-angle)) at 100% 100%, var(--cone-bottom-right-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 100% 100% / 50% 50%, conic-gradient(from var(--outer-inline-angle) at 0 100%, var(--cone-bottom-left-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 0 100% / 50% 50%; }</code></pre> </details> <p class="figure"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20221219/20221219105906.png" alt="「星形」の画像" width="520" height="270"></p> <h4>例4. 吹き出し</h4> <p><a href="https://jsfiddle.net/qr47Lfv0/">「吹き出し」のデモ</a></p> <details> <summary>「吹き出し」のCSSコード</summary> <pre><code>p { --bg-color: #fed; --padding: 1em; --border-width: 0.5em; --border-color: #f93; --border-radius: 1em; --balloon-tail-angle: 60deg; --balloon-tail-from-angle: calc(90deg - var(--balloon-tail-angle) / 2); --balloon-tail-width: 2em; --balloon-tail-height: calc(1.15470054 * var(--balloon-tail-width)); /* 1.15470054 = 2 / sqrt(3) */ --balloon-tail-bottom-offset: 1em; margin: 0 0 0 var(--balloon-tail-width); padding: var(--padding); border: var(--border-width) solid var(--border-color); border-radius: var(--border-radius); background: var(--bg-color); } p::after { content: ''; position: absolute; display: block; width: calc(var(--balloon-tail-width) + var(--border-width)); height: var(--balloon-tail-height); background: conic-gradient(from var(--balloon-tail-from-angle) at calc(2 * var(--border-width)) calc(var(--balloon-tail-height) / 2), var(--bg-color) var(--balloon-tail-angle), transparent var(--balloon-tail-angle)) no-repeat, conic-gradient(from var(--balloon-tail-from-angle) at 0 calc(var(--balloon-tail-height) / 2), var(--border-color) var(--balloon-tail-angle), transparent var(--balloon-tail-angle)) no-repeat; transform: translate(calc(0em - var(--padding) - var(--balloon-tail-width) - var(--border-width)), calc(var(--padding) + var(--border-width) - var(--border-radius) - var(--balloon-tail-height) - var(--balloon-tail-bottom-offset))); }</code></pre> </details> <p class="figure"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20221219/20221219105910.png" alt="「吹き出し」の画像" width="520" height="250"></p> <h3>感想</h3> <p>やってはみたもののどうも微妙です。直線的な模様ならだいたいは<a href="https://developer.mozilla.org/ja/docs/Web/CSS/gradient/linear-gradient"><code>linear-gradient</code>関数</a>で表現できますし、要素を切り抜くのなら<a href="https://developer.mozilla.org/ja/docs/Web/CSS/clip-path"><code>clip-path</code>プロパティ</a>のほうが自由度が高いです。<code>conic-gradient</code>関数を使えば記述量を減らせるかというと、そういうこともそんなになく、やはり目的外の利用はあまりうまくいかないのかもしれません。</p> <p>今回ひとつ得られた教訓は、背景画像を使って疑似的にボーダーを表現する場合、疑似的なボーダーと同じ大きさの透明なボーダー(<code>border: &lt;border-wdith&gt; solid transparent</code>)を指定したほうがよいということです。そうすることで、<code>overflow: auto</code>で内容がはみ出したときなどにスクロール領域が疑似的なボーダーにかからず、より自然に見えます。</p> Web 関連技術 CSSの絶対配置の要素の静的位置矩形 http://nanto.asablo.jp/blog/2022/12/11/9547196 http://nanto.asablo.jp/blog/2022/12/11/9547196 Sun, 11 Dec 2022 12:52:51 +0900 2022-12-11T12:55:08+09:00 2022-12-11T12:55:08+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/cascading_style_sheets">CSS Advent Calendar 2022</a>の11日目の分です。</p> <hr> <p>CSSで<code>positoin: absolute</code>(絶対配置)の要素の位置を指定するときには、<code>top</code>、<code>left</code>、<code>right</code>、<code>bottom</code>プロパティ(またはこれらを一括指定する<code>inset</code>プロパティ)がよく使われます。もし絶対配置の要素にそれらのプロパティが指定されていなかったら、その要素の位置はどこになるでしょうか?</p> <p>その場合、絶対配置の要素は原則として「その要素が<code>positoin: static</code>(静的配置)だった場合の位置(静的位置矩形; <span lang="en">static-position rectangle</span>)」に置かれます。絶対配置の要素の親要素がインライン要素だった場合、絶対配置の要素自身が<code>display: inline</code>(インライン要素)か<code>display: block</code>(ブロック要素)かによって位置が異なってくることになります(<a href="https://jsfiddle.net/14kvjd9b/">インライン要素の子要素が絶対配置のときのデモ</a>)。</p> <p>この挙動をうまく利用すれば、絶対配置の要素の親要素に<code>position: relative</code>をつけて回らなくても、絶対配置の要素を期待する位置に置けることがあります。その場合、細かな位置の調整に<code>inset</code>プロパティなどを使うことはできないので、<code>transform: translate(...)</code>や<code>margin</code>プロパティを使うことになります。</p> <h3>フレックスアイテムが絶対配置のとき</h3> <p>「原則として」というからには例外もあります。絶対配置のフレックスアイテムで<code>inset</code>プロパティなどが指定されていないものは、フレックスコンテナの位置に置かれます。</p> <p><small>(「フレックスコンテナ」は<code>display: flex</code>または<code>display: inline-flex</code>が指定された要素、「フレックスアイテム」はフレックスコンテナの子要素です。)</small></p> <h3>グリッドアイテムが絶対配置のとき</h3> <p>グリッドアイテムが絶対配置のときはちょっと複雑です。グリッドコンテナが静的配置なら、絶対配置のグリッドアイテムで<code>inset</code>プロパティなどが指定されていないものは、グリッドコンテナの位置に置かれます。</p> <p>グリッドコンテナが静的配置でないなら、絶対配置のグリッドアイテムで<code>inset</code>プロパティが指定されていないものは、<code>grid</code>プロパティなどで指定されたグリッド領域の位置に置かれます(<a href="https://jsfiddle.net/h30tzwmk/">グリッドアイテムが絶対配置のときのデモ</a>)。</p> <p><small>(「グリッドコンテナ」は<code>display: grid</code>または<code>display: inline-grid</code>が指定された要素、「グリッドアイテム」はグリッドコンテナの子要素です。)</small></p> <p>とはいえ、フレックスボックスやグリッドと絶対配置を組み合わせるとCSSのコードがだいぶ複雑になるので、普段は避けたほうがよいと思います。</p> Web 関連技術 PerlのText::Markdown::Discountで囲い付きコードブロックを扱う http://nanto.asablo.jp/blog/2022/12/11/9547105 http://nanto.asablo.jp/blog/2022/12/11/9547105 Sun, 11 Dec 2022 01:31:47 +0900 2022-12-11T01:49:44+09:00 2022-12-11T01:49:44+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/perl">Perl Advent Calendar 2022</a>の11日目の分です。</p> <hr> <p>Perlの<a href="https://metacpan.org/pod/Text::Markdown::Discount" hreflang="en"><code>Text::Markdown::Discount</code>モジュール</a>を使うと、MarkdownをHTMLに変換できます。</p> <pre><code>use feature qw(say); use Encode qw(encode_utf8); use Text::Markdown::Discount qw(markdown); say encode_utf8 markdown(&lt;&lt;'MARKDOWN'); こんにちは、世界。 * 順序 * なし * リスト MARKDOWN</code></pre> <pre><code>&lt;p&gt;こんにちは、世界。&lt;/p&gt; &lt;ul&gt; &lt;li&gt;順序&lt;/li&gt; &lt;li&gt;なし&lt;/li&gt; &lt;li&gt;リスト&lt;/li&gt; &lt;/ul&gt;</code></pre> <p><a href="https://daringfireball.net/projects/markdown/syntax" hreflang="en">オリジナルのMarkdown</a>にはありませんが、<a href="https://commonmark.org/" hreflang="en">CommonMark</a>およびその拡張である<a href="https://github.github.com/gfm/" hreflang="en">GitHub Flavored Markdown</a>などには囲い付きコードブロック(<span lang="en">fenced code block</span>)が存在します。3つ以上のバッククォート(<code>`</code>)の並びまたはチルダ(<code>~</code>)の並びで囲んだ部分が、HTMLの<code>pre</code>要素と<code>code</code>要素を使って出力されるというものです。</p> <p><code>Text::Markdown::Discount</code>モジュール(の内部で使われている<a href="https://www.pell.portland.or.us/~orc/Code/markdown/" hreflang="en">DiscountというMarkdown処理系</a>)でも囲い付きコードブロックに対応していますが、扱い方がバージョンによって異なります。</p> <h3><code>Text::Markdown::Discount</code> 0.14以降</h3> <p>囲い付きコードブロックを有効にするには、<code>markdown</code>関数の第2引数に<code>MKD_FENCEDCODE</code>フラグ(<code>0x02000000</code>)を指定します。</p> <pre><code>my $html = markdown(&lt;&lt;'MARKDOWN', Text::Markdown::Discount::MKD_NOHEADER | Text::Markdown::Discount::MKD_NOPANTS | 0x02000000); ``` fenced code block ``` MARKDOWN</code></pre> <pre><code>&lt;pre&gt;&lt;code&gt;fenced code block &lt;/code&gt;&lt;/pre&gt;</code></pre> <p><code>MKD_FENCEDCODE</code>フラグはDiscount処理系で定義されているものの、<code>Text::Markdown::Discount</code>モジュールでは定数が定義されていていないため、フラグの値である<code>0x02000000</code>を直接指定しています。(<a href="https://github.com/sekimura/text-markdown-discount/pull/32" hreflang="en">定数を定義するpull request</a>が提出されています。)</p> <p><code>MKD_NOHEADER</code>フラグおよび<code>MKD_NOPANTS</code>フラグは<code>markdown</code>関数の第2引数を省略したときにデフォルトで適用されるフラグなので、元の挙動を変えずに別のフラグを追加する際には、このふたつのフラグも明示的に指定する必要があります。</p> <p><code>MKD_FENCEDCODE</code>フラグを指定しない場合、3つ以上のバッククォートの並びはインラインのコード範囲として解釈されます。</p> <pre><code>my $html = markdown(&lt;&lt;'MARKDOWN'); ``` fenced code block ``` MARKDOWN</code></pre> <pre><code>&lt;p&gt;&lt;code&gt; fenced code block &lt;/code&gt;&lt;/p&gt;</code></pre> <h3><code>Text::Markdown::Discount</code> 0.13</h3> <p>特にフラグを指定しなくても、囲い付きコードブロックが有効になっています。</p> <pre><code>my $html = markdown(&lt;&lt;'MARKDOWN'); ``` fenced code block ``` MARKDOWN</code></pre> <pre><code>&lt;pre&gt;&lt;code&gt;fenced code block &lt;/code&gt;&lt;/pre&gt;</code></pre> <h3><code>Text::Markdown::Discount</code> 0.12以前</h3> <p>囲い付きコードブロックに対応していません。3つ以上のバッククォートの並びはインラインのコード範囲として解釈されます。</p> Perl CSSでスクロールバーの有無によるがたつきをなくす http://nanto.asablo.jp/blog/2022/12/09/9546683 http://nanto.asablo.jp/blog/2022/12/09/9546683 Fri, 09 Dec 2022 09:42:57 +0900 2022-12-09T10:41:52+09:00 2022-12-09T09:44:03+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/cascading_style_sheets">CSS Advent Calendar 2022</a>の9日目の分です。</p> <hr> <p>CSSのボックスモデルにおいては、ボーダーの内辺とパディングの外辺の間にスクロールバーが配置されます。最近はどのOSでもオーバーレイスクロールバー(スクロールバーが内容の前面に覆いかぶさるようなもの)が主流となり、スクロールバーが存在してもしなくても内容の幅が変わらないようになっています。一方、クラシックスクロールバー(スクロールバーが常に表示されるようなもの)が使われる環境では、<code>overflow: auto</code>な要素において内容がはみ出すときとはみ出さないときで内容の幅が変わってきます。</p> <p>クラシックスクロールバーが使われる環境でも内容の幅を一定に保ちたいという場合は、<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter" hreflang="en"><code>scrollbar-gutter</code>プロパティ</a>を使います。<code>scrollbar-gutter: stable</code>を指定すれば、スクロールバーが表示されないときでもスクロールバーと同じだけの領域が確保され、内容の幅はその分狭くなります。左右中央に配置したいのにスクロールバーの領域の分だけずれて困るというときは、<code>scrollbar-gutter: stable both-edges</code>を指定することで、左右どちらにもスクロールバーと同じだけの領域が確保されます。</p> <figure> <figcaption><a href="https://jsfiddle.net/n8xojLp0/"><code>scrollbar-gutter</code>プロパティの各値を指定した例</a></figcaption> <p class="figure"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20221209/20221209093615_original.png" alt="scrollbar-gutterプロパティを指定した効果の図" width="846" height="381"></p> <p><small>(上図はWindows 10 Firefox nightly 109.0a1での表示)</small></p> </figure> <p><code>scrollbar-gutter</code>プロパティは<code>overflow: hidden</code>な要素にも適用されますが、<code>overflow: visible</code>な要素には適用されません。また、<code>scrollbar-gutter</code>プロパティの効果は、縦スクロールバー(縦書きなら横スクロールバー)にのみ影響します。</p> <p>オーバーレイスクロールバーが使われる環境では、<code>scrollbar-gutter: stable</code>の効果はありません。</p> <h3>使用事例</h3> <p>使用事例としては、<a href="https://twitter.com/about_hiroppy/status/1599381695392002048">@about_hiroppyさんの紹介している「HTMLの<code>dialog</code>要素によるモーダルダイアログが開いているときに、背景の文書をスクロールさせない」</a>というのが考えられます(<a href="https://jsfiddle.net/nt76gprk/"><code>scrollbar-gutter</code>プロパティを指定した例</a>と<a href="https://jsfiddle.net/v1jfsuqa/">指定しなかった例</a>)。</p> <pre><code>html { scrollbar-gutter: stable; } html:has(dialog:modal) { overflow: hidden; }</code></pre> <p>クラシックスクロールバーが使われる環境では、文書をスクロールさせないために<code>overflow: hidden</code>を指定すると、スクロールバーが消えて文書の内容ががたついてしまいます。<code>scrollbar-gutter: stable</code>をあらかじめ指定しておけば、スクロールバーが消えてもその分の領域が確保され続けるので、文書の内容ががたつきません。</p> Web 関連技術 Perlで配列の先頭何要素か以外を抜き出す http://nanto.asablo.jp/blog/2022/12/05/9545777 http://nanto.asablo.jp/blog/2022/12/05/9545777 Mon, 05 Dec 2022 01:47:17 +0900 2022-12-05T01:49:22+09:00 2022-12-05T01:49:22+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/perl">Perl Advent Calendar 2022</a>の5日目の分です。</p> <hr> <p>Perlで配列の先頭n要素以外を抜き出したい——例えば配列<code>('a', 'b', 'c', 'd', 'e')</code>から先頭2要素以外を抜き出して配列<code>('c', 'd', 'e')</code>を得たい——とき、最近は<a href="https://metacpan.org/pod/List::Util" hreflang="en"><code>List::Util</code>モジュール</a>の<a href="https://metacpan.org/pod/List::Util#tail" hreflang="en"><code>tail</code>関数</a>を使えます。</p> <p><code>tail</code>関数は配列の末尾n要素を抜き出す関数ですが、抜き出す要素数として負数-mを指定すると、先頭m要素以外の要素を返します。</p> <pre><code>use List::Util qw(tail); my @array1 = qw(a b c d e); my @array2 = tail -2, @array1; # @array2の内容は('c', 'd', 'e')</code></pre> <p><code>List::Util</code>モジュールはコアモジュール(Perl本体と一緒にインストールされるモジュール)であり、Perl 5.28以降なら追加のモジュールインストールなしに<code>tail</code>関数を使えます。それより古いPerlでは、<code>List::Util</code>の新しいバージョン(1.50以降)をインストールする必要があります。</p> <p>以前からある方法として、配列スライスを使うこともできます。</p> <pre><code>my @array1 = qw(a b c d e); my @array2 = @array1[2 .. $#array1]; # @array2の内容は('c', 'd', 'e')</code></pre> Perl TypeScriptでイベントをPromise化する関数の型を定義したい http://nanto.asablo.jp/blog/2022/12/03/9545391 http://nanto.asablo.jp/blog/2022/12/03/9545391 Sat, 03 Dec 2022 10:41:33 +0900 2022-12-03T10:45:04+09:00 2022-12-03T10:45:04+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/typescript">TypeScript Advent Calendar 2022</a>の3日目の分です。</p> <hr> <p>「<a href="https://zenn.dev/reactance/articles/d805b61fd65177">addEventListenerでリッスンしているイベントをPromise化する</a>」という記事で、イベントを<code>Promise</code>で受け取る関数が紹介されています。Node.jsの<code>events</code>モジュールの<a href="https://nodejs.org/api/events.html#eventsonceemitter-name-options" hreflang="en"><code>events.once</code>メソッド</a>と同じ機能を実現するものですね。<a href="https://github.com/whatwg/dom/issues/1038" hreflang="en">Webブラウザ組み込みのDOMでも同じ機能を提供しようという提案</a>もなされています。</p> <p>最初の記事では余談として<q cite="https://zenn.dev/reactance/articles/d805b61fd65177#%E4%BD%99%E8%AB%87">今回紹介したeventPromisifyはTypeScriptで書こうとすると型の定義が難しいなと思いました</q>と書かれています。例えば<code>eventPromisify(document, 'click')</code>と呼び出したら返り値の型が<code>Promise&lt;MouseEvent&gt;</code>になってほしいのですが、そのような型定義を記述できるでしょうか? (以下、TypeScript 4.9を想定しています。)</p> <p>イベントが発生する対象(<code>target</code>)とイベント名(<code>type</code>)を決め打ちできるのなら、</p> <pre><code>type EventForDocumentClick = typeof document.addEventListener&lt;'click'&gt; extends (type: 'click', listener: (event: infer E) =&gt; void) =&gt; void ? E : never; // = MouseEvent</code></pre> <p>のように、<code>document</code>と<code>'click'</code>から<code>MouseEvent</code>を導出できます。しかし、変数<code>document</code>ではなく<code>Document</code>型だけが与えられているとき、<code>Document['addEventListener']&lt;'click'&gt;</code>のように型引数を指定することはできません。</p> <p>また<code>Document</code>型における<code>addEventListener</code>メソッドの定義は、イベント名が<code>Document</code>固有(<code>keyof DocumentEventMap</code>型)のものとイベント名が文字列全般(<code>string</code>型)のものがオーバーロードされています。型引数が絡んでいなければ、「<a href="https://zenn.dev/uhyo/articles/typescript-overload-infer">オーバーロードされた関数型から引数の型や返り値の型を取り出す方法</a>」に書かれているように型を取り出せます。しかし、型引数を持つメソッドがオーバーロードされているときに、意図した型引数が指定された場合の引数や返り値の型を取り出せるのかどうか、私にはわかりませんでした。</p> <p>結局私にできたのは以下の状態までです(<a href="https://www.typescriptlang.org/play?#code/C4TwDgpgBAogbhAdsAYgewE4B4AqBDDAcwmCggA9gkATAZ1gWXyJID4oBeKZ44AbQDkeatXhJgAGQCWtKoggYBAXTKUa9AN4AoKFAAUoSAC4osjFMSEANFAA2MuQpN6IJiwDMFsAJSd2cNClqbxMAoIBuHX1DV1Ngc0sbe1kkJwZxaRT5DAB5DDFkTMdcgCMAKwgAY2AQqDDqSIBfKAB+WCgTeQQMSK0Y9KZwCHRsHhJVOToB4DHgdi5ZwWFRRkkHVMUVCknNKIMht0RPDG4k9eznWIKavzrA4ND7yN194ziE6ztztOuijbzft9ShVqrV6k1WtwOlAugpepU0IhZGRVgAFDBoAC2Mik7hAnCguAm6mms1YeiiwAIvBMOCslIOpKGI1wrC0tXRWJkECw1xZOFY83Y2l0wAAFhiAO4wiDSmAYDEYPQCAByaFIUkxYFsEEx4gg1AE3iaWi0CKRpDABIgaIx2NouJAemoaEqAFc9cgbAJKvZKgBrI2RAD0wagYBMnPtPOuUAAPlAALJoN20CCxhMAVQAkhmoABhexgEpoAgrcTxqAAQUQmrwwCkiLz2cQYDdwDz6HdtDzADp+1AAIyDqCYzDQfu9ysAdTFEAgtmubKAA" hreflang="en">TypeScript Playgroundで確認</a>)。</p> <pre><code>type EventTypeFor&lt;Target extends EventTarget&gt; = Target['addEventListener'] extends { (type: infer T, listener: (e: Event) =&gt; void): void; (type: string, listener: EventListenerOrEventListenerObject): void; } ? T : never; type EventFor&lt;Target extends EventTarget&gt; = Target['addEventListener'] extends { (type: string, listener: (e: infer E) =&gt; void): void; (type: string, listener: EventListenerOrEventListenerObject): void; } ? E : never; const eventPromisify = &lt;T extends EventTarget&gt;( target: T, type: EventTypeFor&lt;T&gt; ): Promise&lt;EventFor&lt;T&gt;&gt; =&gt; { throw new Error('Not implemented'); } const p = eventPromisify(document, 'click'); // p: Promise&lt;Event | MouseEvent | UIEvent | ClipboardEvent | AnimationEvent | InputEvent | FocusEvent | ... 11 more ... | WheelEvent&gt;</code></pre> <p>第2引数を<code>Document</code>固有のイベント名に限定することはできています。しかしながら、返り値の<code>Promise</code>の値の型を<code>MouseEvent</code>に限定することはできず、<code>Document</code>固有のイベントすべての共用体型となってしまいます。</p> JavaScript Perlで配列の先頭何要素かを抜き出す http://nanto.asablo.jp/blog/2022/12/02/9545120 http://nanto.asablo.jp/blog/2022/12/02/9545120 Fri, 02 Dec 2022 02:16:35 +0900 2022-12-02T02:18:36+09:00 2022-12-02T02:18:36+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/perl">Perl Advent Calendar 2022</a>の2日目の分です。</p> <hr> <p>Perlで配列の先頭n要素を抜き出したいとき、最近は<a href="https://metacpan.org/pod/List::Util" hreflang="en"><code>List::Util</code>モジュール</a>の<a href="https://metacpan.org/pod/List::Util#head" hreflang="en"><code>head</code>関数</a>を使えます。</p> <pre><code>use List::Util qw(head); my @array1 = qw(a b c d e); my @array2 = head 3, @array1; # @array2の内容は('a', 'b', 'c')</code></pre> <p><code>List::Util</code>モジュールはコアモジュール(Perl本体と一緒にインストールされるモジュール)であり、Perl 5.28以降なら追加のモジュールインストールなしに<code>head</code>関数を使えます。それより古いPerlでは、<code>List::Util</code>の新しいバージョン(1.50以降)をインストールする必要があります。</p> <h3>以前からある方法</h3> <p>配列スライスを使うこともできますが、抜き出す要素数から1引いた値を指定することになって、ちょっと紛らわしいです。</p> <pre><code>my @array1 = qw(a b c d e); my @array2 = @array1[0 .. 2]; # @array2の内容は('a', 'b', 'c')</code></pre> <p>また、元の配列の要素数が抜き出す要素数より少ないときは、不足分が<code>undef</code>で埋められてしまいます。</p> <pre><code>my @array1 = qw(a); my @array2 = @array1[0 .. 2]; # @array2の内容は('a', undef, undef)</code></pre> <p><code>undef</code>で埋められたくなければ、<code>min</code>関数を使うなどひと工夫する必要があります。</p> <pre><code>use List::Util qw(min); my @array1 = qw(a); my @array2 = @array1[0 .. min(2, $#array1)]; # @array2の内容は('a')</code></pre> <p><code>splice</code>関数を使うこともできますが、元の配列も変更されてしまいます。</p> <pre><code>my @array1 = qw(a b c d e); my @array2 = splice @array1, 0, 3; # @array1の内容は('d', 'e') # @array2の内容は('a', 'b', 'c')</code></pre> Perl CSSでモーダルダイアログの背景をスクロールさせないようにできるかもしれない http://nanto.asablo.jp/blog/2022/12/01/9544904 http://nanto.asablo.jp/blog/2022/12/01/9544904 Thu, 01 Dec 2022 01:41:10 +0900 2022-12-09T11:06:39+09:00 2022-12-01T01:44:48+09:00 <p>この記事は<a href="https://qiita.com/advent-calendar/2022/cascading_style_sheets">CSS Advent Calendar 2022</a>の1日目の分です。</p> <hr> <p>HTMLの<a href="https://developer.mozilla.org/ja/docs/Web/HTML/Element/dialog"><code>dialog</code>要素</a>を使うとモーダルダイアログを表現できます(使い方によってはモードレスダイアログも表現できます)。ただし、そのままだとモーダルダイアログを開いているときに、マウスホイールなどによってダイアログの背景(文書全体)までスクロールしてしまいます。</p> <p>モーダルダイアログの背景をスクロールさせたくない場合、これを書いている現在のCSS仕様草案によれば、以下の記述で実現できるはずです(<a href="https://jsfiddle.net/6yv230xp/">デモ</a>)。</p> <pre><code>dialog { overscroll-behavior: contain; }</code></pre> <p>しかしながら、この方法はChrome canary 110では<del datetime="2022-12-02T13:40:00+09:00">期待通り動作しますが</del><ins datetime="2022-12-02T13:40:00+09:00">マウスホイールによるスクロールは防げますが、矢印キーやPageUp/PageDownキーによるスクロールは防げず</ins>、Firefox nightly 109では動作しません。</p> <p>このあたりの事情はちょっと複雑で、</p> <ul> <li>CSS仕様草案によれば、<code>overflow: auto</code>な要素においてはスクロール可能な領域がないときも<code>overscroll-behavior</code>プロパティが適用されるはず。 <ul> <li><a href="https://drafts.csswg.org/css-overscroll-1/" hreflang="en">CSS Overscroll Behaviorモジュール草案</a>によれば、スクロールコンテナに対して<code>overscroll-behavior</code>プロパティが適用される。</li> <li><a href="https://drafts.csswg.org/css-overflow-3/" hreflang="en">CSS Overflowモジュール草案</a>によれば、<code>overflow: auto</code>な要素(および<code>overflow: hidden</code>な要素)はスクロール可能な領域がないときもスクロールコンテナである。</li> </ul> </li> <li>各ブラウザ(少なくとも<a href="https://bugs.chromium.org/p/chromium/issues/detail?id=759209" hreflang="en">Chromeの場合</a>と<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1724358" hreflang="en">Firefoxの場合</a>)は、あえて仕様草案を無視し、スクロール可能な領域がないときに<code>overscroll-behavior</code>プロパティを適用しない。 <ul> <li>しかし、Chromeはなぜか、<code>dialog</code>要素においてはスクロール可能な領域がないときも<ins datetime="2022-12-02T13:40:00+09:00">マウスホイールによるスクロールに関しては</ins><code>overscroll-behavior</code>プロパティを適用する模様(そうなっている経緯は未調査)。</li> </ul> </li> <li>CSS仕様草案に対して、<a href="https://github.com/w3c/csswg-drafts/issues/6523" hreflang="en">スクロール可能な領域がないときは<code>overscroll-behavior</code>プロパティが適用されないようにしよう(仕様側を現状のブラウザ実装に合わせよう)という提案</a>がなされている。 <ul> <li><ins datetime="2022-12-02T13:40:00+09:00"><a href="https://github.com/whatwg/html/issues/7732" hreflang="en">HTML標準に対して、モーダルダイアログの背景のスクロールを防止しようという提案</a>があると<a href="https://github.com/w3c/csswg-drafts/issues/6523#issuecomment-1334519687" hreflang="en">教えてもらいました</a>。</ins></li> </ul> </li> </ul> <p>となっています。上述のCSSコードが将来的にも機能するかどうかは不透明です。</p> <p><ins datetime="2022-12-09T11:10:00+09:00"><a href="https://nanto.asablo.jp/blog/2022/12/09/9546683"><code>overflow: hidden</code>を使ってスクロールを防止する方法でも、<code>:has</code>疑似クラスと組み合わせることで、CSSのみでモーダルダイアログの背景のスクロールを防止できるそうです</a>。</ins></p> Web 関連技術 HTMLのa要素にはhref属性を指定しなくてもよい http://nanto.asablo.jp/blog/2022/10/20/9534734 http://nanto.asablo.jp/blog/2022/10/20/9534734 Thu, 20 Oct 2022 23:30:22 +0900 2022-10-20T23:31:56+09:00 2022-10-20T23:31:56+09:00 <p>HTMLの<code>a</code>要素はハイパーリンクを表す要素であり、リンク先のURLを<code>href</code>属性に指定します。しかし、<code>a</code>要素の役割はそれだけではありません。HTML標準によれば、<code>a</code>要素は「リンクとなりうる箇所のプレースホルダー」として使うこともできます。この場合は<code>href</code>属性を指定しません。</p> <p>リンクとなりうる箇所の例として、ナビゲーションやタブUI、パンくずリストなどでの「現在の項目」があります。</p> <pre><code>&lt;nav&gt; &lt;ul&gt; &lt;li&gt;&lt;a href="/"&gt;ホーム&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a&gt;最新記事&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="/archives"&gt;アーカイブ&lt;/a&gt;&lt;/li&gt; &lt;li&gt;&lt;a href="/settings"&gt;設定&lt;/a&gt;&lt;/li&gt; &lt;/ul&gt; &lt;/nav&gt;</code></pre> <p>ReactなどJSXで<code>a</code>要素を生成する場合、<code>href</code>属性を指定しないためには<code>href</code>プロパティに<code>undefined</code>を指定します。</p> <pre><code>import React from "react"; type Item = { label: string; url: string; isCurrent: boolean; }; type Props = { items: readonly Item[]; }; const Navigation: React.FC&lt;Props&gt; = ({ items }) =&gt; ( &lt;nav&gt; &lt;ul&gt; {items.map((item) =&gt; ( &lt;li&gt; &lt;a href={item.isCurrent ? undefined : item.url}&gt; {item.label} &lt;/a&gt; &lt;/li&gt; ))} &lt;/ul&gt; &lt;/nav&gt; );</code></pre> <p>リンクのプレースホルダーとしての<code>a</code>要素は、うまく使えばテンプレートやCSSの記述を簡潔にできます。覚えておいて損はないでしょう。</p> <p>なお、<code>a</code>要素に<code>href</code>属性を指定しないと聞いて<code>name</code>属性を指定するのかと思った人もいるでしょうが、現在のHTML標準では<code>a</code>要素の<code>name</code>属性は廃止済みであり指定すべきでないとされています。</p> JavaScript Web 関連技術 私とIEとフィードバック(IE卒業式) http://nanto.asablo.jp/blog/2022/06/19/9501405 http://nanto.asablo.jp/blog/2022/06/19/9501405 Sun, 19 Jun 2022 12:28:08 +0900 2022-06-19T12:36:34+09:00 2022-06-19T12:30:11+09:00 <p>2022年6月16日に開催された「<a href="https://web-study.connpass.com/event/250191/">IE卒業式</a>」というイベントで、「私とIEとフィードバック」という発表(5分間のライトニングトーク)をしてきました。以下に話した内容を掲載します。</p> <hr> <header> <h3>私とIEとフィードバック</h3> <div class="right-side"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220619/20220619120141_original.jpg" alt="" width="960" height="540" style="width: 320px; max-width: 100%; height: auto;"></div> <p>2022-06-16</p> <p>nanto_vi (株式会社はてな)</p> </header> <section> <h4>Web開発者によるフィードバック</h4> <div class="right-side"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220619/20220619120340_original.jpg" alt="" width="960" height="540" style="width: 320px; max-width: 100%; height: auto;"></div> <ul> <li>標準準拠</li> <li>相互運用性の向上</li> </ul> <p>➜ より開発しやすく</p> <div class="quote"> <blockquote> <p>ブラウザベンダが不具合に気づくことを期待するという受身の立場から、積極的に熱心でいる(自らバグを報告する)ことへの移行は、信じられないほど多くの力をあなたにもたらします。</p> </blockquote> <p class="cite"><cite><a href="https://nanto.asablo.jp/blog/2009/05/06/4289222">John Resig</a></cite></p> </div> <p>[発話] ソフトウェアを作るうえでも使う上でもフィードバックは重要ですね。Webブラウザの場合、Web開発者からのフィードバックによって標準準拠の度合いが進んだり、ブラウザ間の相互運用性が向上したりして、Web開発者にとってはより開発しやすくなります。John Resigさん——jQueryを作った方です——も、バグが直るのを待つのではなく自らバグを報告するとめっちゃええことあるというようなことをおっしゃっています。</p> </section> <section> <h4>個人開発者によるIEへのフィードバック手段</h4> <div class="right-side"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220619/20220619120458_original.jpg" alt="" width="960" height="540" style="width: 320px; max-width: 100%; height: auto;"></div> <ul> <li>IE 7以前: 主にニュースグループ <ul> <li><a href="https://groups.google.com/search/groups?q=microsoft.public.windows.inetexplorer">microsoft.public.windows.inetexplorer.*</a></li> </ul> </li> <li>IE 7以降: 主にMicrosoft Connect <ul> <li>バグトラッキングシステム</li> </ul> </li> </ul> <div class="quote" lang="en"> <blockquote> <p>go to http://131.107.85.110/msdn/bugreports/ to report issues</p> </blockquote> <p class="cite"><cite><a href="https://web.archive.org/web/20000302042733/http://msdn.microsoft.com/downloads/webtechnology/ie/iepreview.asp">Internet Explorer 5.5 Preview</a></cite></p> </div> <p>[発話] 個人がIEにフィードバックする手段は、IE 7の前後で大きく変わっています。IE 7以前はニュースグループ——掲示板やメーリングリストのようなものです——が中心でした。IE 7以降はMicrosoft Connectというサービスを使うようになっています。Microsoft Connectにはバグトラッキングシステムが備わっており、他人の登録したバグを検索したり、自分の登録したバグの状態を知れたりと、バグ報告者からすると使いやすくなっています。ちなみに、IE 5.5 Previewのときにはバグ報告用のURLがIPアドレス丸出しで、のどかな時代だったんだなというのを感じさせます。</p> </section> <section> <h4>IE 8 Betaへのフィードバック</h4> <div class="right-side"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220619/20220619120547_original.jpg" alt="" width="960" height="540" style="width: 320px; max-width: 100%; height: auto;"></div> <div class="quote"> <blockquote> <p>開発者の支持のないブラウザは廃れるとの思いで、標準準拠路線を進めてきた</p> </blockquote> <p class="cite"><cite><a href="http://itpro.admintech.jp/wiki/wiki.cgi?page=%C2%E813%B2%F3+Admintech%2Ejp%CA%D9%B6%AF%B2%F1">「ここが変わった! IE8 Beta2」(第13回 Admintech.jp勉強会)</a></cite></p> </div> <ul> <li>筆者もフィードバックに参加 <ul> <li>Web Storageがオリジンごとではなくドメインごと</li> <li>Web Storageのイベントが同期的</li> <li>文書間メッセージング(postMessage)のイベントが同期的</li> </ul> </li> </ul> <p>[発話] 私がIEにフィードバックするようになったのはIE 8 Betaが出たあたりからです。このころはFirefoxとSafariがIEを追い上げ、GoogleがChromeを発表し、Microsoftとしては非常に危機感を持っていたのではないかと思います。実際に、当時あったイベントでMicrosoftの方が、開発者の支持のないブラウザは廃れるとの思いで標準準拠路線を進めてきたというようなことをおっしゃっていました。このころはWeb Storage APIや文書間メッセージングが新機能としてもてはやされ、私も標準仕様とIEの実装との差異をいくつか報告しています。Web Storage API——localStorageなど——の範囲がオリジンではなくドメインである、すなわちhttpsのページで書き込んだデータをhttpのページで読み込めるというったことがありました。</p> </section> <section> <h4>フィードバックの返礼品</h4> <div class="right-side"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220619/20220619120620_original.jpg" alt="" width="960" height="540" style="width: 320px; max-width: 100%; height: auto;"></div> <ul> <li>IE 8の工具セット</li> <li>FirefoxとThunderbirdの傘</li> </ul> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220615/20220615215827_original.jpg" alt="" width="576" height="768" style="max-width: 100%; height: auto;"></p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220615/20220615221002_original.jpg" alt="" width="768" height="576" style="max-width: 100%; height: auto;"></p> <p>[発話] そうしたバグ報告をしているとMicrosoftからメールが来て、グッズを送るから住所を登録してくれと言われました。何かしらと思って住所を入力すると後日IE 8の工具セットが届きました。これでIEを直せということでしょうかね。こうしたノベルティの贈呈はMicrosoft以外のブラウザベンダもやっており、MozillaからはFirefoxとThunderbirdの傘をもらったことがあります。</p> </section> <section> <h4>フィードバックしよう!</h4> <div class="right-side"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nanto_vi/20220619/20220619120645_original.jpg" alt="" width="960" height="540" style="width: 320px; max-width: 100%; height: auto;"></div> <ul> <li>個別のブラウザ <ul> <li><a href="https://bugs.chromium.org/p/chromium/issues/list">Chromium issues</a></li> <li><a href="https://bugs.webkit.org/">WebKit Bugzilla</a></li> <li><a href="https://bugzilla.mozilla.org/">Mozilla Bugzilla</a></li> </ul> </li> <li>ブラウザ間の相互運用性 <ul> <li><a href="https://web-platform-tests.org/">web-platform-tests</a></li> </ul> </li> </ul> <p>[発話] 皆さんもどんどんフィードバックしていきましょう。個別のブラウザにフィードバックする以外にも、ブラウザ間の相互運用性を高めるためにweb-platform-testsというものがあります。web-platform-testsにコミットすると各ブラウザで実行され、場合によってはブラウザ側で挙動が変更されることもあります。フィードバックを通じてWebの将来を作っていきましょう。ありがとうございました。</p> </section> <hr> <p>以上が発表内容です。久々の発表、しかもリモートからの参加ということで緊張しましたが、無事終えられてよかったです。</p> <p>イベント自体は懐かしい機能を思い出したりIEの功績を振り返ったりと、IEの存在感の大きさを改めて感じられるものでした。イベントを企画・運営してくださった方々に感謝します。(IEのケーキが思ったより大きくて、現地で食べてみたかったです。)</p> <p>IE 6も登場した時点ではそこまで悪いものではなく(個人的にはまだCSS 2ではなくCSS 1なのかと思いましたが……)、開発者の落胆を招いた要因としては、その後5年もメジャーバージョンアップがなかったという部分も大きいかと思います。</p> <p>発表するにあたって、発表内容で触れた以外にも以下のページを参考にしました。また、<a href="https://forest.watch.impress.co.jp/">窓の杜</a>および<a href="https://archive.org/">Internet Archive</a>には当時の記事が多数残っており、調査の大きな助けとなりました。</p> <ul> <li><a href="https://web.archive.org/web/20011020051916/http://www.microsoft.com/products/windows/ie_intl/ja/download/preview/ie6/ie6preview.htm">Internet Explorer Product Downloads (Internet Explorer 6 Public Preview)</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/please-send-us-your-feedback-on-the-ie7-beta-2-preview">Please send us your feedback on the IE7 Beta 2 Preview | Microsoft Docs</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/announcing-internet-explorer-feedback">Announcing Internet Explorer Feedback | Microsoft Docs</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/feedback-and-support-for-ie7-beta-2">Feedback and Support for IE7 Beta 2 | Microsoft Docs</a></li> <li><a href="https://web.archive.org/web/20060716083122/http://www.microsoft.com/japan/windows/ie/support/default.mspx">Microsoft Internet Explorer 7: サポート</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/ie7-feedback-and-support">IE7 Feedback and Support | Microsoft Docs</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/ie8-beta-feedback">IE8 Beta Feedback | Microsoft Docs</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/re-ie8-beta-feedback">RE: IE8 Beta Feedback | Microsoft Docs</a></li> <li><a href="https://web.archive.org/web/20080403204224/http://www.exconn.net/Blogs/windows/archive/2008/03/31/23660.aspx">Internet Explorer 8 ベータ フィードバック について</a></li> <li><a href="https://snow-white.cocolog-nifty.com/first/2008/03/ie8_073f.html">IE8 β版でのもっと積極的なフィードバック: 世の中は不思議なことだらけ</a></li> <li><a href="https://nanto.asablo.jp/blog/2008/09/30/3792686">第 13 回 Admintech.jp 勉強会: Days on the Moon</a></li> <li><a href="https://gigazine.net/news/20090512_ie9/">IE8の次、「Internet Explorer 9」に搭載して欲しい機能をマイクロソフトが募集中 - GIGAZINE</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/ie9-platform-preview-feedback">IE9 Platform Preview Feedback | Microsoft Docs</a></li> <li><a href="https://hebikuzure.wordpress.com/2010/03/22/ie9-platform-preview-%E3%81%B8%E3%81%AE%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%83%90%E3%83%83%E3%82%AF/">IE9 Platform Preview へのフィードバック | Hebikuzure's Tech Memo</a></li> <li><a href="https://docs.microsoft.com/en-us/archive/blogs/ie/how-ie9-platform-preview-feedback-changed-the-javascript-standard">How IE9 Platform Preview Feedback Changed the JavaScript Standard | Microsoft Docs</a></li> <li><a href="https://snow-white.cocolog-nifty.com/first/2010/09/internet-expl-3.html">Internet Explorer 9 Beta で気になる現象を見つけたら: 世の中は不思議なことだらけ</a></li> </ul> Web 関連技術 TypeScript の可変長タプル型における共用体の分配 http://nanto.asablo.jp/blog/2022/06/09/9498227 http://nanto.asablo.jp/blog/2022/06/09/9498227 Thu, 09 Jun 2022 01:29:18 +0900 2022-06-09T01:34:53+09:00 2022-06-09T01:34:53+09:00 <p>TypeScript の可変長タプル型 <span lang="en">(variadic tuple types)</span> とは、配列型やタプル型を展開して別のタプル型の一部として使える機能のことです (「<a href="https://qiita.com/uhyo/items/7e31bbd93a80ce9cec84">TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす - Qiita</a>」に詳しいです)。記法としては、展開する型の直前に三連続のドット <code>...</code> を記述します。例えば、</p> <pre><code>type Sandwich&lt;Fillings extends unknown[]&gt; = ['bread', ...Fillings, 'bread'];</code></pre> <p>のように具材 <span lang="en">(filling)</span> をパン <span lang="en">(bread)</span> で挟む <code>Sandwich</code> 型があったとき、<code>Sandwich&lt;['ham']&gt;</code> 型は <code>['bread', 'ham', 'bread']</code> 型に展開されます。</p> <pre><code>type HamSandwich = Sandwich&lt;['ham']&gt;; // → ['bread', 'ham', 'bread'] type BLTSandwich = Sandwich&lt;['bacon', 'lettuce', 'tomato']&gt;; // → ['bread', 'bacon', 'lettuce', 'tomato', 'bread'] type RichHamSandwich = Sandwich&lt;'ham'[]&gt;; // → ['bread', ...'ham'[], 'bread'] type TwoSlicesOfBread = Sandwich&lt;[]&gt;; // → ['bread', 'bread']</code></pre> <p>ここで型引数 <code>Fillings</code> に共用体型 (縦線 <code>|</code> で区切った複数の型のうちのいずれかを表す型) を渡すとどうなるでしょうか。具材がハムか卵なら、できあがるのはハムサンドか卵サンドになります。つまり、<code>Sandwich&lt;['ham'] | ['egg']&gt;</code> 型は <code>Sandwich&lt;['ham']&gt; | Sandwich&lt;['egg']&gt;</code> 型と同等に扱われます。</p> <pre><code>type HamOrEggSandwich = Sandwich&lt;['ham'] | ['egg']&gt;; // → ['bread', 'ham', 'bread'] | ['bread', 'egg', 'bread']</code></pre> <p>このように、<code>T&lt;A | B | C | (略)&gt;</code> 型が <code>T&lt;A&gt; | T&lt;B&gt; | T&lt;C&gt; | (略)</code> 型として扱われる挙動を「共用体の分配 <span lang="en">(union distribution)</span>」と呼びます。 共用体の分配は条件型 <code>P extends Q ? R : S</code> でも発生しますが (「<a href="https://qiita.com/uhyo/items/da21e2b3c10c8a03952f">TypeScriptの型初級 - Qiita</a>」に詳しいです)、可変長タプル型でも発生するのです。</p> <p>条件型における共用体の分配では、<code>never</code> 型を分配しようとすると展開結果も <code>never</code> 型になります。これは可変長タプル型においても同じです。</p> <pre><code>type ImpossibleSandwich = Sandwich&lt;never&gt; // → never</code></pre> <p>なお、可変長タプル型において <code>any</code> 型は <code>any[]</code> 型に展開されます。<code>unknown</code> 型を展開することはできません。</p> <pre><code>type AnySandwich = Sandwich&lt;any&gt;; // → ['bread', ...any[], 'bread'] type UnknownSandwich = Sandwich&lt;unknown&gt;; // → Error: Type 'unknown' does not satisfy the constraint 'unknown[]'.</code></pre> <p>可変長タプル型で共用体の分配が発生するというのは、<a href="https://github.com/microsoft/TypeScript/pull/39094" hreflang="en">可変長タプル型を導入した pull request</a> の説明に書かれています。私はこの挙動を、<a href="https://github.com/type-challenges/type-challenges/issues/3968" hreflang="en">type-challenges の回答のひとつ</a>をきっかけに知りました (その回答は誤答なのですが)。</p> <div class="quote"> <blockquote lang="en"> <ul> <li>When the type argument for <code>T</code> is a union type, the union is spread over the tuple type. For example, <code>[A, ...T, B]</code> instantiated with <code>X | Y | Z</code> as the type argument for <code>T</code> yields a union of instantiations of <code>[A, ...T, B]</code> with <code>X</code>, <code>Y</code> and <code>Z</code> as the type argument for <code>T</code> respectively.</li> </ul> </blockquote> <p class="cite"><cite><a href="https://github.com/microsoft/TypeScript/pull/39094" hreflang="en" lang="en">Variadic tuple types by ahejlsberg · Pull Request #39094 · microsoft/TypeScript</a></cite></p> </div> JavaScript HTML のフォームコントロール要素と label 要素の紐づけ http://nanto.asablo.jp/blog/2021/12/24/9450536 http://nanto.asablo.jp/blog/2021/12/24/9450536 Fri, 24 Dec 2021 21:11:54 +0900 2021-12-24T21:14:34+09:00 2021-12-24T21:13:32+09:00 <p>この記事は <a href="https://qiita.com/advent-calendar/2021/html">HTML アドベントカレンダー</a>の 24 日目の分、兼 <a href="https://qiita.com/advent-calendar/2021/javascript">JavaScript アドベントカレンダー</a>の 24 日目の分です。</p> <hr> <p>HTML のフォームコントロール要素 (<code>input</code>、<code>textarea</code>、<code>select</code>、<code>button</code> 要素など) には、<code>label</code> 要素を使ってラベルを指定できます。ここでいうラベルとは、そのフォームコントロールに何を入力するか・そのフォームコントロールで何ができるのかの簡単な説明であり、人間が読んで理解できるようなフォームコントロールの名前です。</p> <p>ある <code>label</code> 要素の子孫にフォームコントロール要素が存在すれば、その <code>label</code> 要素の内容が、そのフォームコントロール要素のラベルとなります。そうでない場合、<code>label</code> 要素の <code>for</code> 属性にフォームコントロール要素の ID (<code>id</code> 属性の値) を指定する必要があり、その <code>label</code> 要素の内容が、その ID を持つフォームコントロール要素のラベルとなります。</p> <p>このフォームコントロール要素と <code>label</code> 要素との紐づきは JavaScript を使って参照できます。フォームコントロール要素オブジェクトの <code>labels</code> プロパティはそのフォームコントロール要素と紐づく <code>label</code> 要素の一覧 (<code>NodeList</code> オブジェクト) を返し、<code>label</code> 要素オブジェクト (<code>HTMLLabelElement</code> オブジェクト) の <code>control</code> プロパティはその <code>label</code> 要素に紐づくフォームコントロール要素を返します。</p> <p><code>labels</code> プロパティの名前が複数形なのは、ひとつのフォームコントロール要素に対して複数の <code>label</code> 要素を紐づけられるからですね。</p> <pre><code>&lt;label id="query-label-1" for="query-field">キーワード&lt;/label> &lt;label id="query-label-2" for="query-field">URL&lt;/label> &lt;input id="query-field" type="search" name="q"></code></pre> <pre><code>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)');</code></pre> <p>あるフォームコントロールにおいて、<code>labels</code> プロパティの返す <code>NodeList</code> オブジェクトは常に同一です。紐づく <code>label</code> 要素に変更があれば、その <code>NodeList</code> オブジェクトの内容が動的に変化します。</p> <pre><code>const oldLabels = field.labels; label1.remove(); const newLabels = field.labels; console.assert(oldLabels === newLabels, 'labels プロパティの値は何度参照しても同一のオブジェクトである'); console.assert(oldLabels.length === 1, 'labels プロパティの値は動的に変化する');</code></pre> <p>ただし、実際のところひとつのフォームコントロール要素に複数の <code>label</code> 要素を紐づけるような場面はほとんどないと思います。</p> <p><code>button</code> 要素にも <code>label</code> 要素を紐づけられます。しかしながら、<code>button</code> 要素の場合は自身の内容がラベルとして扱われるので (<code>&lt;button type="submit">検索する&lt;/button></code> なら「検索する」がそのボタンのラベルになります)、実際のところ <code>button</code> 要素に <code>label</code> 要素を紐づけるような場面はほとんどないと思います。</p> <p>ラベルは <a href="https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-label_attribute"><code>aria-label</code> 属性</a>や <a href="https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-labelledby_attribute"><code>aria-labelledby</code> 属性</a>を使って指定することもできます。</p> <pre><code>&lt;form action="/search"> &lt;p> &lt;input type="search" name="q" aria-label="キーワード"> &lt;button type="submit" aria-label="検索する">🔍&lt;/button> &lt;/p> &lt;/form></code></pre> JavaScript Web 関連技術