diff --git a/messages/de.json b/messages/de.json index 66a787e6a..25fffb059 100644 --- a/messages/de.json +++ b/messages/de.json @@ -336,6 +336,7 @@ "invalidTimestamp": "Bitte einen gültigen Zeitpunkt wählen.", "loginRequired": "Bitte anmelden, um Messwerte zu sehen.", "allTypes": "Alle Typen", + "filterByType": "Nach Typ filtern", "measurementCount": "{count} Messwerte", "emptyTitle": "Noch keine Messwerte", "emptyDescription": "Erfasse Gewicht, Blutdruck, Puls oder andere Werte, um Trends zu erkennen.", diff --git a/messages/en.json b/messages/en.json index aa147e399..e0808411f 100644 --- a/messages/en.json +++ b/messages/en.json @@ -336,6 +336,7 @@ "invalidTimestamp": "Please select a valid time.", "loginRequired": "Please sign in to view measurements.", "allTypes": "All types", + "filterByType": "Filter by type", "measurementCount": "{count} measurements", "emptyTitle": "No measurements yet", "emptyDescription": "Log your weight, blood pressure, pulse, or other vitals to start tracking trends.", diff --git a/src/components/measurements/measurement-form.tsx b/src/components/measurements/measurement-form.tsx index d4012b943..18fe636d6 100644 --- a/src/components/measurements/measurement-form.tsx +++ b/src/components/measurements/measurement-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useId, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { @@ -139,6 +139,13 @@ export function MeasurementForm({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // v1.4.27 MB3 — wire `aria-describedby` on every required field so + // screen readers announce the form-level error banner the moment it + // surfaces. The banner already carries `role="alert"`, so the + // descriptor relationship is purely additive. + const errorId = useId(); + const errorDescriptor = error ? errorId : undefined; + const typeInfo = MEASUREMENT_TYPES.find((t) => t.value === type); const isBpMode = type === "BLOOD_PRESSURE"; const isGlucoseMode = type === "BLOOD_GLUCOSE"; @@ -270,11 +277,16 @@ export function MeasurementForm({ setSysBp(e.target.value)} placeholder="120" required + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} min={60} max={280} /> @@ -284,11 +296,16 @@ export function MeasurementForm({ setDiaBp(e.target.value)} placeholder="80" required + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} min={30} max={200} /> @@ -298,10 +315,14 @@ export function MeasurementForm({ setPulse(e.target.value)} placeholder="72" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} min={30} max={220} /> @@ -321,6 +342,7 @@ export function MeasurementForm({ setValue(e.target.value)} @@ -330,6 +352,9 @@ export function MeasurementForm({ : undefined } required + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} /> )} @@ -364,6 +389,9 @@ export function MeasurementForm({ value={measuredAt} onChange={(e) => setMeasuredAt(e.target.value)} required + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} /> @@ -385,11 +413,14 @@ export function MeasurementForm({ onChange={(e) => setNotes(e.target.value)} placeholder={t("measurements.notesPlaceholder")} maxLength={MAX_COMMENT_LENGTH} + enterKeyHint="done" + autoCapitalize="sentences" /> {error && ( {editError && ( @@ -554,6 +563,8 @@ export function IntakeHistoryList({ value={editTakenAt} onChange={(e) => setEditTakenAt(e.target.value)} disabled={editSkipped} + aria-invalid={!!editError || undefined} + aria-describedby={editErrorDescriptor} /> @@ -573,6 +584,7 @@ export function IntakeHistoryList({ {editError && ( @@ -695,6 +710,8 @@ export function IntakeHistoryList({ value={createTakenAt} onChange={(e) => setCreateTakenAt(e.target.value)} disabled={createSkipped} + aria-invalid={!!createError || undefined} + aria-describedby={createErrorDescriptor} /> @@ -714,6 +731,7 @@ export function IntakeHistoryList({ {createError && ( {formError && ( -

{formError}

+ )}
diff --git a/src/components/medications/medication-form.tsx b/src/components/medications/medication-form.tsx index 6d299a4fa..dda262861 100644 --- a/src/components/medications/medication-form.tsx +++ b/src/components/medications/medication-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useId, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -304,6 +304,12 @@ export function MedicationForm({ const [purgeDialogOpen, setPurgeDialogOpen] = useState(false); const [phaseConfigOpen, setPhaseConfigOpen] = useState(false); + // v1.4.27 MB3 — link the form-level error banner to every required + // input via `aria-describedby` so screen readers announce the + // validation failure on save. + const errorId = useId(); + const errorDescriptor = error ? errorId : undefined; + const isEdit = !!initial; const dose = doseAmount ? `${doseAmount}${doseUnit ? ` ${doseUnit}` : ""}` @@ -527,6 +533,12 @@ export function MedicationForm({ placeholder={t("medications.namePlaceholder")} required maxLength={100} + enterKeyHint="next" + autoCapitalize="words" + autoComplete="off" + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} />
@@ -613,6 +625,11 @@ export function MedicationForm({ placeholder={t("medications.dosePlaceholder")} required maxLength={20} + enterKeyHint="next" + autoComplete="off" + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} />
@@ -625,6 +642,8 @@ export function MedicationForm({ placeholder="mg" maxLength={30} className="w-full" + enterKeyHint="next" + autoComplete="off" />
@@ -690,11 +709,14 @@ export function MedicationForm({ id="med-doses-per-unit" type="number" inputMode="numeric" + enterKeyHint="next" min={1} max={100} value={dosesPerUnit} onChange={(e) => setDosesPerUnit(e.target.value)} placeholder={t("medications.glp1DosesPerUnitPlaceholder")} + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} />

{t("medications.glp1DosesPerUnitHelp")} @@ -773,16 +795,23 @@ export function MedicationForm({ {t("medications.scheduleFrom")} updateSchedule(i, "windowStart", e.target.value) } required + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} maxLength={5} /> @@ -791,16 +820,18 @@ export function MedicationForm({ {t("medications.scheduleTo")} updateSchedule(i, "windowEnd", e.target.value) } required + aria-required="true" + aria-invalid={!!error || undefined} + aria-describedby={errorDescriptor} maxLength={5} /> @@ -810,10 +841,13 @@ export function MedicationForm({ updateSchedule(i, "label", e.target.value)} placeholder={t("medications.labelPlaceholder")} maxLength={50} + enterKeyHint="next" + autoCapitalize="sentences" + autoComplete="off" />

@@ -822,10 +856,12 @@ export function MedicationForm({ updateSchedule(i, "dose", e.target.value)} placeholder={dose || t("medications.defaultDose")} maxLength={50} + enterKeyHint="done" + autoComplete="off" />
@@ -910,6 +946,7 @@ export function MedicationForm({ {error && ( @@ -176,6 +183,9 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) { value={tagsInput} onChange={(e) => setTagsInput(e.target.value)} placeholder={t("mood.tagsPlaceholder")} + enterKeyHint="done" + autoCapitalize="none" + autoComplete="off" /> {/* v1.4.25 W4d — GLP-1 side-effect quick-tags. Tapping a chip appends the localised label to the free-text tag list. @@ -228,6 +238,7 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) { {error && (