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

Zurück zum Demo-Shop

Einzelbeispiel · v1

Lighthouse und axe: dieselbe Produktseite.

Eine Schreibtischleuchte mit gezielt eingebauten Barrieren. Vergleiche Regelauswahl, Tastaturbedienung und einen geöffneten Dialog in drei getrennten Fällen.

Du brauchst weder Anmeldung noch Warenkorb oder Cookie-Auswahl. Vorbereiten leert nur die vorübergehende Aktion und den Dialog dieses Beispiels. Setze für wiederholbare Scans die Darstellung auf 100 % Textgröße, ohne Textabstände und Vorschau.

Referenzversionen: Lighthouse 13.5.0, axe-core 4.13.0. Halte die Versionen Deiner eigenen Berichte fest.

Gehe mit Tab von den Testfall-Links zur Hinzufügen-Aktion. In der Barrieren-Variante wird das klickbare div übersprungen. Der native Button der verbesserten Variante funktioniert mit Enter und Leertaste. Ein unauffälliger Scan bestätigt nicht, dass Du diese Aktion ausführen kannst.

Demo-Produkt · kein Kauf

Schreibtischleuchte

49,00 €

Olivgrünes Metall, ein runder Schirm und warmes Licht für Deinen Schreibtisch. Dieses Produkt ist erfunden.

Leuchte zum Demo-Warenkorb hinzufügen

In diesem Beispiel wurde keine Leuchte hinzugefügt.

Weiter zum Quellcode
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/examples/lighthouse-product.tsx

'use client';
import Image from 'next/image';
import Link from 'next/link';
import {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 {asset} from '@/lib/fixtures/assets';
import {formatPrice} from '@/lib/format';
import {lighthouseExamplePath,type DemoRoute,type LighthouseCase} from '@/lib/routes';
import {lighthouseVersions} from '@/lib/examples/lighthouse';

export function LighthouseProduct({route,testCase}:{route:DemoRoute;testCase:LighthouseCase}){
 const t=useTranslations('Lighthouse');
 const assets=useTranslations('Assets');
 const improved=route.variant==='improved';
 const photo=asset('desk-lamp');
 const [added,setAdded]=useState(false);
 const [open,setOpen]=useState(false);
 const trigger=useRef<HTMLButtonElement>(null);
 const care=useRef<HTMLDetailsElement>(null);
 function showCare(){if(care.current)care.current.open=true;}
 return <section className="lighthouse-product" aria-labelledby="lh-product-title">
  <nav className="lighthouse-case-nav" aria-label={t('cases')}>
   {(['rules','keyboard','dialog'] as const).map(value=><Link key={value} href={lighthouseExamplePath(route,value)} aria-current={testCase===value?'page':undefined}>{t(`${value}.label`)}</Link>)}
  </nav>
  <p className="field-help">{t('versions',{...lighthouseVersions})}</p>
  <p id="lh-case-instructions">{t(`${testCase}.instructions`)}</p>
  <div className="product-detail-grid">
   <div className="product-gallery"><Image src={photo.file} width={photo.width} height={photo.height} alt={assets('desk-lamp.alt')} sizes="(max-width: 760px) 100vw, 55vw" priority/></div>
   <div className="product-information">
    <p className="eyebrow">{t('edition')}</p><h2 id="lh-product-title">{t('product')}</h2>
    <p className="product-detail-price">{formatPrice(4900,route.locale)}</p><p>{t('description')}</p>
    {testCase==='rules'&&<>
     <h3>{t('lightOutput')}</h3>
     {/* Intentional role-img-alt: a meaningful chart has no name only in barriers. */}
     <div id="lh-chart" role="img" aria-label={improved?t('chartAlternative'):undefined} className="lighthouse-chart"/>
     <p className="field-help">{t('chartCaption')}</p>
     <h3>{t('delivery')}</h3>
     {/* Explicit -1 prevents Chromium's automatic scroll-container tab stop in barriers. */}
     <div id="lh-scroll" className="lighthouse-scroll" tabIndex={improved?0:-1}>{[1,2,3].map(number=><p key={number}>{t(`delivery${number}`)}</p>)}</div>
     {/* Intentional nested-interactive; shared Button remains unchanged. */}
     <div id="lh-nested" className="lighthouse-nested" role={improved?undefined:'button'} tabIndex={improved?undefined:0} onClick={improved?undefined:showCare} onKeyDown={improved?undefined:event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();showCare();}}}><a href="#lh-details" onClick={showCare}>{t('detailsLink')}</a></div>
     {/* Intentional avoid-inline-spacing; !important is applied after hydration. */}
     <p id="lh-spacing" ref={node=>{if(node&&!improved)node.style.setProperty('letter-spacing','0','important');}}>{t('spacing')}</p>
     <details ref={care} id="lh-details"><summary id="lh-summary" className="lighthouse-summary">{improved?t('care'):null}</summary><p>{t('careText')}</p></details>
     <div role="tablist" aria-label={t('information')}><div id="lh-tab" className="lighthouse-tab" role="tab" aria-selected="true" aria-controls="lh-panel" tabIndex={0}>{improved?t('specifications'):null}</div></div>
     <div id="lh-panel" role="tabpanel" aria-label={t('specifications')}><p>{t('specificationText')}</p></div>
    </>}
    {testCase==='keyboard'&&<>
     {/* Intentional manual case: no role, tab stop or keyboard handler on this div. */}
     {improved?<Button id="lh-cart" type="button" onClick={()=>setAdded(true)}>{t('add')}</Button>:<div id="lh-cart" className="button button-primary button-default" onClick={()=>setAdded(true)}>{t('add')}</div>}
     <p id="lh-status" role="status" className="sample-result">{added?t('added'):t('empty')}</p>
     <a id="lh-after-action" href="#example-source">{t('afterAction')}</a>
    </>}
    {testCase==='dialog'&&<>
     <Button ref={trigger} id="lh-open" type="button" onClick={()=>setOpen(true)}>{t('open')}</Button>
     <Dialog open={open} onOpenChange={setOpen}><DialogPortal><DialogOverlay className="dialog-overlay"/><DialogContent className="cookie-dialog" id="lh-dialog" onCloseAutoFocus={event=>{event.preventDefault();trigger.current?.focus();}}><DialogTitle>{t('dialogTitle')}</DialogTitle><DialogDescription>{t('dialogDescription')}</DialogDescription>
      {/* The dialog, focus trap and Escape recovery are correct in both variants. */}
      <Button id="lh-close" type="button" variant="outline" className="lighthouse-dialog-close" aria-label={improved?t('close'):undefined} onClick={()=>setOpen(false)}><svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m6 6 12 12M18 6 6 18" fill="none" stroke="currentColor" strokeWidth="2"/></svg></Button>
     </DialogContent></DialogPortal></Dialog>
    </>}
   </div>
  </div>
 </section>;
}

