From db8c7d63940e121af8897f9078edfdaafca878ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 00:44:46 +0100 Subject: [PATCH 01/17] fix(b-tabs): tabs detection for SSR --- src/components/tabs/tabs.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index d04947e5e3b..3c32eb9029f 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -281,9 +281,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, registeredTabs() { - // Each b-tab will register/unregister itself. + // Each `` will register/unregister itself // We use this to detect when tabs are added/removed - // to trigger the update of the tabs. + // to trigger the update of the tabs this.$nextTick(() => { requestAF(() => { this.updateTabs() @@ -323,11 +323,13 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ created() { // Create private non-reactive props this.$_observer = null - this.currentTab = toInteger(this[MODEL_PROP_NAME], -1) // For SSR and to make sure only a single tab is shown on mount - // We wrap this in a `$nextTick()` to ensure the child tabs have been created + // Wrapped in `$nextTick()` and `requestAF()` to ensure the + // child tabs have been created this.$nextTick(() => { - this.updateTabs() + requestAF(() => { + this.updateTabs() + }) }) }, mounted() { @@ -336,7 +338,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ this.$nextTick(() => { // Flag we are now mounted and to switch to DOM for tab probing // As `$slots.default` appears to lie about component instances - // after b-tabs is destroyed and re-instantiated + // after `` is destroyed and re-instantiated // And `$children` does not respect DOM order this.isMounted = true }) @@ -347,10 +349,11 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ }, /* istanbul ignore next */ activated() { - this.currentTab = toInteger(this[MODEL_PROP_NAME], -1) this.$nextTick(() => { - this.updateTabs() this.isMounted = true + requestAF(() => { + this.updateTabs() + }) }) }, beforeDestroy() { From 2f3c7b8f11f867d9fe1f999423d5addebbd1de3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 01:12:11 +0100 Subject: [PATCH 02/17] Update tab.js --- src/components/tabs/tab.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index e21df1ca004..786571976b9 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -1,5 +1,6 @@ import { Vue } from '../../vue' import { NAME_TAB } from '../../constants/components' +import { IS_BROWSER } from '../../constants/env' import { MODEL_EVENT_NAME_PREFIX } from '../../constants/events' import { PROP_TYPE_ARRAY_OBJECT_STRING, @@ -116,6 +117,15 @@ export const BTab = /*#__PURE__*/ Vue.extend({ } } }, + created() { + /* istanbul ignore next */ + if (!IS_BROWSER) { + // Inform b-tabs of our presence + this.registerTab() + // Initially show on mount if active and not disabled + this.show = this.localActive + } + }, mounted() { // Inform b-tabs of our presence this.registerTab() From eb51bc7c1b46f65040597eec2a387fb7c92b7f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 01:12:15 +0100 Subject: [PATCH 03/17] Update tabs.js --- src/components/tabs/tabs.js | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 3c32eb9029f..d1b802fb981 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -324,12 +324,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // Create private non-reactive props this.$_observer = null // For SSR and to make sure only a single tab is shown on mount - // Wrapped in `$nextTick()` and `requestAF()` to ensure the - // child tabs have been created + // Wrapped in `$nextTick()` to ensure the child tabs have been created this.$nextTick(() => { - requestAF(() => { - this.updateTabs() - }) + this.updateTabs() }) }, mounted() { @@ -351,9 +348,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ activated() { this.$nextTick(() => { this.isMounted = true - requestAF(() => { - this.updateTabs() - }) + this.updateTabs() }) }, beforeDestroy() { @@ -405,23 +400,23 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // We also filter out any `` components that are extended // `` with a root child `` // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 - const tabs = this.registeredTabs.filter( + const $tabs = this.registeredTabs.filter( tab => tab.$children.filter(t => t._isTab).length === 0 ) // DOM Order of Tabs let order = [] - if (this.isMounted && tabs.length > 0) { + if (this.isMounted && $tabs.length > 0) { // We rely on the DOM when mounted to get the 'true' order of the `` children // `querySelectorAll()` always returns elements in document order, regardless of // order specified in the selector - const selector = tabs.map(tab => `#${tab.safeId()}`).join(', ') + const selector = $tabs.map(tab => `#${tab.safeId()}`).join(', ') order = selectAll(selector, this.$el) - .map(el => el.id) + .map($el => $el.id) .filter(identity) } // Stable sort keeps the original order if not found in the `order` array, // which will be an empty array before mount - return stableSort(tabs, (a, b) => order.indexOf(a.safeId()) - order.indexOf(b.safeId())) + return stableSort($tabs, (a, b) => order.indexOf(a.safeId()) - order.indexOf(b.safeId())) }, // Update list of `` children updateTabs() { From 9e0e7f98ba1b47e8d86ba925ac87d56389cdb661 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 07:49:43 +0100 Subject: [PATCH 04/17] testing --- src/components/tabs/tab.js | 3 +++ src/components/tabs/tabs.js | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 786571976b9..1c02735742d 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -119,8 +119,10 @@ export const BTab = /*#__PURE__*/ Vue.extend({ }, created() { /* istanbul ignore next */ + console.log({ IS_BROWSER }) if (!IS_BROWSER) { // Inform b-tabs of our presence + console.log('tab: created') this.registerTab() // Initially show on mount if active and not disabled this.show = this.localActive @@ -147,6 +149,7 @@ export const BTab = /*#__PURE__*/ Vue.extend({ methods: { // Private methods registerTab() { + console.log('tab: registerTab') // Inform `` of our presence const { registerTab } = this.bvTabs if (registerTab) { diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index d1b802fb981..de2a0f96f3d 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -326,7 +326,10 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // For SSR and to make sure only a single tab is shown on mount // Wrapped in `$nextTick()` to ensure the child tabs have been created this.$nextTick(() => { - this.updateTabs() + requestAF(() => { + console.log('tabs: created') + this.updateTabs() + }) }) }, mounted() { @@ -420,6 +423,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ }, // Update list of `` children updateTabs() { + console.log('tabs: updateTabs') // Probe tabs const tabs = this.getTabs() From 2474c9c14f64fe1d32201323f7c66a54397becab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 08:09:50 +0100 Subject: [PATCH 05/17] Update safe-types.js --- src/constants/safe-types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constants/safe-types.js b/src/constants/safe-types.js index 39b088ba9f6..f7daf6ccecf 100644 --- a/src/constants/safe-types.js +++ b/src/constants/safe-types.js @@ -1,7 +1,7 @@ import { HAS_WINDOW_SUPPORT, WINDOW } from './env' /* istanbul ignore next */ -const Element = HAS_WINDOW_SUPPORT ? WINDOW.Element : class Element extends Object {} +export const Element = HAS_WINDOW_SUPPORT ? WINDOW.Element : class Element extends Object {} /* istanbul ignore next */ export const HTMLElement = HAS_WINDOW_SUPPORT From b439be9050b0387209aee76a2d22aba07d0ea717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 08:09:56 +0100 Subject: [PATCH 06/17] Update dom.js --- src/utils/dom.js | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/utils/dom.js b/src/utils/dom.js index fdd3ed67157..d9d230f788c 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -1,4 +1,5 @@ -import { HAS_WINDOW_SUPPORT, HAS_DOCUMENT_SUPPORT } from '../constants/env' +import { DOCUMENT, WINDOW } from '../constants/env' +import { Element } from '../constants/safe-types' import { from as arrayFrom } from './array' import { isFunction, isNull } from './inspect' import { toFloat } from './number' @@ -6,6 +7,8 @@ import { toString } from './string' // --- Constants --- +const ELEMENT_PROTO = Element.prototype + const TABABLE_SELECTOR = [ 'button', '[href]:not(.disabled)', @@ -18,21 +21,17 @@ const TABABLE_SELECTOR = [ .map(s => `${s}:not(:disabled):not([disabled])`) .join(', ') -const w = HAS_WINDOW_SUPPORT ? window : {} -const d = HAS_DOCUMENT_SUPPORT ? document : {} -const elProto = typeof Element !== 'undefined' ? Element.prototype : {} - // --- Normalization utils --- // See: https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill /* istanbul ignore next */ export const matchesEl = - elProto.matches || elProto.msMatchesSelector || elProto.webkitMatchesSelector + ELEMENT_PROTO.matches || ELEMENT_PROTO.msMatchesSelector || ELEMENT_PROTO.webkitMatchesSelector // See: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest /* istanbul ignore next */ export const closestEl = - elProto.closest || + ELEMENT_PROTO.closest || function(sel) { let el = this do { @@ -48,18 +47,18 @@ export const closestEl = // `requestAnimationFrame()` convenience method /* istanbul ignore next: JSDOM always returns the first option */ export const requestAF = - w.requestAnimationFrame || - w.webkitRequestAnimationFrame || - w.mozRequestAnimationFrame || - w.msRequestAnimationFrame || - w.oRequestAnimationFrame || + WINDOW.requestAnimationFrame || + WINDOW.webkitRequestAnimationFrame || + WINDOW.mozRequestAnimationFrame || + WINDOW.msRequestAnimationFrame || + WINDOW.oRequestAnimationFrame || // Fallback, but not a true polyfill // Only needed for Opera Mini /* istanbul ignore next */ (cb => setTimeout(cb, 16)) export const MutationObs = - w.MutationObserver || w.WebKitMutationObserver || w.MozMutationObserver || null + WINDOW.MutationObserver || WINDOW.WebKitMutationObserver || WINDOW.MozMutationObserver || null // --- Utils --- @@ -71,7 +70,7 @@ export const isElement = el => !!(el && el.nodeType === Node.ELEMENT_NODE) // Get the currently active HTML element export const getActiveElement = (excludes = []) => { - const activeElement = d.activeElement + const { activeElement } = DOCUMENT return activeElement && !excludes.some(el => el === activeElement) ? activeElement : null } @@ -83,7 +82,7 @@ export const isActiveElement = el => isElement(el) && el === getActiveElement() // Determine if an HTML element is visible - Faster than CSS check export const isVisible = el => { - if (!isElement(el) || !el.parentNode || !contains(d.body, el)) { + if (!isElement(el) || !el.parentNode || !contains(DOCUMENT.body, el)) { // Note this can fail for shadow dom elements since they // are not a direct descendant of document.body return false @@ -113,11 +112,11 @@ export const reflow = el => { // Select all elements matching selector. Returns `[]` if none found export const selectAll = (selector, root) => - arrayFrom((isElement(root) ? root : d).querySelectorAll(selector)) + arrayFrom((isElement(root) ? root : DOCUMENT).querySelectorAll(selector)) // Select a single element, returns `null` if not found export const select = (selector, root) => - (isElement(root) ? root : d).querySelector(selector) || null + (isElement(root) ? root : DOCUMENT).querySelector(selector) || null // Determine if an element matches a selector export const matches = (el, selector) => (isElement(el) ? matchesEl.call(el, selector) : false) @@ -140,7 +139,7 @@ export const contains = (parent, child) => parent && isFunction(parent.contains) ? parent.contains(child) : false // Get an element given an ID -export const getById = id => d.getElementById(/^#/.test(id) ? id.slice(1) : id) || null +export const getById = id => DOCUMENT.getElementById(/^#/.test(id) ? id.slice(1) : id) || null // Add a class to an element export const addClass = (el, className) => { @@ -220,12 +219,18 @@ export const getBCR = el => (isElement(el) ? el.getBoundingClientRect() : null) // Get computed style object for an element /* istanbul ignore next: getComputedStyle() doesn't work in JSDOM */ -export const getCS = el => (HAS_WINDOW_SUPPORT && isElement(el) ? w.getComputedStyle(el) : {}) +export const getCS = el => { + const { getComputedStyle } = WINDOW + return getComputedStyle && isElement(el) ? getComputedStyle(el) : {} +} // Returns a `Selection` object representing the range of text selected // Returns `null` if no window support is given /* istanbul ignore next: getSelection() doesn't work in JSDOM */ -export const getSel = () => (HAS_WINDOW_SUPPORT && w.getSelection ? w.getSelection() : null) +export const getSel = () => { + const { getSelection } = WINDOW + return getSelection ? WINDOW.getSelection() : null +} // Return an element's offset with respect to document element // https://j11y.io/jquery/#v=git&fn=jQuery.fn.offset From 57386552b2279b6ec604989fafd25a9fac42da67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 08:14:12 +0100 Subject: [PATCH 07/17] Update tabs.js --- src/components/tabs/tabs.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index de2a0f96f3d..8b391342da6 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -284,11 +284,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // Each `` will register/unregister itself // We use this to detect when tabs are added/removed // to trigger the update of the tabs - this.$nextTick(() => { - requestAF(() => { - this.updateTabs() - }) - }) + this.updateTabs() }, tabs(newValue, oldValue) { // If tabs added, removed, or re-ordered, we emit a `changed` event @@ -326,10 +322,8 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // For SSR and to make sure only a single tab is shown on mount // Wrapped in `$nextTick()` to ensure the child tabs have been created this.$nextTick(() => { - requestAF(() => { - console.log('tabs: created') - this.updateTabs() - }) + console.log('tabs: created') + this.updateTabs() }) }, mounted() { @@ -351,7 +345,6 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ activated() { this.$nextTick(() => { this.isMounted = true - this.updateTabs() }) }, beforeDestroy() { @@ -360,6 +353,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ destroyed() { // Ensure no references to child instances exist this.tabs = [] + this.registeredTabs = [] }, methods: { registerTab(tab) { From 332a451efe9dc7e4ee15f21a8a45d9a5192b807d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 08:20:59 +0100 Subject: [PATCH 08/17] Update tabs.js --- src/components/tabs/tabs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 8b391342da6..8a4b8827dac 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -417,9 +417,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ }, // Update list of `` children updateTabs() { - console.log('tabs: updateTabs') // Probe tabs const tabs = this.getTabs() + console.log('tabs: updateTabs', tabs.length, this.registeredTabs.length) // Find *last* active non-disabled tab in current tabs // We trust tab state over `currentTab`, in case tabs were added/removed/re-ordered From 097da3aed6d3cf693ffff45d778b515d80484b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 08:31:47 +0100 Subject: [PATCH 09/17] Update tabs.js --- src/components/tabs/tabs.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 8a4b8827dac..b2ca955125f 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -584,6 +584,8 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // Tab button to allow focusing when no active tab found (keynav only) const fallbackTab = tabs.find(tab => !tab.disabled) + console.log('tag: render', tabs.length) + // For each `` found create the tab buttons const $buttons = tabs.map((tab, index) => { // Ensure at least one tab button is focusable when keynav enabled (if possible) From 007455ef369e3af73f79be876d5e35fb3a196bc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 08:48:06 +0100 Subject: [PATCH 10/17] Update tabs.js --- src/components/tabs/tabs.js | 38 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index b2ca955125f..b8b7cdf423e 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -393,26 +393,36 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, getTabs() { - // We use `registeredTabs` as the source of truth for child tab components - // We also filter out any `` components that are extended - // `` with a root child `` - // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 - const $tabs = this.registeredTabs.filter( - tab => tab.$children.filter(t => t._isTab).length === 0 - ) - // DOM Order of Tabs + let $tabs = [] let order = [] - if (this.isMounted && $tabs.length > 0) { + + // Filter out any `` components that are extended + // `` with a root child `` + const filterTabs = $tab => $tab.$children.filter($t => $t._isTab).length === 0 + + if (this.isMounted) { + // We use `registeredTabs` as the source of truth for child tab components + // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 + $tabs = this.registeredTabs.filter(filterTabs) + // We rely on the DOM when mounted to get the 'true' order of the `` children // `querySelectorAll()` always returns elements in document order, regardless of // order specified in the selector const selector = $tabs.map(tab => `#${tab.safeId()}`).join(', ') - order = selectAll(selector, this.$el) - .map($el => $el.id) - .filter(identity) + order = selector + ? selectAll(selector, this.$el) + .map($el => $el.id) + .filter(identity) + : [] + } else { + // We can't rely on `registeredTabs` for SSR, since ``'s + // register themselves after our first render + $tabs = (this.normalizeSlot() || []) + .map(vNode => vNode.componentInstance) + .filter(filterTabs) } - // Stable sort keeps the original order if not found in the `order` array, - // which will be an empty array before mount + + // Stable sort keeps the original order if not found in the `order` array return stableSort($tabs, (a, b) => order.indexOf(a.safeId()) - order.indexOf(b.safeId())) }, // Update list of `` children From 2cdc7a3ac9e274089e11ab4082026c1efe72c7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 09:51:10 +0100 Subject: [PATCH 11/17] Update tabs.js --- src/components/tabs/tabs.js | 79 ++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index b8b7cdf423e..d3556b6d4b3 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -19,7 +19,6 @@ import { CODE_UP } from '../../constants/key-codes' import { - PROP_TYPE_ARRAY, PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_NUMBER, @@ -82,8 +81,7 @@ const BVTabButton = /*#__PURE__*/ Vue.extend({ setSize: makeProp(PROP_TYPE_NUMBER), // Reference to the child instance tab: makeProp(), - tabIndex: makeProp(PROP_TYPE_NUMBER), - tabs: makeProp(PROP_TYPE_ARRAY, []) + tabIndex: makeProp(PROP_TYPE_NUMBER) }, methods: { focus() { @@ -284,6 +282,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // Each `` will register/unregister itself // We use this to detect when tabs are added/removed // to trigger the update of the tabs + console.log('tabs: registeredTabs') this.updateTabs() }, tabs(newValue, oldValue) { @@ -368,9 +367,10 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ this.registeredTabs = this.registeredTabs.slice().filter(t => t !== tab) }, // DOM observer is needed to detect changes in order of tabs - setObserver(on) { + setObserver(on = true) { this.$_observer && this.$_observer.disconnect() this.$_observer = null + if (on) { const self = this /* istanbul ignore next: difficult to test mutation observer in JSDOM */ @@ -393,33 +393,23 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, getTabs() { - let $tabs = [] - let order = [] - + // We use `registeredTabs` as the source of truth for child tab components + // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 // Filter out any `` components that are extended // `` with a root child `` - const filterTabs = $tab => $tab.$children.filter($t => $t._isTab).length === 0 - - if (this.isMounted) { - // We use `registeredTabs` as the source of truth for child tab components - // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 - $tabs = this.registeredTabs.filter(filterTabs) + const $tabs = this.registeredTabs.filter( + $tab => $tab.$children.filter($t => $t._isTab).length === 0 + ) + let order = [] + if (this.isMounted && $tabs.length > 0) { // We rely on the DOM when mounted to get the 'true' order of the `` children // `querySelectorAll()` always returns elements in document order, regardless of // order specified in the selector const selector = $tabs.map(tab => `#${tab.safeId()}`).join(', ') - order = selector - ? selectAll(selector, this.$el) - .map($el => $el.id) - .filter(identity) - : [] - } else { - // We can't rely on `registeredTabs` for SSR, since ``'s - // register themselves after our first render - $tabs = (this.normalizeSlot() || []) - .map(vNode => vNode.componentInstance) - .filter(filterTabs) + order = selectAll(selector, this.$el) + .map($el => $el.id) + .filter(identity) } // Stable sort keeps the original order if not found in the `order` array @@ -586,7 +576,23 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, render(h) { - const { tabs, noKeyNav, card, vertical, end, firstTab, previousTab, nextTab, lastTab } = this + const { + align, + card, + end, + fill, + firstTab, + justified, + lastTab, + nextTab, + noKeyNav, + noNavStyle, + pills, + previousTab, + small, + tabs, + vertical + } = this // Currently active tab const activeTab = tabs.find(tab => tab.localActive && !tab.disabled) @@ -611,14 +617,13 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ return h(BVTabButton, { props: { - tab, - tabs, - id: tab.controlledBy || (tab.safeId ? tab.safeId(`_BV_tab_button_`) : null), controls: tab.safeId ? tab.safeId() : null, - tabIndex, - setSize: tabs.length, + id: tab.controlledBy || (tab.safeId ? tab.safeId(`_BV_tab_button_`) : null), + noKeyNav, posInSet: index + 1, - noKeyNav + setSize: tabs.length, + tab, + tabIndex }, on: { [EVENT_NAME_CLICK]: event => { @@ -645,13 +650,13 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ id: this.safeId('_BV_tab_controls_') }, props: { - fill: this.fill, - justified: this.justified, - align: this.align, - tabs: !this.noNavStyle && !this.pills, - pills: !this.noNavStyle && this.pills, + fill, + justified, + align, + tabs: !noNavStyle && !pills, + pills: !noNavStyle && pills, vertical, - small: this.small, + small, cardHeader: card && !vertical }, ref: 'nav' From 7bf628062f6d2ae9032e324ca910c10d8d0f1f43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 13:13:08 +0100 Subject: [PATCH 12/17] fix(b-tabs): improve rendering logic --- src/components/tabs/tab.js | 57 +---- src/components/tabs/tab.spec.js | 5 +- src/components/tabs/tabs.js | 352 ++++++++++--------------------- src/components/tabs/tabs.spec.js | 41 +--- src/constants/events.js | 1 + 5 files changed, 120 insertions(+), 336 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 1c02735742d..950ef91c652 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -1,6 +1,5 @@ import { Vue } from '../../vue' import { NAME_TAB } from '../../constants/components' -import { IS_BROWSER } from '../../constants/env' import { MODEL_EVENT_NAME_PREFIX } from '../../constants/events' import { PROP_TYPE_ARRAY_OBJECT_STRING, @@ -55,11 +54,14 @@ export const BTab = /*#__PURE__*/ Vue.extend({ props, data() { return { - localActive: this[MODEL_PROP_NAME_ACTIVE] && !this.disabled, - show: false + localActive: this[MODEL_PROP_NAME_ACTIVE] && !this.disabled } }, computed: { + // For parent sniffing of child + _isTab() { + return true + }, tabClasses() { const { localActive: active, disabled } = this @@ -81,17 +83,9 @@ export const BTab = /*#__PURE__*/ Vue.extend({ }, computedLazy() { return this.bvTabs.lazy || this.lazy - }, - // For parent sniffing of child - _isTab() { - return true } }, watch: { - localActive(newValue) { - // Make `active` prop work with `.sync` modifier - this.$emit(MODEL_EVENT_NAME_ACTIVE, newValue) - }, [MODEL_PROP_NAME_ACTIVE](newValue, oldValue) { if (newValue !== oldValue) { if (newValue) { @@ -115,25 +109,12 @@ export const BTab = /*#__PURE__*/ Vue.extend({ firstTab() } } + }, + localActive(newValue) { + // Make `active` prop work with `.sync` modifier + this.$emit(MODEL_EVENT_NAME_ACTIVE, newValue) } }, - created() { - /* istanbul ignore next */ - console.log({ IS_BROWSER }) - if (!IS_BROWSER) { - // Inform b-tabs of our presence - console.log('tab: created') - this.registerTab() - // Initially show on mount if active and not disabled - this.show = this.localActive - } - }, - mounted() { - // Inform b-tabs of our presence - this.registerTab() - // Initially show on mount if active and not disabled - this.show = this.localActive - }, updated() { // Force the tab button content to update (since slots are not reactive) // Only done if we have a title slot, as the title prop is reactive @@ -142,27 +123,7 @@ export const BTab = /*#__PURE__*/ Vue.extend({ updateButton(this) } }, - destroyed() { - // inform b-tabs of our departure - this.unregisterTab() - }, methods: { - // Private methods - registerTab() { - console.log('tab: registerTab') - // Inform `` of our presence - const { registerTab } = this.bvTabs - if (registerTab) { - registerTab(this) - } - }, - unregisterTab() { - // Inform `` of our departure - const { unregisterTab } = this.bvTabs - if (unregisterTab) { - unregisterTab(this) - } - }, // Public methods activate() { // Not inside a `` component or tab is disabled diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index b5a75a00671..c47a9fbe16d 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils' -import { waitNT, waitRAF } from '../../../tests/utils' +import { waitNT } from '../../../tests/utils' import { BTab } from './tab' describe('tab', () => { @@ -31,7 +31,6 @@ describe('tab', () => { expect(wrapper.vm._isTab).toBe(true) expect(wrapper.vm.localActive).toBe(false) - expect(wrapper.vm.show).toBe(false) wrapper.destroy() }) @@ -86,7 +85,6 @@ describe('tab', () => { await wrapper.setData({ localActive: true }) await waitNT(wrapper.vm) - await waitRAF() expect(wrapper.classes()).toContain('active') expect(wrapper.classes()).not.toContain('disabled') @@ -94,7 +92,6 @@ describe('tab', () => { await wrapper.setData({ localActive: false }) await waitNT(wrapper.vm) - await waitRAF() expect(wrapper.classes()).not.toContain('active') expect(wrapper.classes()).not.toContain('disabled') diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index d3556b6d4b3..a214b6cf372 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -1,13 +1,13 @@ import { COMPONENT_UID_KEY, Vue } from '../../vue' import { NAME_TABS, NAME_TAB_BUTTON_HELPER } from '../../constants/components' import { + EVENT_NAME_ACTIVATE_TAB, EVENT_NAME_CHANGED, EVENT_NAME_CLICK, EVENT_NAME_FIRST, EVENT_NAME_LAST, EVENT_NAME_NEXT, - EVENT_NAME_PREV, - HOOK_EVENT_NAME_DESTROYED + EVENT_NAME_PREV } from '../../constants/events' import { CODE_DOWN, @@ -30,20 +30,16 @@ import { SLOT_NAME_TABS_START, SLOT_NAME_TITLE } from '../../constants/slots' -import { arrayIncludes } from '../../utils/array' import { BvEvent } from '../../utils/bv-event.class' -import { attemptFocus, requestAF, selectAll } from '../../utils/dom' +import { attemptFocus } from '../../utils/dom' import { stopEvent } from '../../utils/events' -import { identity } from '../../utils/identity' import { isEvent } from '../../utils/inspect' import { looseEqual } from '../../utils/loose-equal' import { mathMax } from '../../utils/math' import { makeModelMixin } from '../../utils/model' import { toInteger } from '../../utils/number' import { omit, sortKeys } from '../../utils/object' -import { observeDom } from '../../utils/observe-dom' import { makeProp, makePropsConfigurable } from '../../utils/props' -import { stableSort } from '../../utils/stable-sort' import { idMixin, props as idProps } from '../../mixins/id' import { normalizeSlotMixin } from '../../mixins/normalize-slot' import { BLink } from '../link/link' @@ -227,8 +223,6 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ currentTab: toInteger(this[MODEL_PROP_NAME], -1), // Array of direct child instances, in DOM order tabs: [], - // Array of child instances registered (for triggering reactive updates) - registeredTabs: [], // Flag to know if we are mounted or not isMounted: false } @@ -247,27 +241,14 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, watch: { - currentTab(newValue) { - let index = -1 - // Ensure only one tab is active at most - this.tabs.forEach((tab, idx) => { - if (newValue === idx && !tab.disabled) { - tab.localActive = true - index = idx - } else { - tab.localActive = false - } - }) - // Update the v-model - this.$emit(MODEL_EVENT_NAME, index) - }, [MODEL_PROP_NAME](newValue, oldValue) { if (newValue !== oldValue) { newValue = toInteger(newValue, -1) oldValue = toInteger(oldValue, 0) - const tabs = this.tabs - if (tabs[newValue] && !tabs[newValue].disabled) { - this.activateTab(tabs[newValue]) + + const $tab = this.tabs[newValue] + if ($tab && !$tab.disabled) { + this.activateTab($tab) } else { // Try next or prev tabs if (newValue < oldValue) { @@ -278,12 +259,21 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } } }, - registeredTabs() { - // Each `` will register/unregister itself - // We use this to detect when tabs are added/removed - // to trigger the update of the tabs - console.log('tabs: registeredTabs') - this.updateTabs() + currentTab(newValue) { + let index = -1 + + // Ensure only one tab is active at most + this.tabs.forEach(($tab, i) => { + if (i === newValue && !$tab.disabled) { + $tab.localActive = true + index = i + } else { + $tab.localActive = false + } + }) + + // Update the v-model + this.$emit(MODEL_EVENT_NAME, index) }, tabs(newValue, oldValue) { // If tabs added, removed, or re-ordered, we emit a `changed` event @@ -302,207 +292,56 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ this.$emit(EVENT_NAME_CHANGED, newValue.slice(), oldValue.slice()) }) } - }, - isMounted(newValue) { - // Trigger an update after mounted - // Needed for tabs inside lazy modals - if (newValue) { - requestAF(() => { - this.updateTabs() - }) - } - // Enable or disable the observer - this.setObserver(newValue) } }, - created() { - // Create private non-reactive props - this.$_observer = null - // For SSR and to make sure only a single tab is shown on mount - // Wrapped in `$nextTick()` to ensure the child tabs have been created - this.$nextTick(() => { - console.log('tabs: created') - this.updateTabs() - }) - }, - mounted() { - // Call `updateTabs()` just in case... - this.updateTabs() - this.$nextTick(() => { - // Flag we are now mounted and to switch to DOM for tab probing - // As `$slots.default` appears to lie about component instances - // after `` is destroyed and re-instantiated - // And `$children` does not respect DOM order - this.isMounted = true - }) - }, - /* istanbul ignore next */ - deactivated() { - this.isMounted = false - }, - /* istanbul ignore next */ - activated() { - this.$nextTick(() => { - this.isMounted = true - }) - }, - beforeDestroy() { - this.isMounted = false - }, destroyed() { // Ensure no references to child instances exist this.tabs = [] - this.registeredTabs = [] }, methods: { - registerTab(tab) { - if (!arrayIncludes(this.registeredTabs, tab)) { - this.registeredTabs.push(tab) - tab.$once(HOOK_EVENT_NAME_DESTROYED, () => { - this.unregisterTab(tab) - }) - } - }, - unregisterTab(tab) { - this.registeredTabs = this.registeredTabs.slice().filter(t => t !== tab) - }, - // DOM observer is needed to detect changes in order of tabs - setObserver(on = true) { - this.$_observer && this.$_observer.disconnect() - this.$_observer = null - - if (on) { - const self = this - /* istanbul ignore next: difficult to test mutation observer in JSDOM */ - const handler = () => { - // We delay the update to ensure that `tab.safeId()` has - // updated with the final ID value - self.$nextTick(() => { - requestAF(() => { - self.updateTabs() - }) - }) - } - // Watch for changes to `` sub components - this.$_observer = observeDom(this.$refs.content, handler, { - childList: true, - subtree: false, - attributes: true, - attributeFilter: ['id'] - }) - } - }, - getTabs() { - // We use `registeredTabs` as the source of truth for child tab components - // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 - // Filter out any `` components that are extended - // `` with a root child `` - const $tabs = this.registeredTabs.filter( - $tab => $tab.$children.filter($t => $t._isTab).length === 0 - ) - - let order = [] - if (this.isMounted && $tabs.length > 0) { - // We rely on the DOM when mounted to get the 'true' order of the `` children - // `querySelectorAll()` always returns elements in document order, regardless of - // order specified in the selector - const selector = $tabs.map(tab => `#${tab.safeId()}`).join(', ') - order = selectAll(selector, this.$el) - .map($el => $el.id) - .filter(identity) - } - - // Stable sort keeps the original order if not found in the `order` array - return stableSort($tabs, (a, b) => order.indexOf(a.safeId()) - order.indexOf(b.safeId())) - }, - // Update list of `` children - updateTabs() { - // Probe tabs - const tabs = this.getTabs() - console.log('tabs: updateTabs', tabs.length, this.registeredTabs.length) - - // Find *last* active non-disabled tab in current tabs - // We trust tab state over `currentTab`, in case tabs were added/removed/re-ordered - let tabIndex = tabs.indexOf( - tabs - .slice() - .reverse() - .find(tab => tab.localActive && !tab.disabled) - ) - - // Else try setting to `currentTab` - if (tabIndex < 0) { - const { currentTab } = this - if (currentTab >= tabs.length) { - // Handle last tab being removed, so find the last non-disabled tab - tabIndex = tabs.indexOf( - tabs - .slice() - .reverse() - .find(notDisabled) - ) - } else if (tabs[currentTab] && !tabs[currentTab].disabled) { - // Current tab is not disabled - tabIndex = currentTab - } - } - - // Else find *first* non-disabled tab in current tabs - if (tabIndex < 0) { - tabIndex = tabs.indexOf(tabs.find(notDisabled)) - } - - // Set the current tab state to active - tabs.forEach(tab => { - // tab.localActive = idx === tabIndex && !tab.disabled - tab.localActive = false - }) - if (tabs[tabIndex]) { - tabs[tabIndex].localActive = true - } - - // Update the array of tab children - this.tabs = tabs - // Set the currentTab index (can be -1 if no non-disabled tabs) - this.currentTab = tabIndex - }, // Find a button that controls a tab, given the tab reference // Returns the button vm instance - getButtonForTab(tab) { - return (this.$refs.buttons || []).find(btn => btn.tab === tab) + getButtonForTab($tab) { + return (this.$refs.buttons || []).find($btn => $btn.tab === $tab) }, // Force a button to re-render its content, given a `` instance // Called by `` on `update()` - updateButton(tab) { - const button = this.getButtonForTab(tab) - if (button && button.$forceUpdate) { - button.$forceUpdate() + updateButton($tab) { + const $button = this.getButtonForTab($tab) + if ($button && $button.$forceUpdate) { + $button.$forceUpdate() } }, // Activate a tab given a `` instance // Also accessed by `` - activateTab(tab) { + activateTab($tab) { + const { currentTab, tabs: $tabs } = this let result = false - if (tab) { - const index = this.tabs.indexOf(tab) - if (!tab.disabled && index > -1 && index !== this.currentTab) { - const tabEvt = new BvEvent('activate-tab', { + + if ($tab) { + const index = $tabs.indexOf($tab) + if (index !== currentTab && index > -1 && !$tab.disabled) { + const tabEvent = new BvEvent(EVENT_NAME_ACTIVATE_TAB, { cancelable: true, vueTarget: this, componentId: this.safeId() }) - this.$emit(tabEvt.type, index, this.currentTab, tabEvt) - if (!tabEvt.defaultPrevented) { - result = true + + this.$emit(tabEvent.type, index, currentTab, tabEvent) + + if (!tabEvent.defaultPrevented) { this.currentTab = index + result = true } } } - // Couldn't set tab, so ensure v-model is set to `currentTab` + + // Couldn't set tab, so ensure v-model is up to date /* istanbul ignore next: should rarely happen */ - if (!result && this.currentTab !== this[MODEL_PROP_NAME]) { - this.$emit(MODEL_EVENT_NAME, this.currentTab) + if (!result && this[MODEL_PROP_NAME] !== currentTab) { + this.$emit(MODEL_EVENT_NAME, currentTab) } + return result }, // Deactivate a tab given a `` instance @@ -590,26 +429,76 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ pills, previousTab, small, - tabs, vertical } = this + const $children = this.normalizeSlot() || [] + + let $empty = h() + if ($children.length === 0) { + $empty = h( + 'div', + { + class: ['tab-pane', 'active', { 'card-body': card }], + key: 'bv-empty-tab' + }, + this.normalizeSlot(SLOT_NAME_EMPTY) + ) + } + + const $content = h( + 'div', + { + staticClass: 'tab-content', + class: [{ col: vertical }, this.contentClass], + attrs: { id: this.safeId('_BV_tab_container_') }, + key: 'bv-content', + ref: 'content' + }, + [$children || $empty] + ) + + const $tabs = ($content.children || []) + .map(vNode => vNode.componentInstance) + .filter($tab => $tab && $tab._isTab && $tab.$children.filter($t => $t._isTab).length === 0) + + let { currentTab } = this + if (currentTab === -1) { + currentTab = $tabs.indexOf( + $tabs + .slice() + .reverse() + .find($tab => $tab.localActive && !$tab.disabled) + ) + + if (currentTab === -1) { + currentTab = $tabs.indexOf($tabs.find(notDisabled)) + } + } + + this.tabs = $tabs + this.currentTab = currentTab + + $tabs.forEach(($tab, index) => { + $tab.localActive = index === currentTab + }) + // Currently active tab - const activeTab = tabs.find(tab => tab.localActive && !tab.disabled) + const $activeTab = $tabs.find($tab => $tab.localActive && !$tab.disabled) // Tab button to allow focusing when no active tab found (keynav only) - const fallbackTab = tabs.find(tab => !tab.disabled) - - console.log('tag: render', tabs.length) + const $fallbackTab = $tabs.find($tab => !$tab.disabled) // For each `` found create the tab buttons - const $buttons = tabs.map((tab, index) => { + const $buttons = $tabs.map(($tab, index) => { + const { safeId } = $tab + // Ensure at least one tab button is focusable when keynav enabled (if possible) let tabIndex = null if (!noKeyNav) { // Buttons are not in tab index unless active, or a fallback tab tabIndex = -1 - if (activeTab === tab || (!activeTab && fallbackTab === tab)) { + if ($tab === $activeTab || (!$activeTab && $tab === $fallbackTab)) { // Place tab button in tab sequence tabIndex = null } @@ -617,24 +506,24 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ return h(BVTabButton, { props: { - controls: tab.safeId ? tab.safeId() : null, - id: tab.controlledBy || (tab.safeId ? tab.safeId(`_BV_tab_button_`) : null), + controls: safeId ? safeId() : null, + id: $tab.controlledBy || (safeId ? safeId(`_BV_tab_button_`) : null), noKeyNav, posInSet: index + 1, - setSize: tabs.length, - tab, + setSize: $tabs.length, + tab: $tab, tabIndex }, on: { [EVENT_NAME_CLICK]: event => { - this.clickTab(tab, event) + this.clickTab($tab, event) }, [EVENT_NAME_FIRST]: firstTab, [EVENT_NAME_PREV]: previousTab, [EVENT_NAME_NEXT]: nextTab, [EVENT_NAME_LAST]: lastTab }, - key: tab[COMPONENT_UID_KEY] || index, + key: $tab[COMPONENT_UID_KEY] || index, ref: 'buttons', // Needed to make `this.$refs.buttons` an array refInFor: true @@ -684,31 +573,6 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ [$nav] ) - let $empty = h() - if (!tabs || tabs.length === 0) { - $empty = h( - 'div', - { - class: ['tab-pane', 'active', { 'card-body': card }], - key: 'bv-empty-tab' - }, - this.normalizeSlot(SLOT_NAME_EMPTY) - ) - } - - // Main content section - const $content = h( - 'div', - { - staticClass: 'tab-content', - class: [{ col: vertical }, this.contentClass], - attrs: { id: this.safeId('_BV_tab_container_') }, - key: 'bv-content', - ref: 'content' - }, - [this.normalizeSlot() || $empty] - ) - // Render final output return h( this.tag, diff --git a/src/components/tabs/tabs.spec.js b/src/components/tabs/tabs.spec.js index 4cb4777e5d2..7e4983e264a 100644 --- a/src/components/tabs/tabs.spec.js +++ b/src/components/tabs/tabs.spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils' -import { waitNT, waitRAF } from '../../../tests/utils' +import { waitNT } from '../../../tests/utils' import { BLink } from '../link/link' import { BTab } from './tab' import { BTabs } from './tabs' @@ -11,7 +11,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() expect(wrapper.element.tagName).toBe('DIV') expect(wrapper.classes()).toContain('tabs') @@ -40,7 +39,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() expect(wrapper.element.tagName).toBe('DIV') expect(wrapper.classes()).toContain('tabs') @@ -62,7 +60,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() expect(wrapper.element.tagName).toBe('DIV') expect(wrapper.classes()).toContain('tabs') @@ -91,7 +88,6 @@ describe('tabs', () => { }) await waitNT(wrapper.vm) - await waitRAF() expect(wrapper.vm.currentTab).toBe(tabIndex) expect(wrapper.vm.tabs.length).toBe(3) @@ -114,7 +110,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() @@ -146,7 +141,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() @@ -178,7 +172,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -225,7 +218,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -238,7 +230,6 @@ describe('tabs', () => { // Set 2nd BTab to be active await tabs.setProps({ value: 1 }) - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tabs.emitted('input').length).toBe(1) // Should emit index of 1 (2nd tab) @@ -246,7 +237,6 @@ describe('tabs', () => { // Set 3rd BTab to be active await tabs.setProps({ value: 2 }) - await waitRAF() expect(tabs.vm.currentTab).toBe(2) expect(tabs.emitted('input').length).toBe(2) // Should emit index of 2 (3rd tab) @@ -269,7 +259,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -281,7 +270,6 @@ describe('tabs', () => { // Try to set 2nd (disabled) BTab to be active await tabs.setProps({ value: 1 }) - await waitRAF() // Will try activate next non-disabled tab instead (3rd tab, index 2) expect(tabs.vm.currentTab).toBe(2) expect(tabs.emitted('input').length).toBe(1) @@ -290,10 +278,8 @@ describe('tabs', () => { // Needed for test since value not bound to actual v-model on App await tabs.setProps({ value: 2 }) - await waitRAF() // Try and set 2nd BTab to be active await tabs.setProps({ value: 1 }) - await waitRAF() // Will find the previous non-disabled tab (1st tab, index 0) expect(tabs.vm.currentTab).toBe(0) expect(tabs.emitted('input').length).toBe(2) @@ -325,7 +311,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -338,7 +323,6 @@ describe('tabs', () => { // Set 2nd BTab to be active await tabs.setProps({ value: 1 }) - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tabs.emitted('input')).toBeDefined() expect(tabs.emitted('input').length).toBe(1) @@ -352,7 +336,6 @@ describe('tabs', () => { // Attempt to set 3rd BTab to be active await tabs.setProps({ value: 2 }) - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tabs.emitted('input')).toBeDefined() expect(tabs.emitted('input').length).toBe(2) @@ -381,7 +364,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -405,7 +387,6 @@ describe('tabs', () => { .findAll('.nav-link') .at(1) .trigger('click') - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(true) @@ -418,7 +399,6 @@ describe('tabs', () => { .findAll('.nav-link') .at(2) .trigger('click') - await waitRAF() expect(tabs.vm.currentTab).toBe(2) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(false) @@ -431,7 +411,6 @@ describe('tabs', () => { .findAll('.nav-link') .at(0) .trigger('keydown.space') - await waitRAF() expect(tabs.vm.currentTab).toBe(0) expect(tab1.vm.localActive).toBe(true) expect(tab2.vm.localActive).toBe(false) @@ -455,7 +434,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -479,7 +457,6 @@ describe('tabs', () => { .findAll('.nav-link') .at(1) .trigger('keydown.space') - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(true) @@ -492,7 +469,6 @@ describe('tabs', () => { .findAll('.nav-link') .at(2) .trigger('keydown.space') - await waitRAF() expect(tabs.vm.currentTab).toBe(2) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(false) @@ -505,7 +481,6 @@ describe('tabs', () => { .findAll('.nav-link') .at(0) .trigger('keydown.space') - await waitRAF() expect(tabs.vm.currentTab).toBe(0) expect(tab1.vm.localActive).toBe(true) expect(tab2.vm.localActive).toBe(false) @@ -529,7 +504,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -552,7 +526,6 @@ describe('tabs', () => { .findAllComponents(BLink) .at(0) .trigger('keydown.right') - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(true) @@ -563,7 +536,6 @@ describe('tabs', () => { .findAllComponents(BLink) .at(1) .trigger('keydown.end') - await waitRAF() expect(tabs.vm.currentTab).toBe(2) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(false) @@ -574,7 +546,6 @@ describe('tabs', () => { .findAllComponents(BLink) .at(2) .trigger('keydown.left') - await waitRAF() expect(tabs.vm.currentTab).toBe(1) expect(tab1.vm.localActive).toBe(false) expect(tab2.vm.localActive).toBe(true) @@ -585,7 +556,6 @@ describe('tabs', () => { .findAllComponents(BLink) .at(1) .trigger('keydown.home') - await waitRAF() expect(tabs.vm.currentTab).toBe(0) expect(tab1.vm.localActive).toBe(true) expect(tab2.vm.localActive).toBe(false) @@ -608,7 +578,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -625,7 +594,6 @@ describe('tabs', () => { // Disable 3rd tab await tab3.setProps({ disabled: true }) - await waitRAF() // Expect 1st tab to be active expect(tabs.vm.currentTab).toBe(0) @@ -636,7 +604,6 @@ describe('tabs', () => { // Enable 3rd tab and Disable 1st tab await tab3.setProps({ disabled: false }) await tab1.setProps({ disabled: true }) - await waitRAF() // Expect 2nd tab to be active expect(tabs.vm.currentTab).toBe(1) @@ -659,7 +626,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(1) @@ -675,7 +641,6 @@ describe('tabs', () => { tabVm.$slots.title = [tabVm.$createElement('span', 'foobar')] tabVm.$forceUpdate() await waitNT(wrapper.vm) - await waitRAF() // Expect tab button content to be `foobar` expect(wrapper.find('.nav-link').text()).toBe('foobar') @@ -698,7 +663,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -714,7 +678,6 @@ describe('tabs', () => { // Set 2nd tab to be active tabs.setProps({ value: 1 }) await waitNT(wrapper.vm) - await waitRAF() expect(tabs.vm.currentTab).toBe(1) // Expect 2nd tabs nav item to have "active-nav-item-class" applied expect(getNavItemByTab(tabs.vm.tabs[1]).classes(activeNavItemClass)).toBe(true) @@ -739,7 +702,6 @@ describe('tabs', () => { expect(wrapper).toBeDefined() await waitNT(wrapper.vm) - await waitRAF() const tabs = wrapper.findComponent(BTabs) expect(tabs).toBeDefined() expect(tabs.findAllComponents(BTab).length).toBe(3) @@ -752,7 +714,6 @@ describe('tabs', () => { // Set 2nd tab to be active await tabs.setProps({ value: 1 }) - await waitRAF() expect(tabs.vm.currentTab).toBe(1) // Expect 2nd tab to have "active-tab-class" applied expect(tabs.vm.tabs[1].$el.classList.contains(activeTabClass)).toBe(true) diff --git a/src/constants/events.js b/src/constants/events.js index ebfdaf352e0..1cb034142dc 100644 --- a/src/constants/events.js +++ b/src/constants/events.js @@ -1,3 +1,4 @@ +export const EVENT_NAME_ACTIVATE_TAB = 'activate-tab' export const EVENT_NAME_BLUR = 'blur' export const EVENT_NAME_CANCEL = 'cancel' export const EVENT_NAME_CHANGE = 'change' From ed1a45b394be638c2c8a355805bc4c6e19dd6bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 16:13:39 +0100 Subject: [PATCH 13/17] Update tabs.js --- src/components/tabs/tabs.js | 146 +++++++++++++++++++++++------------- 1 file changed, 94 insertions(+), 52 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index a214b6cf372..44271d6d09e 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -39,6 +39,7 @@ import { mathMax } from '../../utils/math' import { makeModelMixin } from '../../utils/model' import { toInteger } from '../../utils/number' import { omit, sortKeys } from '../../utils/object' +import { observeDom } from '../../utils/observe-dom' import { makeProp, makePropsConfigurable } from '../../utils/props' import { idMixin, props as idProps } from '../../mixins/id' import { normalizeSlotMixin } from '../../mixins/normalize-slot' @@ -294,11 +295,76 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } } }, - destroyed() { + created() { + // Create private non-reactive props + this.$_observer = null + }, + mounted() { + this.$nextTick(() => { + this.updateTabs() + this.setObserver(true) + }) + }, + beforeDestroy() { + this.setObserver(false) // Ensure no references to child instances exist this.tabs = [] }, methods: { + // DOM observer is needed to detect changes in order of tabs + setObserver(on = true) { + this.$_observer && this.$_observer.disconnect() + this.$_observer = null + + if (on) { + /* istanbul ignore next: difficult to test mutation observer in JSDOM */ + const handler = () => { + this.$nextTick(() => { + this.updateTabs() + }) + } + + // Watch for changes to `` sub components + this.$_observer = observeDom(this.$refs.content, handler, { + childList: true, + subtree: false, + attributes: true, + attributeFilter: ['id'] + }) + } + }, + getTabs() { + return this.$children.filter( + $tab => $tab && $tab._isTab && $tab.$children.filter($t => $t._isTab).length === 0 + ) + }, + updateTabs() { + const $tabs = this.getTabs() + + // Normalize `currentTab` + let { currentTab } = this + const $tab = $tabs[currentTab] + if (!$tab || $tab.disabled) { + currentTab = $tabs.indexOf( + $tabs + .slice() + .reverse() + .find($tab => $tab.localActive && !$tab.disabled) + ) + + if (currentTab === -1) { + currentTab = $tabs.indexOf($tabs.find(notDisabled)) + } + } + + // Ensure only one tab is active at a time + $tabs.forEach(($tab, index) => { + $tab.localActive = index === currentTab + }) + + this.tabs = $tabs + this.currentTab = currentTab + }, // Find a button that controls a tab, given the tab reference // Returns the button vm instance getButtonForTab($tab) { @@ -346,20 +412,20 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ }, // Deactivate a tab given a `` instance // Accessed by `` - deactivateTab(tab) { - if (tab) { + deactivateTab($tab) { + if ($tab) { // 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)) + return this.activateTab(this.tabs.filter($t => $t !== $tab).find(notDisabled)) } /* istanbul ignore next: should never/rarely happen */ return false }, // Focus a tab button given its `` instance - focusButton(tab) { - // Wrap in `$nextTick()` to ensure DOM has completed rendering/updating before focusing + focusButton($tab) { + // Wrap in `$nextTick()` to ensure DOM has completed rendering this.$nextTick(() => { - attemptFocus(this.getButtonForTab(tab)) + attemptFocus(this.getButtonForTab($tab)) }) }, // Emit a click event on a specified `` component instance @@ -369,48 +435,48 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, // Click handler - clickTab(tab, event) { - this.activateTab(tab) - this.emitTabClick(tab, event) + clickTab($tab, event) { + this.activateTab($tab) + this.emitTabClick($tab, event) }, // Move to first non-disabled tab firstTab(focus) { - const tab = this.tabs.find(notDisabled) - if (this.activateTab(tab) && focus) { - this.focusButton(tab) - this.emitTabClick(tab, focus) + const $tab = this.tabs.find(notDisabled) + if (this.activateTab($tab) && focus) { + this.focusButton($tab) + this.emitTabClick($tab, focus) } }, // Move to previous non-disabled tab previousTab(focus) { const currentIndex = mathMax(this.currentTab, 0) - const tab = this.tabs + const $tab = this.tabs .slice(0, currentIndex) .reverse() .find(notDisabled) - if (this.activateTab(tab) && focus) { - this.focusButton(tab) - this.emitTabClick(tab, focus) + if (this.activateTab($tab) && focus) { + this.focusButton($tab) + this.emitTabClick($tab, focus) } }, // Move to next non-disabled tab nextTab(focus) { const currentIndex = mathMax(this.currentTab, -1) - const tab = this.tabs.slice(currentIndex + 1).find(notDisabled) - if (this.activateTab(tab) && focus) { - this.focusButton(tab) - this.emitTabClick(tab, focus) + const $tab = this.tabs.slice(currentIndex + 1).find(notDisabled) + if (this.activateTab($tab) && focus) { + this.focusButton($tab) + this.emitTabClick($tab, focus) } }, // Move to last non-disabled tab lastTab(focus) { - const tab = this.tabs + const $tab = this.tabs .slice() .reverse() .find(notDisabled) - if (this.activateTab(tab) && focus) { - this.focusButton(tab) - this.emitTabClick(tab, focus) + if (this.activateTab($tab) && focus) { + this.focusButton($tab) + this.emitTabClick($tab, focus) } } }, @@ -429,6 +495,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ pills, previousTab, small, + tabs: $tabs, vertical } = this @@ -455,34 +522,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ key: 'bv-content', ref: 'content' }, - [$children || $empty] + [$children, $empty] ) - const $tabs = ($content.children || []) - .map(vNode => vNode.componentInstance) - .filter($tab => $tab && $tab._isTab && $tab.$children.filter($t => $t._isTab).length === 0) - - let { currentTab } = this - if (currentTab === -1) { - currentTab = $tabs.indexOf( - $tabs - .slice() - .reverse() - .find($tab => $tab.localActive && !$tab.disabled) - ) - - if (currentTab === -1) { - currentTab = $tabs.indexOf($tabs.find(notDisabled)) - } - } - - this.tabs = $tabs - this.currentTab = currentTab - - $tabs.forEach(($tab, index) => { - $tab.localActive = index === currentTab - }) - // Currently active tab const $activeTab = $tabs.find($tab => $tab.localActive && !$tab.disabled) From 8aa41bdf4723ce4caa776b5ab17a0bc5b8d289da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 16:19:13 +0100 Subject: [PATCH 14/17] Update tabs.js --- src/components/tabs/tabs.js | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 44271d6d09e..1715793d895 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -499,32 +499,6 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ vertical } = this - const $children = this.normalizeSlot() || [] - - let $empty = h() - if ($children.length === 0) { - $empty = h( - 'div', - { - class: ['tab-pane', 'active', { 'card-body': card }], - key: 'bv-empty-tab' - }, - this.normalizeSlot(SLOT_NAME_EMPTY) - ) - } - - const $content = h( - 'div', - { - staticClass: 'tab-content', - class: [{ col: vertical }, this.contentClass], - attrs: { id: this.safeId('_BV_tab_container_') }, - key: 'bv-content', - ref: 'content' - }, - [$children, $empty] - ) - // Currently active tab const $activeTab = $tabs.find($tab => $tab.localActive && !$tab.disabled) @@ -615,6 +589,32 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ [$nav] ) + const $children = this.normalizeSlot() || [] + + let $empty = h() + if ($children.length === 0) { + $empty = h( + 'div', + { + class: ['tab-pane', 'active', { 'card-body': card }], + key: 'bv-empty-tab' + }, + this.normalizeSlot(SLOT_NAME_EMPTY) + ) + } + + const $content = h( + 'div', + { + staticClass: 'tab-content', + class: [{ col: vertical }, this.contentClass], + attrs: { id: this.safeId('_BV_tab_container_') }, + key: 'bv-content', + ref: 'content' + }, + [$children, $empty] + ) + // Render final output return h( this.tag, From bff2a7a2cf1f6d796029675fe7011384ab3349d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 16:34:46 +0100 Subject: [PATCH 15/17] Update tab.js --- src/components/tabs/tab.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 950ef91c652..1a527432004 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -115,6 +115,10 @@ export const BTab = /*#__PURE__*/ Vue.extend({ this.$emit(MODEL_EVENT_NAME_ACTIVE, newValue) } }, + mounted() { + // Inform `` of our presence + this.registerTab() + }, updated() { // Force the tab button content to update (since slots are not reactive) // Only done if we have a title slot, as the title prop is reactive @@ -123,7 +127,26 @@ export const BTab = /*#__PURE__*/ Vue.extend({ updateButton(this) } }, + beforeDestroy() { + // Inform `` of our departure + this.unregisterTab() + }, methods: { + // Private methods + registerTab() { + // Inform `` of our presence + const { registerTab } = this.bvTabs + if (registerTab) { + registerTab(this) + } + }, + unregisterTab() { + // Inform `` of our departure + const { unregisterTab } = this.bvTabs + if (unregisterTab) { + unregisterTab(this) + } + }, // Public methods activate() { // Not inside a `` component or tab is disabled From c329d38fb9175b561a585a227c3c5e5a5ff115ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Tue, 8 Dec 2020 16:34:52 +0100 Subject: [PATCH 16/17] Update tabs.js --- src/components/tabs/tabs.js | 46 ++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 1715793d895..211b26c0549 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -1,5 +1,6 @@ import { COMPONENT_UID_KEY, Vue } from '../../vue' import { NAME_TABS, NAME_TAB_BUTTON_HELPER } from '../../constants/components' +import { IS_BROWSER } from '../../constants/env' import { EVENT_NAME_ACTIVATE_TAB, EVENT_NAME_CHANGED, @@ -30,9 +31,11 @@ import { SLOT_NAME_TABS_START, SLOT_NAME_TITLE } from '../../constants/slots' +import { arrayIncludes } from '../../utils/array' import { BvEvent } from '../../utils/bv-event.class' -import { attemptFocus } from '../../utils/dom' +import { attemptFocus, selectAll } from '../../utils/dom' import { stopEvent } from '../../utils/events' +import { identity } from '../../utils/identity' import { isEvent } from '../../utils/inspect' import { looseEqual } from '../../utils/loose-equal' import { mathMax } from '../../utils/math' @@ -41,6 +44,7 @@ import { toInteger } from '../../utils/number' import { omit, sortKeys } from '../../utils/object' import { observeDom } from '../../utils/observe-dom' import { makeProp, makePropsConfigurable } from '../../utils/props' +import { stableSort } from '../../utils/stable-sort' import { idMixin, props as idProps } from '../../mixins/id' import { normalizeSlotMixin } from '../../mixins/normalize-slot' import { BLink } from '../link/link' @@ -222,8 +226,10 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ return { // Index of current tab currentTab: toInteger(this[MODEL_PROP_NAME], -1), - // Array of direct child instances, in DOM order + // Array of direct child `` instances, in DOM order tabs: [], + // Array of child instances registered (for triggering reactive updates) + registeredTabs: [], // Flag to know if we are mounted or not isMounted: false } @@ -293,6 +299,9 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ this.$emit(EVENT_NAME_CHANGED, newValue.slice(), oldValue.slice()) }) } + }, + registeredTabs() { + this.updateTabs() } }, created() { @@ -300,10 +309,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ this.$_observer = null }, mounted() { - this.$nextTick(() => { - this.updateTabs() - this.setObserver(true) - }) + this.setObserver(true) }, beforeDestroy() { this.setObserver(false) @@ -311,6 +317,14 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ this.tabs = [] }, methods: { + registerTab($tab) { + if (!arrayIncludes(this.registeredTabs, $tab)) { + this.registeredTabs.push($tab) + } + }, + unregisterTab($tab) { + this.registeredTabs = this.registeredTabs.slice().filter($t => $t !== $tab) + }, // DOM observer is needed to detect changes in order of tabs setObserver(on = true) { this.$_observer && this.$_observer.disconnect() @@ -334,9 +348,25 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ } }, getTabs() { - return this.$children.filter( - $tab => $tab && $tab._isTab && $tab.$children.filter($t => $t._isTab).length === 0 + const $tabs = this.registeredTabs.filter( + $tab => $tab.$children.filter($t => $t._isTab).length === 0 ) + + // DOM Order of Tabs + let order = [] + if (IS_BROWSER && $tabs.length > 0) { + // We rely on the DOM when mounted to get the 'true' order of the `` children + // `querySelectorAll()` always returns elements in document order, regardless of + // order specified in the selector + const selector = $tabs.map($tab => `#${$tab.safeId()}`).join(', ') + order = selectAll(selector, this.$el) + .map($el => $el.id) + .filter(identity) + } + + // Stable sort keeps the original order if not found in the `order` array, + // which will be an empty array before mount + return stableSort($tabs, (a, b) => order.indexOf(a.safeId()) - order.indexOf(b.safeId())) }, updateTabs() { const $tabs = this.getTabs() From a0c0d0f29d5894834f1e4281831176661d556c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Wed, 9 Dec 2020 11:35:21 +0100 Subject: [PATCH 17/17] Update tabs.js --- src/components/tabs/tabs.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 211b26c0549..cc7faafdc34 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -229,9 +229,7 @@ export const BTabs = /*#__PURE__*/ Vue.extend({ // Array of direct child `` instances, in DOM order tabs: [], // Array of child instances registered (for triggering reactive updates) - registeredTabs: [], - // Flag to know if we are mounted or not - isMounted: false + registeredTabs: [] } }, computed: {