ボタングループのスタイリングをJavaScriptからCSS継承に移行
wa-button-groupのグループスタイリングがJavaScriptによるクラス操作からCSSカスタムプロパティの継承に刷新されました。これにより、outlined ボタンの境界線が二重になるレースコンディション(#1975)が根本的に解消されます。
背景
従来の実装は、スロットに配置されたボタンを検出してJavaScriptでクラスを付与する方式でした。wa-button-groupはスロットの変更を検知するたびにupdateClassNames()を呼び出し、各ボタンにwa-button-group__button-first・wa-button-group__button-inner・wa-button-group__button-lastといったクラスを付与していました。このDOMトラバーサルは、ボタンのappearanceプロパティを参照してhasOutlinedフラグを切り替える処理も含んでおり、コンポーネントの初期化タイミングによってはappearanceの値が未確定の状態でスタイルが計算されるレースコンディションを引き起こしていました。
その結果、outlined ボタンをwa-button-group内に並べると、隣接するボタン間の境界線が二重に表示されるという不具合(#1975)が発生していました。JavaScriptがappearance === 'outlined'を検知してgap: 0に切り替えるまでの間、デフォルトのgap: 1pxでレンダリングされるためです。CSSによる純粋な宣言的スタイリングへの移行は、この問題を構造的に解消するアプローチです。
技術的な変更
スタイリングの制御はCSSカスタムプロパティの継承に一本化され、JavaScriptのDOM操作は削除されました。
button-group.tsからupdateClassNames()メソッドとhandleSlotChange()メソッドが完全に削除されました。classMapディレクティブも不要になり、lit/directives/class-map.jsのインポートも除去されています。orientation変更時にupdateClassNames()を呼び出していたupdated()フックの該当行も削除されました。
代わりにbutton-group.styles.tsで、wa-button-groupホスト要素が内部カスタムプロパティ(--_wa-button-*プレフィックス)をCSSの継承を通じてスロット内のボタンへ伝達するようになりました:
/* 水平方向のグループ設定 */
:host([orientation='horizontal']) {
--_wa-button-horizontal-indent: 1px;
--_wa-button-horizontal-indent-outlined: calc(var(--wa-border-width-s) * -1);
}
/* 垂直方向のグループ設定 */
:host([orientation='vertical']) {
--_wa-button-vertical-indent: 1px;
--_wa-button-vertical-indent-outlined: calc(var(--wa-border-width-s) * -1);
}
/* 中間のボタンはすべての角丸を除去 */
::slotted(:not(:first-child):not(:last-child)) {
--_wa-button-start-start-radius: 0;
--_wa-button-start-end-radius: 0;
--_wa-button-end-start-radius: 0;
--_wa-button-end-end-radius: 0;
}
/* 先頭ボタン(水平)は末尾側の角丸のみ除去 */
:host([orientation='horizontal']) ::slotted(:first-child) {
--_wa-button-start-end-radius: 0;
--_wa-button-end-end-radius: 0;
}
button.styles.tsでは、従来の単一のborder-radius指定が4つの論理プロパティ(border-start-start-radiusなど)に分割され、各プロパティがデフォルト値として--wa-form-control-border-radiusを持つカスタムプロパティで上書き可能になりました。また、間隔制御はグループ側のgapではなくボタン側のmargin-inline-start・margin-block-startとして実装され、appearanceごとに異なるプロパティを参照します:
:host([appearance='outlined']) {
margin-inline-start: var(--_wa-button-horizontal-indent-outlined);
margin-block-start: var(--_wa-button-vertical-indent-outlined);
}
:host([appearance='filled']) {
margin-inline-start: var(--_wa-button-horizontal-indent);
margin-block-start: var(--_wa-button-vertical-indent);
}
outlined ボタンでは--_wa-button-horizontal-indent-outlinedがcalc(var(--wa-border-width-s) * -1)(負のマージン)に設定されるため、隣接する境界線を重ねて一本に見せます。JavaScriptでhasOutlinedフラグを検出してgapを切り替える処理がなくなり、初期レンダリングから正しくスタイルが適用されます。
テストもクラスの存在確認からCSSカスタムプロパティの継承確認に書き換えられ、各ボタンの位置に応じた--_wa-button-*-radiusプロパティの値を直接アサートする形になっています。
設計判断
--_wa-button-*という命名規則によって内部プロパティとパブリックAPIの境界を明確化したことが、この変更の重要な設計判断です。アンダースコアプレフィックスはJavaScriptの慣習から着想を得たもので、PRではトリプルダッシュ(---wa-*)や--$wa-*なども検討されましたが、変更容易性を考慮してアンダースコアが採用されています。
もう一つの注目すべき判断は「角丸の除去方針」です。グループがボタンの角丸をすべて一旦ゼロにしてから必要な分だけ戻す方式ではなく、「不要な角丸のみを除去する」方式が採用されました。ボタン側が自身のサイズやpill設定に応じた角丸の値を保持し、グループは「この角は不要」という情報だけを0で上書きする設計です。これにより、グループがボタンの内部的な角丸計算ロジックを知る必要がなくなっています。
間隔制御をグループのgapからボタンのmarginに移したことで、outlined とそれ以外のボタンを混在させたグループもappearance属性ベースのCSSセレクタで自動的に正しい間隔が適用されます。JavaScriptでボタンのappearanceを読んで判断する必要がなくなった結果として、レースコンディションが消滅しています。
まとめ
この変更は、JavaScriptによる命令的なDOM操作を宣言的なCSSカスタムプロパティの継承に置き換えることで、レースコンディション起因のスタイル崩れを構造的に排除しています。--_wa-button-*という内部プロパティの規約によって親子コンポーネント間の暗黙的な結合を明文化しつつ、ボタン側がグループのロジックを知らなくて済む責務分離を実現した設計といえます。