diff --git a/components/__tests__/form-labels-calendar.test.tsx b/components/__tests__/form-labels-calendar.test.tsx
new file mode 100644
index 000000000..ee46e423c
--- /dev/null
+++ b/components/__tests__/form-labels-calendar.test.tsx
@@ -0,0 +1,25 @@
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+import { unnamedFields } from '@/lib/__tests__/helpers/form-labels';
+import { EventModal } from '@/components/calendar/event-modal';
+import type { Calendar } from '@/lib/jmap/types';
+
+// The event editor names every field (see form-labels.test.tsx). The live
+// audit found the title, the date and time fields and two selects unnamed.
+
+const calendars = [
+ { id: 'cal-1', name: 'Work', isDefault: true, myRights: { mayWriteAll: true, mayWriteOwn: true } },
+ { id: 'cal-2', name: 'Home', myRights: { mayWriteAll: true, mayWriteOwn: true } },
+] as unknown as Calendar[];
+
+describe('event editor fields have labels', () => {
+ it('names the title, dates, times, calendar and repeat fields', () => {
+ render();
+ expect(screen.getByRole('textbox', { name: 'form.title' })).toBeInTheDocument();
+ expect(screen.getByLabelText('form.start_date')).toHaveAttribute('type', 'date');
+ expect(screen.getByLabelText('form.end_time')).toHaveAttribute('type', 'time');
+ expect(screen.getByRole('combobox', { name: 'form.calendar_select' })).toBeInTheDocument();
+ expect(screen.getByRole('combobox', { name: 'recurrence.title' })).toBeInTheDocument();
+ expect(unnamedFields()).toEqual([]);
+ });
+});
diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx
index ff3dac654..871acc626 100644
--- a/components/calendar/event-modal.tsx
+++ b/components/calendar/event-modal.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState, useEffect, useCallback, useRef, useMemo } from "react";
+import { useState, useEffect, useCallback, useRef, useMemo, useId } from "react";
import { useTranslations, useLocale } from "next-intl";
import { toast } from "@/stores/toast-store";
import { Button } from "@/components/ui/button";
@@ -269,6 +269,9 @@ export function EventModal({
}: EventModalProps) {
const t = useTranslations("calendar");
const paint = useCvdColor();
+ // Ids that tie each visible label to its field.
+ const labelIdBase = useId();
+ const labelFor = (key: string) => `${labelIdBase}-${key}`;
const tCommon = useTranslations("common");
const locale = useLocale();
const timeFormat = useSettingsStore((s) => s.timeFormat);
@@ -1218,8 +1221,8 @@ export function EventModal({
-
- {t("form.title")}
+ setTitle(e.target.value)}
@@ -1229,8 +1232,8 @@ export function EventModal({
-
-
-
- {t("form.location")}
+ setLocation(e.target.value)}
placeholder={t("form.location")}
@@ -1251,13 +1254,14 @@ export function EventModal({
-
-
+
-
+
{t("participants.title")}
{/* The organizer decides who is invited; an attendee's save
keeps the list as it came. */}
- {t("form.start_date")}
- {t("form.start_date")}
+ handleStartDateChange(e.target.value)}
@@ -1348,8 +1353,8 @@ export function EventModal({
{!allDay && (
- {t("form.start_time")}
- {t("form.start_time")}
+ handleStartTimeChange(e.target.value)}
@@ -1358,8 +1363,8 @@ export function EventModal({
)}
- {t("form.end_date")}
- {t("form.end_date")}
+ setEndDate(e.target.value)}
@@ -1368,8 +1373,8 @@ export function EventModal({
{!allDay && (
-
{t("form.end_time")}
-
{t("form.end_time")}
+
setEndTime(e.target.value)}
@@ -1401,8 +1406,8 @@ export function EventModal({
{selectableCalendars.length > 1 && (
-
{t("form.calendar_select")}
-