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>
85 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|