21c85a8b88
Vier der fuenf noch offenen Regeln aus Teillieferung B bereinigt:
- noRedundantRoles (4): ueberfluessige role-Angaben auf button/time/select
entfernt (maschineller --unsafe-Fix, gelesen).
- useAriaPropsForRole (1): entfaellt automatisch mit obigem Fix -- das
<select role="combobox"> in search-widget.tsx verlangte die fehlenden
ARIA-Attribute nur wegen der ueberfluessigen Rolle.
- useSemanticElements (4): admin-sidebar/settings-sidebar tragen role=
"navigation" jetzt am <nav> statt am <aside> (kein doppeltes Landmark
mehr); widget-wrapper.tsx ist jetzt ein echtes <article> statt
div role="article"; DropZone.tsx trennt die Datei-Entfernen-Schaltflaeche
als Geschwister ab, damit die Drop-Flaeche selbst ein echtes <button>
werden kann (ein <button> darf kein zweites <button> verschachteln).
Die Drop-Flaeche traegt darum jetzt Klick- UND Drag-Handler direkt am
<button>, sonst waere sie ein "statisches" Element mit Ereignis-Handlern
geworden (die zurueckgestellten Regeln noStaticElementInteractions /
noNoninteractiveElementInteractions waeren neu angeschlagen -- geprueft,
bleiben bei 5/11).
- noNoninteractiveTabindex (1): calculator-widget.tsx traegt jetzt
tabIndex={-1} statt {0}. Die Zifferntasten sind bereits echte <button>
und damit selbst Teil der Tab-Reihenfolge; Tastendruecke erreichen
handleKeyboard weiterhin per Bubbling, sobald eine Taste fokussiert ist.
Verhalten unveraendert, nur ein wirkungsloser Tab-Stopp auf dem Container
selbst entfaellt.
Verbleibend: noLabelWithoutControl (22), naechster Schritt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
122 lines
4.6 KiB
TypeScript
122 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { useTranslations } from 'next-intl';
|
|
import { WIDGET_REGISTRY, type WidgetType } from '../widget-registry';
|
|
|
|
interface WidgetWrapperProps {
|
|
widget: {
|
|
id: string;
|
|
widgetType: string;
|
|
config: Record<string, unknown>;
|
|
};
|
|
isEditMode: boolean;
|
|
onRemove: (id: string) => void;
|
|
}
|
|
|
|
/**
|
|
* Wrapper card for all dashboard widgets.
|
|
*
|
|
* quick-260916-dyv: Im Bearbeitungsmodus ist die GANZE Karte der Griff
|
|
* (`widget-drag-handle`, `cursor-grab`). Eine 20 px hohe Kopfleiste mit
|
|
* Griff-Symbol liegt als Overlay ueber dem oberen Kachelrand — nur optischer
|
|
* Hinweis (Tooltip `widgets.dragHint`), kein eigener Griff. Der Loesch-Knopf
|
|
* sitzt rechts in dieser Kopfleiste und traegt `data-no-drag`; welche Ziele
|
|
* KEIN Ziehen starten (Eingabefelder, Knoepfe, Links, [data-no-drag],
|
|
* .widgetNoDrag), entscheidet der cancel-Selektor in dashboard-grid.tsx.
|
|
* Renders the actual widget component via WIDGET_REGISTRY lookup.
|
|
*/
|
|
export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperProps) {
|
|
const t = useTranslations('widgets');
|
|
const definition = WIDGET_REGISTRY[widget.widgetType as WidgetType];
|
|
const WidgetComponent = definition?.component;
|
|
|
|
const cardClassName = isEditMode
|
|
? 'widget-drag-handle relative h-full w-full cursor-grab overflow-hidden rounded-lg border border-primary/40 bg-card shadow-sm active:cursor-grabbing'
|
|
: 'relative h-full w-full overflow-hidden rounded-lg border border-primary/20 bg-card shadow-sm';
|
|
|
|
return (
|
|
<article
|
|
className={cardClassName}
|
|
data-widget-id={widget.id}
|
|
aria-label={definition ? t(definition.nameKey) : widget.widgetType}
|
|
>
|
|
{/* Edit mode: Overlay-Kopfleiste (Griff-Hinweis) mit Loesch-Knopf rechts.
|
|
Bewusst `absolute` und NICHT im Fluss: der Rumpf bleibt h-full, die
|
|
Hoehenkette Karte -> Rumpf bleibt definit (cqh loest weiter auf). */}
|
|
{isEditMode && (
|
|
<div
|
|
className="absolute inset-x-0 top-0 z-10 flex h-5 items-center justify-center bg-muted/70"
|
|
title={t('dragHint')}
|
|
data-testid="widget-drag-head"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="currentColor"
|
|
className="text-muted-foreground"
|
|
aria-hidden="true"
|
|
>
|
|
<circle cx="5" cy="9" r="1.5" />
|
|
<circle cx="12" cy="9" r="1.5" />
|
|
<circle cx="19" cy="9" r="1.5" />
|
|
<circle cx="5" cy="15" r="1.5" />
|
|
<circle cx="12" cy="15" r="1.5" />
|
|
<circle cx="19" cy="15" r="1.5" />
|
|
</svg>
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onRemove(widget.id);
|
|
}}
|
|
className="absolute right-0.5 top-0 flex h-5 w-5 items-center justify-center rounded-full bg-card text-muted-foreground transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
|
aria-label={t('deleteTooltip')}
|
|
title={t('deleteTooltip')}
|
|
data-no-drag=""
|
|
>
|
|
<svg
|
|
aria-hidden="true"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="12"
|
|
height="12"
|
|
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>
|
|
</div>
|
|
)}
|
|
|
|
{/* Widget body.
|
|
quick-260916-bwo: Groessen-Container (@container-size -> container-type: size),
|
|
damit die Widget-Inhalte per cqw/cqh mit der Kachel skalieren. Braucht eine
|
|
definite Hoehe — die kommt ueber h-full aus der Karte, die das RGL-Element
|
|
mit Pixelhoehe fuellt. Steht am Rumpf statt an der Karte, weil die Karte
|
|
im Bearbeitungsmodus zusaetzlich den Griff traegt.
|
|
quick-260916-dyv: Die Kopfleiste liegt als Overlay ueber dem Rumpf und
|
|
aendert die Hoehenkette nicht. */}
|
|
<div className="@container-size h-full">
|
|
{WidgetComponent ? (
|
|
<WidgetComponent
|
|
instanceId={widget.id}
|
|
config={widget.config}
|
|
isEditMode={isEditMode}
|
|
/>
|
|
) : (
|
|
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
|
{widget.widgetType}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|