Files
tessera-ctl/apps/web/src/components/dashboard/widgets/clock-widget.tsx
T
schalli 21c85a8b88 fix(quick-260921-bi2): a11y - Rollen, Semantik, Tab-Reihenfolge (32er-Rest, Teil 1/2)
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
2026-09-21 09:33:56 +02:00

72 lines
2.5 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import type { WidgetProps } from '../widget-registry';
import { CLOCK_DATE_FACTOR, resolveClockTimeFontSizePt } from './clock-font-size';
/**
* Digital clock widget displaying time in a configurable timezone.
* Uses Intl.DateTimeFormat — never computes UTC offsets manually (RESEARCH).
*
* Config:
* - timezone: IANA timezone string (default 'Europe/Berlin')
* - showDate: boolean (default false) — show date below time
* - timeFontSizePt: number | null (quick-260916-bwo) — feste Schriftgroesse der
* Uhrzeit in Punkt; leer/null = automatisch ueber Container-Queries mit der
* Kachelgroesse. Grenzen 8..200 in `clock-font-size.ts` (T-BWO-01).
*/
export function ClockWidget({ config }: WidgetProps) {
const timezone = (config.timezone as string) || 'Europe/Berlin';
const showDate = config.showDate === true;
const fixedPt = resolveClockTimeFontSizePt(config);
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const timer = setInterval(() => {
setNow(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
const timeFormatter = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: timezone,
});
const dateFormatter = new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
timeZone: timezone,
});
return (
<div className="flex h-full flex-col items-center justify-center gap-1 p-1">
{/* Automatisch = CSS (Container-Query-Klasse, skaliert mit der Kachel),
fest = Zahl als Inline-Style in pt. Die Trennung ist bewusst: jsdom
verwirft clamp() im Inline-Style, der Browser bekommt die Klasse
ohnehin — und "auto = Klasse, fest = Zahl" ist die einfachere Form. */}
<time
data-font-mode={fixedPt === null ? 'auto' : 'fixed'}
className="font-semibold tabular-nums leading-none text-foreground text-[clamp(12px,min(20cqw,50cqh),400px)]"
style={fixedPt === null ? undefined : { fontSize: `${fixedPt}pt` }}
dateTime={now.toISOString()}
>
{timeFormatter.format(now)}
</time>
{showDate && (
<span
data-testid="clock-date"
className="text-muted-foreground text-[clamp(10px,min(8cqw,20cqh),160px)]"
style={fixedPt === null ? undefined : { fontSize: `${fixedPt * CLOCK_DATE_FACTOR}pt` }}
>
{dateFormatter.format(now)}
</span>
)}
</div>
);
}