Skip to content

Commit 8058c03

Browse files
tmorehousejacobmllr95
authored andcommitted
fix(form-group): don't render aria-labelledby on group when label-for provided (fixes #2933) (#2936)
* fix(form-group): don't render aria-labelledby on group when label-for provided (fixes #2933) Only render the `aria-labelledby` attribute on the `form-group` when `label-for` is not provided. Fixes #2933 * Update form-group.js * Update form-group.html * Update form-group.spec.js * Update form-group.js * Update form-group.js
1 parent 97e8ece commit 8058c03

File tree

3 files changed

+52
-41
lines changed

3 files changed

+52
-41
lines changed

src/components/form-group/fixtures/form-group.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
:invalid-feedback="text.length ? '' : 'Please enter something'"
99
:valid-feedback="text.length ? 'Thank you' : ''"
1010
:state="text.length ? true : false">
11-
<b-form-input id="input1" v-model="text"></b-form-input>
11+
<b-form-input id="input1" ref="input1" v-model="text"></b-form-input>
1212
</b-form-group>
1313
<!-- horizontal with label -->
1414
<b-form-group id="group2"

src/components/form-group/form-group.js

Lines changed: 47 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import warn from '../../utils/warn'
99
import { select, selectAll, isVisible, setAttr, removeAttr, getAttr } from '../../utils/dom'
1010
import { arrayIncludes } from '../../utils/array'
1111
import { keys, create } from '../../utils/object'
12+
import { inBrowser } from '../../utils/env'
1213
// Sub components
1314
import BFormRow from '../layout/form-row'
1415
import BCol from '../layout/col'
@@ -19,16 +20,17 @@ import BFormValidFeedback from '../form/form-valid-feedback'
1920
// Selector for finding first input in the form-group
2021
const SELECTOR = 'input:not(:disabled),textarea:not(:disabled),select:not(:disabled)'
2122

22-
// Memoize this function to return cached values to save time in computed functions
23+
// Memoize this function to return cached values to
24+
// save time in computed functions
2325
const makePropName = memoize((breakpoint = '', prefix) => {
2426
return `${prefix}${upperFirst(breakpoint)}`
2527
})
2628

2729
const DEPRECATED_MSG =
2830
'Props "horizontal" and "breakpoint" are deprecated. Use "label-cols(-{breakpoint})" props instead.'
2931

