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:
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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}`
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user