8bf3601a18
Befunde aus dem Browser-Rundgang am 21.09.2026:
- Die Grossansicht lag in einem `react-grid-item` mit CSS-`transform`; ein
transformierter Vorfahr wird fuer `position: fixed` zum Bezugsrahmen, der
Dialog war deshalb auf die Kachelflaeche (531x216) beschraenkt statt den
Viewport zu fuellen. Jetzt per `createPortal` in `document.body`, wie der
Kalender-Tooltip.
- Wechselintervall "1 Minuten" -> ICU-Plural (`one {# Minute}`), de + en; der
Formular-Test nutzt dafuer den echten `createTranslator` von next-intl auf
der echten de.json statt eines `{n}`-Ersatzes.
- Standardgroesse 8x8 (216 px hoch) war zu flach fuer ein Foto -> 8x12 wie die
Kalender-Vorgabe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useRef } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { useTranslations } from 'next-intl';
|
|
|
|
interface PictureFrameLightboxProps {
|
|
src: string;
|
|
caption?: string;
|
|
onClose: () => void;
|
|
}
|
|
|
|
/**
|
|
* Grossansicht des Bilderrahmen-Widgets (quick-260921-pi9).
|
|
*
|
|
* Aufbau wie widget-catalog-modal.tsx: Hintergrund als echte, benannte
|
|
* Schaltflaeche (schliesst), Dialog `role="dialog" aria-modal` mit
|
|
* `tabIndex={-1}` und Fokus beim Einhaengen, Escape-Listener mit Aufraeumung,
|
|
* Schliessen-Knopf oben rechts. Die Fokus-RUECKGABE an den Bild-Knopf macht
|
|
* das Widget selbst (es kennt den Ausloeser), nicht diese Komponente.
|
|
*
|
|
* `referrerPolicy="no-referrer"` auch hier (T-PI9-09): ein Fremdhost erfaehrt
|
|
* die Tessera-Adresse nicht. Die Unterschrift ist ein React-Textknoten,
|
|
* kein HTML (T-PI9-06).
|
|
*
|
|
* Gerendert per `createPortal` in `document.body` (wie der Kalender-Tooltip):
|
|
* die Kachel liegt in einem `react-grid-item` mit CSS-`transform`, und ein
|
|
* transformierter Vorfahr macht sich fuer `position: fixed` zum Bezugsrahmen —
|
|
* ohne Portal waere die Grossansicht auf die Kachelflaeche beschraenkt
|
|
* (im Browser-Rundgang am 21.09.2026 so gesehen: Dialog 531x216 statt Viewport).
|
|
*/
|
|
export function PictureFrameLightbox({ src, caption, onClose }: PictureFrameLightboxProps) {
|
|
const t = useTranslations('widgets');
|
|
const tCommon = useTranslations('common');
|
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
}
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
dialogRef.current?.focus();
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
}, [onClose]);
|
|
|
|
if (typeof document === 'undefined') return null;
|
|
|
|
return createPortal(
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label={t('pictureFrame.close')}
|
|
data-testid="picture-frame-lightbox-backdrop"
|
|
className="fixed inset-0 bg-black/80"
|
|
/>
|
|
|
|
<div
|
|
ref={dialogRef}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={caption ?? t('pictureFrame.open')}
|
|
className="relative z-50 flex max-h-[95vh] max-w-[95vw] flex-col items-center gap-2 p-4"
|
|
tabIndex={-1}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
data-testid="picture-frame-lightbox-close"
|
|
className="absolute -right-1 -top-1 z-10 rounded-full bg-black/60 p-1.5 text-white transition-colors hover:bg-black/80"
|
|
aria-label={tCommon('close')}
|
|
>
|
|
<svg
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="20"
|
|
height="20"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
</svg>
|
|
</button>
|
|
|
|
<img
|
|
src={src}
|
|
alt={caption ?? ''}
|
|
referrerPolicy="no-referrer"
|
|
data-testid="picture-frame-lightbox-image"
|
|
className="max-h-[85vh] max-w-[90vw] rounded object-contain shadow-2xl"
|
|
/>
|
|
{caption && (
|
|
<p className="max-w-[90vw] truncate text-center text-sm text-white" data-testid="picture-frame-lightbox-caption">
|
|
{caption}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|