Skip to content

Commit 6d92a43

Browse files
authored
fix(b-tabs): restore correct active tab detection logic (closes #6205) (#6208)
* fix(b-tabs): restore correct active tab detection logic * Update tabs.js * Update tabs.js * Update tabs.js * Update tabs.js * Update tabs.spec.js
1 parent 627a753 commit 6d92a43

File tree

2 files changed

+243
-176
lines changed

2 files changed

+243
-176
lines changed

src/components/tabs/tabs.js

+43-22
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
} from '../../constants/slots'
3434
import { arrayIncludes } from '../../utils/array'
3535
import { BvEvent } from '../../utils/bv-event.class'
36-
import { attemptFocus, selectAll } from '../../utils/dom'
36+
import { attemptFocus, selectAll, requestAF } from '../../utils/dom'
3737
import { stopEvent } from '../../utils/events'
3838
import { identity } from '../../utils/identity'
3939
import { isEvent } from '../../utils/inspect'
@@ -280,14 +280,14 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
280280
// Update the v-model
281281
this.$emit(MODEL_EVENT_NAME, index)
282282
},
283+
// If tabs added, removed, or re-ordered, we emit a `changed` event
283284
tabs(newValue, oldValue) {
284-
// If tabs added, removed, or re-ordered, we emit a `changed` event
285-
// We use `tab._uid` instead of `tab.safeId()`, as the later is changed
286-
// in a `$nextTick()` if no explicit ID is provided, causing duplicate emits
285+
// We use `_uid` instead of `safeId()`, as the later is changed in a `$nextTick()`
286+
// if no explicit ID is provided, causing duplicate emits
287287
if (
288288
!looseEqual(
289-
newValue.map(t => t[COMPONENT_UID_KEY]),
290-
oldValue.map(t => t[COMPONENT_UID_KEY])
289+
newValue.map($tab => $tab[COMPONENT_UID_KEY]),
290+
oldValue.map($tab => $tab[COMPONENT_UID_KEY])
291291
)
292292
) {
293293
// In a `$nextTick()` to ensure `currentTab` has been set first
@@ -298,6 +298,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
298298
})
299299
}
300300
},
301+
// Each `<b-tab>` will register/unregister itself
302+
// We use this to detect when tabs are added/removed
303+
// to trigger the update of the tabs
301304
registeredTabs() {
302305
this.updateTabs()
303306
}
@@ -332,7 +335,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
332335
/* istanbul ignore next: difficult to test mutation observer in JSDOM */
333336
const handler = () => {
334337
this.$nextTick(() => {
335-
this.updateTabs()
338+
requestAF(() => {
339+
this.updateTabs()
340+
})
336341
})
337342
}
338343

@@ -352,8 +357,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
352357

353358
// DOM Order of Tabs
354359
let order = []
360+
/* istanbul ignore next: too difficult to test */
355361
if (IS_BROWSER && $tabs.length > 0) {
356-
// We rely on the DOM when mounted to get the 'true' order of the `<b-tab>` children
362+
// We rely on the DOM when mounted to get the "true" order of the `<b-tab>` children
357363
// `querySelectorAll()` always returns elements in document order, regardless of
358364
// order specified in the selector
359365
const selector = $tabs.map($tab => `#${$tab.safeId()}`).join(', ')
@@ -369,29 +375,44 @@ export const BTabs = /*#__PURE__*/ Vue.extend({
369375
updateTabs() {
370376
const $tabs = this.getTabs()
371377

372-
// Normalize `currentTab`
373-
let { currentTab } = this
374-
const $tab = $tabs[currentTab]
375-
if (!$tab || $tab.disabled) {
376-
currentTab = $tabs.indexOf(
377-
$tabs
378-
.slice()
379-
.reverse()
380-
.find($tab => $tab.localActive && !$tab.disabled)
381-
)
378+
// Find last active non-disabled tab in current tabs
379+
// We trust tab state over `currentTab`, in case tabs were added/removed/re-ordered
380+
let tabIndex = $tabs.indexOf(
381+
$tabs
382+
.slice()
383+
.reverse()
384+
.find($tab => $tab.localActive && !$tab.disabled)
385+
)
382386

383-
if (currentTab === -1) {
384-
currentTab = $tabs.indexOf($tabs.find(notDisabled))
387+
// Else try setting to `currentTab`
388+
if (tabIndex < 0) {
389+
const { currentTab } = this
390+
if (currentTab >= $tabs.length) {
391+
// Handle last tab being removed, so find the last non-disabled tab
392+
tabIndex = $tabs.indexOf(
393+
$tabs
394+
.slice()
395+
.reverse()
396+
.find(notDisabled)
397+
)
398+
} else if ($tabs[currentTab] && !$tabs[currentTab].disabled) {
399+
// Current tab is not disabled
400+
tabIndex = currentTab
385401
}
386402
}
387403

404+
// Else find first non-disabled tab in current tabs
405+
if (tabIndex < 0) {
406+
tabIndex = $tabs.indexOf($tabs.find(notDisabled))
407+
}
408+
388409
// Ensure only one tab is active at a time
389410
$tabs.forEach(($tab, index) => {
390-
$tab.localActive = index === currentTab
411+
$tab.localActive = index === tabIndex
391412
})
392413

393414
this.tabs = $tabs
394-
this.currentTab = currentTab
415+
this.currentTab = tabIndex
395416
},
396417
// Find a button that controls a tab, given the tab reference
397418
// Returns the button vm instance

0 commit comments

Comments
 (0)