@@ -93,6 +106,8 @@ or set the value to `'auto'` so that the label occupies only the width that is n
id="fieldset-horizontal"
label-cols-sm="4"
label-cols-lg="3"
+ content-cols-sm
+ content-cols-lg="7"
description="Let us know your name."
label="Enter your name"
label-for="input-horizontal"
@@ -105,7 +120,7 @@ or set the value to `'auto'` so that the label occupies only the width that is n
```
-The ability to set the label cols to `'auto'` was added in BootstrapVue version
2.1.0 .
+The ability to set the label cols to `'auto'` was added in BootstrapVue version `v2.1.0`.
### Label size
@@ -136,7 +151,7 @@ for both horizontal and non-horizontal form groups.
The label text may also optionally be aligned `left`, `center` or `right` by setting the respective
value via the prop `label-text-align` and/or `label-align-{breakpoint}`.
-| prop | description |
+| Prop | Description |
| ---------------- | --------------------------------- |
| `label-align` | Applies to breakpoint `xs` up |
| `label-align-sm` | Applies to breakpoint `sm` and up |
@@ -168,49 +183,52 @@ of related form controls:
class="mb-0"
>
@@ -220,7 +238,7 @@ of related form controls:
```
-## Disabled form-group
+## Disabled form group
Setting the `disabled` prop will disable the rendered `
` and, on most browsers, will
disable all the input elements contained within the fieldset.
@@ -290,17 +308,13 @@ the invalid feedback to show when using one of the above mentioned form controls
## Accessibility
-To enable auto-generation of `aria-*` attributes, you should supply a unique `id` prop to
-``. This will associate the help text and feedback text to the `` and,
-indirectly to its input control(s).
-
By default, when no `label-for` value is provided, `` renders the input control(s)
inside a an HTML `` element with the label content placed inside the fieldset's ``
element. By nature of this markup, the legend content is automatically associated to the containing
input control(s).
It is **highly recommended** that you provide a unique `id` prop on your input element and set the
-`label-for` prop to this id, when you have only a single input in the ``.
+`label-for` prop to this ID, when you have only a single input in the ``.
When multiple form controls are placed inside `` (i.e. a series or radio or checkbox
inputs, or a series of related inputs), **do not set** the `label-for` prop, as a label can only be
@@ -308,11 +322,16 @@ associated with a single input. It is best to use the default rendered markup th
`` + `` which will describe the group of inputs.
When placing multiple form controls inside a `` (and you are not nesting
-``components), it is recommended to give each control its own associated ``
+`` components), it is recommended to give each control its own associated ``
(which may be visually hidden using the `.sr-only` class) and set the labels `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 ``. For `` and `` (or the
group versions), you do not need to set individual labels, as the rendered markup for these types of
inputs already includes a `` element.
+When the `` has a `label-for` prop set, the `aria-describedby` attribute will be
+auto-assigned to the input. When the form group has multiple form controls, make sure to set the
+attribute to each control yourself by using the `ariaDescribedby` prop value from the optionally
+scoped `default` slot.
+
diff --git a/src/components/form-group/form-group.js b/src/components/form-group/form-group.js
index 38fcb431150..c9c7a990080 100644
--- a/src/components/form-group/form-group.js
+++ b/src/components/form-group/form-group.js
@@ -6,7 +6,9 @@ import {
PROP_TYPE_BOOLEAN_NUMBER_STRING,
PROP_TYPE_STRING
} from '../../constants/props'
+import { RX_SPACE_SPLIT } from '../../constants/regex'
import {
+ SLOT_NAME_DEFAULT,
SLOT_NAME_DESCRIPTION,
SLOT_NAME_INVALID_FEEDBACK,
SLOT_NAME_LABEL,
@@ -40,11 +42,13 @@ import { BFormValidFeedback } from '../form/form-valid-feedback'
// --- Constants ---
-// Selector for finding first input in the form-group
-const INPUT_SELECTOR = 'input:not([disabled]),textarea:not([disabled]),select:not([disabled])'
+const INPUTS = ['input', 'select', 'textarea']
+
+// Selector for finding first input in the form group
+const INPUT_SELECTOR = INPUTS.map(v => `${v}:not([disabled])`).join()
// A list of interactive elements (tag names) inside ``'s legend
-const LEGEND_INTERACTIVE_ELEMENTS = ['input', 'select', 'textarea', 'label', 'button', 'a']
+const LEGEND_INTERACTIVE_ELEMENTS = [...INPUTS, 'a', 'button', 'label']
// --- Props ---
@@ -55,10 +59,12 @@ export const generateProps = () =>
...idProps,
...formStateProps,
...getBreakpointsUpCached().reduce((props, breakpoint) => {
+ // i.e. 'content-cols', 'content-cols-sm', 'content-cols-md', ...
+ props[suffixPropName(breakpoint, 'contentCols')] = makeProp(PROP_TYPE_BOOLEAN_NUMBER_STRING)
+ // i.e. 'label-align', 'label-align-sm', 'label-align-md', ...
+ props[suffixPropName(breakpoint, 'labelAlign')] = makeProp(PROP_TYPE_STRING)
// i.e. 'label-cols', 'label-cols-sm', 'label-cols-md', ...
props[suffixPropName(breakpoint, 'labelCols')] = makeProp(PROP_TYPE_BOOLEAN_NUMBER_STRING)
- // 'label-align', 'label-align-sm', 'label-align-md', ...
- props[suffixPropName(breakpoint, 'labelAlign')] = makeProp(PROP_TYPE_STRING)
return props
}, create(null)),
description: makeProp(PROP_TYPE_STRING),
@@ -79,7 +85,7 @@ export const generateProps = () =>
// --- Main component ---
-// We do not use Vue.extend here as that would evaluate the props
+// We do not use `Vue.extend()` here as that would evaluate the props
// immediately, which we do not want to happen
// @vue/component
export const BFormGroup = {
@@ -95,135 +101,150 @@ export const BFormGroup = {
},
data() {
return {
- describedByIds: ''
+ ariaDescribedby: null
}
},
computed: {
- labelColProps() {
- const props = {}
- getBreakpointsUpCached().forEach(breakpoint => {
- // Grab the value if the label column breakpoint prop
- let propValue = this[suffixPropName(breakpoint, 'labelCols')]
- // Handle case where the prop's value is an empty string,
- // which represents `true`
- propValue = propValue === '' ? true : propValue || false
- if (!isBoolean(propValue) && propValue !== 'auto') {
- // Convert to column size to number
- propValue = toInteger(propValue, 0)
- // Ensure column size is greater than `0`
- propValue = propValue > 0 ? propValue : false
- }
- if (propValue) {
- // Add the prop to the list of props to give to ``
- // If breakpoint is '' (`labelCols` is `true`), then we use the
- // col prop to make equal width at 'xs'
- props[breakpoint || (isBoolean(propValue) ? 'col' : 'cols')] = propValue
- }
- })
- return props
+ contentColProps() {
+ return this.getColProps(this.$props, 'content')
},
labelAlignClasses() {
- const classes = []
- getBreakpointsUpCached().forEach(breakpoint => {
- // Assemble the label column breakpoint align classes
- const propValue = this[suffixPropName(breakpoint, 'labelAlign')] || null
- if (propValue) {
- const className = breakpoint ? `text-${breakpoint}-${propValue}` : `text-${propValue}`
- classes.push(className)
- }
- })
- return classes
+ return this.getAlignClasses(this.$props, 'label')
+ },
+ labelColProps() {
+ return this.getColProps(this.$props, 'label')
},
isHorizontal() {
- // Determine if the resultant form-group will be rendered
- // horizontal (meaning it has label-col breakpoints)
- return keys(this.labelColProps).length > 0
+ // Determine if the form group will be rendered horizontal
+ // based on the existence of 'content-col' or 'label-col' props
+ return keys(this.contentColProps).length > 0 || keys(this.labelColProps).length > 0
}
},
watch: {
- describedByIds(newValue, oldValue) {
+ ariaDescribedby(newValue, oldValue) {
if (newValue !== oldValue) {
- this.setInputDescribedBy(newValue, oldValue)
+ this.updateAriaDescribedby(newValue, oldValue)
}
}
},
mounted() {
this.$nextTick(() => {
- // Set the `aria-describedby` IDs on the input specified by `label-for`
+ // Set `aria-describedby` on the input specified by `labelFor`
// We do this in a `$nextTick()` to ensure the children have finished rendering
- this.setInputDescribedBy(this.describedByIds)
+ this.updateAriaDescribedby(this.ariaDescribedby)
})
},
methods: {
- legendClick(event) {
- // Don't do anything if labelFor is set
+ getAlignClasses(props, prefix) {
+ return getBreakpointsUpCached().reduce((result, breakpoint) => {
+ const propValue = props[suffixPropName(breakpoint, `${prefix}Align`)] || null
+ if (propValue) {
+ result.push(['text', breakpoint, propValue].filter(identity).join('-'))
+ }
+
+ return result
+ }, [])
+ },
+ getColProps(props, prefix) {
+ return getBreakpointsUpCached().reduce((result, breakpoint) => {
+ let propValue = props[suffixPropName(breakpoint, `${prefix}Cols`)]
+
+ // Handle case where the prop's value is an empty string,
+ // which represents `true`
+ propValue = propValue === '' ? true : propValue || false
+
+ if (!isBoolean(propValue) && propValue !== 'auto') {
+ // Convert to column size to number
+ propValue = toInteger(propValue, 0)
+ // Ensure column size is greater than `0`
+ propValue = propValue > 0 ? propValue : false
+ }
+
+ // Add the prop to the list of props to give to ``
+ // If breakpoint is '' (`${prefix}Cols` is `true`), then we use
+ // the 'col' prop to make equal width at 'xs'
+ if (propValue) {
+ result[breakpoint || (isBoolean(propValue) ? 'col' : 'cols')] = propValue
+ }
+
+ return result
+ }, {})
+ },
+ // Sets the `aria-describedby` attribute on the input if `labelFor` is set
+ // Optionally accepts a string of IDs to remove as the second parameter
+ // Preserves any `aria-describedby` value(s) user may have on input
+ updateAriaDescribedby(newValue, oldValue) {
+ const { labelFor } = this
+ if (IS_BROWSER && labelFor) {
+ // We need to escape `labelFor` since it can be user-provided
+ const $input = select(`#${cssEscape(labelFor)}`, this.$refs.content)
+ if ($input) {
+ const attr = 'aria-describedby'
+ const newIds = (newValue || '').split(RX_SPACE_SPLIT)
+ const oldIds = (oldValue || '').split(RX_SPACE_SPLIT)
+
+ // Update ID list, preserving any original IDs
+ // and ensuring the ID's are unique
+ const ids = (getAttr($input, attr) || '')
+ .split(RX_SPACE_SPLIT)
+ .filter(id => !arrayIncludes(oldIds, id))
+ .concat(newIds)
+ .filter((id, index, ids) => ids.indexOf(id) === index)
+ .filter(identity)
+ .join(' ')
+ .trim()
+
+ if (ids) {
+ setAttr($input, attr, ids)
+ } else {
+ removeAttr($input, attr)
+ }
+ }
+ }
+ },
+ onLegendClick(event) {
+ // Don't do anything if `labelFor` is set
/* istanbul ignore next: clicking a label will focus the input, so no need to test */
if (this.labelFor) {
return
}
+
const { target } = event
const tagName = target ? target.tagName : ''
+
// If clicked an interactive element inside legend,
// we just let the default happen
/* istanbul ignore next */
if (LEGEND_INTERACTIVE_ELEMENTS.indexOf(tagName) !== -1) {
return
}
- const inputs = selectAll(INPUT_SELECTOR, this.$refs.content).filter(isVisible)
+
// If only a single input, focus it, emulating label behaviour
- if (inputs && inputs.length === 1) {
+ const inputs = selectAll(INPUT_SELECTOR, this.$refs.content).filter(isVisible)
+ if (inputs.length === 1) {
attemptFocus(inputs[0])
}
- },
- // 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
- // Preserves any `aria-describedby` value(s) user may have on input
- setInputDescribedBy(add, remove) {
- if (this.labelFor && IS_BROWSER) {
- // We need to escape `labelFor` since it can be user-provided
- const input = select(`#${cssEscape(this.labelFor)}`, this.$refs.content)
- if (input) {
- const adb = 'aria-describedby'
- let ids = (getAttr(input, adb) || '').split(/\s+/)
- add = (add || '').split(/\s+/)
- remove = (remove || '').split(/\s+/)
- // Update ID list, preserving any original IDs
- // and ensuring the ID's are unique
- ids = ids
- .filter(id => !arrayIncludes(remove, id))
- .concat(add)
- .filter(identity)
- ids = keys(ids.reduce((memo, id) => ({ ...memo, [id]: true }), {}))
- .join(' ')
- .trim()
- if (ids) {
- setAttr(input, adb, ids)
- } else {
- // No IDs, so remove the attribute
- removeAttr(input, adb)
- }
- }
- }
}
},
render(h) {
const {
- labelFor,
- tooltip,
- feedbackAriaLive,
computedState: state,
+ feedbackAriaLive,
isHorizontal,
- normalizeSlot
+ labelFor,
+ normalizeSlot,
+ safeId,
+ tooltip
} = this
+ const id = safeId()
const isFieldset = !labelFor
let $label = h()
const labelContent = normalizeSlot(SLOT_NAME_LABEL) || this.label
- const labelId = labelContent ? this.safeId('_BV_label_') : null
+ const labelId = labelContent ? safeId('_BV_label_') : null
if (labelContent || isHorizontal) {
const { labelSize, labelColProps } = this
- const isLegend = isFieldset
- const labelTag = isLegend ? 'legend' : 'label'
+ const labelTag = isFieldset ? 'legend' : 'label'
if (this.labelSrOnly) {
if (labelContent) {
$label = h(
@@ -242,28 +263,28 @@ export const BFormGroup = {
$label = h(
isHorizontal ? BCol : labelTag,
{
- on: isLegend ? { click: this.legendClick } : {},
- props: isHorizontal ? { tag: labelTag, ...labelColProps } : {},
+ on: isFieldset ? { click: this.onLegendClick } : {},
+ props: isHorizontal ? { ...labelColProps, tag: labelTag } : {},
attrs: {
id: labelId,
for: labelFor || null,
// We add a `tabindex` to legend so that screen readers
// will properly read the `aria-labelledby` in IE
- tabindex: isLegend ? '-1' : null
+ tabindex: isFieldset ? '-1' : null
},
class: [
// Hide the focus ring on the legend
- isLegend ? 'bv-no-focus-ring' : '',
+ isFieldset ? 'bv-no-focus-ring' : '',
// When horizontal or if a legend is rendered, add 'col-form-label' class
// for correct sizing as Bootstrap has inconsistent font styling for
- // legend in non-horizontal form-groups
+ // legend in non-horizontal form groups
// See: https://github.com/twbs/bootstrap/issues/27805
- isHorizontal || isLegend ? 'col-form-label' : '',
+ isHorizontal || isFieldset ? 'col-form-label' : '',
// Emulate label padding top of `0` on legend when not horizontal
- !isHorizontal && isLegend ? 'pt-0' : '',
+ !isHorizontal && isFieldset ? 'pt-0' : '',
// If not horizontal and not a legend, we add 'd-block' class to label
// so that label-align works
- !isHorizontal && !isLegend ? 'd-block' : '',
+ !isHorizontal && !isFieldset ? 'd-block' : '',
labelSize ? `col-form-label-${labelSize}` : '',
this.labelAlignClasses,
this.labelClass
@@ -276,18 +297,18 @@ export const BFormGroup = {
let $invalidFeedback = h()
const invalidFeedbackContent = normalizeSlot(SLOT_NAME_INVALID_FEEDBACK) || this.invalidFeedback
- const invalidFeedbackId = invalidFeedbackContent ? this.safeId('_BV_feedback_invalid_') : null
+ const invalidFeedbackId = invalidFeedbackContent ? safeId('_BV_feedback_invalid_') : null
if (invalidFeedbackContent) {
$invalidFeedback = h(
BFormInvalidFeedback,
{
props: {
+ ariaLive: feedbackAriaLive,
id: invalidFeedbackId,
+ role: feedbackAriaLive ? 'alert' : null,
// If state is explicitly `false`, always show the feedback
state,
- tooltip,
- ariaLive: feedbackAriaLive,
- role: feedbackAriaLive ? 'alert' : null
+ tooltip
},
attrs: { tabindex: invalidFeedbackContent ? '-1' : null }
},
@@ -297,18 +318,18 @@ export const BFormGroup = {
let $validFeedback = h()
const validFeedbackContent = normalizeSlot(SLOT_NAME_VALID_FEEDBACK) || this.validFeedback
- const validFeedbackId = validFeedbackContent ? this.safeId('_BV_feedback_valid_') : null
+ const validFeedbackId = validFeedbackContent ? safeId('_BV_feedback_valid_') : null
if (validFeedbackContent) {
$validFeedback = h(
BFormValidFeedback,
{
props: {
+ ariaLive: feedbackAriaLive,
id: validFeedbackId,
+ role: feedbackAriaLive ? 'alert' : null,
// If state is explicitly `true`, always show the feedback
state,
- tooltip,
- ariaLive: feedbackAriaLive,
- role: feedbackAriaLive ? 'alert' : null
+ tooltip
},
attrs: { tabindex: validFeedbackContent ? '-1' : null }
},
@@ -318,48 +339,48 @@ export const BFormGroup = {
let $description = h()
const descriptionContent = normalizeSlot(SLOT_NAME_DESCRIPTION) || this.description
- const descriptionId = descriptionContent ? this.safeId('_BV_description_') : null
+ const descriptionId = descriptionContent ? safeId('_BV_description_') : null
if (descriptionContent) {
$description = h(
BFormText,
{
attrs: {
id: descriptionId,
- tabindex: descriptionContent ? '-1' : null
+ tabindex: '-1'
}
},
[descriptionContent]
)
}
+ // Update `ariaDescribedby`
+ // Screen readers will read out any content linked to by `aria-describedby`
+ // even if the content is hidden with `display: none;`, hence we only include
+ // feedback IDs if the form group's state is explicitly valid or invalid
+ const ariaDescribedby = (this.ariaDescribedby =
+ [
+ descriptionId,
+ state === false ? invalidFeedbackId : null,
+ state === true ? validFeedbackId : null
+ ]
+ .filter(identity)
+ .join(' ') || null)
+
const $content = h(
isHorizontal ? BCol : 'div',
{
- // Hide focus ring
- staticClass: 'bv-no-focus-ring',
- attrs: {
- tabindex: isFieldset ? '-1' : null,
- role: isFieldset ? 'group' : null,
- 'aria-labelledby': isFieldset ? labelId : null
- },
+ props: isHorizontal ? this.contentColProps : {},
ref: 'content'
},
- [normalizeSlot() || h(), $invalidFeedback, $validFeedback, $description]
+ [
+ normalizeSlot(SLOT_NAME_DEFAULT, { ariaDescribedby, descriptionId, id, labelId }) || h(),
+ $invalidFeedback,
+ $validFeedback,
+ $description
+ ]
)
- // Update the `aria-describedby` IDs
- // Screen readers will read out any content linked to by `aria-describedby`
- // even if the content is hidden with `display: none;`, hence we only include
- // feedback IDs if the form-group's state is explicitly valid or invalid
- this.describedByIds = [
- descriptionId,
- state === false ? invalidFeedbackId : null,
- state === true ? validFeedbackId : null
- ]
- .filter(identity)
- .join(' ')
-
- // Return it wrapped in a form-group
+ // Return it wrapped in a form group
// Note: Fieldsets do not support adding `row` or `form-row` directly
// to them due to browser specific render issues, so we move the `form-row`
// to an inner wrapper div when horizontal and using a fieldset
@@ -367,18 +388,15 @@ export const BFormGroup = {
isFieldset ? 'fieldset' : isHorizontal ? BFormRow : 'div',
{
staticClass: 'form-group',
- class: [this.validated ? 'was-validated' : null, this.stateClass],
+ class: [{ 'was-validated': this.validated }, this.stateClass],
attrs: {
- id: this.safeId(),
+ id,
disabled: isFieldset ? this.disabled : null,
role: isFieldset ? null : 'group',
'aria-invalid': this.computedAriaInvalid,
- // Only apply aria-labelledby if we are a horizontal fieldset
+ // Only apply `aria-labelledby` if we are a horizontal fieldset
// as the legend is no longer a direct child of fieldset
- 'aria-labelledby': isFieldset && isHorizontal ? labelId : null,
- // Only apply `aria-describedby` IDs if we are a fieldset
- // as the input will have the IDs when not a fieldset
- 'aria-describedby': isFieldset ? this.describedByIds : null
+ 'aria-labelledby': isFieldset && isHorizontal ? labelId : null
}
},
isHorizontal && isFieldset ? [h(BFormRow, [$label, $content])] : [$label, $content]
diff --git a/src/components/form-group/form-group.spec.js b/src/components/form-group/form-group.spec.js
index d56acda2df3..f1f1cc95527 100644
--- a/src/components/form-group/form-group.spec.js
+++ b/src/components/form-group/form-group.spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'
import { createContainer, waitNT } from '../../../tests/utils'
+import { BCol } from '../layout/col'
import { BFormGroup } from './form-group'
describe('form-group', () => {
@@ -27,8 +28,6 @@ describe('form-group', () => {
const wrapper = mount(BFormGroup)
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
await waitNT(wrapper.vm)
expect(wrapper.element.tagName).toBe('FIELDSET')
@@ -38,9 +37,6 @@ describe('form-group', () => {
expect(wrapper.attributes('aria-labelledby')).toBeUndefined()
expect(wrapper.find('label').exists()).toBe(false)
expect(wrapper.find('legend').exists()).toBe(false)
- expect(wrapper.find('div').exists()).toBe(true)
- expect(wrapper.find('div').attributes('role')).toEqual('group')
- expect(wrapper.find('div').attributes('tabindex')).toEqual('-1')
expect(wrapper.text()).toEqual('')
wrapper.destroy()
@@ -54,18 +50,46 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
await waitNT(wrapper.vm)
- expect(wrapper.find('div').exists()).toBe(true)
- expect(wrapper.find('div').attributes('role')).toEqual('group')
- expect(wrapper.find('div[role="group"]').text()).toEqual('foobar')
expect(wrapper.text()).toEqual('foobar')
wrapper.destroy()
})
+ it('default slot is optionally scoped', async () => {
+ const label = 'my-label'
+ const description = 'my-description'
+ let slotScope
+
+ const wrapper = mount(BFormGroup, {
+ propsData: {
+ label,
+ description
+ },
+ scopedSlots: {
+ default(scope) {
+ slotScope = scope
+ return 'foobar'
+ }
+ }
+ })
+
+ expect(wrapper.vm).toBeDefined()
+ await waitNT(wrapper.vm)
+
+ expect(slotScope).toBeDefined()
+ expect(typeof slotScope.ariaDescribedby).toBe('string')
+ expect(typeof slotScope.descriptionId).toBe('string')
+ expect(typeof slotScope.id).toBe('string')
+ expect(typeof slotScope.labelId).toBe('string')
+
+ expect(wrapper.text()).toContain(label)
+ expect(wrapper.text()).toContain(description)
+
+ wrapper.destroy()
+ })
+
it('has user supplied ID', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
@@ -79,6 +103,8 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
+ await waitNT(wrapper.vm)
+
expect(wrapper.attributes('id')).toEqual('foo')
expect(wrapper.attributes('aria-labelledby')).toBeUndefined()
expect(wrapper.find('label').attributes('id')).toEqual('foo__BV_label_')
@@ -86,7 +112,31 @@ describe('form-group', () => {
wrapper.destroy()
})
- it('does not render a fieldset if prop label-for set', async () => {
+ it('sets `aria-describedby` even when special characters are used in IDs', async () => {
+ const wrapper = mount(BFormGroup, {
+ propsData: {
+ id: '/group-id',
+ label: 'test',
+ labelFor: '/input-id',
+ // Description is needed to set `aria-describedby`
+ description: 'foo'
+ },
+ slots: {
+ default: ' '
+ }
+ })
+
+ expect(wrapper.vm).toBeDefined()
+ await waitNT(wrapper.vm)
+
+ const $input = wrapper.find('input')
+ expect($input.exists()).toBe(true)
+ expect($input.attributes('aria-describedby')).toEqual('/group-id__BV_description_')
+
+ wrapper.destroy()
+ })
+
+ it('does not render a FIELDSET if prop `label-for` set', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
label: 'test',
@@ -98,39 +148,32 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
await waitNT(wrapper.vm)
const formGroupId = wrapper.attributes('id')
- expect(wrapper.element.tagName).not.toBe('FIELDSET')
+
expect(wrapper.element.tagName).toBe('DIV')
expect(wrapper.classes()).toContain('form-group')
expect(wrapper.classes().length).toBe(1)
expect(wrapper.attributes('id')).toBeDefined()
expect(wrapper.attributes('role')).toEqual('group')
expect(wrapper.attributes('aria-labelledby')).toBeUndefined()
+
expect(wrapper.find('legend').exists()).toBe(false)
- expect(wrapper.find('label').exists()).toBe(true)
- expect(wrapper.find('label').classes()).toContain('d-block')
- expect(wrapper.find('label').text()).toEqual('test')
- expect(wrapper.find('label').attributes('for')).toEqual('input-id')
- expect(wrapper.find('div > div').exists()).toBe(true)
- expect(wrapper.find('div > div').classes()).toContain('bv-no-focus-ring')
- expect(wrapper.find('div > div').classes().length).toBe(1)
- expect(wrapper.find('div > div').attributes('role')).toBeUndefined()
- expect(wrapper.find('div > div').attributes('tabindex')).toBeUndefined()
- expect(wrapper.find('div > div').attributes('aria-labelledby')).toBeUndefined()
- expect(wrapper.find('div > div > input').exists()).toBe(true)
- expect(wrapper.find('div > div > input').attributes('aria-describedby')).toBeUndefined()
- expect(wrapper.find('div > div > input').attributes('aria-labelledby')).toBeUndefined()
- expect(wrapper.find('div > div').text()).toEqual('')
- expect(wrapper.find('label').attributes('id')).toEqual(`${formGroupId}__BV_label_`)
+
+ const $label = wrapper.find('label')
+ expect($label.exists()).toBe(true)
+ expect($label.classes()).toContain('d-block')
+ expect($label.text()).toEqual('test')
+ expect($label.attributes('id')).toEqual(`${formGroupId}__BV_label_`)
+ expect($label.attributes('for')).toEqual('input-id')
+ expect($label.attributes('aria-describedby')).toBeUndefined()
+ expect($label.attributes('aria-labelledby')).toBeUndefined()
wrapper.destroy()
})
- it('horizontal layout with prop label-for set has expected structure', async () => {
+ it('has expected structure for horizontal layout with prop `label-for` set', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
label: 'test',
@@ -147,61 +190,35 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
+ await waitNT(wrapper.vm)
- expect(wrapper.element.tagName).not.toBe('FIELDSET')
- expect(wrapper.find('legend').exists()).toBe(false)
expect(wrapper.element.tagName).toBe('DIV')
expect(wrapper.classes()).toContain('form-group')
expect(wrapper.classes()).toContain('form-row')
expect(wrapper.classes().length).toBe(2)
expect(wrapper.attributes('role')).toEqual('group')
expect(wrapper.attributes('aria-labelledby')).toBeUndefined()
- expect(wrapper.find('label').exists()).toBe(true)
- expect(wrapper.find('label').classes()).toContain('col-form-label')
- expect(wrapper.find('label').classes()).toContain('col-1')
- expect(wrapper.find('label').classes()).toContain('col-sm-2')
- expect(wrapper.find('label').classes()).toContain('col-md-3')
- expect(wrapper.find('label').classes()).toContain('col-lg-4')
- expect(wrapper.find('label').classes()).toContain('col-xl-5')
- expect(wrapper.find('label').classes().length).toBe(6)
- expect(wrapper.find('label').text()).toEqual('test')
- expect(wrapper.find('div > div').exists()).toBe(true)
- expect(wrapper.find('div > div').classes()).toContain('col')
- expect(wrapper.find('div > div').classes()).toContain('bv-no-focus-ring')
- expect(wrapper.find('div > div').classes().length).toBe(2)
- expect(wrapper.find('div > div').attributes('role')).toBeUndefined()
- expect(wrapper.find('div > div').attributes('tabindex')).toBeUndefined()
- expect(wrapper.find('div > div').attributes('aria-labelledby')).toBeUndefined()
- wrapper.destroy()
- })
+ const $cols = wrapper.findAllComponents(BCol)
+ expect($cols.length).toBe(2)
- it('sets "aria-describedby" even when special characters are used in IDs', async () => {
- const wrapper = mount(BFormGroup, {
- propsData: {
- id: '/group-id',
- label: 'test',
- labelFor: '/input-id',
- description: 'foo' // Description is needed to set "aria-describedby"
- },
- slots: {
- default: ' '
- }
- })
-
- expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
- await waitNT(wrapper.vm)
+ const $label = wrapper.find('label')
+ expect($label.exists()).toBe(true)
+ expect($label.classes()).toContain('col-form-label')
+ expect($label.classes()).toContain('col-1')
+ expect($label.classes()).toContain('col-sm-2')
+ expect($label.classes()).toContain('col-md-3')
+ expect($label.classes()).toContain('col-lg-4')
+ expect($label.classes()).toContain('col-xl-5')
+ expect($label.classes().length).toBe(6)
+ expect($label.text()).toEqual('test')
- const $input = wrapper.find('input')
- expect($input.exists()).toBe(true)
- expect($input.attributes('aria-describedby')).toEqual('/group-id__BV_description_')
+ expect(wrapper.find('legend').exists()).toBe(false)
wrapper.destroy()
})
- it('horizontal layout without prop label-for set has expected structure', async () => {
+ it('has expected structure for horizontal layout without prop `label-for` set', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
label: 'test',
@@ -217,39 +234,32 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
await waitNT(wrapper.vm)
expect(wrapper.element.tagName).toBe('FIELDSET')
- expect(wrapper.element.tagName).not.toBe('DIV')
- expect(wrapper.find('legend').exists()).toBe(true)
- expect(wrapper.find('fieldset > div > legend').exists()).toBe(true)
expect(wrapper.classes()).toContain('form-group')
expect(wrapper.classes().length).toBe(1)
expect(wrapper.attributes('role')).toBeUndefined()
expect(wrapper.attributes('aria-labelledby')).toBeDefined()
- expect(wrapper.find('legend').classes()).toContain('col-form-label')
- expect(wrapper.find('legend').classes()).toContain('col-1')
- expect(wrapper.find('legend').classes()).toContain('col-sm-2')
- expect(wrapper.find('legend').classes()).toContain('col-md-3')
- expect(wrapper.find('legend').classes()).toContain('col-lg-4')
- expect(wrapper.find('legend').classes()).toContain('col-xl-5')
- expect(wrapper.find('legend').classes()).toContain('bv-no-focus-ring')
- expect(wrapper.find('legend').classes().length).toBe(7)
- expect(wrapper.find('legend').text()).toEqual('test')
- expect(wrapper.find('fieldset > div > div').exists()).toBe(true)
- expect(wrapper.find('fieldset > div > div').classes()).toContain('col')
- expect(wrapper.find('fieldset > div > div').classes()).toContain('bv-no-focus-ring')
- expect(wrapper.find('fieldset > div > div').classes().length).toBe(2)
- expect(wrapper.find('fieldset > div > div').attributes('role')).toEqual('group')
- expect(wrapper.find('fieldset > div > div').attributes('tabindex')).toEqual('-1')
- expect(wrapper.find('fieldset > div > div').attributes('aria-labelledby')).toBeDefined()
+
+ const $legend = wrapper.find('legend')
+ expect($legend.exists()).toBe(true)
+ expect($legend.classes()).toContain('col-form-label')
+ expect($legend.classes()).toContain('col-1')
+ expect($legend.classes()).toContain('col-sm-2')
+ expect($legend.classes()).toContain('col-md-3')
+ expect($legend.classes()).toContain('col-lg-4')
+ expect($legend.classes()).toContain('col-xl-5')
+ expect($legend.classes()).toContain('bv-no-focus-ring')
+ expect($legend.classes().length).toBe(7)
+ expect($legend.text()).toEqual('test')
+
+ expect(wrapper.find('label').exists()).toBe(false)
wrapper.destroy()
})
- it('horizontal layout without label content has expected structure', async () => {
+ it('has expected structure for horizontal layout without label content', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
labelCols: 1
@@ -260,28 +270,21 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
await waitNT(wrapper.vm)
expect(wrapper.element.tagName).toBe('FIELDSET')
- expect(wrapper.element.tagName).not.toBe('DIV')
- expect(wrapper.find('legend').exists()).toBe(true)
- expect(wrapper.find('fieldset > div > legend').exists()).toBe(true)
expect(wrapper.classes()).toContain('form-group')
expect(wrapper.classes().length).toBe(1)
expect(wrapper.attributes('role')).toBeUndefined()
expect(wrapper.attributes('aria-labelledby')).toBeUndefined()
- expect(wrapper.find('legend').classes()).toContain('col-form-label')
- expect(wrapper.find('legend').classes()).toContain('col-1')
- expect(wrapper.find('legend').classes()).toContain('bv-no-focus-ring')
- expect(wrapper.find('legend').text()).toEqual('')
- expect(wrapper.find('fieldset > div > div').exists()).toBe(true)
- expect(wrapper.find('fieldset > div > div').classes()).toContain('col')
- expect(wrapper.find('fieldset > div > div').classes()).toContain('bv-no-focus-ring')
- expect(wrapper.find('fieldset > div > div').classes().length).toBe(2)
- expect(wrapper.find('fieldset > div > div').attributes('role')).toEqual('group')
- expect(wrapper.find('fieldset > div > div').attributes('tabindex')).toEqual('-1')
+
+ const $legend = wrapper.find('legend')
+ expect($legend.classes()).toContain('col-form-label')
+ expect($legend.classes()).toContain('col-1')
+ expect($legend.classes()).toContain('bv-no-focus-ring')
+ expect($legend.text()).toEqual('')
+
+ expect(wrapper.find('label').exists()).toBe(false)
wrapper.destroy()
})
@@ -302,21 +305,9 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
await waitNT(wrapper.vm)
- // With state = null (default), all helpers are rendered
- expect(wrapper.find('.invalid-feedback').exists()).toBe(true)
- expect(wrapper.find('.invalid-feedback').text()).toEqual('bar')
- expect(wrapper.find('.invalid-feedback').attributes('role')).toEqual('alert')
- expect(wrapper.find('.invalid-feedback').attributes('aria-live')).toEqual('assertive')
- expect(wrapper.find('.invalid-feedback').attributes('aria-atomic')).toEqual('true')
- expect(wrapper.find('.valid-feedback').exists()).toBe(true)
- expect(wrapper.find('.valid-feedback').text()).toEqual('baz')
- expect(wrapper.find('.valid-feedback').attributes('role')).toEqual('alert')
- expect(wrapper.find('.valid-feedback').attributes('aria-live')).toEqual('assertive')
- expect(wrapper.find('.valid-feedback').attributes('aria-atomic')).toEqual('true')
+ // When `state` is `null` (default), all helpers are rendered
expect(wrapper.find('.form-text').exists()).toBe(true)
expect(wrapper.find('.form-text').text()).toEqual('foo')
expect(wrapper.attributes('aria-invalid')).toBeUndefined()
@@ -327,33 +318,41 @@ describe('form-group', () => {
expect($input.exists()).toBe(true)
expect($input.attributes('aria-describedby')).toEqual('group-id__BV_description_')
- // With state = true, description and valid are visible
- await wrapper.setProps({
- state: true
- })
- await waitNT(wrapper.vm)
- expect($input.attributes('aria-describedby')).toBeDefined()
- expect($input.attributes('aria-describedby')).toEqual(
- 'group-id__BV_description_ group-id__BV_feedback_valid_'
- )
+ const $invalidFeedback = wrapper.find('.invalid-feedback')
+ expect($invalidFeedback.exists()).toBe(true)
+ expect($invalidFeedback.text()).toEqual('bar')
+ expect($invalidFeedback.attributes('role')).toEqual('alert')
+ expect($invalidFeedback.attributes('aria-live')).toEqual('assertive')
+ expect($invalidFeedback.attributes('aria-atomic')).toEqual('true')
+
+ const $validFeedback = wrapper.find('.valid-feedback')
+ expect($validFeedback.exists()).toBe(true)
+ expect($validFeedback.text()).toEqual('baz')
+ expect($validFeedback.attributes('role')).toEqual('alert')
+ expect($validFeedback.attributes('aria-live')).toEqual('assertive')
+ expect($validFeedback.attributes('aria-atomic')).toEqual('true')
+
+ // When `state` is `true`, description and valid are visible
+ await wrapper.setProps({ state: true })
expect(wrapper.attributes('aria-invalid')).toBeUndefined()
expect(wrapper.classes()).not.toContain('is-invalid')
expect(wrapper.classes()).toContain('is-valid')
-
- // With state = true, description and valid are visible
- await wrapper.setProps({
- state: false
- })
- await waitNT(wrapper.vm)
+ expect($input.attributes('aria-describedby')).toBeDefined()
expect($input.attributes('aria-describedby')).toEqual(
- 'group-id__BV_description_ group-id__BV_feedback_invalid_'
+ 'group-id__BV_description_ group-id__BV_feedback_valid_'
)
+
+ // When `state` is `false`, description and valid are visible
+ await wrapper.setProps({ state: false })
expect(wrapper.attributes('aria-invalid')).toEqual('true')
expect(wrapper.classes()).not.toContain('is-valid')
expect(wrapper.classes()).toContain('is-invalid')
+ expect($input.attributes('aria-describedby')).toEqual(
+ 'group-id__BV_description_ group-id__BV_feedback_invalid_'
+ )
})
- it('validation elements respect feedback-aria-live attribute', async () => {
+ it('has validation elements that respect `feedback-aria-live` prop', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
id: 'group-id',
@@ -369,40 +368,40 @@ describe('form-group', () => {
})
expect(wrapper.vm).toBeDefined()
-
- // Auto ID is created after mounted
- await waitNT(wrapper.vm)
-
- expect(wrapper.find('.invalid-feedback').exists()).toBe(true)
- expect(wrapper.find('.invalid-feedback').text()).toEqual('bar')
- expect(wrapper.find('.invalid-feedback').attributes('role')).toEqual('alert')
- expect(wrapper.find('.invalid-feedback').attributes('aria-live')).toEqual('polite')
- expect(wrapper.find('.invalid-feedback').attributes('aria-atomic')).toEqual('true')
- expect(wrapper.find('.valid-feedback').exists()).toBe(true)
- expect(wrapper.find('.valid-feedback').text()).toEqual('baz')
- expect(wrapper.find('.valid-feedback').attributes('role')).toEqual('alert')
- expect(wrapper.find('.valid-feedback').attributes('aria-live')).toEqual('polite')
- expect(wrapper.find('.valid-feedback').attributes('aria-atomic')).toEqual('true')
-
- // With feedback-aria-live set to null
- await wrapper.setProps({
- feedbackAriaLive: null
- })
await waitNT(wrapper.vm)
- expect(wrapper.find('.invalid-feedback').exists()).toBe(true)
- expect(wrapper.find('.invalid-feedback').text()).toEqual('bar')
- expect(wrapper.find('.invalid-feedback').attributes('role')).toBeUndefined()
- expect(wrapper.find('.invalid-feedback').attributes('aria-live')).toBeUndefined()
- expect(wrapper.find('.invalid-feedback').attributes('aria-atomic')).toBeUndefined()
- expect(wrapper.find('.valid-feedback').exists()).toBe(true)
- expect(wrapper.find('.valid-feedback').text()).toEqual('baz')
- expect(wrapper.find('.valid-feedback').attributes('role')).toBeUndefined()
- expect(wrapper.find('.valid-feedback').attributes('aria-live')).toBeUndefined()
- expect(wrapper.find('.valid-feedback').attributes('aria-atomic')).toBeUndefined()
+ let $invalidFeedback = wrapper.find('.invalid-feedback')
+ expect($invalidFeedback.exists()).toBe(true)
+ expect($invalidFeedback.text()).toEqual('bar')
+ expect($invalidFeedback.attributes('role')).toEqual('alert')
+ expect($invalidFeedback.attributes('aria-live')).toEqual('polite')
+ expect($invalidFeedback.attributes('aria-atomic')).toEqual('true')
+
+ let $validFeedback = wrapper.find('.valid-feedback')
+ expect($validFeedback.exists()).toBe(true)
+ expect($validFeedback.text()).toEqual('baz')
+ expect($validFeedback.attributes('role')).toEqual('alert')
+ expect($validFeedback.attributes('aria-live')).toEqual('polite')
+ expect($validFeedback.attributes('aria-atomic')).toEqual('true')
+
+ await wrapper.setProps({ feedbackAriaLive: null })
+
+ $invalidFeedback = wrapper.find('.invalid-feedback')
+ expect($invalidFeedback.exists()).toBe(true)
+ expect($invalidFeedback.text()).toEqual('bar')
+ expect($invalidFeedback.attributes('role')).toBeUndefined()
+ expect($invalidFeedback.attributes('aria-live')).toBeUndefined()
+ expect($invalidFeedback.attributes('aria-atomic')).toBeUndefined()
+
+ $validFeedback = wrapper.find('.valid-feedback')
+ expect($validFeedback.exists()).toBe(true)
+ expect($validFeedback.text()).toEqual('baz')
+ expect($validFeedback.attributes('role')).toBeUndefined()
+ expect($validFeedback.attributes('aria-live')).toBeUndefined()
+ expect($validFeedback.attributes('aria-atomic')).toBeUndefined()
})
- it('Label alignment works', async () => {
+ it('aligns the LABEL based on `label-align` props', async () => {
const wrapper = mount(BFormGroup, {
propsData: {
id: 'group-id',
@@ -419,6 +418,7 @@ describe('form-group', () => {
expect(wrapper.vm).toBeDefined()
await waitNT(wrapper.vm)
+
const $label = wrapper.find('label')
expect($label.exists()).toBe(true)
expect($label.classes()).toContain('text-left')
diff --git a/src/components/form-group/package.json b/src/components/form-group/package.json
index 7f4d4fd1f3b..492ef0fae1c 100644
--- a/src/components/form-group/package.json
+++ b/src/components/form-group/package.json
@@ -11,17 +11,42 @@
"BFormFieldset"
],
"props": [
+ {
+ "prop": "contentCols",
+ "version": "2.21.0",
+ "description": "Number of columns for the content width 'xs' screens and up"
+ },
+ {
+ "prop": "contentColsLg",
+ "version": "2.21.0",
+ "description": "Number of columns for the content width 'lg' screens and up"
+ },
+ {
+ "prop": "contentColsMd",
+ "version": "2.21.0",
+ "description": "Number of columns for the content width 'md' screens and up"
+ },
+ {
+ "prop": "contentColsSm",
+ "version": "2.21.0",
+ "description": "Number of columns for the content width 'sm' screens and up"
+ },
+ {
+ "prop": "contentColsXl",
+ "version": "2.21.0",
+ "description": "Number of columns for the content width 'xl' screens and up"
+ },
{
"prop": "description",
"description": "Text to place in the help text area of the form group"
},
{
"prop": "disabled",
- "description": "Disabled the fieldset element, which in turn disables the form controls (on browsers that support disabled fieldsets). Has no effect if 'label-for' is set"
+ "description": "Disabled the fieldset element, which in turn disables the form controls (on browsers that support disabled fieldsets). Has no effect if `label-for` is set"
},
{
"prop": "feedbackAriaLive",
- "description": "Value to use for the 'aria-live' attribute on the feedback text"
+ "description": "Value to use for the `aria-live` attribute on the feedback text"
},
{
"prop": "invalidFeedback",
@@ -33,27 +58,23 @@
},
{
"prop": "labelAlign",
- "description": "Text alignment 'left', 'center', 'right' for the label xs screens and up"
+ "description": "Text alignment 'left', 'center', 'right' for the label 'xs' screens and up"
},
{
"prop": "labelAlignLg",
- "description": "Text alignment 'left', 'center', 'right' for the label lg screens and up"
+ "description": "Text alignment 'left', 'center', 'right' for the label 'lg' screens and up"
},
{
"prop": "labelAlignMd",
- "description": "Text alignment 'left', 'center', 'right' for the label md screens and up"
+ "description": "Text alignment 'left', 'center', 'right' for the label 'md' screens and up"
},
{
"prop": "labelAlignSm",
- "description": "Text alignment 'left', 'center', 'right' for the label sm screens and up"
+ "description": "Text alignment 'left', 'center', 'right' for the label 'sm' screens and up"
},
{
"prop": "labelAlignXl",
- "description": "Text alignment 'left', 'center', 'right' for the label xl screens and up"
- },
- {
- "prop": "labelClass",
- "description": "CSS class (or classes) to add to the label/legend element"
+ "description": "Text alignment 'left', 'center', 'right' for the label 'xl' screens and up"
},
{
"prop": "labelClass",
@@ -61,27 +82,27 @@
},
{
"prop": "labelCols",
- "description": "Number of columns for the label width xs screens and up"
+ "description": "Number of columns for the label width 'xs' screens and up"
},
{
"prop": "labelColsLg",
- "description": "Number of columns for the label width lg screens and up"
+ "description": "Number of columns for the label width 'lg' screens and up"
},
{
"prop": "labelColsMd",
- "description": "Number of columns for the label width md screens and up"
+ "description": "Number of columns for the label width 'md' screens and up"
},
{
"prop": "labelColsSm",
- "description": "Number of columns for the label width sm screens and up"
+ "description": "Number of columns for the label width 'sm' screens and up"
},
{
"prop": "labelColsXl",
- "description": "Number of columns for the label width xl screens and up"
+ "description": "Number of columns for the label width 'xl' screens and up"
},
{
"prop": "labelFor",
- "description": "Set to the ID of the singular form-control in the form-group. Do not set a value if there is more than one form control in the group"
+ "description": "Set to the ID of the singular form control in the form group. Do not set a value if there is more than one form control in the group"
},
{
"prop": "labelSize",
@@ -93,7 +114,7 @@
},
{
"prop": "state",
- "description": "Controls the validation state of the feedback. 'true' force shows valid-feedback, 'false' force shows invalid feedback, 'null' does not force show the feedback"
+ "description": "Controls the validation state of the feedback. `true` force shows valid-feedback, `false` force shows invalid feedback, `null` does not force show the feedback"
},
{
"prop": "tooltip",
@@ -111,7 +132,33 @@
"slots": [
{
"name": "default",
- "description": "Content to place in the form group"
+ "description": "Content to place in the form group",
+ "scope": [
+ {
+ "prop": "ariaDescribedby",
+ "type": "String",
+ "version": "2.21.0",
+ "description": "The value for the `aria-describedby` attribute for input elements in the form group. Will be auto-assigned when `label-for` prop is given"
+ },
+ {
+ "prop": "id",
+ "type": "String",
+ "version": "2.21.0",
+ "description": "The ID of the form group. Will equal `id` prop, when provided"
+ },
+ {
+ "prop": "descriptionId",
+ "type": "String",
+ "version": "2.21.0",
+ "description": "The ID of the description element. Will be `null` when no description content given"
+ },
+ {
+ "prop": "labelId",
+ "type": "String",
+ "version": "2.21.0",
+ "description": "The ID of the label element. Will be `null` when no description content given"
+ }
+ ]
},
{
"name": "description",
@@ -123,7 +170,7 @@
},
{
"name": "label",
- "description": "Content to place inside the element. Overrides the `label` prop"
+ "description": "Content to place inside the label element. Overrides the `label` prop"
},
{
"name": "valid-feedback",
diff --git a/src/components/form-input/README.md b/src/components/form-input/README.md
index 353e3f21f83..4e660156eb0 100644
--- a/src/components/form-input/README.md
+++ b/src/components/form-input/README.md
@@ -353,10 +353,10 @@ Formatting does not occur if a `formatter` is not provided.
Value: {{ text1 }}
-
- Option A
- Option B
+
+ Option A
+ Option B
Selected: {{ selected }}
@@ -41,17 +41,23 @@ v-model from the ``.
```html
-
+
-
-
+
+
Toggle this custom radio
Or toggle this other custom radio
This one is Disabled
@@ -89,11 +95,12 @@ To have them appear _above_ the inputs generated by `options`, place them in the
```html
-
+
@@ -272,18 +279,20 @@ render them inline.
```html
-
+
-
+
@@ -348,36 +357,42 @@ in the checked state.
```html
-
+
-
+
-
+
@@ -410,22 +425,24 @@ by setting the `plain` prop.
```html
-
+
-
+
diff --git a/src/components/form-tags/README.md b/src/components/form-tags/README.md
index 040c4c164c7..42e8d837448 100644
--- a/src/components/form-tags/README.md
+++ b/src/components/form-tags/README.md
@@ -192,19 +192,20 @@ not validated.
```html
-
+
-
+
You must provide at least 3 tags and no more than 8
+