src/lib/examples/lighthouse.ts

// These are intended rule candidates, not recorded scanner findings.
export const lighthouseRuleCases=[
 {id:'lighthouse-role-image',targetId:'lh-chart',rule:'role-img-alt',sc:'1.1.1'},
 {id:'lighthouse-scroll-region',targetId:'lh-scroll',rule:'scrollable-region-focusable',sc:'2.1.1'},
 {id:'lighthouse-nested-control',targetId:'lh-nested',rule:'nested-interactive',sc:'4.1.2'},
 {id:'lighthouse-text-spacing',targetId:'lh-spacing',rule:'avoid-inline-spacing',sc:'1.4.12'},
 {id:'lighthouse-summary-name',targetId:'lh-summary',rule:'summary-name',sc:'4.1.2'},
 {id:'lighthouse-tab-name',targetId:'lh-tab',rule:'aria-tab-name',sc:'4.1.2'}
] as const;

export const lighthouseVersions={lighthouse:'13.5.0',axe:'4.13.0'} as const;

src/app/[locale]/globals.css

.lighthouse-product {min-width:0;}
.lighthouse-scroll {height:6rem;overflow:auto;border:1px solid var(--border);padding:.75rem;margin-bottom:1rem;}
.lighthouse-chart {display:flex;gap:1rem;align-items:end;height:5rem;margin-bottom:.5rem;}
.lighthouse-chart::before {content:'';display:block;width:40%;height:60%;background:var(--primary);}
.lighthouse-chart::after {content:'';display:block;width:40%;height:100%;background:var(--primary);}
.lighthouse-summary {min-height:44px;padding:.5rem 0;cursor:pointer;}
.lighthouse-tab {display:inline-flex;align-items:center;min-height:44px;min-width:100px;background:var(--card);color:var(--foreground);border:1px solid var(--border);padding:.5rem 1rem;cursor:pointer;}
.lighthouse-nested {padding:.5rem 0;}.lighthouse-nested>a {display:inline-flex;min-height:44px;align-items:center;}
.lighthouse-case-nav {display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1rem;}
.lighthouse-dialog-close {min-width:48px;}