Files
nexarch/web/dms-browser/components/FolderTree.tsx
patrickandClaude Sonnet 5 9701bb81f0
CI / Core-Modul (Build/Vet/Test) (push) Failing after 3s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Successful in 3s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Failing after 2m11s
CI / Web (dms-browser) (push) Failing after 1m3s
CI / Web (pdf-viewer) (push) Failing after 43s
CI / Web (shl) (push) Failing after 26s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Failing after 2s
QA-11: aria-row ohne grid, sichtbarer Fokus am Ordnerbaum, Linkfarbe aus Design-Tokens
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 07:02:28 +02:00

127 lines
3.9 KiB
TypeScript

"use client";
// DOC-10: Navigationsbaum — lädt Kinder lazy je Ebene (kein kompletter
// Teilbaum auf einmal), per Tastatur bedienbar (Akzeptanzkriterium/
// Pflichtprüfung 2: Pfeiltasten, Enter/Leertaste).
import { useCallback, useEffect, useRef, useState } from "react";
import { useI18n } from "@nexarch/shl";
import "../lib/messages";
import type { FolderDTO } from "../lib/api";
import { fetchChildren } from "../lib/api";
import { mapTreeKeyToAction, moveFocus } from "../lib/treeKeyboard";
interface TreeNode {
folder: FolderDTO;
depth: number;
expanded: boolean;
children: TreeNode[] | null; // null = noch nicht geladen
}
export interface FolderTreeProps {
selectedFolderID: string | null;
onSelect: (folderID: string) => void;
}
export function FolderTree({ selectedFolderID, onSelect }: FolderTreeProps) {
const { t } = useI18n();
const [roots, setRoots] = useState<TreeNode[] | null>(null);
const [focusedIndex, setFocusedIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetchChildren(null).then((children) => {
setRoots(children.map((f) => ({ folder: f, depth: 0, expanded: false, children: null })));
});
}, []);
// Flache, sichtbare Reihenfolge (nur eingeklappte Zweige werden
// aufgeführt) — Grundlage für die Tastaturnavigation, dieselbe Idee wie
// lib/treeKeyboard.ts erwartet.
function flatten(nodes: TreeNode[]): TreeNode[] {
const out: TreeNode[] = [];
for (const n of nodes) {
out.push(n);
if (n.expanded && n.children) {
out.push(...flatten(n.children));
}
}
return out;
}
const visible = roots ? flatten(roots) : [];
const toggleExpand = useCallback(
async (node: TreeNode) => {
if (!node.expanded && node.children === null) {
const children = await fetchChildren(node.folder.id);
node.children = children.map((f) => ({ folder: f, depth: node.depth + 1, expanded: false, children: null }));
}
node.expanded = !node.expanded;
setRoots((r) => (r ? [...r] : r));
},
[]
);
function onKeyDown(e: React.KeyboardEvent) {
const action = mapTreeKeyToAction(e.key);
if (!action) return;
e.preventDefault();
const node = visible[focusedIndex];
switch (action.type) {
case "moveDown":
setFocusedIndex((i) => moveFocus(i, visible.length, 1));
break;
case "moveUp":
setFocusedIndex((i) => moveFocus(i, visible.length, -1));
break;
case "expand":
if (node && !node.expanded) void toggleExpand(node);
else setFocusedIndex((i) => moveFocus(i, visible.length, 1));
break;
case "collapse":
if (node?.expanded) void toggleExpand(node);
break;
case "select":
if (node) onSelect(node.folder.id);
break;
}
}
if (!roots) {
return <p>{t("dms.browse.loadingFolderTree", "Lade Ordnerbaum…")}</p>;
}
return (
<div
ref={containerRef}
role="tree"
aria-label={t("dms.browse.folderTreeLabel", "Ordnerbaum")}
tabIndex={0}
onKeyDown={onKeyDown}
>
{visible.map((node, index) => (
<div
key={node.folder.id}
role="treeitem"
aria-selected={node.folder.id === selectedFolderID}
aria-expanded={node.children !== null ? node.expanded : undefined}
tabIndex={-1}
style={{
paddingLeft: node.depth * 16,
background: index === focusedIndex ? "var(--shl-color-accent-subtle, #eef3ff)" : "transparent",
fontWeight: node.folder.id === selectedFolderID ? 600 : 400,
cursor: "pointer",
}}
onClick={() => {
setFocusedIndex(index);
onSelect(node.folder.id);
}}
onDoubleClick={() => void toggleExpand(node)}
>
{node.expanded ? "▾" : "▸"} {node.folder.name}
</div>
))}
</div>
);
}