UNPKG

bootstrap-vue

Version:

BootstrapVue provides one of the most comprehensive implementations of Bootstrap 4 components and grid system for Vue.js and with extensive and automated WAI-ARIA accessibility markup.

336 lines (327 loc) 9.86 kB
import warn from '../../utils/warn' import { select, selectAll, isVisible, setAttr, removeAttr, getAttr } from '../../utils/dom' import idMixin from '../../mixins/id' import formStateMixin from '../../mixins/form-state' import bFormRow from '../layout/form-row' import bFormText from '../form/form-text' import bFormInvalidFeedback from '../form/form-invalid-feedback' import bFormValidFeedback from '../form/form-valid-feedback' // Selector for finding firt input in the form-group const SELECTOR = 'input:not(:disabled),textarea:not(:disabled),select:not(:disabled)' export default { mixins: [ idMixin, formStateMixin ], components: { bFormRow, bFormText, bFormInvalidFeedback, bFormValidFeedback }, render (h) { const t = this const $slots = t.$slots // Label / Legend let legend = h(false) if (t.hasLabel) { let children = $slots['label'] const legendTag = t.labelFor ? 'label' : 'legend' const legendDomProps = children ? {} : { innerHTML: t.label } const legendAttrs = { id: t.labelId, for: t.labelFor || null } const legendClick = (t.labelFor || t.labelSrOnly) ? {} : { click: t.legendClick } if (t.horizontal) { // Horizontal layout with label if (t.labelSrOnly) { // SR Only we wrap label/legend in a div to preserve layout children = h( legendTag, { class: [ 'sr-only' ], attrs: legendAttrs, domProps: legendDomProps }, children ) legend = h('div', { class: t.labelLayoutClasses }, [ children ]) } else { legend = h( legendTag, { class: [ t.labelLayoutClasses, t.labelClasses ], attrs: legendAttrs, domProps: legendDomProps, on: legendClick }, children ) } } else { // Vertical layout with label legend = h( legendTag, { class: t.labelSrOnly ? [ 'sr-only' ] : t.labelClasses, attrs: legendAttrs, domProps: legendDomProps, on: legendClick }, children ) } } else if (t.horizontal) { // No label but has horizontal layout, so we need a spacer element for layout legend = h('div', { class: t.labelLayoutClasses }) } // Invalid feeback text (explicitly hidden if state is valid) let invalidFeedback = h(false) if (t.hasInvalidFeedback) { let domProps = {} if (!$slots['invalid-feedback'] && !$slots['feedback']) { domProps = { innerHTML: t.invalidFeedback || t.feedback || '' } } invalidFeedback = h( 'b-form-invalid-feedback', { props: { id: t.invalidFeedbackId, forceShow: t.computedState === false }, attrs: { role: 'alert', 'aria-live': 'assertive', 'aria-atomic': 'true' }, domProps: domProps }, $slots['invalid-feedback'] || $slots['feedback'] ) } // Valid feeback text (explicitly hidden if state is invalid) let validFeedback = h(false) if (t.hasValidFeedback) { const domProps = $slots['valid-feedback'] ? {} : { innerHTML: t.validFeedback || '' } validFeedback = h( 'b-form-valid-feedback', { props: { id: t.validFeedbackId, forceShow: t.computedState === true }, attrs: { role: 'alert', 'aria-live': 'assertive', 'aria-atomic': 'true' }, domProps: domProps }, $slots['valid-feedback'] ) } // Form help text (description) let description = h(false) if (t.hasDescription) { const domProps = $slots['description'] ? {} : { innerHTML: t.description || '' } description = h( 'b-form-text', { attrs: { id: t.descriptionId }, domProps: domProps }, $slots['description'] ) } // Build content layout const content = h( 'div', { ref: 'content', class: t.inputLayoutClasses, attrs: t.labelFor ? {} : { role: 'group', 'aria-labelledby': t.labelId } }, [ $slots['default'], invalidFeedback, validFeedback, description ] ) // Generate main form-group wrapper return h( t.labelFor ? 'div' : 'fieldset', { class: t.groupClasses, attrs: { id: t.safeId(), disabled: t.disabled, role: 'group', 'aria-invalid': t.computedState === false ? 'true' : null, 'aria-labelledby': t.labelId, 'aria-describedby': t.labelFor ? null : t.describedByIds } }, t.horizontal ? [ h('b-form-row', {}, [ legend, content ]) ] : [ legend, content ] ) }, props: { horizontal: { type: Boolean, default: false }, labelCols: { type: [Number, String], default: 3, validator (value) { if (Number(value) >= 1 && Number(value) <= 11) { return true } warn('b-form-group: label-cols must be a value between 1 and 11') return false } }, breakpoint: { type: String, default: 'sm' }, labelTextAlign: { type: String, default: null }, label: { type: String, default: null }, labelFor: { type: String, default: null }, labelSize: { type: String, default: null }, labelSrOnly: { type: Boolean, default: false }, labelClass: { type: [String, Array], default: null }, description: { type: String, default: null }, invalidFeedback: { type: String, default: null }, feedback: { // Deprecated in favor of invalid-feedback type: String, default: null }, validFeedback: { type: String, default: null }, validated: { type: Boolean, default: false } }, computed: { groupClasses () { return [ 'b-form-group', 'form-group', this.validated ? 'was-validated' : null, this.stateClass ] }, labelClasses () { return [ 'col-form-label', this.labelSize ? `col-form-label-${this.labelSize}` : null, this.labelTextAlign ? `text-${this.labelTextAlign}` : null, this.horizontal ? null : 'pt-0', this.labelClass ] }, labelLayoutClasses () { return [ this.horizontal ? `col-${this.breakpoint}-${this.labelCols}` : null ] }, inputLayoutClasses () { return [ this.horizontal ? `col-${this.breakpoint}-${12 - Number(this.labelCols)}` : null ] }, hasLabel () { return this.label || this.$slots['label'] }, hasDescription () { return this.description || this.$slots['description'] }, hasInvalidFeedback () { if (this.computedState === true) { // If the form-group state is explicityly valid, we return false return false } return this.invalidFeedback || this.feedback || this.$slots['invalid-feedback'] || this.$slots['feedback'] }, hasValidFeedback () { if (this.computedState === false) { // If the form-group state is explicityly invalid, we return false return false } return this.validFeedback || this.$slots['valid-feedback'] }, labelId () { return this.hasLabel ? this.safeId('_BV_label_') : null }, descriptionId () { return this.hasDescription ? this.safeId('_BV_description_') : null }, invalidFeedbackId () { return this.hasInvalidFeedback ? this.safeId('_BV_feedback_invalid_') : null }, validFeedbackId () { return this.hasValidFeedback ? this.safeId('_BV_feedback_valid_') : null }, describedByIds () { return [ this.descriptionId, this.invalidFeedbackId, this.validFeedbackId ].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 : '' if (/^(input|select|textarea|label)$/i.test(tagName)) { // If clicked an input inside legend, we just let the default happen return } // Focus the first non-disabled visible input when the legend element is clicked const inputs = selectAll(SELECTOR, this.$refs.content).filter(isVisible) 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) }) } }