Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/runtime/components/FormField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ provide(formFieldInjectionKey, computed(() => ({
<Primitive :as="props.as" :data-orientation="props.orientation" data-slot="root" :class="ui.root({ class: [props.ui?.root, props.class] })">
<div data-slot="wrapper" :class="ui.wrapper({ class: props.ui?.wrapper })">
<div v-if="props.label || !!slots.label" data-slot="labelWrapper" :class="ui.labelWrapper({ class: props.ui?.labelWrapper })">
<Label :for="id" data-slot="label" :class="ui.label({ class: props.ui?.label })">
<Label :id="`${ariaId}-label`" :for="id" data-slot="label" :class="ui.label({ class: props.ui?.label })">
<slot name="label" :label="props.label">
{{ props.label }}
</slot>
Expand Down
4 changes: 2 additions & 2 deletions src/runtime/components/Listbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const virtualizerProps = toRef(() => {
})
const inputProps = toRef(() => defu(typeof props.filter === 'object' ? props.filter : {}, { placeholder: t('listbox.search'), variant: 'none' }) as Omit<InputProps, 'modelValue' | 'defaultValue'>)

const { emitFormChange, emitFormInput, name, size, color, id, highlight, disabled, ariaAttrs } = useFormField<InputProps>(_props, { bind: false })
const { emitFormChange, emitFormInput, name, size, color, id, highlight, disabled, ariaAttrs } = useFormField<InputProps>(_props)

const [DefineItemTemplate, ReuseItemTemplate] = createReusableTemplate<{ item: ListboxItem, index: number }>({
props: {
Expand Down Expand Up @@ -375,7 +375,7 @@ const filteredItems = computed(() => filteredGroups.value.flatMap(group => group
/>
</ListboxFilter>

<ListboxContent data-slot="content" :class="ui.content({ class: props.ui?.content })">
<ListboxContent data-slot="content" :class="ui.content({ class: props.ui?.content })" :aria-labelledby="id !== undefined ? `${id}-label` : undefined">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | πŸ—οΈ Heavy lift

Derive aria-labelledby from the FormField label id, not the control id.

When a Listbox supplies a custom id, Line 213 makes id differ from FormField’s stable ariaId; this emits ${customId}-label, while FormField renders ${ariaId}-label. Standalone Listboxes with explicit ids, and FormFields without labels, can also receive dangling ARIA references. Expose/use the injected label id and bind it only when a label is rendered; add regression tests for both cases.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/components/Listbox.vue` at line 378, Update the Listbox
component’s aria-labelledby binding on ListboxContent to use the injected
FormField label id rather than the control id, and only set it when a label is
actually rendered. Preserve no reference for standalone Listboxes or FormFields
without labels, and add regression coverage for custom ids and unlabeled usage.

<div v-if="props.loading" data-slot="loading" :class="ui.loading({ class: props.ui?.loading })">
<slot name="loading">
<UIcon :name="props.loadingIcon || appConfig.ui.icons.loading" data-slot="loadingIcon" :class="ui.loadingIcon({ class: props.ui?.loadingIcon })" />
Expand Down
80 changes: 40 additions & 40 deletions test/components/__snapshots__/AuthForm-vue.spec.ts.snap

Large diffs are not rendered by default.

80 changes: 40 additions & 40 deletions test/components/__snapshots__/AuthForm.spec.ts.snap

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions test/components/__snapshots__/FormField-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ exports[`FormField > renders with hint slot correctly 1`] = `
exports[`FormField > renders with label and description correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -137,7 +137,7 @@ exports[`FormField > renders with label and description correctly 1`] = `
exports[`FormField > renders with label slot correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Label slot</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Label slot</label>
<!--v-if-->
</div>
<!--v-if-->
Expand All @@ -151,7 +151,7 @@ exports[`FormField > renders with label slot correctly 1`] = `
exports[`FormField > renders with orientation horizontal correctly 1`] = `
"<div data-orientation="horizontal" data-slot="root" class="text-sm flex justify-between place-items-baseline gap-2">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -165,7 +165,7 @@ exports[`FormField > renders with orientation horizontal correctly 1`] = `
exports[`FormField > renders with orientation vertical correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -179,7 +179,7 @@ exports[`FormField > renders with orientation vertical correctly 1`] = `
exports[`FormField > renders with required correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default after:content-['*'] after:ms-0.5 after:text-error">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default after:content-['*'] after:ms-0.5 after:text-error">Username</label>
<!--v-if-->
</div>
<!--v-if-->
Expand All @@ -193,7 +193,7 @@ exports[`FormField > renders with required correctly 1`] = `
exports[`FormField > renders with size lg correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -207,7 +207,7 @@ exports[`FormField > renders with size lg correctly 1`] = `
exports[`FormField > renders with size md correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -221,7 +221,7 @@ exports[`FormField > renders with size md correctly 1`] = `
exports[`FormField > renders with size sm correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-xs">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -235,7 +235,7 @@ exports[`FormField > renders with size sm correctly 1`] = `
exports[`FormField > renders with size xl correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-base">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -249,7 +249,7 @@ exports[`FormField > renders with size xl correctly 1`] = `
exports[`FormField > renders with size xs correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-xs">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand Down
20 changes: 10 additions & 10 deletions test/components/__snapshots__/FormField.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ exports[`FormField > renders with hint slot correctly 1`] = `
exports[`FormField > renders with label and description correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -137,7 +137,7 @@ exports[`FormField > renders with label and description correctly 1`] = `
exports[`FormField > renders with label slot correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Label slot</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Label slot</label>
<!--v-if-->
</div>
<!--v-if-->
Expand All @@ -151,7 +151,7 @@ exports[`FormField > renders with label slot correctly 1`] = `
exports[`FormField > renders with orientation horizontal correctly 1`] = `
"<div data-orientation="horizontal" data-slot="root" class="text-sm flex justify-between place-items-baseline gap-2">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -165,7 +165,7 @@ exports[`FormField > renders with orientation horizontal correctly 1`] = `
exports[`FormField > renders with orientation vertical correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -179,7 +179,7 @@ exports[`FormField > renders with orientation vertical correctly 1`] = `
exports[`FormField > renders with required correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default after:content-['*'] after:ms-0.5 after:text-error">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default after:content-['*'] after:ms-0.5 after:text-error">Username</label>
<!--v-if-->
</div>
<!--v-if-->
Expand All @@ -193,7 +193,7 @@ exports[`FormField > renders with required correctly 1`] = `
exports[`FormField > renders with size lg correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -207,7 +207,7 @@ exports[`FormField > renders with size lg correctly 1`] = `
exports[`FormField > renders with size md correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-sm">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -221,7 +221,7 @@ exports[`FormField > renders with size md correctly 1`] = `
exports[`FormField > renders with size sm correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-xs">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -235,7 +235,7 @@ exports[`FormField > renders with size sm correctly 1`] = `
exports[`FormField > renders with size xl correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-base">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand All @@ -249,7 +249,7 @@ exports[`FormField > renders with size xl correctly 1`] = `
exports[`FormField > renders with size xs correctly 1`] = `
"<div data-orientation="vertical" data-slot="root" class="text-xs">
<div data-slot="wrapper" class="">
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" data-slot="label" class="block font-medium text-default">Username</label>
<div data-slot="labelWrapper" class="flex content-center items-center justify-between gap-1"><label for="v-0-0" id="v-0-0-label" data-slot="label" class="block font-medium text-default">Username</label>
<!--v-if-->
</div>
<p id="v-0-0-description" data-slot="description" class="text-muted">Enter your username</p>
Expand Down
2 changes: 1 addition & 1 deletion test/components/__snapshots__/Listbox-vue.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ exports[`Listbox > renders with highlight neutral correctly 1`] = `
exports[`Listbox > renders with id correctly 1`] = `
"<div dir="ltr" id="id" data-slot="root" class="flex flex-col min-h-0 min-w-0 ring ring-inset ring-default rounded-lg overflow-hidden outline-primary/25 has-focus-visible:outline-3 has-focus-visible:ring-primary">
<!--v-if-->
<div data-slot="content" class="relative overflow-y-auto flex-1 max-h-60 scroll-py-1 focus:outline-none divide-y divide-default" role="listbox" tabindex="0" aria-orientation="vertical" aria-multiselectable="false" data-orientation="vertical">
<div data-slot="content" class="relative overflow-y-auto flex-1 max-h-60 scroll-py-1 focus:outline-none divide-y divide-default" aria-labelledby="id-label" role="listbox" tabindex="0" aria-orientation="vertical" aria-multiselectable="false" data-orientation="vertical">
<div role="group" aria-labelledby="reka-listbox-group-v-0" data-slot="group" class="p-1 isolate">
<div data-slot="item" class="group relative w-full flex items-start select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50 transition-colors before:transition-colors p-1.5 text-sm gap-1.5" data-reka-collection-item="" id="reka-listbox-item-v-1" role="option" tabindex="-1" aria-selected="false" data-state="unchecked"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" viewBox="0 0 16 16" data-slot="itemLeadingIcon" class="shrink-0 text-dimmed group-data-highlighted:not-group-data-disabled:text-default transition-colors size-5"></svg><span data-slot="itemWrapper" class="flex-1 flex flex-col min-w-0"><span data-slot="itemLabel" class="truncate">France</span>
<!--v-if--></span><span data-slot="itemTrailing" class="ms-auto inline-flex gap-1.5 items-center"><!--v-if--></span>
Expand Down
Loading
Loading