bootstrap-vue
Version:
With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens
200 lines (182 loc) • 6.54 kB
JavaScript
import Vue, { mergeData } from '../../vue'
import { NAME_BUTTON } from '../../constants/components'
import { CODE_ENTER, CODE_SPACE } from '../../constants/key-codes'
import { concat } from '../../utils/array'
import { getComponentConfig } from '../../utils/config'
import { addClass, isTag, removeClass } from '../../utils/dom'
import { stopEvent } from '../../utils/events'
import { isBoolean, isEvent, isFunction } from '../../utils/inspect'
import { omit } from '../../utils/object'
import { pluckProps } from '../../utils/props'
import { isLink as isLinkStrict } from '../../utils/router'
import { BLink, props as BLinkProps } from '../link/link'
// --- Props ---
const linkProps = omit(BLinkProps, ['event', 'routerTag'])
delete linkProps.href.default
delete linkProps.to.default
const btnProps = {
block: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
size: {
type: String,
default: () => getComponentConfig(NAME_BUTTON, 'size')
},
variant: {
type: String,
default: () => getComponentConfig(NAME_BUTTON, 'variant')
},
type: {
type: String,
default: 'button'
},
tag: {
type: String,
default: 'button'
},
pill: {
type: Boolean,
default: false
},
squared: {
type: Boolean,
default: false
},
pressed: {
// Tri-state: `true`, `false` or `null`
// => On, off, not a toggle
type: Boolean,
default: null
}
}
export const props = { ...btnProps, ...linkProps }
// --- Helper methods ---
// Focus handler for toggle buttons
// Needs class of 'focus' when focused
const handleFocus = evt => {
if (evt.type === 'focusin') {
addClass(evt.target, 'focus')
} else if (evt.type === 'focusout') {
removeClass(evt.target, 'focus')
}
}
// Is the requested button a link?
// If tag prop is set to `a`, we use a <b-link> to get proper disabled handling
const isLink = props => isLinkStrict(props) || isTag(props.tag, 'a')
// Is the button to be a toggle button?
const isToggle = props => isBoolean(props.pressed)
// Is the button "really" a button?
const isButton = props => !(isLink(props) || (props.tag && !isTag(props.tag, 'button')))
// Is the requested tag not a button or link?
const isNonStandardTag = props => !isLink(props) && !isButton(props)
// Compute required classes (non static classes)
const computeClass = props => [
`btn-${props.variant || getComponentConfig(NAME_BUTTON, 'variant')}`,
{
[`btn-${props.size}`]: props.size,
'btn-block': props.block,
'rounded-pill': props.pill,
'rounded-0': props.squared && !props.pill,
disabled: props.disabled,
active: props.pressed
}
]
// Compute the link props to pass to b-link (if required)
const computeLinkProps = props => (isLink(props) ? pluckProps(linkProps, props) : {})
// Compute the attributes for a button
const computeAttrs = (props, data) => {
const button = isButton(props)
const link = isLink(props)
const toggle = isToggle(props)
const nonStandardTag = isNonStandardTag(props)
const hashLink = link && props.href === '#'
const role = data.attrs && data.attrs.role ? data.attrs.role : null
let tabindex = data.attrs ? data.attrs.tabindex : null
if (nonStandardTag || hashLink) {
tabindex = '0'
}
return {
// Type only used for "real" buttons
type: button && !link ? props.type : null,
// Disabled only set on "real" buttons
disabled: button ? props.disabled : null,
// We add a role of button when the tag is not a link or button for ARIA
// Don't bork any role provided in `data.attrs` when `isLink` or `isButton`
// Except when link has `href` of `#`
role: nonStandardTag || hashLink ? 'button' : role,
// We set the `aria-disabled` state for non-standard tags
'aria-disabled': nonStandardTag ? String(props.disabled) : null,
// For toggles, we need to set the pressed state for ARIA
'aria-pressed': toggle ? String(props.pressed) : null,
// `autocomplete="off"` is needed in toggle mode to prevent some browsers
// from remembering the previous setting when using the back button
autocomplete: toggle ? 'off' : null,
// `tabindex` is used when the component is not a button
// Links are tabbable, but don't allow disabled, while non buttons or links
// are not tabbable, so we mimic that functionality by disabling tabbing
// when disabled, and adding a `tabindex="0"` to non buttons or non links
tabindex: props.disabled && !button ? '-1' : tabindex
}
}
// --- Main component ---
// @vue/component
export const BButton = /*#__PURE__*/ Vue.extend({
name: NAME_BUTTON,
functional: true,
props,
render(h, { props, data, listeners, children }) {
const toggle = isToggle(props)
const link = isLink(props)
const nonStandardTag = isNonStandardTag(props)
const hashLink = link && props.href === '#'
const on = {
keydown(evt) {
// When the link is a `href="#"` or a non-standard tag (has `role="button"`),
// we add a keydown handlers for CODE_SPACE/CODE_ENTER
/* istanbul ignore next */
if (props.disabled || !(nonStandardTag || hashLink)) {
return
}
const { keyCode } = evt
// Add CODE_SPACE handler for `href="#"` and CODE_ENTER handler for non-standard tags
if (keyCode === CODE_SPACE || (keyCode === CODE_ENTER && nonStandardTag)) {
const target = evt.currentTarget || evt.target
stopEvent(evt, { propagation: false })
target.click()
}
},
click(evt) {
/* istanbul ignore if: blink/button disabled should handle this */
if (props.disabled && isEvent(evt)) {
stopEvent(evt)
} else if (toggle && listeners && listeners['update:pressed']) {
// Send `.sync` updates to any "pressed" prop (if `.sync` listeners)
// `concat()` will normalize the value to an array without
// double wrapping an array value in an array
concat(listeners['update:pressed']).forEach(fn => {
if (isFunction(fn)) {
fn(!props.pressed)
}
})
}
}
}
if (toggle) {
on.focusin = handleFocus
on.focusout = handleFocus
}
const componentData = {
staticClass: 'btn',
class: computeClass(props),
props: computeLinkProps(props),
attrs: computeAttrs(props, data),
on
}
return h(link ? BLink : props.tag, mergeData(data, componentData), children)
}
})