APG Patterns
English
English

Disclosure

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

デモ

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

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

初期状態で展開

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

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

無効化状態

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

デモのみ表示 →

Native 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.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構造(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) を参照してください。

リソース