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.
291 lines (281 loc) • 9.98 kB
JavaScript
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
var SELECTOR = 'input:not(:disabled),textarea:not(:disabled),select:not(:disabled)';
export default {
mixins: [idMixin, formStateMixin],
components: { bFormRow: bFormRow, bFormText: bFormText, bFormInvalidFeedback: bFormInvalidFeedback, bFormValidFeedback: bFormValidFeedback },
render: function render(h) {
var t = this;
var $slots = t.$slots;
// Label / Legend
var legend = h(false);
if (t.hasLabel) {
var children = $slots['label'];
var legendTag = t.labelFor ? 'label' : 'legend';
var legendDomProps = children ? {} : { innerHTML: t.label };
var legendAttrs = { id: t.labelId, for: t.labelFor || null };
var 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)
var invalidFeedback = h(false);
if (t.hasInvalidFeedback) {
var 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)
var validFeedback = h(false);
if (t.hasValidFeedback) {
var _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)
var description = h(false);
if (t.hasDescription) {
var _domProps2 = $slots['description'] ? {} : { innerHTML: t.description || '' };
description = h('b-form-text', { attrs: { id: t.descriptionId }, domProps: _domProps2 }, $slots['description']);
}
// Build content layout
var 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: function 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: function groupClasses() {
return ['b-form-group', 'form-group', this.validated ? 'was-validated' : null, this.stateClass];
},
labelClasses: function 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: function labelLayoutClasses() {
return [this.horizontal ? 'col-' + this.breakpoint + '-' + this.labelCols : null];
},
inputLayoutClasses: function inputLayoutClasses() {
return [this.horizontal ? 'col-' + this.breakpoint + '-' + (12 - Number(this.labelCols)) : null];
},
hasLabel: function hasLabel() {
return this.label || this.$slots['label'];
},
hasDescription: function hasDescription() {
return this.description || this.$slots['description'];
},
hasInvalidFeedback: function 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: function 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: function labelId() {
return this.hasLabel ? this.safeId('_BV_label_') : null;
},
descriptionId: function descriptionId() {
return this.hasDescription ? this.safeId('_BV_description_') : null;
},
invalidFeedbackId: function invalidFeedbackId() {
return this.hasInvalidFeedback ? this.safeId('_BV_feedback_invalid_') : null;
},
validFeedbackId: function validFeedbackId() {
return this.hasValidFeedback ? this.safeId('_BV_feedback_valid_') : null;
},
describedByIds: function describedByIds() {
return [this.descriptionId, this.invalidFeedbackId, this.validFeedbackId].filter(function (i) {
return i;
}).join(' ') || null;
}
},
watch: {
describedByIds: function describedByIds(add, remove) {
if (add !== remove) {
this.setInputDescribedBy(add, remove);
}
}
},
methods: {
legendClick: function legendClick(evt) {
var 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
var inputs = selectAll(SELECTOR, this.$refs.content).filter(isVisible);
if (inputs[0] && inputs[0].focus) {
inputs[0].focus();
}
},
setInputDescribedBy: function 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') {
var input = select('#' + this.labelFor, this.$refs.content);
if (input) {
var adb = 'aria-describedby';
var ids = (getAttr(input, adb) || '').split(/\s+/);
remove = (remove || '').split(/\s+/);
// Update ID list, preserving any original IDs
ids = ids.filter(function (id) {
return remove.indexOf(id) === -1;
}).concat(add || '').join(' ').trim();
if (ids) {
setAttr(input, adb, ids);
} else {
removeAttr(input, adb);
}
}
}
}
},
mounted: function mounted() {
var _this = this;
this.$nextTick(function () {
// 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);
});
}
};