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
7 changes: 7 additions & 0 deletions .changeset/fix-numberfield-labelled-by.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@vuetify/v0": patch
---

fix(NumberField): add `labelledBy` prop for `aria-labelledby` on the spinbutton (#640)

`NumberField.Control` previously only supported accessible naming via the `label` string prop, rendered as `aria-label`. Consumers who render a visible `<label>` element outside the component had no way to wire it to the spinbutton. `labelledBy` now flows through `NumberField.Root`'s context and renders as `aria-labelledby` on the spinbutton, suppressing `aria-label` when both are set to avoid a conflicting accessible name.
3 changes: 2 additions & 1 deletion packages/0/src/components/NumberField/NumberFieldControl.vue
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@
'aria-valuemin': Number.isFinite(root.numeric.min) ? root.numeric.min : undefined,
'aria-valuemax': Number.isFinite(root.numeric.max) ? root.numeric.max : undefined,
'aria-invalid': invalid || undefined,
'aria-label': root.label || undefined,
'aria-label': root.labelledBy ? undefined : (root.label || undefined),
'aria-labelledby': root.labelledBy || undefined,
'aria-describedby': describedby.value,
'aria-errormessage': (root.hasError.value && root.errors.value.length > 0) ? root.errorId : undefined,
'aria-required': root.required || undefined,
Expand Down
10 changes: 8 additions & 2 deletions packages/0/src/components/NumberField/NumberFieldRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
export interface NumberFieldRootContext extends NumberFieldContext {
/** Unique identifier */
readonly id: ID
/** Optional display label */
/** Optional display label (used as aria-label on spinbutton) */
readonly label?: string
/** ID(s) of external element(s) labelling the spinbutton (aria-labelledby) */
readonly labelledBy?: string
/** Form field name */
readonly name?: string
/** Associate with form by ID */
Expand Down Expand Up @@ -84,8 +86,10 @@
export interface NumberFieldRootProps extends AtomProps {
/** Unique identifier (auto-generated if not provided) */
id?: ID
/** Optional display label */
/** Optional display label (rendered as aria-label on the spinbutton) */
label?: string
/** ID(s) of an external visible label element to associate via aria-labelledby */
labelledBy?: string
/** Form field name */
name?: string
/** Associate with form by ID */
Expand Down Expand Up @@ -201,6 +205,7 @@
renderless,
id = useId(),
label,
labelledBy,
name,
form,
required,
Expand Down Expand Up @@ -288,6 +293,7 @@
input,
id: input.id,
label,
labelledBy,
name,
form,
required,
Expand Down
28 changes: 28 additions & 0 deletions packages/0/src/components/NumberField/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,34 @@ describe('numberField', () => {
await wait()
expect(decrementEl().attributes('type')).toBe('button')
})

it('should set aria-labelledby from labelledBy prop', async () => {
const model = ref<number | null>(5)
const { controlEl, wait } = mountNumberField({
model,
props: { labelledBy: 'my-label' },
})
await wait()
expect(controlEl().attributes('aria-labelledby')).toBe('my-label')
})

it('should suppress aria-label when labelledBy prop is set', async () => {
const model = ref<number | null>(5)
const { controlEl, wait } = mountNumberField({
model,
props: { label: 'Quantity', labelledBy: 'my-label' },
})
await wait()
expect(controlEl().attributes('aria-label')).toBeUndefined()
expect(controlEl().attributes('aria-labelledby')).toBe('my-label')
})

it('should not set aria-label when no label prop is provided', async () => {
const model = ref<number | null>(5)
const { controlEl, wait } = mountNumberField({ model })
await wait()
expect(controlEl().attributes('aria-label')).toBeUndefined()
})
})

describe('data attributes', () => {
Expand Down
Loading