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.upcomingTitle': 'Nächste Termine',
|
||||||
'calendar.tooltipMore': 'Weitere Termine vorhanden',
|
'calendar.tooltipMore': 'Weitere Termine vorhanden',
|
||||||
'calendar.allDay': 'ganztägig',
|
'calendar.allDay': 'ganztägig',
|
||||||
|
'calendar.dayEventsOne': '{date}: 1 Termin',
|
||||||
|
'calendar.dayEventsMany': '{date}: {count} Termine',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const raw = map[ns]?.[key] ?? key;
|
const raw = map[ns]?.[key] ?? key;
|
||||||
@@ -514,4 +516,86 @@ describe('CalendarWidget', () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(mockFetchEvents).toHaveBeenCalledTimes(1));
|
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)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
return (
|
const cellContent = (
|
||||||
<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)}
|
|
||||||
>
|
|
||||||
<span>{day.date.getDate()}</span>
|
<span>{day.date.getDate()}</span>
|
||||||
{hasEvents && (
|
{hasEvents && (
|
||||||
// Wrapper traegt die Positionierung, die einzelnen Kreise nicht
|
// Wrapper traegt die Positionierung, die einzelnen Kreise nicht
|
||||||
@@ -298,6 +286,61 @@ export function CalendarWidget({ config }: WidgetProps) {
|
|||||||
})}
|
})}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -244,6 +244,8 @@
|
|||||||
"upcomingTitle": "Nächste Termine",
|
"upcomingTitle": "Nächste Termine",
|
||||||
"tooltipMore": "Weitere Termine vorhanden",
|
"tooltipMore": "Weitere Termine vorhanden",
|
||||||
"allDay": "ganztägig",
|
"allDay": "ganztägig",
|
||||||
|
"dayEventsOne": "{date}: 1 Termin",
|
||||||
|
"dayEventsMany": "{date}: {count} Termine",
|
||||||
"configShowMonth": "Monatsansicht anzeigen",
|
"configShowMonth": "Monatsansicht anzeigen",
|
||||||
"configMaxEvents": "Anzahl Termine",
|
"configMaxEvents": "Anzahl Termine",
|
||||||
"configMaxEventsNone": "Ausblenden",
|
"configMaxEventsNone": "Ausblenden",
|
||||||
|
|||||||
@@ -244,6 +244,8 @@
|
|||||||
"upcomingTitle": "Upcoming events",
|
"upcomingTitle": "Upcoming events",
|
||||||
"tooltipMore": "More events available",
|
"tooltipMore": "More events available",
|
||||||
"allDay": "all day",
|
"allDay": "all day",
|
||||||
|
"dayEventsOne": "{date}: 1 event",
|
||||||
|
"dayEventsMany": "{date}: {count} events",
|
||||||
"configShowMonth": "Show month view",
|
"configShowMonth": "Show month view",
|
||||||
"configMaxEvents": "Number of events",
|
"configMaxEvents": "Number of events",
|
||||||
"configMaxEventsNone": "Hide",
|
"configMaxEventsNone": "Hide",
|
||||||
|
|||||||
Reference in New Issue
Block a user