007_nested
アコーディオンの中にさらにアコーディオンを入れた 入れ子(ネスト)アコーディオン の実装例です。
FAQ の親質問の中に、関連するサブ質問をたたんで配置するパターンを想定しています。
骨格は基本実装(aria-expanded + hidden)を継承しつつ、
親と子のクリックハンドラが二重発火しないよう closest(".c-accordion") ベースのスコープガードを入れています。
視覚的には子アコーディオンを インデント + 薄い背景色 で親と区別しています。
親アコーディオンの本文 .c-accordion__body の内側に、もう1つ .c-accordion を入れるだけで作れます。
以下のサブ質問は、親アコーディオンを開いた時にだけ表示される「ネスト子アコーディオン」です。
親と子は独立した data-accordion root として扱われ、それぞれにクリックハンドラが仕掛けられます。
変える必要はありません。親も子も data-accordion を 値なしの共通属性 として付けるだけで動きます。
JS 側で各 .c-accordion[data-accordion] を独立した root として取得し、それぞれに個別のクリックハンドラを仕掛けるためです。
技術的にはさらに深くもできますが、UI として読みやすいのは 1段階まで です。 2段以上の入れ子は視覚的な階層が伝わりにくく、操作経路も長くなるため、 本実装も「親 → 子の1段階のみ」に絞っています。
残ります。親を閉じても子の aria-expanded は変更しないため、
親を再度開いた時に子の前回状態(開いていた項目は開いたまま)が復元されます。
「親を閉じたら子もすべて閉じる」リセット仕様にしたい場合は別途実装してください。
クリックハンドラ内で、ヘッダーから最も近い .c-accordion が自分の root と一致するかを判定します。
これにより、親 root のハンドラが子の .c-accordion__header を拾って二重発火する事故を防げます。
root.contains(header) は親 root が子の .c-accordion__header も true で拾ってしまうため、
二重発火を防げません。header.closest(".c-accordion") === root で「ヘッダーが直属する root」を厳密に判定する必要があります。
止めません。event.stopPropagation() は他の機能(モーダルの外側クリック判定など)に副作用を与えるため、
スコープガード方式で「自分の管轄外なら何もしない」と振る舞う方が安全です。
子 root の addEventListener 自体は独立しているため、子のクリックは子のハンドラだけで処理されます。
独立します。クリック対象に <button type="button"> を使っているため、Tab フォーカスは親ヘッダー → 子ヘッダーの順にネイティブな DOM 順で移動し、
Enter / Space は各ボタンが個別に発火します。スコープガードは「親の click ハンドラから子を見ない」「子の click ハンドラから親を見ない」を実現するためのものなので、
キーボード操作のフォーカス順序や発火タイミング自体には影響しません。
なお、親項目を閉じると子のボタンは hidden 配下に入るため Tab フォーカスからも外れます。
親を再度開けば自然に Tab 順序へ戻ります。