Files
nexarch/web/mail-postfach/components/FolderTree.tsx
patrickandClaude Sonnet 5 c4bb5457b7 QA-08: barrierefreiheits-audit
Zwei echte a11y-Befunde aus dem Audit von web/mail-postfach behoben:
Ordnerbaum-Treeitems waren per Tastatur erreichbar, aber ohne
onKeyDown-Handler nicht per Enter/Leertaste aktivierbar; die
scrollbare Nachrichtenliste hatte keinen tabIndex (axe:
scrollable-region-focusable, serious).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-16 07:17:56 +00:00

85 lines
2.9 KiB
TypeScript

"use client";
// Akzeptanzkriterium 1: Ordnerbaum zeigt alle Postfächer/Ordner des
// angemeldeten Mandanten. Die komplette (mandantengebundene) Liste kommt
// in einem Aufruf (keine Lazy-Load-Ebenen wie im DMS-Pendant nötig — die
// Mail-API liefert ohnehin die gesamte, bereits kleine Facettenliste).
import { useMemo, useState, type KeyboardEvent } from "react";
import { useI18n } from "@nexarch/shl";
import "../lib/messages";
import type { MailboxDTO } from "../lib/api";
import { buildFolderTree, flattenVisible, type FolderNode } from "../lib/folderTree";
export interface FolderTreeProps {
mailboxes: MailboxDTO[];
selected: string | null;
onSelect: (mailbox: string | null) => void;
}
export function FolderTree({ mailboxes, selected, onSelect }: FolderTreeProps) {
const { t } = useI18n();
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const tree = useMemo(() => buildFolderTree(mailboxes), [mailboxes]);
const visible = flattenVisible(tree, expanded);
function toggle(node: FolderNode) {
setExpanded((prev) => {
const next = new Set(prev);
if (next.has(node.name)) next.delete(node.name);
else next.add(node.name);
return next;
});
}
function activateNode(node: FolderNode) {
if (node.children.length > 0) toggle(node);
if (node.messageCount !== null) onSelect(node.name);
}
// div[role=treeitem] loest Enter/Space nicht wie ein nativer Button aus.
function onActivateKey(handler: () => void) {
return (e: KeyboardEvent) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handler();
}
};
}
return (
<div role="tree" aria-label={t("mail.postfach.folderTreeLabel", "Ordnerbaum")}>
<div
role="treeitem"
aria-selected={selected === null}
tabIndex={0}
style={{ fontWeight: selected === null ? 600 : 400, cursor: "pointer", padding: "4px 0" }}
onClick={() => onSelect(null)}
onKeyDown={onActivateKey(() => onSelect(null))}
>
{t("mail.postfach.title", "Postfach")}
</div>
{visible.map((node) => (
<div
key={node.name}
role="treeitem"
aria-selected={node.name === selected}
aria-expanded={node.children.length > 0 ? expanded.has(node.name) : undefined}
tabIndex={0}
style={{
paddingLeft: node.depth * 16,
fontWeight: node.name === selected ? 600 : 400,
cursor: node.messageCount !== null ? "pointer" : "default",
padding: "4px 0",
}}
onClick={() => activateNode(node)}
onKeyDown={onActivateKey(() => activateNode(node))}
>
{node.children.length > 0 ? (expanded.has(node.name) ? "▾ " : "▸ ") : ""}
{node.label}
{node.messageCount !== null ? ` (${node.messageCount})` : ""}
</div>
))}
</div>
);
}