fix: name the remaining search boxes, filter fields and settings inputs that only had a placeholder

This commit is contained in:
Linus Rath
2026-10-29 18:03:00 +01:00
parent f73643bbbd
commit 4e2affe406
50 changed files with 105 additions and 29 deletions
+5
View File
@@ -345,6 +345,7 @@ export function PolicyPage() {
<div className="sm:col-span-4">
<label className={fieldLabelClass}>Name</label>
<input
aria-label="Name"
type="text"
autoComplete="off"
spellCheck={false}
@@ -358,6 +359,7 @@ export function PolicyPage() {
<label className={fieldLabelClass}>Relay URL</label>
<div className="flex items-center gap-2">
<input
aria-label="Relay URL"
type="url"
inputMode="url"
autoComplete="off"
@@ -439,6 +441,7 @@ export function PolicyPage() {
<div className="sm:col-span-4">
<label className={fieldLabelClass}>Name</label>
<input
aria-label="Name"
type="text"
autoComplete="off"
spellCheck={false}
@@ -452,6 +455,7 @@ export function PolicyPage() {
<div className="sm:col-span-5">
<label className={fieldLabelClass}>URL</label>
<input
aria-label="URL"
type="url"
inputMode="url"
autoComplete="off"
@@ -474,6 +478,7 @@ export function PolicyPage() {
</label>
<div className="flex items-center gap-2">
<input
aria-label="Icon"
type="text"
autoComplete="off"
spellCheck={false}
+12
View File
@@ -7,6 +7,8 @@ import { TemplateForm } from '@/components/templates/template-form';
import { MvSearchForm } from '@/components/search/mv-search-form';
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
import { JmapServersSection } from '@/app/(main)/admin/_pages/_jmap-servers-section';
import { ContactGroupForm } from '@/components/contacts/contact-group-form';
import { PromptDialog } from '@/components/ui/prompt-dialog';
// Every field of the big forms has a name a screen reader reads out (WCAG
// 1.3.1, 3.3.2, 4.1.2): a <label> tied to it or an aria-label, never just
@@ -63,4 +65,14 @@ describe('form fields have labels', () => {
expect(document.querySelectorAll('input').length).toBeGreaterThanOrEqual(8);
expect(unnamedFields()).toEqual([]);
});
it('contact group form', () => {
render(<ContactGroupForm individuals={[]} onSave={vi.fn()} onCancel={vi.fn()} />);
expect(unnamedFields()).toEqual([]);
});
it('the shared one-field prompt', () => {
render(<PromptDialog isOpen onClose={vi.fn()} onSubmit={vi.fn()} title="Rename" placeholder="New name" />);
expect(unnamedFields()).toEqual([]);
});
});
@@ -644,6 +644,7 @@ export function EventDetailPopover({
{noteExpanded ? (
<div className="space-y-2">
<textarea
aria-label={t("detail.add_note")}
ref={noteInputRef}
value={noteText}
onChange={(e) => setNoteText(e.target.value)}
@@ -106,6 +106,7 @@ export function ContactGroupForm({
{t("groups.name_label")}
</label>
<Input
aria-label={t("groups.name_label")}
value={name}
onChange={(e) => setName(e.target.value)}
placeholder={t("groups.name_placeholder")}
@@ -120,6 +121,7 @@ export function ContactGroupForm({
<div className="relative mb-2">
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
aria-label={t("groups.search_members")}
placeholder={t("groups.search_members")}
value={memberSearch}
onChange={(e) => setMemberSearch(e.target.value)}
+4
View File
@@ -477,6 +477,7 @@ export function ContactList({
<div>
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.organization")}</label>
<Input
aria-label={t("filters.organization")}
value={filters.organization}
onChange={(e) => setFilters((f) => ({ ...f, organization: e.target.value }))}
placeholder={t("filters.organization_placeholder")}
@@ -486,6 +487,7 @@ export function ContactList({
<div>
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.job_title")}</label>
<Input
aria-label={t("filters.job_title")}
value={filters.jobTitle}
onChange={(e) => setFilters((f) => ({ ...f, jobTitle: e.target.value }))}
placeholder={t("filters.job_title_placeholder")}
@@ -498,6 +500,7 @@ export function ContactList({
<div>
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.location")}</label>
<Input
aria-label={t("filters.location")}
value={filters.location}
onChange={(e) => setFilters((f) => ({ ...f, location: e.target.value }))}
placeholder={t("filters.location_placeholder")}
@@ -507,6 +510,7 @@ export function ContactList({
<div>
<label className="text-xs text-muted-foreground mb-1 block">{t("filters.email_domain")}</label>
<Input
aria-label={t("filters.email_domain")}
value={filters.emailDomain}
onChange={(e) => setFilters((f) => ({ ...f, emailDomain: e.target.value }))}
placeholder={t("filters.email_domain_placeholder")}
+1
View File
@@ -256,6 +256,7 @@ export function FilePickerDialog({ client, onClose, onPick }: FilePickerDialogPr
<div className="relative">
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
aria-label={t("search_placeholder")}
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("search_placeholder")}
+1
View File
@@ -475,6 +475,7 @@ export function FilterRuleModal({
{t("rule_name")}
</label>
<Input
aria-label={t("rule_name")}
ref={nameRef}
value={name}
onChange={(e) => setName(e.target.value)}
+1
View File
@@ -56,6 +56,7 @@ export function IconPicker({ value, onChange, className }: IconPickerProps) {
<div className="relative flex-1">
<Search className="absolute start-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
<Input
aria-label={t('search_icons')}
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t('search_icons')}
+1
View File
@@ -379,6 +379,7 @@ function ContactsPanel({ onClose }: { onClose: () => void }) {
{searching && (
<div className="px-4 pb-2">
<input
aria-label={tContacts("search_placeholder")}
autoFocus
type="search"
value={query}
+1
View File
@@ -4640,6 +4640,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
// is running; Mountain View's form waits for its Search button instead.
const renderSearchFolderSelect = (className: string, rerun: boolean) => (
<select
aria-label={t('advanced_search.search_in')}
value={searchMailboxId}
onChange={(e) => {
setSearchMailboxId(e.target.value);
@@ -42,6 +42,7 @@ function AddressBookEditRow({
{t("name_label")}
</label>
<input
aria-label={t("name_label")}
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
@@ -132,6 +132,7 @@ export function CalendarEditForm({
{t('name')}
</label>
<input
aria-label={t('name')}
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
@@ -190,6 +190,7 @@ export function ComposingSettings() {
}}
>
<input
aria-label={t('attachment_reminder.add_placeholder')}
type="text"
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
@@ -77,6 +77,7 @@ function TemplateEditor({
<div className="flex flex-col gap-2">
<div className="flex items-stretch gap-2">
<input
aria-label={label}
ref={inputRef}
type="text"
value={value}
+2
View File
@@ -529,6 +529,7 @@ export function FolderSettings() {
</span>
)}
<input
aria-label={parentId ? t('subfolder_name') : t('new_folder_name')}
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
@@ -583,6 +584,7 @@ export function FolderSettings() {
<div className="flex items-center gap-2 py-2 px-3" style={{ paddingInlineStart: 12 + depth * 16 }}>
<Icon className="w-4 h-4 text-muted-foreground flex-shrink-0" />
<input
aria-label={t('new_folder_name')}
type="text"
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
+1
View File
@@ -242,6 +242,7 @@ function KeywordEditForm({
{t("label_field")}
</label>
<input
aria-label={t("label_field")}
type="text"
value={label}
onChange={(e) => setLabel(e.target.value)}
@@ -194,6 +194,7 @@ export function ShareCollectionDialog({
</div>
<div className="relative">
<select
aria-label={t("access_for", { name: principal?.name || principal?.email || principalId })}
value={preset}
onChange={(e) => handleSetRights(principalId, e.target.value as RolePreset)}
disabled={savingId === principalId}
@@ -240,6 +241,7 @@ export function ShareCollectionDialog({
{showAdd && (
<div className="space-y-2 border border-border rounded-md p-3">
<input
aria-label={t("search_placeholder")}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
@@ -84,6 +84,7 @@ function AppForm({
<div>
<label className="text-sm font-medium">{t("name_label")}</label>
<Input
aria-label={t("name_label")}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={t("name_placeholder")}
@@ -95,6 +96,7 @@ function AppForm({
<div>
<label className="text-sm font-medium">{t("url_label")}</label>
<Input
aria-label={t("url_label")}
value={formData.url}
onChange={(e) => setFormData({ ...formData, url: e.target.value })}
placeholder="https://example.com"
+1
View File
@@ -237,6 +237,7 @@ export function TemplatePicker({ isOpen, onClose, onSelect, accountId }: Templat
<div className="relative">
<Search className="absolute start-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
aria-label={t('search_placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('search_placeholder')}
+1
View File
@@ -122,6 +122,7 @@ export function PromptDialog({
}
>
<Input
aria-label={title}
ref={inputRef}
type="text"
value={value}
+5
View File
@@ -0,0 +1,5 @@
// dom-accessibility-api ships types, but its package.json "exports" hides
// them from this project's module resolution. The one function the tests use:
declare module 'dom-accessibility-api' {
export function computeAccessibleName(root: Element, options?: Record<string, unknown>): string;
}
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "قراءة وكتابة",
"manager": "مدير",
"custom": "مخصص"
}
},
"access_for": "صلاحية الوصول لـ {name}"
},
"delegates": {
"tab": "المشاركة",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Lectura i escriptura",
"manager": "Gestor",
"custom": "Personalitzat"
}
},
"access_for": "Accés per a {name}"
},
"delegates": {
"tab": "Compartició",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Čtení a zápis",
"manager": "Správce",
"custom": "Vlastní"
}
},
"access_for": "Přístup pro {name}"
},
"delegates": {
"tab": "Sdílení",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Læs & skriv",
"manager": "Administrator",
"custom": "Brugerdefineret"
}
},
"access_for": "Adgang for {name}"
},
"delegates": {
"tab": "Deling",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Lesen & schreiben",
"manager": "Verwalten",
"custom": "Benutzerdefiniert"
}
},
"access_for": "Zugriff für {name}"
},
"delegates": {
"tab": "Freigaben",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Read & write",
"manager": "Manager",
"custom": "Custom"
}
},
"access_for": "Access for {name}"
},
"delegates": {
"tab": "Sharing",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Lectura y escritura",
"manager": "Administrador",
"custom": "Personalizado"
}
},
"access_for": "Acceso para {name}"
},
"delegates": {
"tab": "Uso compartido",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Lugemine ja kirjutamine",
"manager": "Haldur",
"custom": "Kohandatud"
}
},
"access_for": "Juurdepääs: {name}"
},
"delegates": {
"tab": "Jagamine",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "خواندن و نوشتن",
"manager": "مدیر",
"custom": "سفارشی"
}
},
"access_for": "دسترسی برای {name}"
},
"delegates": {
"tab": "اشتراک‌گذاری",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Lecture & écriture",
"manager": "Gestionnaire",
"custom": "Personnalisé"
}
},
"access_for": "Accès pour {name}"
},
"delegates": {
"tab": "Partage",
+2 -1
View File
@@ -5193,7 +5193,8 @@
"readWrite": "קרא וכתוב",
"manager": "מנהל",
"custom": "מותאם אישית"
}
},
"access_for": "גישה עבור {name}"
},
"delegates": {
"tab": "שיתוף",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Olvasás és írás",
"manager": "Kezelő",
"custom": "Egyéni"
}
},
"access_for": "Hozzáférés: {name}"
},
"delegates": {
"tab": "Megosztás",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Lettura e scrittura",
"manager": "Gestore",
"custom": "Personalizzato"
}
},
"access_for": "Accesso per {name}"
},
"delegates": {
"tab": "Condivisione",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "読み取り・書き込み",
"manager": "管理者",
"custom": "カスタム"
}
},
"access_for": "{name} のアクセス権"
},
"delegates": {
"tab": "共有",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "읽기 및 쓰기",
"manager": "관리자",
"custom": "사용자 지정"
}
},
"access_for": "{name}의 액세스 권한"
},
"delegates": {
"tab": "공유",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Lasīšana un rakstīšana",
"manager": "Pārvaldnieks",
"custom": "Pielāgots"
}
},
"access_for": "Piekļuve: {name}"
},
"delegates": {
"tab": "Kopīgošana",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Унших, бичих",
"manager": "Менежер",
"custom": "Захиалгат"
}
},
"access_for": "{name}-н хандалт"
},
"delegates": {
"tab": "Хуваалцах",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Kan lese og skrive",
"manager": "Kan administrere",
"custom": "Egendefinert"
}
},
"access_for": "Tilgang for {name}"
},
"delegates": {
"tab": "Deling",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Lezen en schrijven",
"manager": "Beheerder",
"custom": "Aangepast"
}
},
"access_for": "Toegang voor {name}"
},
"delegates": {
"tab": "Delen",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Odczyt i zapis",
"manager": "Menedżer",
"custom": "Niestandardowe"
}
},
"access_for": "Dostęp dla {name}"
},
"delegates": {
"tab": "Udostępnianie",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Leitura e escrita",
"manager": "Gerente",
"custom": "Personalizado"
}
},
"access_for": "Acesso para {name}"
},
"delegates": {
"tab": "Compartilhamento",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Citire și scriere",
"manager": "Manager",
"custom": "Personalizat"
}
},
"access_for": "Acces pentru {name}"
},
"delegates": {
"tab": "Partajare",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Чтение и запись",
"manager": "Управляющий",
"custom": "Пользовательский"
}
},
"access_for": "Доступ для {name}"
},
"delegates": {
"tab": "Общий доступ",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Čítanie a zápis",
"manager": "Správca",
"custom": "Vlastné"
}
},
"access_for": "Prístup pre {name}"
},
"delegates": {
"tab": "Zdieľanie",
+2 -1
View File
@@ -2137,7 +2137,8 @@
"readWrite": "อ่านและเขียน",
"manager": "ผู้จัดการ (จัดการสิทธิ์ได้)",
"custom": "กำหนดเอง"
}
},
"access_for": "สิทธิ์การเข้าถึงของ {name}"
},
"delegates": {
"tab": "การแชร์",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "Okuma ve yazma",
"manager": "Yönetici",
"custom": "Özel"
}
},
"access_for": "{name} için erişim"
},
"delegates": {
"tab": "Paylaşım",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "Читання та запис",
"manager": "Керівник",
"custom": "Власне"
}
},
"access_for": "Доступ для {name}"
},
"delegates": {
"tab": "Спільний доступ",
+2 -1
View File
@@ -4781,7 +4781,8 @@
"readWrite": "讀取與寫入",
"manager": "管理員",
"custom": "自訂"
}
},
"access_for": "{name} 的存取權限"
},
"delegates": {
"tab": "共用",
+2 -1
View File
@@ -5221,7 +5221,8 @@
"readWrite": "读写",
"manager": "管理员",
"custom": "自定义"
}
},
"access_for": "{name} 的访问权限"
},
"delegates": {
"tab": "共享",