APG Patterns
English GitHub
English GitHub

Disclosure

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

🤖 AI 実装ガイド

デモ

基本的なディスクロージャー

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

初期状態で展開

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

This content is visible when the page loads because defaultExpanded is set to true.

無効化状態

disabled プロパティを使用すると、ディスクロージャーの操作を無効化できます。

デモのみ表示 →

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

ソースコード

Disclosure.tsx
import { useId, useState, useCallback } from 'react';
import { cn } from '@/lib/utils';

/**
 * Props for the Disclosure component
 *
 * @see https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/
 *
 * @example
 * ```tsx
 * <Disclosure trigger="Show details">
 *   <p>Hidden content that can be revealed</p>
 * </Disclosure>
 * ```
 */
export interface DisclosureProps {
  /**
   * Content displayed in the disclosure trigger button
   */
  trigger: React.ReactNode;
  /**
   * Content displayed in the collapsible panel
   */
  children: React.ReactNode;
  /**
   * When true, the panel is expanded on initial render
   * @default false
   */
  defaultExpanded?: boolean;
  /**
   * Callback fired when the expanded state changes
   * @param expanded - The new expanded state
   */
  onExpandedChange?: (expanded: boolean) => void;
  /**
   * When true, the disclosure cannot be expanded/collapsed
   * @default false
   */
  disabled?: boolean;
  /**
   * Additional CSS class to apply to the disclosure container
   * @default ""
   */
  className?: string;
}

export function Disclosure({
  trigger,
  children,
  defaultExpanded = false,
  onExpandedChange,
  disabled = false,
  className = '',
}: DisclosureProps): React.ReactElement {
  const instanceId = useId();
  const panelId = `${instanceId}-panel`;

  const [expanded, setExpanded] = useState(defaultExpanded);

  const handleToggle = useCallback(() => {
    if (disabled) return;

    const newExpanded = !expanded;
    setExpanded(newExpanded);
    onExpandedChange?.(newExpanded);
  }, [expanded, disabled, onExpandedChange]);

  return (
    <div className={cn('apg-disclosure', className)}>
      <button
        type="button"
        aria-expanded={expanded}
        aria-controls={panelId}
        disabled={disabled}
        className="apg-disclosure-trigger"
        onClick={handleToggle}
      >
        <span className="apg-disclosure-icon" aria-hidden="true">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
            <polyline points="9 6 15 12 9 18" />
          </svg>
        </span>
        <span className="apg-disclosure-trigger-content">{trigger}</span>
      </button>
      <div
        id={panelId}
        className="apg-disclosure-panel"
        aria-hidden={!expanded}
        inert={!expanded ? true : undefined}
      >
        <div className="apg-disclosure-panel-content">{children}</div>
      </div>
    </div>
  );
}

export default Disclosure;

使い方

Example
import { Disclosure } from './Disclosure';

function App() {
  return (
    <Disclosure
      trigger="Show details"
      defaultExpanded={false}
      onExpandedChange={(expanded) => console.log('Expanded:', expanded)}
    >
      <p>Hidden content that can be revealed</p>
    </Disclosure>
  );
}

API

DisclosureProps

プロパティ デフォルト 説明
trigger ReactNode 必須 トリガーボタンに表示されるコンテンツ
children ReactNode 必須 パネルに表示されるコンテンツ
defaultExpanded boolean false 初期展開状態
onExpandedChange (expanded: boolean) => void - 展開状態変更時のコールバック
disabled boolean false ディスクロージャーを無効化
className string "" 追加の CSS クラス

テスト

テストは、キーボード操作、ARIA属性、アクセシビリティ要件の観点からAPG準拠を検証します。Disclosureコンポーネントは4つのフレームワーク全体でE2Eテストを使用しています。

テスト戦略

E2Eテスト (Playwright)

4つのフレームワーク全体で実際のブラウザ環境でのコンポーネント動作を検証します。フルブラウザコンテキストが必要なインタラクションをカバーします。

  • キーボード操作(Space、Enter)
  • aria-expanded状態の切り替え
  • aria-controlsによるパネル関連付け
  • パネル表示の同期
  • 無効状態の動作
  • フォーカス管理とTabナビゲーション
  • クロスフレームワーク一貫性

テストカテゴリ

高優先度: APG ARIA構造

テスト 説明
button要素 トリガーがセマンティックな<button>要素である
aria-expanded ボタンがaria-expanded属性を持つ
aria-controls ボタンがaria-controlsでパネルIDを参照
アクセシブル名 ボタンがコンテンツまたはaria-labelからアクセシブルな名前を持つ

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

テスト 説明
Spaceキーでトグル Spaceキーを押すとDisclosureの状態が切り替わる
Enterキーでトグル Enterキーを押すとDisclosureの状態が切り替わる
Tabナビゲーション Tabキーでフォーカスをボタンに移動
無効時のTabスキップ 無効化されたDisclosureはTabの順序でスキップされる

高優先度: 状態の同期

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

高優先度: 無効状態

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

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

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

テストツール

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

リソース