Fiktiver Demo-Shop mit absichtlichen Barrieren. Keine echten Käufe.

Zurück zum Demo-Shop

Einzelbeispiel · v1

Cookie-Auswahl und Tastaturfokus.

Öffne die echten Cookie-Einstellungen des Shops. Vergleiche den ersten Fokus, die Begrenzung im Dialog und den Fokus nach dem Schließen.

Vorbereiten setzt nur die Cookie-Auswahl auf offen zurück und schließt den Dialog. Eine Anmeldung ist nicht nötig.

Gemeinsamen Quellcode ansehen

Dies sind die tatsächlichen Quelldateien dieses Beispiels. Der erzeugte Stand wird bei der Build-Validierung mit der Implementierung verglichen. Imports und gemeinsame Hilfsfunktionen bleiben sichtbar; es gibt keine getrennte Lehrkopie.

src/components/features/consent/cookie-controls.tsx

'use client';
import {useEffect, useRef, useState} from 'react';
import {useTranslations} from 'next-intl';
import {Button} from '@/components/ui/button';
import {Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription} from '@/components/ui/dialog';
import {useDemoState} from '@/lib/state/use-demo-state';
import {consentChoice} from '@/lib/state/consent';
import type {Variant} from '@/lib/routes';

export function CookieControls({variant}: {variant: Variant}) {
  const t = useTranslations('Cookies');
  const [consent, setConsent] = useDemoState('consent');
  const [open, setOpen] = useState(false);
  const [analytics, setAnalytics] = useState(false);
  const [marketing, setMarketing] = useState(false);
  const title = useRef<HTMLHeadingElement>(null);
  const returnTarget = useRef<HTMLElement | null>(null);
  const footerTrigger = useRef<HTMLButtonElement>(null);
  useEffect(() => {const close = () => setOpen(false); window.addEventListener('demo-reset', close); return () => window.removeEventListener('demo-reset', close);}, []);
  useEffect(() => {
    if (!open || variant !== 'barriers') return;
    const escape = (event: KeyboardEvent) => {if (event.key === 'Escape') setOpen(false);};
    window.addEventListener('keydown', escape);
    return () => window.removeEventListener('keydown', escape);
  }, [open, variant]);
  function showSettings() {
    returnTarget.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
    setAnalytics(consent.analytics); setMarketing(consent.marketing); setOpen(true);
  }
  function choose(analyticsValue: boolean, marketingValue: boolean) {
    if (setConsent(consentChoice(analyticsValue, marketingValue))) {
      setOpen(false);
      if (!open) footerTrigger.current?.focus();
    }
  }
  function restoreFocus() {
    const target = returnTarget.current?.isConnected ? returnTarget.current : footerTrigger.current;
    target?.focus();
  }
  const fields = <>
    <div className="consent-options">
      <label><input type="checkbox" checked disabled/><span><strong>{t('necessary')}</strong><span>{t('necessaryHelp')}</span></span></label>
      <label><input type="checkbox" checked={analytics} onChange={event => setAnalytics(event.target.checked)}/><span><strong>{t('analytics')}</strong><span>{t('analyticsHelp')}</span></span></label>
      <label><input type="checkbox" checked={marketing} onChange={event => setMarketing(event.target.checked)}/><span><strong>{t('marketing')}</strong><span>{t('marketingHelp')}</span></span></label>
    </div>
    <div className="cookie-actions"><Button onClick={() => choose(analytics, marketing)}>{t('save')}</Button><Button variant="outline" onClick={() => choose(false, false)}>{t('reject')}</Button><Button variant="outline" onClick={() => setOpen(false)}>{t('close')}</Button></div>
  </>;
  return <>
    <div className="cookie-footer-control"><Button ref={footerTrigger} id="cookie-settings-trigger" variant="outline" size="sm" onClick={showSettings}>{t('settings')}</Button><span role="status">{t('status', {choice: t(consent.choice)})}</span></div>
    {consent.choice === 'unset' && !open && <section className="cookie-banner" aria-labelledby="cookie-banner-heading" data-testid="cookie-banner"><div><h2 id="cookie-banner-heading">{t('bannerTitle')}</h2><p>{t('bannerText')}</p></div><div className="cookie-actions"><Button size="sm" onClick={() => choose(false, false)} variant="outline">{t('reject')}</Button><Button size="sm" onClick={() => choose(true, true)}>{t('accept')}</Button><Button size="sm" variant="outline" onClick={showSettings}>{t('manage')}</Button></div></section>}
    {variant === 'improved' ? <Dialog open={open} onOpenChange={setOpen}><DialogPortal><DialogOverlay className="dialog-overlay"/><DialogContent className="cookie-dialog" id="cookie-dialog" data-testid="cookie-dialog" onOpenAutoFocus={event => {event.preventDefault(); title.current?.focus();}} onCloseAutoFocus={event => {event.preventDefault(); restoreFocus();}}><DialogTitle ref={title} tabIndex={-1} className="dialog-heading">{t('dialogTitle')}</DialogTitle><DialogDescription>{t('dialogText')}</DialogDescription>{fields}</DialogContent></DialogPortal></Dialog> : open &&
      // Intentional cookie-modal-focus failure: this modal-looking fixture does not move or contain focus.
      <div className="cookie-dialog scenario-cookie-modal-barriers" id="cookie-dialog" data-testid="cookie-dialog" role="dialog" aria-modal="true" aria-labelledby="broken-cookie-title" aria-describedby="broken-cookie-description"><h2 id="broken-cookie-title">{t('dialogTitle')}</h2><p id="broken-cookie-description">{t('dialogText')}</p>{fields}</div>}
  </>;
}

src/components/ui/dialog.tsx

'use client';
import * as Primitive from '@radix-ui/react-dialog';
export const Dialog = Primitive.Root;
export const DialogPortal = Primitive.Portal;
export const DialogOverlay = Primitive.Overlay;
export const DialogContent = Primitive.Content;
export const DialogTitle = Primitive.Title;
export const DialogDescription = Primitive.Description;
export const DialogClose = Primitive.Close;

src/app/[locale]/globals.css

.cookie-dialog {position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:min(calc(100% - 2rem),36rem);max-height:calc(100dvh - 2rem);overflow:auto;z-index:51;background:var(--card);color:var(--foreground);border:1px solid var(--border);border-radius:.75rem;padding:2rem;}
.scenario-cookie-modal-barriers {inset:auto 1rem 1rem auto;transform:none;}