APG Patterns
English
English

Disclosure

コンテンツセクションの表示/非表示を制御するボタン。

デモ

基本的な Disclosure

クリックするとコンテンツの表示/非表示を切り替えるシンプルなディスクロージャー。

初期展開状態

defaultExpanded プロパティを使用して、初期レンダリング時にコンテンツを表示します。

defaultExpandedtrue に設定されているため、このコンテンツはページロード時に表示されます。

無効化状態

disabled プロパティを使用して、ディスクロージャーの操作を防ぎます。

デモのみ表示 →

ネイティブ HTML

ネイティブ HTML を優先

このカスタムコンポーネントを使用する前に、ネイティブの <details> と <summary> 要素の使用を検討してください。 ネイティブ要素は組み込みのアクセシビリティを提供し、JavaScript なしで動作し、ARIA 属性を必要としません。

<details>
  <summary>Show details</summary>
  <p>Hidden content here...</p>
</details>

カスタム実装は、滑らかな高さアニメーション、外部状態制御、またはネイティブ要素では提供できないスタイリングが必要な場合にのみ使用してください。

ユースケース ネイティブ HTML カスタム実装
シンプルなトグルコンテンツ 推奨 不要
JavaScript 無効時のサポート ネイティブで動作 フォールバックが必要
滑らかなアニメーション 限定的なサポート 完全に制御可能
外部状態制御 制限あり 完全に制御可能
カスタムスタイリング ブラウザ依存 完全に制御可能

アクセシビリティ

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>要素の動作をキーボードインタラクションに使用します。追加のキーボードハンドラーは必要ありません。

ソースコード

Disclosure.astro
---
/**
 * 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 element トリガーがセマンティックな<button>要素である
aria-expanded ボタンがaria-expanded属性を持つ
aria-controls ボタンがaria-controls経由でパネルIDを参照
accessible name ボタンがコンテンツまたはaria-labelからアクセシブルな名前を持つ

高優先度 : APGキーボード操作(E2E)

テスト 説明
Space key toggles Spaceキーを押すとDisclosureの状態が切り替わる
Enter key toggles Enterキーを押すとDisclosureの状態が切り替わる
Tab navigation Tabキーでフォーカスをボタンに移動
Disabled Tab skip 無効化されたDisclosureはTabの順序でスキップされる

高優先度 : 状態の同期(E2E)

テスト 説明
aria-expanded toggle クリックでaria-expanded値が変わる
panel visibility パネルの表示がaria-expanded状態と一致
collapsed hidden 折りたたみ時にパネルコンテンツが非表示
expanded visible 展開時にパネルコンテンツが表示

高優先度 : 無効状態(E2E)

テスト 説明
disabled attribute 無効なDisclosureがdisabled属性を持つ
click blocked 無効なDisclosureはクリックでトグルしない
keyboard blocked 無効なDisclosureはキーボードでトグルしない

中優先度 : アクセシビリティ(E2E)

テスト 説明
axe (collapsed) 折りたたみ状態でWCAG 2.1 AA違反なし
axe (expanded) 展開状態でWCAG 2.1 AA違反なし

テストの実行

# DisclosureのE2Eテストを実行(全フレームワーク)
npm run test:e2e:pattern --pattern=disclosure

テストツール

詳細は testing-strategy.md (opens in new tab) を参照してください。

リソース