Permalink
Browse files

feat(form-group): set aria-describedby attribute on input if label-fo…

…r provided (#1431)

* fet(form-group): add aria-describedby on input if label-for set

Helps ensure that all screen readers associate the feedback and description to the input, when label-for is set.

Preserves any original aria-describedby attributes that may be present on the input.

* address edge case where no feedback or description exists

* remov aria-describedby attribute if no ids to set

* Update README.md

* Update form-group.js

* If aria-describedby set on input (via label-for), dont set it on outer group
  • Loading branch information...
tmorehouse committed Dec 6, 2017
1 parent 00a4be5 commit 6bd12bb77da7ab27a9f0bc829d30961d644e2eb0
Showing with 48 additions and 14 deletions.
  1. +13 −12 src/components/form-group/README.md
  2. +35 −2 src/components/form-group/form-group.js
@@ -209,7 +209,7 @@ Boostrap V4 uses sibling CSS slectors of `:invalid` or `:valid` inputs to show t
form controls (such as checkboxes, radios, and file inputs, or inputs inside input-groups) are
wrapped in additional markup that will no longer make the feedback text a sibling of the input, and
hence the feedback will not show. In these situations you will ned to set the validity `state` on
the `<b-form-group>` as well as the input.
the `<b-form-group>` _as well as_ the input.
Feedback will be shown if the parent `<b-form>` component does _not_ have the
`novalidate` prop set (or set to `false`) along with the `vadidated` prop set (and the input
@@ -257,19 +257,20 @@ inside a an HTML `<fieldset>` element with the label content placed inside the f
the containing input control(s).
It is **highly recommended** that you provide a unique `id` prop on your input element and set
thhe `label-for` prop to this id.
thhe `label-for` prop to this id, when you have only a single input in the `<b-form-group>`.
When multiple form controls are placed inside `<b-form-group>` (i.e. a series or radio or
checkbox inputs), **do not set** the `label-for` prop, as a label can only be associated with
a single input. It is best to use the default rendered markup that produces a `<legend>` which
will describe the group of inputs.
When placing multiple form controls inside a form-group, it is recommended to give each
control its own associated `<label>` (which may be visually hidden using the `.sr-only` class)
and set the label's `for` attribute to the `id` of the associated input control. Alternatively,
you can set the `aria-label` attribute on each input control instead of using a `<label>`.
For `<b-form-radio>` and `<b-form-checkbox>` (or the group version), you do not need to set
individual labels, as the rendered markup for these types of inputs already includes a label.
checkbox inputs, or a series of related inputs), **do not set** the `label-for` prop, as a
label can only be associated with a single input. It is best to use the default rendered
markup that produces a `<fieldset>` + `<legend>` which will describe the group of inputs.
When placing multiple form controls inside a `<b-form-group>` (and you are not nesting
`<b-form-group>`components), it is recommended to give each control its own associated
`<label>` (which may be visually hidden using the `.sr-only` class) and set the label's
`for` attribute to the `id` of the associated input control. Alternatively, you can set the
`aria-label` attribute on each input control instead of using a `<label>`. For `<b-form-radio>`
and `<b-form-checkbox>` (or the group versions), you do not need to set individual labels, as
the rendered markup for these types of inputs already includes a `<label>` element.
## Component alias
@@ -1,5 +1,5 @@
import { warn } from '../../utils'
import { selectAll, isVisible } from '../../utils/dom'
import { select, selectAll, isVisible, setAttr, removeAttr, getAttr } from '../../utils/dom'
import { idMixin, formStateMixin } from '../../mixins'
import bFormRow from '../layout/form-row'
import bFormText from '../form/form-text'
@@ -144,7 +144,7 @@ export default {
role: 'group',
'aria-invalid': t.computedState === false ? 'true' : null,
'aria-labelledby': t.labelId,
'aria-describedby': t.describedByIds
'aria-describedby': t.labelFor ? null : t.describedByIds
}
},
t.horizontal ? [ h('b-form-row', {}, [ legend, content ]) ] : [ legend, content ]
@@ -286,6 +286,13 @@ export default {
].filter(i => i).join(' ') || null
}
},
watch: {
describedByIds (add, remove) {
if (add !== remove) {
this.setInputDescribedBy(add, remove)
}
}
},
methods: {
legendClick (evt) {
const tagName = evt.target ? evt.target.tagName : ''
@@ -298,6 +305,32 @@ export default {
if (inputs[0] && inputs[0].focus) {
inputs[0].focus()
}
},
setInputDescribedBy (add, remove = '') {
// Sets the `aria-describedby` attribute on the input if label-for is set.
// Optionally accepts a string of IDs to remove as the second parameter
if (this.labelFor && typeof document !== 'undefined') {
const input = select(`#${this.labelFor}`, this.$refs.content)
if (input) {
const adb = 'aria-describedby'
let ids = (getAttr(input, adb) || '').split(/\s+/)
remove = remove.split(/\s+/)
// Update ID list, preserving any original IDs
ids = ids.filter(id => remove.indexOf(id) === -1).concat(add || '').join(' ').trim()
if (ids) {
setAttr(input, adb, ids)
} else {
removeAttr(input, adb)
}
}
}
}
},
mounted () {
this.$nextTick(() => {
// Set the adia-describedby IDs on the input specified by label-for
// We do this in a nextTick to ensure the children have finished rendering
this.setInputDescribedBy(this.describedByIds)
})
}
}

0 comments on commit 6bd12bb

Please sign in to comment.