`wa-card` の `body` パートをラッパー `div` に変更し、横向きレイアウトの `::slotted` ルールを修正

shoelace-style/webawesome

wa-cardbody CSS パートを <slot> 要素自体から <div> ラッパーに移動し、横向きレイアウトで機能していなかった ::slotted() セレクターを修正しました。これにより、wa-dialogwa-drawer などの他コンポーネントとマークアップパターンが統一されます。

背景

wa-cardbody パートの実装は、他のコンポーネントと異なるパターンを採用していました。wa-dialogwa-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 では、bodyactions それぞれの ::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() の正しい使い方をコードベース全体で徹底する一歩となっています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
8aa0654e

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術的な変更・設計判断(各論)→まとめ(結論)という「総論→各論→結論」の構成が明確に適用されています。各セクションの役割も適切です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```typescript:path/to/file.ts)の形式、PR番号のリンク記法([#2198](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Web Componentsの`part`属性や`::slotted()`セレクターなど、専門知識を持つエンジニアを対象とした適切な技術レベルで記述されており、冗長な説明がありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフがトピックセンテンスで始まり、1段落1トピックの原則が守られています。段落の長さも適切で、非常に読みやすい構成です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されている`card.ts`、`card.styles.ts`、`card.test.ts`のコードブロックは、提供されたDiff情報と完全に一致しており、変更点が正確に反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`::slotted()`、`part`、`slot`、`wrapper`要素などの技術用語が、PR情報と整合性が取れており、文脈に応じて正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

無効だったCSSセレクターの具体的な問題点や、ラッパー`div`を導入する理由について、PR情報に基づいて技術的に正確かつ論理的に説明されています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(マークアップパターンの統一、CSSルールの修正理由など)は、PRのTitle, Description, Diffの内容によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#2198)が記事のフッターで正しく参照されています。その他の固有名詞(コンポーネント名、ファイルパスなど)も正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRのタイトル「Card: Body Part Wrapper + Fix Horizontal `::slotted` Rules」の内容を日本語で的確に要約しており、主題に相違はありません。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事内容はPR情報に限定されており、バージョンサポートやリリース日程といったPR外の知識の追記や捏造はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「機能していなかった」「問題が潜んでいました」など、変更前の状態を示す時間表現が正確に使われており、PRの文脈と一致しています。