fix(desktop): give the delivery-target group an accessible name

Field renders its visible label as <label htmlFor>, but that only
associates with labelable elements (input, select, ...), not the
div[role="group"] DeliverCheckboxes renders. The checkbox group had
no accessible name for assistive tech.

Field now stamps an id on its label (`${htmlFor}-label`) and
DeliverCheckboxes references it via aria-labelledby, so the group
picks up the same visible label text instead of duplicating it.

Addresses review feedback on PR #73886.
This commit is contained in:
owner
2026-07-31 09:59:42 +03:00
committed by Teknium
parent 67927808be
commit 3a8d95a93b
3 changed files with 43 additions and 3 deletions
@@ -0,0 +1,31 @@
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import { Field } from '@/components/ui/field'
import { en } from '@/i18n/en'
import { DeliverCheckboxes } from './index'
afterEach(cleanup)
// Regression for the delivery-target group losing its accessible name: `Field`
// renders the visible label as `<label htmlFor>`, which only associates with
// labelable elements (input, select, ...) — not a `div[role="group"]`. The
// group must pick up the label via `aria-labelledby` instead.
describe('DeliverCheckboxes accessible name', () => {
it('exposes the Field label as the group name', () => {
render(
<Field htmlFor="cron-deliver" label={en.cron.deliverLabel}>
<DeliverCheckboxes
c={en.cron}
id="cron-deliver"
onChange={() => {}}
targets={[{ home_env_var: null, home_target_set: true, id: 'local', name: 'This desktop' }]}
value="local"
/>
</Field>
)
expect(screen.getByRole('group', { name: en.cron.deliverLabel })).toBeTruthy()
})
})
+7 -2
View File
@@ -789,7 +789,7 @@ function deliverTargetLabel(target: CronDeliveryTarget, c: Translations['cron'])
// blueprint form. The scheduler accepts comma-separated targets, so users can
// keep results local while also sending them to connected platforms. Preserve
// selected targets missing from discovery so editing never drops a saved route.
function DeliverCheckboxes({
export function DeliverCheckboxes({
c,
id,
onChange,
@@ -813,7 +813,12 @@ function DeliverCheckboxes({
]
return (
<div className="grid gap-2 rounded-md border border-input px-3 py-2.5" id={id} role="group">
<div
aria-labelledby={`${id}-label`}
className="grid gap-2 rounded-md border border-input px-3 py-2.5"
id={id}
role="group"
>
{options.map((target, index) => {
const checked = selected.includes(target.id)
const checkboxId = `${id}-${index}`
+5 -1
View File
@@ -23,7 +23,11 @@ export function Field({
}) {
return (
<div className="grid gap-1.5">
<label className="flex items-baseline gap-2 text-xs font-medium text-foreground" htmlFor={htmlFor}>
<label
className="flex items-baseline gap-2 text-xs font-medium text-foreground"
htmlFor={htmlFor}
id={htmlFor ? `${htmlFor}-label` : undefined}
>
{label}
{optional && optionalLabel && (
<span className="text-[0.65rem] font-normal text-muted-foreground">{optionalLabel}</span>