Disclosure
コンテンツセクションの表示/非表示を制御するボタン。
🤖 AI Implementation Guideデモ
基本的な Disclosure
クリックするとコンテンツの表示/非表示を切り替えるシンプルなディスクロージャー。
初期展開状態
defaultExpanded プロパティを使用して、初期レンダリング時にコンテンツを表示します。
defaultExpanded が true
に設定されているため、このコンテンツはページロード時に表示されます。
無効化状態
disabled プロパティを使用して、ディスクロージャーの操作を防ぎます。
ネイティブ HTML
Use Native HTML First
Before using this custom component, consider using native <details> and <summary> elements.
They provide built-in accessibility, work without JavaScript, and require no ARIA attributes.
<details>
<summary>Show details</summary>
<p>Hidden content here...</p>
</details> Use custom implementations only when you need smooth height animations, external state control, or styling that native elements cannot provide.
| Use Case | Native HTML | Custom Implementation |
|---|---|---|
| Simple toggle content | Recommended | Not needed |
| JavaScript disabled support | Works natively | Requires fallback |
| Smooth animations | Limited support | Full control |
| External state control | Limited | Full control |
| Custom styling | Browser-dependent | Full control |
アクセシビリティ
WAI-ARIA ロール
| ロール | 対象要素 | 説明 |
|---|---|---|
button | トリガー要素 | パネルの表示を切り替えるインタラクティブな要素(ネイティブの<button>を使用) |
WAI-ARIA Disclosure Pattern (opens in new tab)
WAI-ARIA プロパティ
| 属性 | 対象 | 値 | 必須 | 説明 |
|---|---|---|---|---|
aria-controls | button | パネルへの ID 参照 | はい | ボタンと制御対象のパネルを関連付けます |
aria-hidden | panel | true | false | いいえ | 折りたたまれた際にパネルを支援技術から隠します |
WAI-ARIA ステート
aria-expanded
ディスクロージャーパネルが展開されているか折りたたまれているかを示します。
| 対象 | button 要素 |
| 値 | true | false |
| 必須 | はい |
| 変更トリガー | Click、Enter、Space |
| 参照 | aria-expanded (opens in new tab) |
キーボードサポート
| キー | アクション |
|---|---|
| Tab | ディスクロージャーボタンにフォーカスを移動します |
| Space / Enter | ディスクロージャーパネルの表示を切り替えます |
ディスクロージャーはネイティブの<button>要素の動作をキーボードインタラクションに使用します。追加のキーボードハンドラーは必要ありません。
ソースコード
---
/**
* APG Disclosure Pattern - Astro Implementation
*
* A button that controls the visibility of a section of content.
* Uses Web Components for client-side state management.
*
* @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
*/
export interface Props {
/** Content displayed in the disclosure trigger button */
trigger: string;
/** When true, the panel is expanded on initial render */
defaultExpanded?: boolean;
/** When true, the disclosure cannot be expanded/collapsed */
disabled?: boolean;
/** Additional CSS class */
class?: string;
}
const { trigger, defaultExpanded = false, disabled = false, class: className = '' } = Astro.props;
// Generate unique ID for this instance
const instanceId = crypto.randomUUID();
const panelId = `${instanceId}-panel`;
---
<apg-disclosure class:list={['apg-disclosure', className]} data-expanded={defaultExpanded}>
<button
type="button"
aria-expanded={defaultExpanded}
aria-controls={panelId}
disabled={disabled}
class="apg-disclosure-trigger"
>
<span class="apg-disclosure-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 6 15 12 9 18"></polyline>
</svg>
</span>
<span class="apg-disclosure-trigger-content">{trigger}</span>
</button>
<div
id={panelId}
class="apg-disclosure-panel"
aria-hidden={!defaultExpanded}
inert={!defaultExpanded ? true : undefined}
>
<div class="apg-disclosure-panel-content">
<slot />
</div>
</div>
</apg-disclosure>
<script>
class ApgDisclosure extends HTMLElement {
private button: HTMLButtonElement | null = null;
private panel: HTMLElement | null = null;
private expanded = false;
private rafId: number | null = null;
connectedCallback() {
// Use requestAnimationFrame to ensure DOM is fully constructed
this.rafId = requestAnimationFrame(() => this.initialize());
}
private initialize() {
this.rafId = null;
this.button = this.querySelector('.apg-disclosure-trigger');
this.panel = this.querySelector('.apg-disclosure-panel');
if (!this.button || !this.panel) {
console.warn('apg-disclosure: button or panel not found');
return;
}
this.expanded = this.dataset.expanded === 'true';
// Attach event listener
this.button.addEventListener('click', this.handleClick);
}
disconnectedCallback() {
// Cancel pending initialization
if (this.rafId !== null) {
cancelAnimationFrame(this.rafId);
this.rafId = null;
}
// Remove event listener
this.button?.removeEventListener('click', this.handleClick);
// Clean up references
this.button = null;
this.panel = null;
}
private toggle() {
this.expanded = !this.expanded;
this.updateDOM();
// Dispatch custom event
this.dispatchEvent(
new CustomEvent('expandedchange', {
detail: { expanded: this.expanded },
bubbles: true,
})
);
}
private updateDOM() {
if (!this.button || !this.panel) return;
// Update button aria-expanded
this.button.setAttribute('aria-expanded', String(this.expanded));
// Update panel aria-hidden
this.panel.setAttribute('aria-hidden', String(!this.expanded));
// Toggle inert attribute
if (this.expanded) {
this.panel.removeAttribute('inert');
} else {
this.panel.setAttribute('inert', '');
}
}
private handleClick = () => {
if (this.button?.disabled) return;
this.toggle();
};
}
// Register the custom element
if (!customElements.get('apg-disclosure')) {
customElements.define('apg-disclosure', ApgDisclosure);
}
</script> 使い方
---
import Disclosure from './Disclosure.astro';
---
<Disclosure trigger="詳細を表示" defaultExpanded={false}>
<p>表示できる隠しコンテンツ</p>
</Disclosure> API
プロパティ
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
trigger | string | 必須 | トリガーボタンに表示されるコンテンツ |
slot (デフォルト) | any | - | パネルに表示されるコンテンツ |
defaultExpanded | boolean | false | 初期展開状態 |
disabled | boolean | false | ディスクロージャーを無効化 |
class | string | "" | 追加の CSS クラス |
カスタムイベント
| イベント | 詳細 | 説明 |
|---|---|---|
expandedchange | { expanded: boolean } | 展開状態が変更されたときにディスパッチ |
テスト
テストは、キーボード操作、ARIA属性、アクセシビリティ要件の観点からAPG準拠を検証します。Disclosureコンポーネントは4つのフレームワーク全体でE2Eテストを使用しています。
テスト戦略
E2Eテスト (Playwright)
4つのフレームワーク全体で実際のブラウザ環境でのコンポーネント動作を検証します。フルブラウザコンテキストが必要なインタラクションをカバーします。
- キーボード操作(Space、Enter)
- aria-expanded状態の切り替え
- aria-controlsによるパネル関連付け
- パネル表示の同期
- 無効状態の動作
- フォーカス管理とTabナビゲーション
- クロスフレームワーク一貫性
テストカテゴリ
高優先度: APG ARIA構造(E2E)
| テスト | 説明 |
|---|---|
button要素 | トリガーがセマンティックな<button>要素である |
aria-expanded | ボタンがaria-expanded属性を持つ |
aria-controls | ボタンがaria-controlsでパネルIDを参照 |
アクセシブル名 | ボタンがコンテンツまたはaria-labelからアクセシブルな名前を持つ |
高優先度: APGキーボード操作(E2E)
| テスト | 説明 |
|---|---|
Spaceキーでトグル | Spaceキーを押すとDisclosureの状態が切り替わる |
Enterキーでトグル | Enterキーを押すとDisclosureの状態が切り替わる |
Tabナビゲーション | Tabキーでフォーカスをボタンに移動 |
無効時のTabスキップ | 無効化されたDisclosureはTabの順序でスキップされる |
高優先度: 状態の同期(E2E)
| テスト | 説明 |
|---|---|
aria-expandedトグル | クリックでaria-expanded値が変わる |
パネル表示 | パネルの表示がaria-expanded状態と一致 |
折りたたみ時は非表示 | 折りたたみ時にパネルコンテンツが非表示 |
展開時は表示 | 展開時にパネルコンテンツが表示 |
高優先度: 無効状態(E2E)
| テスト | 説明 |
|---|---|
disabled属性 | 無効なDisclosureがdisabled属性を持つ |
クリック無効 | 無効なDisclosureはクリックでトグルしない |
キーボード無効 | 無効なDisclosureはキーボードでトグルしない |
中優先度: アクセシビリティ(E2E)
| テスト | 説明 |
|---|---|
axe(折りたたみ時) | 折りたたみ状態でWCAG 2.1 AA違反なし |
axe(展開時) | 展開状態でWCAG 2.1 AA違反なし |
テストツール
- Vitest (opens in new tab) - ユニットテストランナー
- Testing Library (opens in new tab) - フレームワーク別テストユーティリティ(React、Vue、Svelte)
- Playwright (opens in new tab) - E2Eテスト用ブラウザ自動化
- axe-core/playwright (opens in new tab) - E2Eでの自動アクセシビリティテスト
詳細は testing-strategy.md (opens in new tab) を参照してください。
リソース
- WAI-ARIA APG: Disclosure パターン (opens in new tab)
- MDN: <details> 要素 (opens in new tab)
- AI Implementation Guide (llm.md) (opens in new tab) - ARIA specs, keyboard support, test checklist