diff --git a/docs/nuxt.config.js b/docs/nuxt.config.js
index a6eeedc4f1c..5fe90f2b7c9 100644
--- a/docs/nuxt.config.js
+++ b/docs/nuxt.config.js
@@ -117,6 +117,7 @@ module.exports = {
.readdirSync(`${root}/${dir}`)
.filter(c => c !== 'index.js' && c[0] !== '_')
.filter(c => excludeDirs.indexOf(c) === -1)
+ .filter(c => !/\.s?css$/.test(c))
.map(page => `/docs/${dir}/${page}`)
return []
diff --git a/src/directives/modal/modal.js b/src/directives/modal/modal.js
index 6b26c4e54c0..0b64b7828eb 100644
--- a/src/directives/modal/modal.js
+++ b/src/directives/modal/modal.js
@@ -1,25 +1,38 @@
-import { bindTargets, unbindTargets } from '../../utils/target'
import { setAttr, removeAttr } from '../../utils/dom'
+import { bindTargets, unbindTargets } from '../../utils/target'
+// Target listen types
const listenTypes = { click: true }
+// Emitted show event for modal
+const EVENT_SHOW = 'bv::show::modal'
+
+const setRole = (el, binding, vnode) => {
+ if (el.tagName !== 'BUTTON') {
+ setAttr(el, 'role', 'button')
+ }
+}
+
+/*
+ * Export our directive
+ */
export default {
// eslint-disable-next-line no-shadow-restricted-names
bind(el, binding, vnode) {
bindTargets(vnode, binding, listenTypes, ({ targets, vnode }) => {
targets.forEach(target => {
- vnode.context.$root.$emit('bv::show::modal', target, vnode.elm)
+ vnode.context.$root.$emit(EVENT_SHOW, target, vnode.elm)
})
})
- if (el.tagName !== 'BUTTON') {
- // If element is not a button, we add `role="button"` for accessibility
- setAttr(el, 'role', 'button')
- }
+ // If element is not a button, we add `role="button"` for accessibility
+ setRole(el, binding, vnode)
},
+ updated: setRole,
+ componentUpdated: setRole,
unbind(el, binding, vnode) {
unbindTargets(vnode, binding, listenTypes)
+ // If element is not a button, we add `role="button"` for accessibility
if (el.tagName !== 'BUTTON') {
- // If element is not a button, we add `role="button"` for accessibility
removeAttr(el, 'role', 'button')
}
}
diff --git a/src/directives/modal/modal.spec.js b/src/directives/modal/modal.spec.js
new file mode 100644
index 00000000000..530cff5dc32
--- /dev/null
+++ b/src/directives/modal/modal.spec.js
@@ -0,0 +1,88 @@
+import modalDirective from './modal'
+import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
+
+const EVENT_SHOW = 'bv::show::modal'
+
+describe('v-b-modal directive', () => {
+ it('works on buttons', async () => {
+ const localVue = new CreateLocalVue()
+ const spy = jest.fn()
+
+ const App = localVue.extend({
+ directives: {
+ bModal: modalDirective
+ },
+ data() {
+ return {}
+ },
+ mounted() {
+ this.$root.$on(EVENT_SHOW, spy)
+ },
+ beforeDestroy() {
+ this.$root.$off(EVENT_SHOW, spy)
+ },
+ template: ''
+ })
+ const wrapper = mount(App, {
+ localVue: localVue
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('button')).toBe(true)
+ expect(spy).not.toHaveBeenCalled()
+
+ const $button = wrapper.find('button')
+ $button.trigger('click')
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).toBeCalledWith('test', $button.element)
+
+ wrapper.destroy()
+ })
+
+ it('works on non-buttons', async () => {
+ const localVue = new CreateLocalVue()
+ const spy = jest.fn()
+
+ const App = localVue.extend({
+ directives: {
+ bModal: modalDirective
+ },
+ data() {
+ return {
+ text: 'span'
+ }
+ },
+ mounted() {
+ this.$root.$on(EVENT_SHOW, spy)
+ },
+ beforeDestroy() {
+ this.$root.$off(EVENT_SHOW, spy)
+ },
+ template: '{{ text }}'
+ })
+ const wrapper = mount(App, {
+ localVue: localVue
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('span')).toBe(true)
+ expect(spy).not.toHaveBeenCalled()
+ expect(wrapper.find('span').attributes('role')).toBe('button')
+ expect(wrapper.find('span').text()).toBe('span')
+
+ const $span = wrapper.find('span')
+ $span.trigger('click')
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).toBeCalledWith('test', $span.element)
+ expect(wrapper.find('span').attributes('role')).toBe('button')
+
+ // Test updating component. should maintain role attribute
+ wrapper.setData({
+ text: 'foobar'
+ })
+ expect(wrapper.find('span').text()).toBe('foobar')
+ expect(wrapper.find('span').attributes('role')).toBe('button')
+
+ wrapper.destroy()
+ })
+})
diff --git a/src/directives/popover/popover.js b/src/directives/popover/popover.js
index 5991f7ddd77..36337ee8c62 100644
--- a/src/directives/popover/popover.js
+++ b/src/directives/popover/popover.js
@@ -1,12 +1,11 @@
import Popper from 'popper.js'
import PopOver from '../../utils/popover.class'
+import { inBrowser } from '../../utils/env'
import { keys } from '../../utils/object'
import warn from '../../utils/warn'
-const inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
-
// Key which we use to store tooltip object on element
-const BVPO = '__BV_PopOver__'
+const BV_POPOVER = '__BV_PopOver__'
// Valid event triggers
const validTriggers = {
@@ -17,9 +16,9 @@ const validTriggers = {
}
// Build a PopOver config based on bindings (if any)
-// Arguments and modifiers take precedence over pased value config object
+// Arguments and modifiers take precedence over passed value config object
/* istanbul ignore next: not easy to test */
-function parseBindings(bindings) {
+const parseBindings = bindings => /* istanbul ignore next: not easy to test */ {
// We start out with a blank config
let config = {}
@@ -35,9 +34,10 @@ function parseBindings(bindings) {
config = { ...config, ...bindings.value }
}
- // If Argument, assume element ID of container element
+ // If argument, assume element ID of container element
if (bindings.arg) {
- // Element ID specified as arg. We must prepend '#' to become a CSS selector
+ // Element ID specified as arg
+ // We must prepend '#' to become a CSS selector
config.container = `#${bindings.arg}`
}
@@ -55,16 +55,16 @@ function parseBindings(bindings) {
// placement of popover
config.placement = mod
} else if (/^(window|viewport)$/.test(mod)) {
- // bounday of popover
+ // Boundary of popover
config.boundary = mod
} else if (/^d\d+$/.test(mod)) {
- // delay value
+ // Delay value
const delay = parseInt(mod.slice(1), 10) || 0
if (delay) {
config.delay = delay
}
} else if (/^o-?\d+$/.test(mod)) {
- // offset value (negative allowed)
+ // Offset value (negative allowed)
const offset = parseInt(mod.slice(1), 10) || 0
if (offset) {
config.offset = offset
@@ -72,10 +72,11 @@ function parseBindings(bindings) {
}
})
- // Special handling of event trigger modifiers Trigger is a space separated list
+ // Special handling of event trigger modifiers trigger is
+ // a space separated list
const selectedTriggers = {}
- // parse current config object trigger
+ // Parse current config object trigger
let triggers = typeof config.trigger === 'string' ? config.trigger.trim().split(/\s+/) : []
triggers.forEach(trigger => {
if (validTriggers[trigger]) {
@@ -83,7 +84,7 @@ function parseBindings(bindings) {
}
})
- // Parse Modifiers for triggers
+ // Parse modifiers for triggers
keys(validTriggers).forEach(trigger => {
if (bindings.modifiers[trigger]) {
selectedTriggers[trigger] = true
@@ -97,70 +98,64 @@ function parseBindings(bindings) {
config.trigger = 'focus'
}
if (!config.trigger) {
- // remove trigger config
+ // Remove trigger config
delete config.trigger
}
return config
}
-//
-// Add or Update popover on our element
-//
-/* istanbul ignore next: not easy to test */
-function applyBVPO(el, bindings, vnode) {
+// Add or update PopOver on our element
+const applyPopover = (el, bindings, vnode) => {
if (!inBrowser) {
+ /* istanbul ignore next */
return
}
+ // Popper is required for PopOvers to work
if (!Popper) {
- // Popper is required for tooltips to work
- warn('v-b-popover: Popper.js is required for popovers to work')
+ /* istanbul ignore next */
+ warn('v-b-popover: Popper.js is required for PopOvers to work')
+ /* istanbul ignore next */
return
}
- if (el[BVPO]) {
- el[BVPO].updateConfig(parseBindings(bindings))
+ const config = parseBindings(bindings)
+ if (el[BV_POPOVER]) {
+ el[BV_POPOVER].updateConfig(config)
} else {
- el[BVPO] = new PopOver(el, parseBindings(bindings), vnode.context.$root)
+ el[BV_POPOVER] = new PopOver(el, config, vnode.context.$root)
}
}
-//
-// Remove popover on our element
-//
-/* istanbul ignore next */
-function removeBVPO(el) {
- if (!inBrowser) {
- return
- }
- if (el[BVPO]) {
- el[BVPO].destroy()
- el[BVPO] = null
- delete el[BVPO]
+// Remove PopOver on our element
+const removePopover = el => {
+ if (el[BV_POPOVER]) {
+ el[BV_POPOVER].destroy()
+ el[BV_POPOVER] = null
+ delete el[BV_POPOVER]
}
}
/*
* Export our directive
*/
-/* istanbul ignore next: not easy to test */
export default {
bind(el, bindings, vnode) {
- applyBVPO(el, bindings, vnode)
+ applyPopover(el, bindings, vnode)
},
inserted(el, bindings, vnode) {
- applyBVPO(el, bindings, vnode)
+ applyPopover(el, bindings, vnode)
},
- update(el, bindings, vnode) {
+ update(el, bindings, vnode) /* istanbul ignore next: not easy to test */ {
if (bindings.value !== bindings.oldValue) {
- applyBVPO(el, bindings, vnode)
+ applyPopover(el, bindings, vnode)
}
},
- componentUpdated(el, bindings, vnode) {
+ componentUpdated(el, bindings, vnode) /* istanbul ignore next: not easy to test */ {
if (bindings.value !== bindings.oldValue) {
- applyBVPO(el, bindings, vnode)
+ applyPopover(el, bindings, vnode)
}
},
unbind(el) {
- removeBVPO(el)
+ removePopover(el)
}
}
diff --git a/src/directives/popover/popover.spec.js b/src/directives/popover/popover.spec.js
new file mode 100644
index 00000000000..b301c23db2d
--- /dev/null
+++ b/src/directives/popover/popover.spec.js
@@ -0,0 +1,37 @@
+import popoverDirective from './popover'
+import PopOver from '../../utils/popover.class'
+import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
+
+// Key which we use to store tooltip object on element
+const BV_POPOVER = '__BV_PopOver__'
+
+describe('v-b-popover directive', () => {
+ it('should have PopOver class instance', async () => {
+ const localVue = new CreateLocalVue()
+
+ const App = localVue.extend({
+ directives: {
+ bPopover: popoverDirective
+ },
+ data() {
+ return {}
+ },
+ template: ``
+ })
+
+ const wrapper = mount(App, {
+ localVue: localVue,
+ attachToDocument: true
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('button')).toBe(true)
+ const $button = wrapper.find('button')
+
+ // Should have instance of popover class on it
+ expect($button.element[BV_POPOVER]).toBeDefined()
+ expect($button.element[BV_POPOVER]).toBeInstanceOf(PopOver)
+
+ wrapper.destroy()
+ })
+})
diff --git a/src/directives/scrollspy/scrollspy.js b/src/directives/scrollspy/scrollspy.js
index 60e252c1e55..e5ea5d74864 100644
--- a/src/directives/scrollspy/scrollspy.js
+++ b/src/directives/scrollspy/scrollspy.js
@@ -1,28 +1,27 @@
-/*
- * ScrollSpy directive v-b-scrollspy
- */
-
import ScrollSpy from './scrollspy.class'
+import { inBrowser } from '../../utils/env'
import { keys } from '../../utils/object'
-import { isServer } from '../../utils/env'
-// Key we use to store our Instance
-const BVSS = '__BV_ScrollSpy__'
+// Key we use to store our instance
+const BV_SCROLLSPY = '__BV_ScrollSpy__'
-// Generate config from bindings
-function makeConfig(binding) /* istanbul ignore next: not easy to test */ {
+// Build a ScrollSpy config based on bindings (if any)
+// Arguments and modifiers take precedence over passed value config object
+/* istanbul ignore next: not easy to test */
+const parseBindings = bindings => /* istanbul ignore next: not easy to test */ {
const config = {}
- // If Argument, assume element ID
- if (binding.arg) {
- // Element ID specified as arg. We must pre-pend #
- config.element = '#' + binding.arg
+ // If argument, assume element ID
+ if (bindings.arg) {
+ // Element ID specified as arg
+ // We must prepend '#' to become a CSS selector
+ config.element = `#${bindings.arg}`
}
// Process modifiers
- keys(binding.modifiers).forEach(mod => {
+ keys(bindings.modifiers).forEach(mod => {
if (/^\d+$/.test(mod)) {
- // Offest value
+ // Offset value
config.offset = parseInt(mod, 10)
} else if (/^(auto|position|offset)$/.test(mod)) {
// Offset method
@@ -31,67 +30,69 @@ function makeConfig(binding) /* istanbul ignore next: not easy to test */ {
})
// Process value
- if (typeof binding.value === 'string') {
+ if (typeof bindings.value === 'string') {
// Value is a CSS ID or selector
- config.element = binding.value
- } else if (typeof binding.value === 'number') {
+ config.element = bindings.value
+ } else if (typeof bindings.value === 'number') {
// Value is offset
- config.offset = Math.round(binding.value)
- } else if (typeof binding.value === 'object') {
+ config.offset = Math.round(bindings.value)
+ } else if (typeof bindings.value === 'object') {
// Value is config object
// Filter the object based on our supported config options
- keys(binding.value)
+ keys(bindings.value)
.filter(k => Boolean(ScrollSpy.DefaultType[k]))
.forEach(k => {
- config[k] = binding.value[k]
+ config[k] = bindings.value[k]
})
}
return config
}
-function addBVSS(el, binding, vnode) /* istanbul ignore next: not easy to test */ {
- if (isServer) {
+// Add or update ScrollSpy on our element
+const applyScrollspy = (el, bindings, vnode) => {
+ if (!inBrowser) {
+ /* istanbul ignore next */
return
}
- const cfg = makeConfig(binding)
- if (!el[BVSS]) {
- el[BVSS] = new ScrollSpy(el, cfg, vnode.context.$root)
+ const config = parseBindings(bindings)
+ if (el[BV_SCROLLSPY]) {
+ el[BV_SCROLLSPY].updateConfig(config, vnode.context.$root)
} else {
- el[BVSS].updateConfig(cfg, vnode.context.$root)
+ el[BV_SCROLLSPY] = new ScrollSpy(el, config, vnode.context.$root)
}
- return el[BVSS]
}
-function removeBVSS(el) /* istanbul ignore next: not easy to test */ {
- if (el[BVSS]) {
- el[BVSS].dispose()
- el[BVSS] = null
+// Remove ScrollSpy on our element
+const removeScrollspy = el => {
+ if (el[BV_SCROLLSPY]) {
+ el[BV_SCROLLSPY].dispose()
+ el[BV_SCROLLSPY] = null
+ delete el[BV_SCROLLSPY]
}
}
/*
* Export our directive
*/
-
export default {
- bind(el, binding, vnode) /* istanbul ignore next: not easy to test */ {
- addBVSS(el, binding, vnode)
+ bind(el, bindings, vnode) {
+ applyScrollspy(el, bindings, vnode)
},
- inserted(el, binding, vnode) /* istanbul ignore next: not easy to test */ {
- addBVSS(el, binding, vnode)
+ inserted(el, bindings, vnode) {
+ applyScrollspy(el, bindings, vnode)
},
- update(el, binding, vnode) /* istanbul ignore next: not easy to test */ {
- addBVSS(el, binding, vnode)
- },
- componentUpdated(el, binding, vnode) /* istanbul ignore next: not easy to test */ {
- addBVSS(el, binding, vnode)
+ update(el, bindings, vnode) /* istanbul ignore next: not easy to test */ {
+ if (bindings.value !== bindings.oldValue) {
+ applyScrollspy(el, bindings, vnode)
+ }
},
- unbind(el) /* istanbul ignore next: not easy to test */ {
- if (isServer) {
- return
+ componentUpdated(el, bindings, vnode) /* istanbul ignore next: not easy to test */ {
+ if (bindings.value !== bindings.oldValue) {
+ applyScrollspy(el, bindings, vnode)
}
- // Remove scroll event listener on scrollElId
- removeBVSS(el)
+ },
+ unbind(el) {
+ removeScrollspy(el)
}
}
diff --git a/src/directives/scrollspy/scrollspy.spec.js b/src/directives/scrollspy/scrollspy.spec.js
new file mode 100644
index 00000000000..93db2855590
--- /dev/null
+++ b/src/directives/scrollspy/scrollspy.spec.js
@@ -0,0 +1,38 @@
+import scrollspyDirective from './scrollspy'
+import ScrollSpy from './scrollspy.class'
+import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
+
+// Key we use to store our instance
+const BV_SCROLLSPY = '__BV_ScrollSpy__'
+
+describe('v-b-scrollspy directive', () => {
+ it('should have ScrollSpy class instance', async () => {
+ const localVue = new CreateLocalVue()
+
+ const App = localVue.extend({
+ directives: {
+ bScrollspy: scrollspyDirective
+ },
+ data() {
+ return {}
+ },
+ // Assumes watching body element for scrolls
+ template: '
content
'
+ })
+
+ const wrapper = mount(App, {
+ localVue: localVue,
+ attachToDocument: true
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('div')).toBe(true)
+ const $div = wrapper.find('div')
+
+ // Should have instance of popover class on it
+ expect($div.element[BV_SCROLLSPY]).toBeDefined()
+ expect($div.element[BV_SCROLLSPY]).toBeInstanceOf(ScrollSpy)
+
+ wrapper.destroy()
+ })
+})
diff --git a/src/directives/toggle/toggle.js b/src/directives/toggle/toggle.js
index 4e2337cbb16..b8a0d066d72 100644
--- a/src/directives/toggle/toggle.js
+++ b/src/directives/toggle/toggle.js
@@ -1,41 +1,52 @@
-import target from '../../utils/target'
import { setAttr, removeAttr, addClass, removeClass } from '../../utils/dom'
import { inBrowser } from '../../utils/env'
+import { bindTargets, unbindTargets } from '../../utils/target'
-// target listen types
+// Target listen types
const listenTypes = { click: true }
// Property key for handler storage
-const BVT = '__BV_toggle__'
-const BVT_STATE = '__BV_toggle_STATE__'
-const BVT_CONTROLS = '__BV_toggle_CONTROLS__'
+const BV_TOGGLE = '__BV_toggle__'
+const BV_TOGGLE_STATE = '__BV_toggle_STATE__'
+const BV_TOGGLE_CONTROLS = '__BV_toggle_CONTROLS__'
-// Emitted Control Event for collapse (emitted to collapse)
+// Emitted control event for collapse (emitted to collapse)
const EVENT_TOGGLE = 'bv::toggle::collapse'
-// Listen to Event for toggle state update (Emited by collapse)
+// Listen to event for toggle state update (emitted by collapse)
const EVENT_STATE = 'bv::collapse::state'
-/* istanbul ignore next */
+// Reset and remove a property from the provided element
+const resetProp = (el, prop) => {
+ el[prop] = null
+ delete el[prop]
+}
+
+// Handle directive updates
+/* istanbul ignore next: not easy to test */
const handleUpdate = (el, binding, vnode) => {
+ if (!inBrowser) {
+ return
+ }
// Ensure the collapse class and aria-* attributes persist
- // after element is updated (eitehr by parent re-rendering
- // or changes to this element or it's contents.
- if (inBrowser) {
- if (el[BVT_STATE] === true) {
- addClass(el, 'collapsed')
- setAttr(el, 'aria-expanded', 'true')
- } else if (el[BVT_STATE] === false) {
- removeClass(el, 'collapsed')
- setAttr(el, 'aria-expanded', 'false')
- }
- setAttr(el, 'aria-controls', el[BVT_CONTROLS])
+ // after element is updated (either by parent re-rendering
+ // or changes to this element or it's contents
+ if (el[BV_TOGGLE_STATE] === true) {
+ addClass(el, 'collapsed')
+ setAttr(el, 'aria-expanded', 'true')
+ } else if (el[BV_TOGGLE_STATE] === false) {
+ removeClass(el, 'collapsed')
+ setAttr(el, 'aria-expanded', 'false')
}
+ setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
}
+/*
+ * Export our directive
+ */
export default {
bind(el, binding, vnode) {
- const targets = target(vnode, binding, listenTypes, ({ targets, vnode }) => {
+ const targets = bindTargets(vnode, binding, listenTypes, ({ targets, vnode }) => {
targets.forEach(target => {
vnode.context.$root.$emit(EVENT_TOGGLE, target)
})
@@ -43,23 +54,23 @@ export default {
if (inBrowser && vnode.context && targets.length > 0) {
// Add aria attributes to element
- el[BVT_CONTROLS] = targets.join(' ')
- // state is initialy collapsed until we receive a state event
- el[BVT_STATE] = false
- setAttr(el, 'aria-controls', el[BVT_CONTROLS])
+ el[BV_TOGGLE_CONTROLS] = targets.join(' ')
+ // State is initially collapsed until we receive a state event
+ el[BV_TOGGLE_STATE] = false
+ setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
setAttr(el, 'aria-expanded', 'false')
+ // If element is not a button, we add `role="button"` for accessibility
if (el.tagName !== 'BUTTON') {
- // If element is not a button, we add `role="button"` for accessibility
setAttr(el, 'role', 'button')
}
- // Toggle state hadnler, stored on element
- el[BVT] = function toggleDirectiveHandler(id, state) {
+ // Toggle state handler, stored on element
+ el[BV_TOGGLE] = function toggleDirectiveHandler(id, state) {
if (targets.indexOf(id) !== -1) {
// Set aria-expanded state
setAttr(el, 'aria-expanded', state ? 'true' : 'false')
// Set/Clear 'collapsed' class state
- el[BVT_STATE] = state
+ el[BV_TOGGLE_STATE] = state
if (state) {
removeClass(el, 'collapsed')
} else {
@@ -69,21 +80,25 @@ export default {
}
// Listen for toggle state changes
- vnode.context.$root.$on(EVENT_STATE, el[BVT])
+ vnode.context.$root.$on(EVENT_STATE, el[BV_TOGGLE])
}
},
componentUpdated: handleUpdate,
updated: handleUpdate,
unbind(el, binding, vnode) /* istanbul ignore next */ {
- if (el[BVT]) {
- // Remove our $root listener
- vnode.context.$root.$off(EVENT_STATE, el[BVT])
- el[BVT] = null
- el[BVT_STATE] = null
- el[BVT_CONTROLS] = null
- removeClass(el, 'collapsed')
- removeAttr(el, 'aria-expanded')
- removeAttr(el, 'aria-controls')
+ unbindTargets(vnode, binding, listenTypes)
+ // Remove our $root listener
+ if (el[BV_TOGGLE]) {
+ vnode.context.$root.$off(EVENT_STATE, el[BV_TOGGLE])
}
+ // Reset custom props
+ resetProp(el, BV_TOGGLE)
+ resetProp(el, BV_TOGGLE_STATE)
+ resetProp(el, BV_TOGGLE_CONTROLS)
+ // Reset classes/attrs
+ removeClass(el, 'collapsed')
+ removeAttr(el, 'aria-expanded')
+ removeAttr(el, 'aria-controls')
+ removeAttr(el, 'role')
}
}
diff --git a/src/directives/toggle/toggle.spec.js b/src/directives/toggle/toggle.spec.js
new file mode 100644
index 00000000000..7baaeac3a66
--- /dev/null
+++ b/src/directives/toggle/toggle.spec.js
@@ -0,0 +1,150 @@
+import toggleDirective from './toggle'
+import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
+
+// Emitted control event for collapse (emitted to collapse)
+const EVENT_TOGGLE = 'bv::toggle::collapse'
+
+// Listen to event for toggle state update (emitted by collapse)
+const EVENT_STATE = 'bv::collapse::state'
+
+describe('v-b-toggle directive', () => {
+ it('works on buttons', async () => {
+ const localVue = new CreateLocalVue()
+ const spy = jest.fn()
+
+ const App = localVue.extend({
+ directives: {
+ bToggle: toggleDirective
+ },
+ data() {
+ return {}
+ },
+ mounted() {
+ this.$root.$on(EVENT_TOGGLE, spy)
+ },
+ beforeDestroy() {
+ this.$root.$off(EVENT_TOGGLE, spy)
+ },
+ template: ''
+ })
+
+ const wrapper = mount(App, {
+ localVue: localVue
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('button')).toBe(true)
+ expect(wrapper.find('button').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('button').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('button').classes()).not.toContain('collapsed')
+ expect(spy).not.toHaveBeenCalled()
+
+ const $button = wrapper.find('button')
+ $button.trigger('click')
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).toBeCalledWith('test')
+ expect(wrapper.find('button').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('button').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('button').classes()).not.toContain('collapsed')
+
+ wrapper.destroy()
+ })
+
+ it('works on non-buttons', async () => {
+ const localVue = new CreateLocalVue()
+ const spy = jest.fn()
+
+ const App = localVue.extend({
+ directives: {
+ bToggle: toggleDirective
+ },
+ data() {
+ return {
+ text: 'span'
+ }
+ },
+ mounted() {
+ this.$root.$on(EVENT_TOGGLE, spy)
+ },
+ beforeDestroy() {
+ this.$root.$off(EVENT_TOGGLE, spy)
+ },
+ template: '{{ text }}'
+ })
+
+ const wrapper = mount(App, {
+ localVue: localVue
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('span')).toBe(true)
+ expect(spy).not.toHaveBeenCalled()
+ expect(wrapper.find('span').attributes('role')).toBe('button')
+ expect(wrapper.find('span').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('span').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('span').classes()).not.toContain('collapsed')
+ expect(wrapper.find('span').text()).toBe('span')
+
+ const $span = wrapper.find('span')
+ $span.trigger('click')
+ expect(spy).toHaveBeenCalledTimes(1)
+ expect(spy).toBeCalledWith('test')
+ expect(wrapper.find('span').attributes('role')).toBe('button')
+ expect(wrapper.find('span').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('span').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('span').classes()).not.toContain('collapsed')
+
+ // Test updating component. should maintain role attribute
+ wrapper.setData({
+ text: 'foobar'
+ })
+ expect(wrapper.find('span').text()).toBe('foobar')
+ expect(wrapper.find('span').attributes('role')).toBe('button')
+ expect(wrapper.find('span').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('span').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('span').classes()).not.toContain('collapsed')
+
+ wrapper.destroy()
+ })
+ it('responds to state update events', async () => {
+ const localVue = new CreateLocalVue()
+
+ const App = localVue.extend({
+ directives: {
+ bToggle: toggleDirective
+ },
+ data() {
+ return {}
+ },
+ template: ''
+ })
+
+ const wrapper = mount(App, {
+ localVue: localVue
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('button')).toBe(true)
+ expect(wrapper.find('button').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('button').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('button').classes()).not.toContain('collapsed')
+
+ const $root = wrapper.vm.$root
+
+ $root.$emit(EVENT_STATE, 'test', true)
+ await wrapper.vm.$nextTick()
+
+ expect(wrapper.find('button').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('button').attributes('aria-expanded')).toBe('true')
+ expect(wrapper.find('button').classes()).not.toContain('collapsed')
+
+ $root.$emit(EVENT_STATE, 'test', false)
+ await wrapper.vm.$nextTick()
+
+ expect(wrapper.find('button').attributes('aria-controls')).toBe('test')
+ expect(wrapper.find('button').attributes('aria-expanded')).toBe('false')
+ expect(wrapper.find('button').classes()).toContain('collapsed')
+
+ wrapper.destroy()
+ })
+})
diff --git a/src/directives/tooltip/tooltip.js b/src/directives/tooltip/tooltip.js
index a16e447a79f..60b20cd53d8 100644
--- a/src/directives/tooltip/tooltip.js
+++ b/src/directives/tooltip/tooltip.js
@@ -1,12 +1,11 @@
import Popper from 'popper.js'
import ToolTip from '../../utils/tooltip.class'
+import { inBrowser } from '../../utils/env'
import { keys } from '../../utils/object'
import warn from '../../utils/warn'
-const inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'
-
// Key which we use to store tooltip object on element
-const BVTT = '__BV_ToolTip__'
+const BV_TOOLTIP = '__BV_ToolTip__'
// Valid event triggers
const validTriggers = {
@@ -19,7 +18,7 @@ const validTriggers = {
// Build a ToolTip config based on bindings (if any)
// Arguments and modifiers take precedence over passed value config object
/* istanbul ignore next: not easy to test */
-function parseBindings(bindings) {
+const parseBindings = bindings => /* istanbul ignore next: not easy to test */ {
// We start out with a blank config
let config = {}
@@ -35,9 +34,10 @@ function parseBindings(bindings) {
config = { ...config, ...bindings.value }
}
- // If Argument, assume element ID of container element
+ // If argument, assume element ID of container element
if (bindings.arg) {
- // Element ID specified as arg. We must prepend '#' to become a CSS selector
+ // Element ID specified as arg
+ // We must prepend '#' to become a CSS selector
config.container = `#${bindings.arg}`
}
@@ -47,24 +47,24 @@ function parseBindings(bindings) {
// Title allows HTML
config.html = true
} else if (/^nofade$/.test(mod)) {
- // no animation
+ // No animation
config.animation = false
} else if (
/^(auto|top(left|right)?|bottom(left|right)?|left(top|bottom)?|right(top|bottom)?)$/.test(mod)
) {
- // placement of tooltip
+ // Placement of tooltip
config.placement = mod
} else if (/^(window|viewport)$/.test(mod)) {
- // bounday of tooltip
+ // Boundary of tooltip
config.boundary = mod
} else if (/^d\d+$/.test(mod)) {
- // delay value
+ // Delay value
const delay = parseInt(mod.slice(1), 10) || 0
if (delay) {
config.delay = delay
}
} else if (/^o-?\d+$/.test(mod)) {
- // offset value. Negative allowed
+ // Offset value, negative allowed
const offset = parseInt(mod.slice(1), 10) || 0
if (offset) {
config.offset = offset
@@ -72,10 +72,11 @@ function parseBindings(bindings) {
}
})
- // Special handling of event trigger modifiers Trigger is a space separated list
+ // Special handling of event trigger modifiers trigger is
+ // a space separated list
const selectedTriggers = {}
- // parse current config object trigger
+ // Parse current config object trigger
let triggers = typeof config.trigger === 'string' ? config.trigger.trim().split(/\s+/) : []
triggers.forEach(trigger => {
if (validTriggers[trigger]) {
@@ -83,7 +84,7 @@ function parseBindings(bindings) {
}
})
- // Parse Modifiers for triggers
+ // Parse modifiers for triggers
keys(validTriggers).forEach(trigger => {
if (bindings.modifiers[trigger]) {
selectedTriggers[trigger] = true
@@ -97,70 +98,64 @@ function parseBindings(bindings) {
config.trigger = 'focus'
}
if (!config.trigger) {
- // remove trigger config
+ // Remove trigger config
delete config.trigger
}
return config
}
-//
-// Add or Update tooltip on our element
-//
-/* istanbul ignore next: not easy to test */
-function applyBVTT(el, bindings, vnode) {
+// Add or update ToolTip on our element
+const applyTooltip = (el, bindings, vnode) => {
if (!inBrowser) {
+ /* istanbul ignore next */
return
}
if (!Popper) {
- // Popper is required for tooltips to work
- warn('v-b-tooltip: Popper.js is required for tooltips to work')
+ // Popper is required for ToolTips to work
+ /* istanbul ignore next */
+ warn('v-b-tooltip: Popper.js is required for ToolTips to work')
+ /* istanbul ignore next */
return
}
- if (el[BVTT]) {
- el[BVTT].updateConfig(parseBindings(bindings))
+ const config = parseBindings(bindings)
+ if (el[BV_TOOLTIP]) {
+ el[BV_TOOLTIP].updateConfig(config)
} else {
- el[BVTT] = new ToolTip(el, parseBindings(bindings), vnode.context.$root)
+ el[BV_TOOLTIP] = new ToolTip(el, config, vnode.context.$root)
}
}
-//
-// Remove tooltip on our element
-//
-/* istanbul ignore next: not easy to test */
-function removeBVTT(el) {
- if (!inBrowser) {
- return
- }
- if (el[BVTT]) {
- el[BVTT].destroy()
- el[BVTT] = null
- delete el[BVTT]
+// Remove ToolTip on our element
+const removeTooltip = el => {
+ if (el[BV_TOOLTIP]) {
+ el[BV_TOOLTIP].destroy()
+ el[BV_TOOLTIP] = null
+ delete el[BV_TOOLTIP]
}
}
/*
* Export our directive
*/
-/* istanbul ignore next: not easy to test */
export default {
bind(el, bindings, vnode) {
- applyBVTT(el, bindings, vnode)
+ applyTooltip(el, bindings, vnode)
},
inserted(el, bindings, vnode) {
- applyBVTT(el, bindings, vnode)
+ applyTooltip(el, bindings, vnode)
},
- update(el, bindings, vnode) {
+ update(el, bindings, vnode) /* istanbul ignore next: not easy to test */ {
if (bindings.value !== bindings.oldValue) {
- applyBVTT(el, bindings, vnode)
+ applyTooltip(el, bindings, vnode)
}
},
- componentUpdated(el, bindings, vnode) {
+ componentUpdated(el, bindings, vnode) /* istanbul ignore next: not easy to test */ {
if (bindings.value !== bindings.oldValue) {
- applyBVTT(el, bindings, vnode)
+ applyTooltip(el, bindings, vnode)
}
},
unbind(el) {
- removeBVTT(el)
+ removeTooltip(el)
}
}
diff --git a/src/directives/tooltip/tooltip.spec.js b/src/directives/tooltip/tooltip.spec.js
new file mode 100644
index 00000000000..08eb172d9b1
--- /dev/null
+++ b/src/directives/tooltip/tooltip.spec.js
@@ -0,0 +1,37 @@
+import tooltipDirective from './tooltip'
+import ToolTip from '../../utils/tooltip.class'
+import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
+
+// Key which we use to store tooltip object on element
+const BV_TOOLTIP = '__BV_ToolTip__'
+
+describe('v-b-tooltip directive', () => {
+ it('should have ToolTip class instance', async () => {
+ const localVue = new CreateLocalVue()
+
+ const App = localVue.extend({
+ directives: {
+ bTooltip: tooltipDirective
+ },
+ data() {
+ return {}
+ },
+ template: ''
+ })
+
+ const wrapper = mount(App, {
+ localVue: localVue,
+ attachToDocument: true
+ })
+
+ expect(wrapper.isVueInstance()).toBe(true)
+ expect(wrapper.is('button')).toBe(true)
+ const $button = wrapper.find('button')
+
+ // Should have instance of popover class on it
+ expect($button.element[BV_TOOLTIP]).toBeDefined()
+ expect($button.element[BV_TOOLTIP]).toBeInstanceOf(ToolTip)
+
+ wrapper.destroy()
+ })
+})