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:
@@ -26,6 +26,8 @@ vi.mock('next-intl', () => ({
|
||||
'calendar.upcomingTitle': 'Nächste Termine',
|
||||
'calendar.tooltipMore': 'Weitere Termine vorhanden',
|
||||
'calendar.allDay': 'ganztägig',
|
||||
'calendar.dayEventsOne': '{date}: 1 Termin',
|
||||
'calendar.dayEventsMany': '{date}: {count} Termine',
|
||||
},
|
||||
};
|
||||
const raw = map[ns]?.[key] ?? key;
|
||||
@@ -514,4 +516,86 @@ describe('CalendarWidget', () => {
|
||||
|
||||
await waitFor(() => expect(mockFetchEvents).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
// quick-260921-jt4 (D-01): Tage MIT Terminen sind echte Schaltflaechen und
|
||||
// per Fokus bedienbar; Tage OHNE Termine bleiben Nicht-Tab-Stopps.
|
||||
it('Test 11: Ein Tag MIT Terminen ist eine echte <button>-Schaltflaeche und zeigt beim Fokussieren dieselbe Einblendung wie beim Ueberfahren mit der Maus', async () => {
|
||||
mockFetchEvents.mockResolvedValue([
|
||||
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||
]);
|
||||
|
||||
const { CalendarWidget } = await import('./calendar-widget');
|
||||
render(<CalendarWidget instanceId="cal-11" config={{}} isEditMode={false} />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Juli 2026')).toBeInTheDocument());
|
||||
|
||||
const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement;
|
||||
expect(day20.tagName).toBe('BUTTON');
|
||||
|
||||
fireEvent.focus(day20);
|
||||
const tooltip = screen.getByTestId('calendar-day-tooltip');
|
||||
expect(tooltip).toHaveTextContent('Team Meeting');
|
||||
|
||||
fireEvent.blur(day20);
|
||||
expect(screen.queryByTestId('calendar-day-tooltip')).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 12: Ein Tag OHNE Termine bleibt eine <div> ohne Handler und ist kein Tab-Stopp', async () => {
|
||||
mockFetchEvents.mockResolvedValue([]);
|
||||
|
||||
const { CalendarWidget } = await import('./calendar-widget');
|
||||
render(<CalendarWidget instanceId="cal-12" config={{}} isEditMode={false} />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Juli 2026')).toBeInTheDocument());
|
||||
|
||||
const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement;
|
||||
expect(day20.tagName).toBe('DIV');
|
||||
});
|
||||
|
||||
it('Test 13: die Schaltflaeche eines Tages mit Terminen traegt einen Namen, der Datum und Terminzahl nennt', async () => {
|
||||
mockFetchEvents.mockResolvedValue([
|
||||
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||
ev('Lunch', new Date(2026, 6, 20, 14, 0), new Date(2026, 6, 20, 15, 0)),
|
||||
]);
|
||||
|
||||
const { CalendarWidget } = await import('./calendar-widget');
|
||||
render(<CalendarWidget instanceId="cal-13" config={{}} isEditMode={false} />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Juli 2026')).toBeInTheDocument());
|
||||
|
||||
const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement;
|
||||
expect(day20.getAttribute('aria-label')).toContain('20.07');
|
||||
expect(day20.getAttribute('aria-label')).toContain('2 Termine');
|
||||
});
|
||||
|
||||
it('Test 14: die Schaltflaeche eines Tages mit genau einem Termin nutzt die Einzahl-Beschriftung', async () => {
|
||||
mockFetchEvents.mockResolvedValue([
|
||||
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||
]);
|
||||
|
||||
const { CalendarWidget } = await import('./calendar-widget');
|
||||
render(<CalendarWidget instanceId="cal-14" config={{}} isEditMode={false} />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Juli 2026')).toBeInTheDocument());
|
||||
|
||||
const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement;
|
||||
expect(day20.getAttribute('aria-label')).toContain('1 Termin');
|
||||
expect(day20.getAttribute('aria-label')).not.toContain('1 Termine');
|
||||
});
|
||||
|
||||
it('Test 15: der Fokus landet tatsaechlich auf der Tages-Schaltflaeche (echter Tab-Stopp, nicht nur optisch)', async () => {
|
||||
mockFetchEvents.mockResolvedValue([
|
||||
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||
]);
|
||||
|
||||
const { CalendarWidget } = await import('./calendar-widget');
|
||||
render(<CalendarWidget instanceId="cal-15" config={{}} isEditMode={false} />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Juli 2026')).toBeInTheDocument());
|
||||
|
||||
const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement;
|
||||
day20.focus();
|
||||
|
||||
expect(document.activeElement).toBe(day20);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -244,6 +244,8 @@
|
||||
"upcomingTitle": "Nächste Termine",
|
||||
"tooltipMore": "Weitere Termine vorhanden",
|
||||
"allDay": "ganztägig",
|
||||
"dayEventsOne": "{date}: 1 Termin",
|
||||
"dayEventsMany": "{date}: {count} Termine",
|
||||
"configShowMonth": "Monatsansicht anzeigen",
|
||||
"configMaxEvents": "Anzahl Termine",
|
||||
"configMaxEventsNone": "Ausblenden",
|
||||
|
||||
@@ -244,6 +244,8 @@
|
||||
"upcomingTitle": "Upcoming events",
|
||||
"tooltipMore": "More events available",
|
||||
"allDay": "all day",
|
||||
"dayEventsOne": "{date}: 1 event",
|
||||
"dayEventsMany": "{date}: {count} events",
|
||||
"configShowMonth": "Show month view",
|
||||
"configMaxEvents": "Number of events",
|
||||
"configMaxEventsNone": "Hide",
|
||||
|
||||
Reference in New Issue
Block a user