wa-checkbox と wa-switch の .checked 同期バグを修正
この PR は <wa-checkbox> と <wa-switch> コンポーネントで、.checked プロパティがシャドウ DOM 内の <input> 要素と正しく同期しないバグを解消し、Lit SSR 環境でも期待通りに動作するように変更します。
背景
Issue #2478 では、バージョン 3.8.0 へのアップグレード後にリアクティブフレームワークからのバインディングが機能せず、.checked プロパティの更新が UI に反映されないことが報告されました。属性ベースの初期化は機能する一方で、プロパティ経由のライブ更新が失われたため、フォームの状態管理やアクセシビリティに支障が出ていました。この問題はコンポーネント内部で属性とプロパティの扱いが分離されたことが根本原因です。
技術的な変更
SSR 環境下での .checked ハンドリングを見直し、属性とプロパティを条件分岐させるロジックを新たに導入しました。didSSR && !this.hasUpdated が真のときは属性 (checkedAttribute) に既存の checked 値を設定し、プロパティ (checkedProperty) は null にして無効化します。そうでない場合は live(this.checked) をプロパティとしてバインドし、属性は defaultChecked を使用します。
@@ -227,11 +227,17 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
- ?checked=${this.defaultChecked}
+ .checked=${ifDefined(checkedProperty)}
+ ?checked=${checkedAttribute}
WaSwitch でも同様のパターンを適用し、テンプレート内で属性とプロパティを分離しました。
@@ -216,6 +216,11 @@ export default class WaSwitch extends WebAwesomeFormAssociatedElement {
- .checked=${live(this.checked)}
- ?checked=${this.defaultChecked}
+ .checked=${ifDefined(checkedProperty)}
+ ?checked=${checkedAttribute}
テストコードも更新し、checked プロパティ変更後に shadowRoot 内の <input> の checked 状態が期待通りになることを expect(...).to.equal(true/false) で検証しています。また、aTimeout(1) を挿入して非同期更新を待機するようにしました。
@@ -53,12 +53,20 @@ describe('<wa-checkbox>', () => {
expect(el.checked).to.be.true;
expect(el.value).to.equal('myvalue');
+ expect(el.shadowRoot?.querySelector('input')?.checked).to.equal(true);
@@ -58,6 +66,7 @@ el.checked = false;
await el.updateComplete;
expect(el.checked).to.be.false;
expect(el.value).to.equal('myvalue');
+ expect(el.shadowRoot?.querySelector('input')?.checked).to.equal(false);
@@ -71,6 +80,13 @@ // let's recheck setting `el.checked = true`
el.checked = true;
await el.updateComplete;
expect(el.shadowRoot?.querySelector('input')?.checked).to.equal(true);
package.json へ @lit-labs/ssr を正式な依存関係として追加し、重複していた QR クリエイターパッケージの位置を整理しました。package-lock.json の不要な dev: true フラグも削除し、ロックファイルを整合させています。
設計判断
既存のコンポーネント API(.checked プロパティ)をそのまま保持しつつ、SSR 時の初期描画を属性ベースに切り替えることで後方互換性を担保しました。新しい設定キーやプロパティを導入せず、内部ロジックだけで対応した点が、利用者側のマイグレーションコストを最小化しています。
didSSR && !this.hasUpdated の条件分岐は、Lit SSR が内部で .checked=${live(this.checked)} を常に true と評価してしまう既知の問題への回避策としてコメントで明示されています。この実装はコンポーネント単位で問題を解決するため、プロジェクト全体への影響範囲が限定的です。
最終的に、属性とプロパティの同期ロジックを明確に分離したことで、リアクティブフレームワークからのバインディングが期待通りに動作し、テストでも確実に検証できるようになりました。
まとめ
.checked プロパティと内部 <input> の状態同期バグが解消され、SSR 環境でも正しい初期化が行われます。依存関係の整理とテスト追加により、回帰リスクが低減し、既存 API の互換性を維持したままコンポーネントの信頼性が向上しました。