fix(quick-260921-bi2): a11y - Administrationsbereich (Gruppen, LDAP, Module, Mandanten, Benutzer)

useButtonType: alle Schaltflaechen ausserhalb der drei echten Formulare
(Mandanten/Benutzer/LDAP) erhalten type="button"; die drei tatsaechlichen
Absende-Buttons behalten type="submit" (Zahl bleibt 1/1/2, siehe <verify>).

noSvgWithoutTitle: Symbole neben sichtbarem Text (Zurueck-Pfeil, Navigations-
Icons in der Admin-Seitenleiste) werden dekorativ (aria-hidden="true"); das
Schloss-Symbol der LDAP-Standardzuordnung traegt jetzt einen eigenen Titel
(admin.ldap.fieldMapping.defaultIcon), weil es ohne begleitenden Text pro
Tabellenzeile die ganze Bedeutung allein traegt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
2026-09-21 09:21:09 +02:00
parent e76f3b8d84
commit 4cff3167ed
8 changed files with 33 additions and 6 deletions
@@ -114,12 +114,14 @@ export function DeleteGroupDialog({ group, onClose, onDeleted }: DeleteGroupDial
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
type="button"
onClick={onClose} onClick={onClose}
className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors" className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors"
> >
{tCommon('cancel')} {tCommon('cancel')}
</button> </button>
<button <button
type="button"
onClick={handleDelete} onClick={handleDelete}
disabled={deleting || loadingImpact || impactError} disabled={deleting || loadingImpact || impactError}
className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity disabled:opacity-50" className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
@@ -126,6 +126,7 @@ export default function AdminGroupsPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-foreground">{t('title')}</h1> <h1 className="text-2xl font-bold text-foreground">{t('title')}</h1>
<button <button
type="button"
onClick={openCreate} onClick={openCreate}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
> >
@@ -146,6 +147,7 @@ export default function AdminGroupsPage() {
<h2 className="text-lg font-semibold text-foreground mb-2">{t('noGroups')}</h2> <h2 className="text-lg font-semibold text-foreground mb-2">{t('noGroups')}</h2>
<p className="text-sm text-muted-foreground mb-6">{t('noGroupsBody')}</p> <p className="text-sm text-muted-foreground mb-6">{t('noGroupsBody')}</p>
<button <button
type="button"
onClick={openCreate} onClick={openCreate}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
> >
@@ -205,6 +207,7 @@ export default function AdminGroupsPage() {
}`} }`}
> >
<svg <svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="16"
height="16" height="16"
@@ -223,18 +226,21 @@ export default function AdminGroupsPage() {
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<button <button
type="button"
onClick={() => openEdit(group)} onClick={() => openEdit(group)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors" className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
> >
{tCommon('edit')} {tCommon('edit')}
</button> </button>
<button <button
type="button"
onClick={() => setMembersGroup(group)} onClick={() => setMembersGroup(group)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors" className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
> >
{t('membersButton')} {t('membersButton')}
</button> </button>
<button <button
type="button"
onClick={() => setDeleteTarget(group)} onClick={() => setDeleteTarget(group)}
className="rounded px-2 py-1 text-xs text-destructive hover:bg-destructive/10 transition-colors" className="rounded px-2 py-1 text-xs text-destructive hover:bg-destructive/10 transition-colors"
> >
@@ -20,6 +20,7 @@ export default function AdminLayout({
className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground" className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
> >
<svg <svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="16"
height="16" height="16"
@@ -677,6 +677,7 @@ export default function AdminLdapPage() {
{t('fieldMapping.title')} {t('fieldMapping.title')}
</h2> </h2>
<button <button
type="button"
onClick={() => setShowMappingForm(true)} onClick={() => setShowMappingForm(true)}
className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:opacity-90 transition-opacity"
> >
@@ -725,6 +726,7 @@ export default function AdminLdapPage() {
strokeLinejoin="round" strokeLinejoin="round"
className="text-muted-foreground" className="text-muted-foreground"
> >
<title>{t('fieldMapping.defaultIcon')}</title>
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" /> <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg> </svg>
@@ -733,6 +735,7 @@ export default function AdminLdapPage() {
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
{!mapping.isDefault && ( {!mapping.isDefault && (
<button <button
type="button"
onClick={() => handleRemoveMapping(mapping.id)} onClick={() => handleRemoveMapping(mapping.id)}
className="rounded px-2 py-1 text-xs text-destructive hover:bg-destructive/10 transition-colors" className="rounded px-2 py-1 text-xs text-destructive hover:bg-destructive/10 transition-colors"
> >
@@ -1281,6 +1284,7 @@ export default function AdminLdapPage() {
{/* Save interval changes */} {/* Save interval changes */}
<button <button
type="button"
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
@@ -1302,6 +1306,7 @@ export default function AdminLdapPage() {
{/* Manual sync button (D-14) */} {/* Manual sync button (D-14) */}
<button <button
type="button"
onClick={handleSync} onClick={handleSync}
disabled={syncing} disabled={syncing}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity disabled:opacity-50"
@@ -197,6 +197,7 @@ export default function AdminModulesPage() {
{isActive ? t('activated') : t('deactivated')} {isActive ? t('activated') : t('deactivated')}
</span> </span>
<button <button
type="button"
onClick={() => { onClick={() => {
if (isActive) { if (isActive) {
// Deactivation is unchanged -- direct toggle, no dialog. // Deactivation is unchanged -- direct toggle, no dialog.
@@ -148,6 +148,7 @@ export default function AdminTenantsPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-foreground">{t('title')}</h1> <h1 className="text-2xl font-bold text-foreground">{t('title')}</h1>
<button <button
type="button"
onClick={openCreate} onClick={openCreate}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
> >
@@ -211,18 +212,21 @@ export default function AdminTenantsPage() {
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<button <button
type="button"
onClick={() => openEdit(tenant)} onClick={() => openEdit(tenant)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors" className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
> >
{tCommon('edit')} {tCommon('edit')}
</button> </button>
<button <button
type="button"
onClick={() => handleToggleActive(tenant)} onClick={() => handleToggleActive(tenant)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors" className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
> >
{tenant.isActive ? tCommon('inactive') : tCommon('active')} {tenant.isActive ? tCommon('inactive') : tCommon('active')}
</button> </button>
<button <button
type="button"
onClick={() => setDeleteConfirm(tenant.id)} onClick={() => setDeleteConfirm(tenant.id)}
className="rounded px-2 py-1 text-xs text-destructive hover:bg-destructive/10 transition-colors" className="rounded px-2 py-1 text-xs text-destructive hover:bg-destructive/10 transition-colors"
> >
@@ -306,12 +310,14 @@ export default function AdminTenantsPage() {
</p> </p>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
type="button"
onClick={() => setDeleteConfirm(null)} onClick={() => setDeleteConfirm(null)}
className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors" className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors"
> >
{tCommon('cancel')} {tCommon('cancel')}
</button> </button>
<button <button
type="button"
onClick={() => handleDelete(deleteConfirm)} onClick={() => handleDelete(deleteConfirm)}
className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity"
> >
@@ -229,6 +229,7 @@ export default function AdminUsersPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-foreground">{t('title')}</h1> <h1 className="text-2xl font-bold text-foreground">{t('title')}</h1>
<button <button
type="button"
onClick={openCreate} onClick={openCreate}
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90 transition-opacity"
> >
@@ -308,6 +309,7 @@ export default function AdminUsersPage() {
<td className="px-4 py-3 text-right"> <td className="px-4 py-3 text-right">
<div className="flex items-center justify-end gap-2"> <div className="flex items-center justify-end gap-2">
<button <button
type="button"
onClick={() => setDetailsUser(user)} onClick={() => setDetailsUser(user)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors" className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
> >
@@ -315,6 +317,7 @@ export default function AdminUsersPage() {
</button> </button>
{canManageRow(user) && ( {canManageRow(user) && (
<button <button
type="button"
onClick={() => openEdit(user)} onClick={() => openEdit(user)}
className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors" className="rounded px-2 py-1 text-xs text-foreground hover:bg-muted transition-colors"
> >
@@ -323,6 +326,7 @@ export default function AdminUsersPage() {
)} )}
{canManageRow(user) && ( {canManageRow(user) && (
<button <button
type="button"
onClick={() => { onClick={() => {
setDeleteConfirm(user.id); setDeleteConfirm(user.id);
setDeleteError(null); setDeleteError(null);
@@ -479,12 +483,14 @@ export default function AdminUsersPage() {
)} )}
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
type="button"
onClick={() => setDeleteConfirm(null)} onClick={() => setDeleteConfirm(null)}
className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors" className="rounded-md border border-border px-4 py-2 text-sm text-foreground hover:bg-muted transition-colors"
> >
{tCommon('cancel')} {tCommon('cancel')}
</button> </button>
<button <button
type="button"
onClick={() => handleDelete(deleteConfirm)} onClick={() => handleDelete(deleteConfirm)}
className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity" className="rounded-md bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground hover:opacity-90 transition-opacity"
> >
@@ -17,7 +17,7 @@ export function AdminSidebar() {
href: '/admin/users', href: '/admin/users',
show: true, show: true,
icon: ( icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" /> <circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" /> <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
@@ -30,7 +30,7 @@ export function AdminSidebar() {
href: '/admin/tenants', href: '/admin/tenants',
show: isSuperAdmin, show: isSuperAdmin,
icon: ( icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /> <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
<polyline points="9 22 9 12 15 12 15 22" /> <polyline points="9 22 9 12 15 12 15 22" />
</svg> </svg>
@@ -41,7 +41,7 @@ export function AdminSidebar() {
href: '/admin/modules', href: '/admin/modules',
show: true, show: true,
icon: ( icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" /> <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" /> <line x1="12" y1="22.08" x2="12" y2="12" />
@@ -53,7 +53,7 @@ export function AdminSidebar() {
href: '/admin/ldap', href: '/admin/ldap',
show: true, show: true,
icon: ( icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z" /> <path d="M12 2L2 7l10 5 10-5-10-5z" />
<path d="M2 17l10 5 10-5" /> <path d="M2 17l10 5 10-5" />
<path d="M2 12l10 5 10-5" /> <path d="M2 12l10 5 10-5" />
@@ -65,7 +65,7 @@ export function AdminSidebar() {
href: '/admin/smtp', href: '/admin/smtp',
show: true, show: true,
icon: ( icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="20" height="16" x="2" y="4" rx="2" /> <rect width="20" height="16" x="2" y="4" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /> <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg> </svg>
@@ -76,7 +76,7 @@ export function AdminSidebar() {
href: '/admin/groups', href: '/admin/groups',
show: true, show: true,
icon: ( icon: (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="8" x2="21" y1="6" y2="6" /> <line x1="8" x2="21" y1="6" y2="6" />
<line x1="8" x2="21" y1="12" y2="12" /> <line x1="8" x2="21" y1="12" y2="12" />
<line x1="8" x2="21" y1="18" y2="18" /> <line x1="8" x2="21" y1="18" y2="18" />