🌐 AI搜索 & 代理 主页
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/components/tabs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,23 @@ methods are `.activate()` and `.deactivate()`, respectively. If activation or de
will remain active and the method will return `false`. You will need a reference to the `<b-tab>` in
order to use these methods.

## Preventing a `<b-tab>` from being activated

To prevent a tab from activating, simply set the `disabled` prop on the `<b-tab>` component.

Alternatively, you can listen for the `activate-tab` event, which provides an option to prevent the
tab from activating. The `activate-tab` event is emitted with three arguments:

- `newTabIndex`: The index of the tab that is going to be activated
- `prevTabIndex`: The index of the currently active tab
- `bvEvent`: The `BvEvent` object. Call `bvEvt.preventDefault()` to prevent `newTabIndex` from being
activated

For accessibility reasons, when using the `activate-tab` event to prevent a tab from activating, you
should provide some means of notification to the user as to why the tab is not able to be activated.
It is recommended to use the `disabled` attribute on the `<b-tab>` component instead of using the
`activate-tab` event (as `disabled` is more intuitive for screen reader users).

## Advanced examples

### External controls using `v-model`
Expand Down
22 changes: 22 additions & 0 deletions src/components/tabs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@
}
]
},
{
"event": "activate-tab",
"version": "2.1.0",
"description": "Emitted just before a tab is shown/activated. Cancelable",
"args": [
{
"arg": "newTabIndex",
"type": "Number",
"description": "Tab being activated (0-based index)"
},
{
"arg": "prevTabIndex",
"type": "Number",
"description": "Tab that is currently active (0-based index). Will be -1 if no current active tab"
},
{
"arg": "bvEvt",
"type": "BvEvent",
"description": "BvEvent object. Call bvEvt.preventDefault() to cancel"
}
]
},
{
"event": "changed",
"description": "Emitted when a tab is added, removed, or tabs are re-ordered",
Expand Down
29 changes: 18 additions & 11 deletions src/components/tabs/tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import looseEqual from '../../utils/loose-equal'
import observeDom from '../../utils/observe-dom'
import stableSort from '../../utils/stable-sort'
import { arrayIncludes, concat } from '../../utils/array'
import { BvEvent } from '../../utils/bv-event.class'
import { requestAF, selectAll } from '../../utils/dom'
import { isEvent } from '../../utils/inspect'
import { omit } from '../../utils/object'
Expand Down Expand Up @@ -262,7 +263,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
old = parseInt(old, 10) || 0
const tabs = this.tabs
if (tabs[val] && !tabs[val].disabled) {
this.currentTab = val
this.activateTab(tabs[val])
} else {
// Try next or prev tabs
if (val < old) {
Expand Down Expand Up @@ -481,14 +482,22 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
let result = false
if (tab) {
const index = this.tabs.indexOf(tab)
if (!tab.disabled && index > -1) {
result = true
this.currentTab = index
if (!tab.disabled && index > -1 && index !== this.currentTab) {
const tabEvt = new BvEvent('activate-tab', {
cancelable: true,
vueTarget: this,
componentId: this.safeId()
})
this.$emit(tabEvt.type, index, this.currentTab, tabEvt)
if (!tabEvt.defaultPrevented) {
result = true
this.currentTab = index
}
}
}
if (!result) {
// Couldn't set tab, so ensure v-model is set to `this.currentTab`
/* istanbul ignore next: should rarely happen */
// Couldn't set tab, so ensure v-model is set to `this.currentTab`
/* istanbul ignore next: should rarely happen */
if (!result && this.currentTab !== this.value) {
this.$emit('input', this.currentTab)
}
return result
Expand All @@ -500,11 +509,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
// Find first non-disabled tab that isn't the one being deactivated
// If no tabs are available, then don't deactivate current tab
return this.activateTab(this.tabs.filter(t => t !== tab).find(notDisabled))
} else {
// No tab specified
/* istanbul ignore next: should never happen */
return false
}
/* istanbul ignore next: should never/rarely happen */
return false
},
// Focus a tab button given it's <b-tab> instance
focusButton(tab) {
Expand Down
66 changes: 66 additions & 0 deletions src/components/tabs/tabs.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,72 @@ describe('tabs', () => {
wrapper.destroy()
})

it('`activate-tab` event works', async () => {
const App = Vue.extend({
methods: {
preventTab(next, prev, bvEvt) {
// Prevent 3rd tab (index === 2) from activating
if (next === 2) {
bvEvt.preventDefault()
}
}
},
render(h) {
return h(BTabs, { props: { value: 0 }, on: { 'activate-tab': this.preventTab } }, [
h(BTab, { props: {} }, 'tab 0'),
h(BTab, { props: {} }, 'tab 1'),
h(BTab, { props: {} }, 'tab 2')
])
}
})
const wrapper = mount(App)
expect(wrapper).toBeDefined()

await waitNT(wrapper.vm)
await waitRAF()
const tabs = wrapper.find(BTabs)
expect(tabs).toBeDefined()
expect(tabs.findAll(BTab).length).toBe(3)

// Expect 1st tab (index 0) to be active
expect(tabs.vm.currentTab).toBe(0)
expect(tabs.vm.tabs[0].localActive).toBe(true)
expect(tabs.emitted('input')).not.toBeDefined()
expect(tabs.emitted('activate-tab')).not.toBeDefined()

// Set 2nd BTab to be active
tabs.setProps({ value: 1 })
await waitNT(wrapper.vm)
await waitRAF()
expect(tabs.vm.currentTab).toBe(1)
expect(tabs.emitted('input')).toBeDefined()
expect(tabs.emitted('input').length).toBe(1)
expect(tabs.emitted('input')[0][0]).toBe(1)
expect(tabs.emitted('activate-tab')).toBeDefined()
expect(tabs.emitted('activate-tab').length).toBe(1)
expect(tabs.emitted('activate-tab')[0][0]).toBe(1)
expect(tabs.emitted('activate-tab')[0][1]).toBe(0)
expect(tabs.emitted('activate-tab')[0][2]).toBeDefined()
expect(tabs.emitted('activate-tab')[0][2].vueTarget).toBe(tabs.vm)

// Attempt to set 3rd BTab to be active
tabs.setProps({ value: 2 })
await waitNT(wrapper.vm)
await waitRAF()
expect(tabs.vm.currentTab).toBe(1)
expect(tabs.emitted('input')).toBeDefined()
expect(tabs.emitted('input').length).toBe(2)
expect(tabs.emitted('input')[1][0]).toBe(1)
expect(tabs.emitted('activate-tab').length).toBe(2)
expect(tabs.emitted('activate-tab')[1][0]).toBe(2)
expect(tabs.emitted('activate-tab')[1][1]).toBe(1)
expect(tabs.emitted('activate-tab')[1][2]).toBeDefined()
expect(tabs.emitted('activate-tab')[1][2].vueTarget).toBe(tabs.vm)
expect(tabs.emitted('activate-tab')[1][2].defaultPrevented).toBe(true)

wrapper.destroy()
})

it('clicking on tab activates the tab, and tab emits click event', async () => {
const App = Vue.extend({
render(h) {
Expand Down