fix(quick-260921-jt4): widget-catalog-modal Hintergrund wird echte Schaltflaeche
- Aeussere Flaeche verliert onClick, die bislang nur optische Hintergrund-Flaeche wird eine benannte <button> und traegt onClose (D-01); verliert dabei aria-hidden, weil ein fokussierbares Element nicht verborgen sein darf - stopPropagation auf der Dialogflaeche entfaellt als toter Code, weil der Hintergrund jetzt Geschwister statt Vorfahr ist - Fest verdrahtetes englisches aria-label="Close" durch common.close ersetzt - Neuer Katalogschluessel widgets.catalogClose in de.json/en.json - Neue Testdatei: Hintergrund schliesst (Klick + Tastatur), Dialogklick schliesst nicht, Escape weiterhin, Kartenauswahl fuegt Widget hinzu Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -0,0 +1,128 @@
|
|||||||
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock next-intl useTranslations, namespaced like the real catalog/common usage.
|
||||||
|
vi.mock('next-intl', () => ({
|
||||||
|
useTranslations: (namespace: string) => (key: string) => {
|
||||||
|
const translations: Record<string, Record<string, string>> = {
|
||||||
|
widgets: {
|
||||||
|
catalogTitle: 'Widget hinzufügen',
|
||||||
|
catalogClose: 'Dialog schließen',
|
||||||
|
'clock.name': 'Uhr',
|
||||||
|
'clock.description': 'Zeigt die aktuelle Zeit',
|
||||||
|
'search.name': 'Suche',
|
||||||
|
'search.description': 'Schnellsuche',
|
||||||
|
'calendar.name': 'Kalender',
|
||||||
|
'calendar.description': 'Kommende Termine',
|
||||||
|
'note.name': 'Notiz',
|
||||||
|
'note.description': 'Schnelle Notizen',
|
||||||
|
'calculator.name': 'Taschenrechner',
|
||||||
|
'calculator.description': 'Einfache Berechnungen',
|
||||||
|
'favorites.name': 'Favoriten',
|
||||||
|
'favorites.description': 'Schnellzugriff auf Favoriten',
|
||||||
|
'stopwatch.name': 'Stoppuhr',
|
||||||
|
'stopwatch.description': 'Zeit messen',
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
close: 'Schließen',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return translations[namespace]?.[key] ?? key;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { WidgetCatalogModal } from './widget-catalog-modal';
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
isOpen: true,
|
||||||
|
onClose: vi.fn(),
|
||||||
|
onAddWidget: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('WidgetCatalogModal', () => {
|
||||||
|
it('renders nothing when isOpen is false', () => {
|
||||||
|
render(<WidgetCatalogModal {...baseProps} isOpen={false} />);
|
||||||
|
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the dialog with title and all widget type options when open', () => {
|
||||||
|
render(<WidgetCatalogModal {...baseProps} />);
|
||||||
|
|
||||||
|
expect(screen.getByRole('dialog', { name: 'Widget hinzufügen' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /Uhr/ })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking the backdrop button closes the dialog (D-01)', async () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Dialog schließen' }));
|
||||||
|
|
||||||
|
expect(onClose).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('activating the backdrop button via a real keyboard Enter closes the dialog (D-01)', async () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
||||||
|
|
||||||
|
const backdrop = screen.getByRole('button', { name: 'Dialog schließen' });
|
||||||
|
backdrop.focus();
|
||||||
|
await user.keyboard('{Enter}');
|
||||||
|
|
||||||
|
expect(onClose).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking inside the dialog surface does not close the dialog', async () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('dialog', { name: 'Widget hinzufügen' }));
|
||||||
|
|
||||||
|
expect(onClose).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clicking a widget type option adds the widget and closes the dialog', async () => {
|
||||||
|
const onAddWidget = vi.fn();
|
||||||
|
const onClose = vi.fn();
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
render(<WidgetCatalogModal {...baseProps} onAddWidget={onAddWidget} onClose={onClose} />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /Uhr/ }));
|
||||||
|
|
||||||
|
expect(onAddWidget).toHaveBeenCalledWith('clock');
|
||||||
|
expect(onClose).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('pressing Escape closes the dialog', () => {
|
||||||
|
const onClose = vi.fn();
|
||||||
|
|
||||||
|
render(<WidgetCatalogModal {...baseProps} onClose={onClose} />);
|
||||||
|
|
||||||
|
fireEvent.keyDown(document, { key: 'Escape' });
|
||||||
|
|
||||||
|
expect(onClose).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('the close button in the dialog header carries the shared common.close label', () => {
|
||||||
|
render(<WidgetCatalogModal {...baseProps} />);
|
||||||
|
|
||||||
|
// Two buttons named "Schließen": the header close icon button.
|
||||||
|
// Query specifically among the dialog's own buttons, excluding the backdrop
|
||||||
|
// (backdrop is named "Dialog schließen", the header button "Schließen").
|
||||||
|
expect(screen.getByRole('button', { name: 'Schließen' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -31,6 +31,7 @@ export function WidgetCatalogModal({
|
|||||||
onAddWidget,
|
onAddWidget,
|
||||||
}: WidgetCatalogModalProps) {
|
}: WidgetCatalogModalProps) {
|
||||||
const t = useTranslations('widgets');
|
const t = useTranslations('widgets');
|
||||||
|
const tCommon = useTranslations('common');
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Focus trap + Escape handler
|
// Focus trap + Escape handler
|
||||||
@@ -53,12 +54,16 @@ export function WidgetCatalogModal({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center"
|
{/* Hintergrund — jetzt eine echte, benannte Schaltflaeche statt einer rein
|
||||||
onClick={onClose}
|
optischen Flaeche: ein fokussierbares Element darf vor der Vorlesehilfe
|
||||||
>
|
nicht verborgen sein (D-01). */}
|
||||||
{/* Backdrop — visually only, hidden from assistive tech */}
|
<button
|
||||||
<div className="fixed inset-0 bg-black/50" aria-hidden="true" />
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label={t('catalogClose')}
|
||||||
|
className="fixed inset-0 bg-black/50"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Dialog */}
|
{/* Dialog */}
|
||||||
<div
|
<div
|
||||||
@@ -67,7 +72,6 @@ export function WidgetCatalogModal({
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={t('catalogTitle')}
|
aria-label={t('catalogTitle')}
|
||||||
className="relative z-50 w-full max-w-[480px] rounded-lg border bg-card p-6 shadow-xl"
|
className="relative z-50 w-full max-w-[480px] rounded-lg border bg-card p-6 shadow-xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@@ -79,7 +83,7 @@ export function WidgetCatalogModal({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
className="rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
||||||
aria-label="Close"
|
aria-label={tCommon('close')}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
"emptyBody": "Klicken Sie auf Bearbeiten, um Widgets hinzuzufügen.",
|
"emptyBody": "Klicken Sie auf Bearbeiten, um Widgets hinzuzufügen.",
|
||||||
"addWidget": "Widget hinzufügen",
|
"addWidget": "Widget hinzufügen",
|
||||||
"catalogTitle": "Widget hinzufügen",
|
"catalogTitle": "Widget hinzufügen",
|
||||||
|
"catalogClose": "Dialog schließen",
|
||||||
"deleteTooltip": "Widget entfernen",
|
"deleteTooltip": "Widget entfernen",
|
||||||
"dragHint": "Ziehen Sie die Kachel, um sie zu verschieben",
|
"dragHint": "Ziehen Sie die Kachel, um sie zu verschieben",
|
||||||
"editMode": "Dashboard bearbeiten",
|
"editMode": "Dashboard bearbeiten",
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
"emptyBody": "Click edit to add widgets to your dashboard.",
|
"emptyBody": "Click edit to add widgets to your dashboard.",
|
||||||
"addWidget": "Add widget",
|
"addWidget": "Add widget",
|
||||||
"catalogTitle": "Add widget",
|
"catalogTitle": "Add widget",
|
||||||
|
"catalogClose": "Close dialog",
|
||||||
"deleteTooltip": "Remove widget",
|
"deleteTooltip": "Remove widget",
|
||||||
"dragHint": "Drag the tile to move it",
|
"dragHint": "Drag the tile to move it",
|
||||||
"editMode": "Edit dashboard",
|
"editMode": "Edit dashboard",
|
||||||
|
|||||||
Reference in New Issue
Block a user