30-
// render helper functions (here rather than polluting the instance with more methods)
31-
function renderInvalidFeedback(h, ctx) {
32+
// Render helper functions (here rather than polluting the instance with more methods)
33+
const renderInvalidFeedback = (h, ctx) => {
3234
let content = ctx.$slots['invalid-feedback'] || ctx.invalidFeedback
3335
let invalidFeedback = h(false)
3436
if (content) {
@@ -54,7 +56,7 @@ function renderInvalidFeedback(h, ctx) {
5456
return invalidFeedback
5557
}
5658

57-
function renderValidFeedback(h, ctx) {
59+
const renderValidFeedback = (h, ctx) => {
5860
const content = ctx.$slots['valid-feedback'] || ctx.validFeedback
5961
let validFeedback = h(false)
6062
if (content) {
@@ -80,7 +82,7 @@ function renderValidFeedback(h, ctx) {
8082
return validFeedback
8183
}
8284

83-
function renderHelpText(h, ctx) {
85+
const renderHelpText = (h, ctx) => {
8486
// Form help text (description)
8587
const content = ctx.$slots['description'] || ctx.description
8688
let description = h(false)
@@ -99,8 +101,8 @@ function renderHelpText(h, ctx) {
99101
return description
100102
}
101103

102-
function renderLabel(h, ctx) {
103-
// render label/legend inside b-col if necessary
104+
const renderLabel = (h, ctx) => {
105+
// Render label/legend inside b-col if necessary
104106
const content = ctx.$slots['label'] || ctx.label
105107
const labelFor = ctx.labelFor
106108
const isLegend = !labelFor
@@ -132,17 +134,20 @@ function renderLabel(h, ctx) {
132134
attrs: {
133135
id: ctx.labelId,
134136
for: labelFor || null,
135-
// We add a tab index to legend so that screen readers will properly read the aria-labelledby in IE.
137+
// We add a tab index to legend so that screen readers
138+
// will properly read the aria-labelledby in IE.
136139
tabindex: isLegend ? '-1' : null
137140
},
138141
class: [
139-
// When horizontal or if a legend is rendered, add col-form-label for correct sizing
140-
// as Bootstrap has inconsitent font styling for legend in non-horiontal form-groups.
142+
// When horizontal or if a legend is rendered, add col-form-label
143+
// for correct sizing as Bootstrap has inconsistent font styling
144+
// for legend in non-horizontal form-groups.
141145
// See: https://github.com/twbs/bootstrap/issues/27805
142146
isHorizontal || isLegend ? 'col-form-label' : '',
143147
// Emulate label padding top of 0 on legend when not horizontal
144148
!isHorizontal && isLegend ? 'pt-0' : '',
145-
// If not horizontal and not a legend, we add d-block to label so that label-align works
149+
// If not horizontal and not a legend, we add d-block to label
150+
// so that label-align works
146151
!isHorizontal && !isLegend ? 'd-block' : '',
147152
ctx.labelSize ? `col-form-label-${ctx.labelSize}` : '',
148153
ctx.labelAlignClasses,
@@ -154,10 +159,9 @@ function renderLabel(h, ctx) {
154159
}
155160
}
156161

157-
//
158162
// Async (lazy) component for BFormGroup
159-
// Needed so that the breakpoint specific props can be computed once hte config is created
160-
//
163+
// Needed so that the breakpoint specific props can be computed
164+
// once the config is created
161165
export default (resolve, reject) => {
162166
// Grab the current config for breakpoints
163167
const BREAKPOINTS = getBreakpointsUp()
@@ -268,13 +272,14 @@ export default (resolve, reject) => {
268272
const bp = this.breakpoint || BREAKPOINTS[1] // 'sm'
269273
const cols = parseInt(this.labelCols, 10) || 3
270274
props[bp] = cols > 0 ? cols : 3
271-
// We then return the single breakpoint prop for legacy compatability
275+
// We then return the single breakpoint prop for legacy compatibility
272276
return props
273277
}
274278
BREAKPOINTS.forEach(breakpoint => {
275279
// Grab the value if the label column breakpoint prop
276280
let propVal = this[makePropName(breakpoint, 'labelCols')]
277-
// Handle case where the prop's value is an empty string, which represents true
281+
// Handle case where the prop's value is an empty string,
282+
// which represents true
278283
propVal = propVal === '' ? true : propVal || false
279284
if (typeof propVal !== 'boolean') {
280285
// Convert to column size to number
@@ -283,8 +288,9 @@ export default (resolve, reject) => {
283288
propVal = propVal > 0 ? propVal : false
284289
}
285290
if (propVal) {
286-
// Add the prop to the list of props to give to b-col.
287-
// if breakpoint is '' (labelCols=true), then we use the col prop to make equal width at xs
291+
// Add the prop to the list of props to give to b-col
292+
// If breakpoint is '' (labelCols=true), then we use the
293+
// col prop to make equal width at xs
288294
const bColPropName = breakpoint || (typeof propVal === 'boolean' ? 'col' : 'cols')
289295
// Add it to the props
290296
props[bColPropName] = propVal
@@ -295,7 +301,7 @@ export default (resolve, reject) => {
295301
labelAlignClasses() {
296302
const classes = []
297303
BREAKPOINTS.forEach(breakpoint => {
298-
// assemble the label column breakpoint align classes
304+
// Assemble the label column breakpoint align classes
299305
const propVal = this[makePropName(breakpoint, 'labelAlign')] || null
300306
if (propVal) {
301307
const className = breakpoint ? `text-${breakpoint}-${propVal}` : `text-${propVal}`
@@ -318,23 +324,23 @@ export default (resolve, reject) => {
318324
: null
319325
},
320326
hasInvalidFeedback() {
321-
// used for computing aria-describedby
327+
// Used for computing aria-describedby
322328
const $slots = this.$slots
323329
return this.computedState === false && ($slots['invalid-feedback'] || this.invalidFeedback)
324330
},
325331
invalidFeedbackId() {
326332
return this.hasInvalidFeedback ? this.safeId('_BV_feedback_invalid_') : null
327333
},
328334
hasValidFeedback() {
329-
// used for computing aria-describedby
335+
// Used for computing aria-describedby
330336
return this.computedState === true && (this.$slots['valid-feedback'] || this.validFeedback)
331337
},
332338
validFeedbackId() {
333339
return this.hasValidFeedback ? this.safeId('_BV_feedback_valid_') : null
334340
},
335341
describedByIds() {
336342
// Screen readers will read out any content linked to by aria-describedby
337-
// even if the content is hidden with 'display: none', hence we only include
343+
// even if the content is hidden with `display: none;`, hence we only include
338344
// feedback IDs if the form-group's state is explicitly valid or invalid.
339345
return (
340346
[this.descriptionId, this.invalidFeedbackId, this.validFeedbackId]
@@ -352,21 +358,22 @@ export default (resolve, reject) => {
352358
},
353359
mounted() {
354360
this.$nextTick(() => {
355-
// Set the adia-describedby IDs on the input specified by label-for
361+
// Set the aria-describedby IDs on the input specified by label-for
356362
// We do this in a nextTick to ensure the children have finished rendering
357363
this.setInputDescribedBy(this.describedByIds)
358364
})
359365
},
360366
methods: {
361367
legendClick(evt) {
362368
if (this.labelFor) {
363-
// don't do anything if labelFor is set
369+
// Don't do anything if labelFor is set
364370
/* istanbul ignore next: clicking a label will focus the input, so no need to test */
365371
return
366372
}
367373
const tagName = evt.target ? evt.target.tagName : ''
368374
if (/^(input|select|textarea|label|button|a)$/i.test(tagName)) {
369-
// If clicked an interactive element inside legend, we just let the default happen
375+
// If clicked an interactive element inside legend,
376+
// we just let the default happen
370377
/* istanbul ignore next */
371378
return
372379
}
@@ -378,8 +385,9 @@ export default (resolve, reject) => {
378385
},
379386
setInputDescribedBy(add, remove) {
380387
// Sets the `aria-describedby` attribute on the input if label-for is set.
381-
// Optionally accepts a string of IDs to remove as the second parameter
382-
if (this.labelFor && typeof document !== 'undefined') {
388+
// Optionally accepts a string of IDs to remove as the second parameter.
389+
// Preserves any aria-describedby value(s) user may have on input.
390+
if (this.labelFor && inBrowser) {
383391
const input = select(`#${this.labelFor}`, this.$refs.content)
384392
if (input) {
385393
const adb = 'aria-describedby'
@@ -413,9 +421,7 @@ export default (resolve, reject) => {
413421
ref: 'content',
414422
attrs: {
415423
tabindex: isFieldset ? '-1' : null,
416-
role: isFieldset ? 'group' : null,
417-
'aria-labelledby': isFieldset ? this.labelId : null,
418-
'aria-describedby': isFieldset ? this.ariaDescribedBy : null
424+
role: isFieldset ? 'group' : null
419425
}
420426
},
421427
[
@@ -434,14 +440,18 @@ export default (resolve, reject) => {
434440
disabled: isFieldset ? this.disabled : null,
435441
role: isFieldset ? null : 'group',
436442
'aria-invalid': this.computedState === false ? 'true' : null,
437-
'aria-labelledby': this.labelId || null,
438-
'aria-describedby': this.describedByIds || null
443+
// Only apply aria-labelledby if we are a horizontal fieldset
444+
// as the legend is no longer a direct child of fieldset
445+
'aria-labelledby': isFieldset && isHorizontal ? this.labelId : null,
446+
// Only apply aria-describedby IDs if we are a fieldset
447+
// as the input will have the IDs when not a fieldset
448+
'aria-describedby': isFieldset ? this.describedByIds : null
439449
}
440450
}
441-
// Return it wrapped in a form-group.
442-
// Note: fieldsets do not support adding `row` or `form-row` directly to them
443-
// due to browser specific render issues, so we move the form-row to an
444-
// inner wrapper div when horizontal and using a fieldset
451+
// Return it wrapped in a form-group
452+
// Note: Fieldsets do not support adding `row` or `form-row` directly
453+
// to them due to browser specific render issues, so we move the `form-row`
454+
// to an inner wrapper div when horizontal and using a fieldset
445455
return h(
446456
isFieldset ? 'fieldset' : isHorizontal ? 'b-form-row' : 'div',
447457
data,
@@ -450,6 +460,6 @@ export default (resolve, reject) => {
450460
}
451461
}
452462

453-
// Return the componwent options reference
463+
// Return the component options reference
454464
resolve(BFormGroup)
455465
}

src/components/form-group/form-group.spec.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,20 @@ describe('form-group', () => {
77
it('app changes validation state when text supplied', async () => {
88
const { app } = window
99
const $group = app.$refs.group1
10+
const $input = app.$refs.input1
1011

1112
expect($group.$el.getAttribute('aria-invalid')).toBe('true')
1213

13-
const oldADB = $group.$el.getAttribute('aria-describedby')
14+
const oldADB = $input.$el.getAttribute('aria-describedby')
1415

1516
await setData(app, 'text', 'foobar doodle')
1617
await nextTick()
1718

1819
expect($group.$el.getAttribute('aria-invalid')).toBe(null)
1920

20-
const newADB = $group.$el.getAttribute('aria-describedby')
21+
const newADB = $input.$el.getAttribute('aria-describedby')
2122

22-
expect(oldADB).not.toBe(newADB)
23+
expect(oldADB).not.toEqual(newADB)
2324
})
2425

2526
describe('form-group > legend click', () => {

0 commit comments

Comments
 (0)