fix(quick-260921-bi2): a11y - Marketplace-Bereich

useButtonType: alle Filter-, Karten- und Dialog-Schaltflaechen (keine davon
in einem Formular) erhalten type="button".

noSvgWithoutTitle: Modul-Icons neben dem Modulnamen und die Erfolgs-/Fehler-
Symbole neben der Toast-Nachricht werden dekorativ (aria-hidden="true"); der
bislang unbeschriftete Toast-Schliessen-Button erhaelt aria-label={t('close')}
aus dem bereits vorhandenen common.close-Schluessel, sein Icon wird dekorativ.

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:17 +02:00
parent 4cff3167ed
commit ae821254ef
8 changed files with 23 additions and 8 deletions
@@ -134,7 +134,7 @@ export default function ModuleDetailPage() {
return ( return (
<div className="max-w-2xl mx-auto space-y-4"> <div className="max-w-2xl mx-auto space-y-4">
<Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"> <Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
<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="m15 18-6-6 6-6" /> <path d="m15 18-6-6 6-6" />
</svg> </svg>
{t('backToMarketplace')} {t('backToMarketplace')}
@@ -150,7 +150,7 @@ export default function ModuleDetailPage() {
return ( return (
<div className="max-w-2xl mx-auto space-y-6"> <div className="max-w-2xl mx-auto space-y-6">
<Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"> <Link href="/marketplace" className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
<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="m15 18-6-6 6-6" /> <path d="m15 18-6-6 6-6" />
</svg> </svg>
{t('backToMarketplace')} {t('backToMarketplace')}
@@ -159,13 +159,13 @@ export default function ModuleDetailPage() {
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="rounded-lg bg-muted p-4 shrink-0"> <div className="rounded-lg bg-muted p-4 shrink-0">
{module.icon === 'Globe' ? ( {module.icon === 'Globe' ? (
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" /> <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
<path d="M2 12h20" /> <path d="M2 12h20" />
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
<path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.315 8.685a.98.98 0 0 1 .837-.276c.47.07.802.48.968.925a2.501 2.501 0 1 0 3.214-3.214c-.446-.166-.855-.497-.925-.968a.979.979 0 0 1 .276-.837l1.61-1.61a2.404 2.404 0 0 1 1.705-.707c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z" /> <path d="M19.439 7.85c-.049.322.059.648.289.878l1.568 1.568c.47.47.706 1.087.706 1.704s-.235 1.233-.706 1.704l-1.611 1.611a.98.98 0 0 1-.837.276c-.47-.07-.802-.48-.968-.925a2.501 2.501 0 1 0-3.214 3.214c.446.166.855.497.925.968a.979.979 0 0 1-.276.837l-1.61 1.61a2.404 2.404 0 0 1-1.705.707 2.402 2.402 0 0 1-1.704-.706l-1.568-1.568a1.026 1.026 0 0 0-.877-.29c-.493.074-.84.504-1.02.968a2.5 2.5 0 1 1-3.237-3.237c.464-.18.894-.527.967-1.02a1.026 1.026 0 0 0-.289-.877l-1.568-1.568A2.402 2.402 0 0 1 1.998 12c0-.617.236-1.234.706-1.704L4.315 8.685a.98.98 0 0 1 .837-.276c.47.07.802.48.968.925a2.501 2.501 0 1 0 3.214-3.214c-.446-.166-.855-.497-.925-.968a.979.979 0 0 1 .276-.837l1.61-1.61a2.404 2.404 0 0 1 1.705-.707c.617 0 1.234.236 1.704.706l1.568 1.568c.23.23.556.338.877.29.493-.074.84-.504 1.02-.968a2.5 2.5 0 1 1 3.237 3.237c-.464.18-.894.527-.967 1.02Z" />
</svg> </svg>
)} )}
@@ -187,6 +187,7 @@ export default function ModuleDetailPage() {
</span> </span>
{isAdmin && ( {isAdmin && (
<button <button
type="button"
onClick={handleToggle} onClick={handleToggle}
disabled={toggling} disabled={toggling}
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:cursor-wait disabled:opacity-50 ${ className={`rounded-md px-4 py-2 text-sm font-medium transition-colors disabled:cursor-wait disabled:opacity-50 ${
@@ -52,6 +52,7 @@ export function ActivationDialog({ open, moduleName, onConfirm, onCancel }: Acti
</p> </p>
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
<button <button
type="button"
ref={cancelRef} ref={cancelRef}
onClick={onCancel} onClick={onCancel}
className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted" className="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-foreground hover:bg-muted"
@@ -59,6 +60,7 @@ export function ActivationDialog({ open, moduleName, onConfirm, onCancel }: Acti
{t('deactivateDialogCancel')} {t('deactivateDialogCancel')}
</button> </button>
<button <button
type="button"
ref={confirmRef} ref={confirmRef}
onClick={onConfirm} onClick={onConfirm}
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90" className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground hover:bg-destructive/90"
@@ -14,6 +14,7 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr
return ( return (
<div className="flex gap-2 overflow-x-auto pb-1"> <div className="flex gap-2 overflow-x-auto pb-1">
<button <button
type="button"
role="button" role="button"
onClick={() => onChange('all')} onClick={() => onChange('all')}
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${ className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
@@ -27,6 +28,7 @@ export function CategoryFilter({ categories, value, onChange }: CategoryFilterPr
{categories.map((cat) => ( {categories.map((cat) => (
<button <button
key={cat} key={cat}
type="button"
role="button" role="button"
onClick={() => onChange(cat)} onClick={() => onChange(cat)}
className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${ className={`rounded-full px-3 py-1 text-xs font-medium whitespace-nowrap transition-colors ${
@@ -33,6 +33,7 @@ function ModuleIcon({ icon }: { icon?: string }) {
if (icon === 'Globe') { if (icon === 'Globe') {
return ( return (
<svg <svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
@@ -54,6 +55,7 @@ function ModuleIcon({ icon }: { icon?: string }) {
// Default puzzle-piece icon for generic modules // Default puzzle-piece icon for generic modules
return ( return (
<svg <svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="24" width="24"
height="24" height="24"
@@ -167,6 +169,7 @@ export function MarketplaceCard({
{canManage && ( {canManage && (
<div className="flex items-center justify-end"> <div className="flex items-center justify-end">
<button <button
type="button"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggle(id, isActive); onToggle(id, isActive);
@@ -33,11 +33,12 @@ export function MarketplaceSearch({ onChange }: MarketplaceSearchProps) {
/> />
{value && ( {value && (
<button <button
type="button"
onClick={() => setValue('')} onClick={() => setValue('')}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
aria-label="Clear search" aria-label="Clear search"
> >
<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="M18 6 6 18" /> <path d="M18 6 6 18" />
<path d="m6 6 12 12" /> <path d="m6 6 12 12" />
</svg> </svg>
@@ -24,6 +24,7 @@ export function StatusFilter({ value, counts, onChange }: StatusFilterProps) {
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.key} key={tab.key}
type="button"
role="tab" role="tab"
aria-selected={value === tab.key} aria-selected={value === tab.key}
onClick={() => onChange(tab.key)} onClick={() => onChange(tab.key)}
@@ -1,5 +1,6 @@
'use client'; 'use client';
import { useTranslations } from 'next-intl';
import { create } from 'zustand'; import { create } from 'zustand';
interface Toast { interface Toast {
@@ -27,6 +28,7 @@ export const useToastStore = create<ToastState>()((set) => ({
})); }));
export function ToastContainer() { export function ToastContainer() {
const t = useTranslations('common');
const toasts = useToastStore((s) => s.toasts); const toasts = useToastStore((s) => s.toasts);
const removeToast = useToastStore((s) => s.removeToast); const removeToast = useToastStore((s) => s.removeToast);
@@ -40,21 +42,23 @@ export function ToastContainer() {
className="flex items-center gap-3 rounded-lg border border-border bg-card p-4 shadow-lg motion-safe:animate-[slideIn_200ms_ease-out]" className="flex items-center gap-3 rounded-lg border border-border bg-card p-4 shadow-lg motion-safe:animate-[slideIn_200ms_ease-out]"
> >
{toast.type === 'success' ? ( {toast.type === 'success' ? (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-green-600 dark:text-green-400 shrink-0"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-green-600 dark:text-green-400 shrink-0">
<path d="M20 6 9 17l-5-5" /> <path d="M20 6 9 17l-5-5" />
</svg> </svg>
) : ( ) : (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-destructive shrink-0"> <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-destructive shrink-0">
<path d="M18 6 6 18" /> <path d="M18 6 6 18" />
<path d="m6 6 12 12" /> <path d="m6 6 12 12" />
</svg> </svg>
)} )}
<span className="text-sm text-foreground">{toast.message}</span> <span className="text-sm text-foreground">{toast.message}</span>
<button <button
type="button"
onClick={() => removeToast(toast.id)} onClick={() => removeToast(toast.id)}
aria-label={t('close')}
className="ml-2 text-muted-foreground hover:text-foreground shrink-0" className="ml-2 text-muted-foreground hover:text-foreground shrink-0"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 6 6 18" /> <path d="M18 6 6 18" />
<path d="m6 6 12 12" /> <path d="m6 6 12 12" />
</svg> </svg>
@@ -200,6 +200,7 @@ export default function MarketplacePage() {
) : modules.length === 0 ? ( ) : modules.length === 0 ? (
<div className="flex flex-col items-center justify-center min-h-[40vh] text-center"> <div className="flex flex-col items-center justify-center min-h-[40vh] text-center">
<svg <svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="64" width="64"
height="64" height="64"