`wa-card` の `body` パートをラッパー `div` に変更し、横向きレイアウトの `::slotted` ルールを修正
wa-card の body CSS パートを <slot> 要素自体から <div> ラッパーに移動し、横向きレイアウトで機能していなかった ::slotted() セレクターを修正しました。これにより、wa-dialog や wa-drawer などの他コンポーネントとマークアップパターンが統一されます。
背景
wa-card の body パートの実装は、他のコンポーネントと異なるパターンを採用していました。wa-dialog や wa-drawer では body CSS パートをデフォルトスロットのラッパー要素に付与しているのに対し、wa-card は <slot part="body"> のようにスロット要素そのものにパートを付与していました。この非一貫性に加えて、横向きレイアウト(orientation='horizontal')の CSS にも問題が潜んでいました。
横向きレイアウトでは ::slotted([slot='body']) というセレクターが使われていましたが、body という名前付きスロットは存在せず、デフォルトスロットに挿入されたコンテンツには一切マッチしないルールでした。同様に ::slotted([slot='actions']) もルートレベルに記述されており、slot::slotted(...) の有効な形式に従っていませんでした。
技術的な変更
card.ts のテンプレートで、垂直・水平両方向のレイアウトにおいてデフォルトスロットが <div> ラッパーで包まれるようになりました。
変更前:
<slot part="body" class="body"></slot>
変更後:
<div part="body" class="body"><slot></slot></div>
この変更は水平・垂直レイアウトの両テンプレートに適用されています。::part(body) を使ったスタイルは引き続き機能し、デフォルトスロットへのコンテンツ挿入方法も変わらないため、公開 API への影響はありません。
card.styles.ts では、body と actions それぞれの ::slotted ルールが修正されました。
変更前:
:host([orientation='horizontal']) ::slotted([slot='body']) {
display: block;
height: 100%;
margin: 0;
}
:host([orientation='horizontal']) ::slotted([slot='actions']) {
display: flex;
align-items: center;
padding: var(--spacing);
}
変更後:
:host([orientation='horizontal']) .body slot::slotted(*) {
display: block;
height: 100%;
margin: 0;
}
:host([orientation='horizontal']) slot[name='actions']::slotted(*) {
display: flex;
align-items: center;
padding: var(--spacing);
}
body のルールは .body slot::slotted(*) に変わり、ラッパー div 内のスロットを経由してデフォルトスロットのコンテンツにマッチするようになりました。actions のルールは slot[name='actions']::slotted(*) に修正され、slot::slotted(...) という仕様に沿った形式に揃えられています。
テストでは、[part="body"] が div 要素であること、その中に名前なしスロットが存在すること、そしてデフォルトスロットに挿入したコンテンツが正しくアサインされることを検証するケースが追加されました。
it('exposes the default slot inside a [part="body"] wrapper (matches dialog-style markup)', async () => {
const el = await fixture<WaCard>(html`<wa-card>Main content</wa-card>`);
const bodyPart = el.shadowRoot!.querySelector('[part="body"]');
expect(bodyPart?.localName).to.eq('div');
expect(bodyPart?.classList.contains('body')).to.be.true;
const defaultSlot = bodyPart?.querySelector('slot:not([name])');
expect(defaultSlot).to.be.instanceOf(HTMLSlotElement);
const assigned = (defaultSlot as HTMLSlotElement).assignedNodes({ flatten: true });
const text = assigned.map(n => n.textContent ?? '').join('');
expect(text).to.contain('Main content');
});
設計判断
スロット要素ではなくラッパー要素にパートを付与するパターン を wa-card にも適用することで、コンポーネントライブラリ内の一貫性が保たれています。
スロット要素自体に part を付与する方法は機能上は動作しますが、display などのスタイルがスロット要素のレンダリングに干渉する場合があります。ラッパー div を挟むことで、パートへのスタイル適用とスロットのレイアウト制御を分離でき、コンテンツの display 特性を損なわずに済みます。これはドキュメントの「container」という表現とも一致するアプローチです。
::slotted() の修正については、ブラウザの仕様として ::slotted() は slot::slotted() の形式(スロット要素を起点とする)でなければ有効に機能しない点が反映されています。今回の修正はその仕様に忠実に従ったものであり、これらのルールが実際にコンテンツへ適用されるようになっています。
まとめ
本 PR は wa-card の内部マークアップを他のコンポーネントと統一しつつ、横向きレイアウトで実質的に無効化されていた CSS ルールを修正したものです。公開 API を変えることなく実装の整合性を高め、::slotted() の正しい使い方をコードベース全体で徹底する一歩となっています。