Skip to content

Commit 9b195dd

Browse files
authored
feat(b-tabs): emit cancelable BvEvent before changing tabs via new activate-tab event (closes #4273) (#4274)
1 parent 009431e commit 9b195dd

File tree

4 files changed

+123
-11
lines changed

4 files changed

+123
-11
lines changed

src/components/tabs/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,23 @@ methods are `.activate()` and `.deactivate()`, respectively. If activation or de
419419
will remain active and the method will return `false`. You will need a reference to the `<b-tab>` in
420420
order to use these methods.
421421

422+
## Preventing a `<b-tab>` from being activated
423+
424+
To prevent a tab from activating, simply set the `disabled` prop on the `<b-tab>` component.
425+
426+
Alternatively, you can listen for the `activate-tab` event, which provides an option to prevent the
427+
tab from activating. The `activate-tab` event is emitted with three arguments:
428+
429+
- `newTabIndex`: The index of the tab that is going to be activated
430+
- `prevTabIndex`: The index of the currently active tab
431+
- `bvEvent`: The `BvEvent` object. Call `bvEvt.preventDefault()` to prevent `newTabIndex` from being
432+
activated
433+
434+
For accessibility reasons, when using the `activate-tab` event to prevent a tab from activating, you
435+
should provide some means of notification to the user as to why the tab is not able to be activated.
436+
It is recommended to use the `disabled` attribute on the `<b-tab>` component instead of using the
437+
`activate-tab` event (as `disabled` is more intuitive for screen reader users).
438+
422439
## Advanced examples
423440

424441
### External controls using `v-model`

src/components/tabs/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,28 @@
9393
}
9494
]
9595
},
96+
{
97+
"event": "activate-tab",
98+
"version": "2.1.0",
99+
"description": "Emitted just before a tab is shown/activated. Cancelable",
100+
"args": [
101+
{
102+
"arg": "newTabIndex",
103+
"type": "Number",
104+
"description": "Tab being activated (0-based index)"
105+
},
106+
{
107+
"arg": "prevTabIndex",
108+
"type": "Number",
109+
"description": "Tab that is currently active (0-based index). Will be -1 if no current active tab"
110+
},
111+
{
112+
"arg": "bvEvt",
113+
"type": "BvEvent",
114+
"description": "BvEvent object. Call bvEvt.preventDefault() to cancel"
115+
}
116+
]
117+
},
96118
{
97119
"event": "changed",
98120
"description": "Emitted when a tab is added, removed, or tabs are re-ordered",

src/components/tabs/tabs.js

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import looseEqual from '../../utils/loose-equal'
44
import observeDom from '../../utils/observe-dom'
55
import stableSort from '../../utils/stable-sort'
66
import { arrayIncludes, concat } from '../../utils/array'
7+
import { BvEvent } from '../../utils/bv-event.class'
78
import { requestAF, selectAll } from '../../utils/dom'
89
import { isEvent } from '../../utils/inspect'
910
import { omit } from '../../utils/object'
@@ -262,7 +263,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
262263
old = parseInt(old, 10) || 0
263264
const tabs = this.tabs
264265
if (tabs[val] && !tabs[val].disabled) {
265-
this.currentTab = val
266+
this.activateTab(tabs[val])
266267
} else {
267268
// Try next or prev tabs
268269
if (val < old) {
@@ -481,14 +482,22 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
481482
let result = false
482483
if (tab) {
483484
const index = this.tabs.indexOf(tab)
484-
if (!tab.disabled && index > -1) {
485-
result = true
486-
this.currentTab = index
485+
if (!tab.disabled && index > -1 && index !== this.currentTab) {
486+
const tabEvt = new BvEvent('activate-tab', {
487+
cancelable: true,
488+
vueTarget: this,
489+
componentId: this.safeId()
490+
})
491+
this.$emit(tabEvt.type, index, this.currentTab, tabEvt)
492+
if (!tabEvt.defaultPrevented) {
493+
result = true
494+
this.currentTab = index
495+
}
487496
}
488497
}
489-
if (!result) {
490-
// Couldn't set tab, so ensure v-model is set to `this.currentTab`
491-
/* istanbul ignore next: should rarely happen */
498+
// Couldn't set tab, so ensure v-model is set to `this.currentTab`
499+
/* istanbul ignore next: should rarely happen */
500+
if (!result && this.currentTab !== this.value) {
492501
this.$emit('input', this.currentTab)
493502
}
494503
return result
@@ -500,11 +509,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
500509
// Find first non-disabled tab that isn't the one being deactivated
501510
// If no tabs are available, then don't deactivate current tab
502511
return this.activateTab(this.tabs.filter(t => t !== tab).find(notDisabled))
503-
} else {
504-
// No tab specified
505-
/* istanbul ignore next: should never happen */
506-
return false
507512
}
513+
/* istanbul ignore next: should never/rarely happen */
514+
return false
508515
},
509516
// Focus a tab button given it's <b-tab> instance
510517
focusButton(tab) {

src/components/tabs/tabs.spec.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,72 @@ describe('tabs', () => {
311311
wrapper.destroy()
312312
})
313313

314+
it('`activate-tab` event works', async () => {
315+
const App = Vue.extend({
316+
methods: {
317+
preventTab(next, prev, bvEvt) {
318+
// Prevent 3rd tab (index === 2) from activating
319+
if (next === 2) {
320+
bvEvt.preventDefault()
321+
}
322+
}
323+
},
324+
render(h) {
325+
return h(BTabs, { props: { value: 0 }, on: { 'activate-tab': this.preventTab } }, [
326+
h(BTab, { props: {} }, 'tab 0'),
327+
h(BTab, { props: {} }, 'tab 1'),
328+
h(BTab, { props: {} }, 'tab 2')
329+
])
330+
}
331+
})
332+
const wrapper = mount(App)
333+
expect(wrapper).toBeDefined()
334+
335+
await waitNT(wrapper.vm)
336+
await waitRAF()
337+
const tabs = wrapper.find(BTabs)
338+
expect(tabs).toBeDefined()
339+
expect(tabs.findAll(BTab).length).toBe(3)
340+
341+
// Expect 1st tab (index 0) to be active
342+
expect(tabs.vm.currentTab).toBe(0)
343+
expect(tabs.vm.tabs[0].localActive).toBe(true)
344+
expect(tabs.emitted('input')).not.toBeDefined()
345+
expect(tabs.emitted('activate-tab')).not.toBeDefined()
346+
347+
// Set 2nd BTab to be active
348+
tabs.setProps({ value: 1 })
349+
await waitNT(wrapper.vm)
350+
await waitRAF()
351+
expect(tabs.vm.currentTab).toBe(1)
352+
expect(tabs.emitted('input')).toBeDefined()
353+
expect(tabs.emitted('input').length).toBe(1)
354+
expect(tabs.emitted('input')[0][0]).toBe(1)
355+
expect(tabs.emitted('activate-tab')).toBeDefined()
356+
expect(tabs.emitted('activate-tab').length).toBe(1)
357+
expect(tabs.emitted('activate-tab')[0][0]).toBe(1)
358+
expect(tabs.emitted('activate-tab')[0][1]).toBe(0)
359+
expect(tabs.emitted('activate-tab')[0][2]).toBeDefined()
360+
expect(tabs.emitted('activate-tab')[0][2].vueTarget).toBe(tabs.vm)
361+
362+
// Attempt to set 3rd BTab to be active
363+
tabs.setProps({ value: 2 })
364+
await waitNT(wrapper.vm)
365+
await waitRAF()
366+
expect(tabs.vm.currentTab).toBe(1)
367+
expect(tabs.emitted('input')).toBeDefined()
368+
expect(tabs.emitted('input').length).toBe(2)
369+
expect(tabs.emitted('input')[1][0]).toBe(1)
370+
expect(tabs.emitted('activate-tab').length).toBe(2)
371+
expect(tabs.emitted('activate-tab')[1][0]).toBe(2)
372+
expect(tabs.emitted('activate-tab')[1][1]).toBe(1)
373+
expect(tabs.emitted('activate-tab')[1][2]).toBeDefined()
374+
expect(tabs.emitted('activate-tab')[1][2].vueTarget).toBe(tabs.vm)
375+
expect(tabs.emitted('activate-tab')[1][2].defaultPrevented).toBe(true)
376+
377+
wrapper.destroy()
378+
})
379+
314380
it('clicking on tab activates the tab, and tab emits click event', async () => {
315381
const App = Vue.extend({
316382
render(h) {

0 commit comments

Comments
 (0)