fix(quick-260921-jt4): Kalender-Tageszelle wird echte Schaltflaeche fuer Tage mit Terminen

- Zelle rendert als <button type="button"> NUR wenn hasEvents gilt,
  sonst unveraendert als <div> ohne Handler (D-01) — 42 neue Tab-Stopps
  waeren eine Verschlechterung
- onFocus/onBlur mit demselben Rumpf wie onMouseEnter/onMouseLeave,
  damit die Termin-Einblendung auch per Tastatur erscheint/verschwindet
- cellClass unveraendert uebernommen, nur w-full text-left ergaenzt
  (D-04, kein optischer Unterschied); widgetNoDrag bleibt erhalten
- aria-label nennt Datum und Terminzahl ueber neue Katalogschluessel
  widgets.calendar.dayEventsOne/dayEventsMany (Mehrzahl-Konvention wie
  configMaxEventsOne/Many, D-05)
- Neue Tests: Tag mit Terminen ist <button> und reagiert auf
  Fokus/Weggehen wie auf Maus-Hover; Tag ohne Termine bleibt <div>;
  Einzahl-/Mehrzahl-Beschriftung; echter Tab-Stopp nachgewiesen

Nach diesem Umbau: klick-regeln 5 (nur die vier <img onError> +
Taschenrechner-Rahmen), semantic 0, a11y gesamt 14, errors 0 — Aufgabe
1 des Plans vollstaendig.

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 14:51:41 +02:00
parent b601141bcf
commit e651c24647
4 changed files with 145 additions and 14 deletions
@@ -249,20 +249,8 @@ export function CalendarWidget({ config }: WidgetProps) {
.filter(Boolean)
.join(' ');
return (
<div
key={day.key}
data-testid="calendar-day"
data-date={day.key}
data-today={day.isToday || undefined}
data-outside={!day.inCurrentMonth || undefined}
className={cellClass}
onMouseEnter={(e) =>
hasEvents &&
setHover({ key: day.key, rect: e.currentTarget.getBoundingClientRect() })
}
onMouseLeave={() => setHover(null)}
>
const cellContent = (
<>
<span>{day.date.getDate()}</span>
{hasEvents && (
// Wrapper traegt die Positionierung, die einzelnen Kreise nicht
@@ -298,6 +286,61 @@ export function CalendarWidget({ config }: WidgetProps) {
})}
</span>
)}
</>
);
// Nur Tage MIT Terminen werden zu Tab-Stopps (D-01) — ein
// Widget mit 42 zusaetzlichen Tab-Stopps waere eine
// Verschlechterung. Wer nicht mit der Maus arbeitet, bekam
// die Termin-Einblendung bislang gar nicht zu sehen; die
// Schaltflaeche traegt deshalb zusaetzlich onFocus/onBlur mit
// demselben Rumpf wie onMouseEnter/onMouseLeave.
if (hasEvents) {
const dayLabel = new Intl.DateTimeFormat('de-DE', {
weekday: 'short',
day: '2-digit',
month: '2-digit',
}).format(day.date);
const eventCount = day.events.length;
const ariaLabel =
eventCount === 1
? t('calendar.dayEventsOne', { date: dayLabel })
: t('calendar.dayEventsMany', { date: dayLabel, count: eventCount });
return (
<button
key={day.key}
type="button"
data-testid="calendar-day"
data-date={day.key}
data-today={day.isToday || undefined}
data-outside={!day.inCurrentMonth || undefined}
className={`${cellClass} w-full text-left`}
aria-label={ariaLabel}
onMouseEnter={(e) =>
setHover({ key: day.key, rect: e.currentTarget.getBoundingClientRect() })
}
onMouseLeave={() => setHover(null)}
onFocus={(e) =>
setHover({ key: day.key, rect: e.currentTarget.getBoundingClientRect() })
}
onBlur={() => setHover(null)}
>
{cellContent}
</button>
);
}
return (
<div
key={day.key}
data-testid="calendar-day"
data-date={day.key}
data-today={day.isToday || undefined}
data-outside={!day.inCurrentMonth || undefined}
className={cellClass}
>
{cellContent}
</div>
);
})}