ナビ・リスト操作
ツリービュー
フォルダを開閉できるファイルツリー。クリックでフォルダが開き、キャレットが回転する。階層はインデントで表現し、キーボード(矢印キー)でも操作できる。
プレビュー
クリック
操作クリック/タップで動きます。
実装メモ
WAI-ARIA の tree パターンに倣い、フォルダ(tv-node)に aria-expanded、行に role=treeitem を付けています。フォーカスは常にツリー内で1つの行だけ tabindex=0 にする「ローミング tabindex」方式。矢印キーで上下移動、→で展開/子へ、←で折りたたみ/親へ、Enter/Space で開閉できます。開閉アニメは grid-template-rows: 0fr→1fr。グリッドアイテムは .tv-group の1つだけにし(複数だと暗黙行が潰れず内容がはみ出す)、折りたたみ中は visibility:hidden で操作対象からも外しています。
コード(コピーして使えます)
<ul class="tv" role="tree" aria-label="ファイル">
<li class="tv-node" role="treeitem" aria-expanded="true">
<div class="tv-row" tabindex="0" style="--depth:0">
<svg class="tv-caret" width="12" height="12" viewBox="0 0 12 12" fill="none"><polyline points="4,2 8,6 4,10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg class="tv-ic tv-folder" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6a1 1 0 0 1 1-1h5l2 2h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>
<span class="tv-name">src</span>
</div>
<div class="tv-children"><ul class="tv-group" role="group">
<li class="tv-node" role="treeitem" aria-expanded="false">
<div class="tv-row" tabindex="-1" style="--depth:1">
<svg class="tv-caret" width="12" height="12" viewBox="0 0 12 12" fill="none"><polyline points="4,2 8,6 4,10" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
<svg class="tv-ic tv-folder" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6a1 1 0 0 1 1-1h5l2 2h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"/></svg>
<span class="tv-name">components</span>
</div>
<div class="tv-children"><ul class="tv-group" role="group">
<li class="tv-leaf" role="treeitem">
<div class="tv-row" tabindex="-1" style="--depth:2">
<span class="tv-caret tv-caret--empty"></span>
<svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
<span class="tv-name">Button.tsx</span>
</div>
</li>
<li class="tv-leaf" role="treeitem">
<div class="tv-row" tabindex="-1" style="--depth:2">
<span class="tv-caret tv-caret--empty"></span>
<svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
<span class="tv-name">Card.tsx</span>
</div>
</li>
</ul></div>
</li>
<li class="tv-leaf" role="treeitem">
<div class="tv-row" tabindex="-1" style="--depth:1">
<span class="tv-caret tv-caret--empty"></span>
<svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
<span class="tv-name">index.ts</span>
</div>
</li>
</ul></div>
</li>
<li class="tv-leaf" role="treeitem">
<div class="tv-row" tabindex="-1" style="--depth:0">
<span class="tv-caret tv-caret--empty"></span>
<svg class="tv-ic" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h8l4 4v14a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1z"/><polyline points="14,3 14,7 18,7"/></svg>
<span class="tv-name">package.json</span>
</div>
</li>
</ul>