From cd462b2a5df92ab365986ff5881b7e102023dd92 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 13:06:12 -0400 Subject: [PATCH 01/83] tab: inject parent tabs reference --- src/components/tabs/tab.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index f5db004cd05..b7333c9905c 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -4,6 +4,9 @@ import idMixin from '../../mixins/id' export default { name: 'BTab', mixins: [idMixin], + inject: { + tabs: { default: null } + }, props: { active: { type: Boolean, From bfb0f3945a7ee81c66aee335609270dac0d960cd Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 13:09:19 -0400 Subject: [PATCH 02/83] tabs: provide reference to self to tab children --- src/components/tabs/tabs.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 703b9d7f744..c4c14338065 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -76,6 +76,11 @@ const BTabButtonHelper = { export default { name: 'BTabs', mixins: [idMixin], + provide () { + return { + tabs: this + } + }, props: { tag: { type: String, From 710148c29fc21a424191080d8edd5185a2d1d096 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 13:20:23 -0400 Subject: [PATCH 03/83] Update tab.js --- src/components/tabs/tab.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index b7333c9905c..7e59c04932d 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -61,8 +61,7 @@ export default { computed: { tabClasses () { return [ - 'tab-pane', - this.$parent && this.$parent.card && !this.noBody ? 'card-body' : '', + this.tabs && this.tabs.card && !this.noBody ? 'card-body' : '', this.show ? 'show' : '', this.computedFade ? 'fade' : '', this.disabled ? 'disabled' : '', @@ -73,10 +72,10 @@ export default { return this.buttonId || this.safeId('__BV_tab_button__') }, computedFade () { - return this.$parent.fade + return this.tabs ? this.tabs.fade : false }, computedLazy () { - return this.$parent.lazy + return this.tabs ? this.tabs.lazy : false }, _isTab () { // For parent sniffing of child @@ -103,6 +102,7 @@ export default { this.tag, { ref: 'panel', + staticClass: 'tab-pane', class: this.tabClasses, directives: [{ name: 'show', value: this.localActive }], attrs: { From d2e5433aaa844740338d8aa36ff5399b1ffb2cc4 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 13:51:01 -0400 Subject: [PATCH 04/83] Update tab.js --- src/components/tabs/tab.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 7e59c04932d..6625269a5a8 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -50,6 +50,10 @@ export default { href: { type: String, default: '#' + }, + order: { + type: [Number, String], + default: 0 } }, data () { @@ -77,6 +81,9 @@ export default { computedLazy () { return this.tabs ? this.tabs.lazy : false }, + computedOrder () { + return parseInt(this.order, 10) || 0 + }, _isTab () { // For parent sniffing of child return true @@ -87,11 +94,19 @@ export default { }, methods: { beforeEnter () { - // change opacity 1 frame after display + // change opacity (add 'show' class) 1 frame after display // otherwise css transition won't happen - window.requestAnimationFrame(() => { this.show = true }) + const raf = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + window.oRequestAnimationFrame || + this.$nextTick + + raf(() => { this.show = true }) }, beforeLeave () { + // remove the 'show' class this.show = false } }, From 5fb36226b816e0d9b831de3128f817c7a584a697 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 14:07:58 -0400 Subject: [PATCH 05/83] Update tabs.js --- src/components/tabs/tabs.js | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index c4c14338065..83cf51e72f9 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -1,5 +1,6 @@ import KeyCodes from '../../utils/key-codes' import observeDom from '../../utils/observe-dom' +import stableSort from '../../utils/stable-sort' import idMixin from '../../mixins/id' // Private Helper component @@ -189,15 +190,11 @@ export default { }) }, methods: { - /** - * Util: Return the sign of a number (as -1, 0, or 1) - */ + // Return the sign of a number (as -1, 0, or 1) sign (x) { return x === 0 ? 0 : x > 0 ? 1 : -1 }, - /* - * handle keyboard navigation - */ + // handle keyboard navigation onKeynav (evt) { if (this.noKeyNav) { return @@ -224,24 +221,18 @@ export default { } } }, - /** - * Move to next tab - */ + // Move to next tab nextTab () { this.setTab(this.currentTab + 1, false, 1) }, - /** - * Move to previous tab - */ + // Move to previous tab previousTab () { this.setTab(this.currentTab - 1, false, -1) }, - /** - * Set active tab on the tabs collection and the child 'tab' component - * Index is the tab we want to activate. Direction is the direction we are moving - * so if the tab we requested is disabled, we can skip over it. - * Force is used by updateTabs to ensure we have cleared any previous active tabs. - */ + // Set active tab on the tabs collection and the child 'tab' component + // Index is the tab we want to activate. Direction is the direction we are moving + // so if the tab we requested is disabled, we can skip over it. + // Force is used by updateTabs to ensure we have cleared any previous active tabs. setTab (index, force, direction) { direction = this.sign(direction || 0) index = index || 0 @@ -277,12 +268,12 @@ export default { // Update currentTab this.currentTab = index }, - /** - * Dynamically update tabs list - */ + // Dynamically update tabs list updateTabs () { // Probe tabs - this.tabs = this.$children.filter(child => child._isTab) + this.tabs = stableSort(this.$children.filter(child => child._isTab), (a, b) => { + a.computedOrder - b.computedOrder + }) // Set initial active tab let tabIndex = null // Find *last* active non-dsabled tab in current tabs From 1fd2c2790f4f87f48031018e9ea33a88cc3ed794 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 14:09:18 -0400 Subject: [PATCH 06/83] Update tab.js --- src/components/tabs/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 6625269a5a8..94a471749d2 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -101,7 +101,7 @@ export default { window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || - this.$nextTick + (cb) => { setTimeout(cb, 16) } raf(() => { this.show = true }) }, From 09be34076e9868d87e27026e013077b8a0940464 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 14:16:25 -0400 Subject: [PATCH 07/83] Update tab.js --- src/components/tabs/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 94a471749d2..95987f34dc4 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -101,7 +101,7 @@ export default { window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || - (cb) => { setTimeout(cb, 16) } + function (cb) { setTimeout(cb, 16) } raf(() => { this.show = true }) }, From 18bb64c3148ffc0a72f391ba72e01af7556c2670 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 14:19:01 -0400 Subject: [PATCH 08/83] lint --- 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 83cf51e72f9..ac9f6d362c5 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -272,7 +272,7 @@ export default { updateTabs () { // Probe tabs this.tabs = stableSort(this.$children.filter(child => child._isTab), (a, b) => { - a.computedOrder - b.computedOrder + return a.computedOrder - b.computedOrder }) // Set initial active tab let tabIndex = null From b4495687799fc2d5030a49fa9741fc99c0c1c98a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 11 Jan 2019 21:59:58 -0400 Subject: [PATCH 09/83] Update tab.js --- src/components/tabs/tab.js | 62 ++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 95987f34dc4..50204c05155 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -5,7 +5,11 @@ export default { name: 'BTab', mixins: [idMixin], inject: { - tabs: { default: null } + bTabs: { + default: function () { + return {} + } + } }, props: { active: { @@ -25,12 +29,12 @@ export default { default: '' }, titleItemClass: { - // Sniffed by tabs.vue and added to nav 'li.nav-item' + // Sniffed by tabs.js and added to nav 'li.nav-item' type: [String, Array, Object], default: null }, titleLinkClass: { - // Sniffed by tabs.vue and added to nav 'a.nav-link' + // Sniffed by tabs.js and added to nav 'a.nav-link' type: [String, Array, Object], default: null }, @@ -48,9 +52,16 @@ export default { default: false }, href: { + // This should be deprecated, as tabs are not navigation (URL) based + // + + / should be used instead + // And we dont support router-links here type: String, default: '#' }, + lazy: { + type: Boolean, + default: false + }, order: { type: [Number, String], default: 0 @@ -65,7 +76,7 @@ export default { computed: { tabClasses () { return [ - this.tabs && this.tabs.card && !this.noBody ? 'card-body' : '', + this.bTabs.card && !this.noBody ? 'card-body' : '', this.show ? 'show' : '', this.computedFade ? 'fade' : '', this.disabled ? 'disabled' : '', @@ -76,10 +87,10 @@ export default { return this.buttonId || this.safeId('__BV_tab_button__') }, computedFade () { - return this.tabs ? this.tabs.fade : false + return this.bTabs.fade || false }, computedLazy () { - return this.tabs ? this.tabs.lazy : false + return this.bTabs.lazy || this.lazy }, computedOrder () { return parseInt(this.order, 10) || 0 @@ -96,6 +107,7 @@ export default { beforeEnter () { // change opacity (add 'show' class) 1 frame after display // otherwise css transition won't happen + // TODO: Move raf method into utils/dom.js const raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || @@ -111,26 +123,24 @@ export default { } }, render (h) { - let content = h(false) - if (this.localActive || !this.computedLazy) { - content = h( - this.tag, - { - ref: 'panel', - staticClass: 'tab-pane', - class: this.tabClasses, - directives: [{ name: 'show', value: this.localActive }], - attrs: { - role: 'tabpanel', - id: this.safeId(), - 'aria-hidden': this.localActive ? 'false' : 'true', - 'aria-expanded': this.localActive ? 'true' : 'false', - 'aria-labelledby': this.controlledBy || null - } - }, - [this.$slots.default] - ) - } + let content = h( + this.tag, + { + ref: 'panel', + staticClass: 'tab-pane', + class: this.tabClasses, + directives: [{ name: 'show', value: this.localActive }], + attrs: { + role: 'tabpanel', + id: this.safeId(), + 'aria-hidden': this.localActive ? 'false' : 'true', + 'aria-expanded': this.localActive ? 'true' : 'false', + 'aria-labelledby': this.controlledBy || null + } + }, + // Render content lazily if requested + [(this.localActive || !this.computedLazy) ? this.$slots.default : h(false)] + ) return h( 'transition', { From d5b81406664bfe7ec34344603d915fe7820e65f0 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 00:06:02 -0400 Subject: [PATCH 10/83] Update tab.js --- src/components/tabs/tab.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 50204c05155..b77d2be2c53 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -7,7 +7,10 @@ export default { inject: { bTabs: { default: function () { - return {} + return { + // Dont set a tab index if not rendered inside b-tabs + noKeyNav: true + } } } }, @@ -101,8 +104,15 @@ export default { } }, mounted () { + // Initially show on mount if active and not disabled this.show = this.localActive }, + updated () { + // Force the tab buton content to update (since slots are not reactive) + // if (this.bTabs.updateButton) { + // this.bTabs.updateButton(this) + // } + }, methods: { beforeEnter () { // change opacity (add 'show' class) 1 frame after display @@ -133,6 +143,7 @@ export default { attrs: { role: 'tabpanel', id: this.safeId(), + tabindex: (this.localActive && !this.bTabs.noKeyNav) ? '0' : null, 'aria-hidden': this.localActive ? 'false' : 'true', 'aria-expanded': this.localActive ? 'true' : 'false', 'aria-labelledby': this.controlledBy || null From 3c38694d9f4f02fd2b729a164c0c2944dc07c051 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 00:35:23 -0400 Subject: [PATCH 11/83] Update tabs.js --- src/components/tabs/tabs.js | 355 ++++++++++++++++++++---------------- 1 file changed, 193 insertions(+), 162 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index ac9f6d362c5..161f886af1e 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -1,6 +1,6 @@ +import BLink from '../link/link' import KeyCodes from '../../utils/key-codes' import observeDom from '../../utils/observe-dom' -import stableSort from '../../utils/stable-sort' import idMixin from '../../mixins/id' // Private Helper component @@ -8,7 +8,7 @@ const BTabButtonHelper = { name: 'BTabButtonHelper', props: { content: { type: [String, Array], default: '' }, - href: { type: String, default: '#' }, + href: { type: String, default: '#' }, // To be deprecated posInSet: { type: Number, default: null }, setSize: { type: Number, default: null }, controls: { type: String, default: null }, @@ -20,28 +20,37 @@ const BTabButtonHelper = { noKeyNav: { type: Boolean, default: false } }, render (h) { - const link = h('a', { - staticClass: 'nav-link', - class: [ - { active: this.active, disabled: this.disabled }, - this.linkClass - ], - attrs: { - role: 'tab', - tabindex: this.noKeyNav ? null : '-1', - href: this.href, - id: this.id, - disabled: this.disabled, - 'aria-selected': this.active ? 'true' : 'false', - 'aria-setsize': this.setSize, - 'aria-posinset': this.posInSet, - 'aria-controls': this.controls + const link = h( + BLink, + { + ref: 'link', + staticClass: 'nav-link', + class: [ + { active: this.active, disabled: this.disabled }, + this.linkClass + ], + props: { + href: this.href, // To be deprecated to always be '#' + disabled: this.disabled, + active: this.active + }, + attrs: { + role: 'tab', + id: this.id, + // Roving tab index when keynav enabled + tabindex: this.noKeyNav ? null : (this.active ? '0' : '-1'), + 'aria-selected': this.active ? 'true' : 'false', + 'aria-setsize': this.setSize, + 'aria-posinset': this.posInSet, + 'aria-controls': this.controls + }, + on: { + click: this.handleClick, + keydown: this.handleClick + } }, - on: { - click: this.handleClick, - keydown: this.handleClick - } - }, this.content) + this.$slots.default || this.content + ) return h( 'li', { class: ['nav-item', this.itemClass], attrs: { role: 'presentation' } }, @@ -49,25 +58,25 @@ const BTabButtonHelper = { ) }, methods: { + focus () { + if (this.$refs.link && this.refs.link.focus) { + this.$refs.link.focus() + } + }, handleClick (evt) { function stop () { evt.preventDefault() evt.stopPropagation() } - if (evt.type !== 'click' && this.noKeyNav) { - return - } if (this.disabled) { - stop() return } - if ( - evt.type === 'click' || - evt.keyCode === KeyCodes.ENTER || - evt.keyCode === KeyCodes.SPACE - ) { + if (evt.type === 'click' || (evt.keyCode === KeyCodes.SPACE && !this.noKeyNav)) { stop() this.$emit('click', evt) + } else if (evt.type === 'keydown' && this.active && !this.noKeyNav) { + // for keyboard navigation + this.$emit('keydown', evt) } } } @@ -148,7 +157,9 @@ export default { }, data () { return { - currentTab: this.value, + // Index of current tab + currentTab: parseInt(this.value, 10) || -1, + // Array of direct child b-tab instances tabs: [] } }, @@ -166,23 +177,36 @@ export default { if (val === old) { return } - this.$root.$emit('changed::tab', this, val, this.tabs[val]) + // Ensure only one tab is active + this.tabs.forEach((tab, idx) => { + tab.localActive = val === idx && !tab.disabled + }) this.$emit('input', val) - this.tabs[val].$emit('click') }, value (val, old) { if (val === old) { return } - if (typeof old !== 'number') { - old = 0 + val = parseInt(val, 10) + const tabs = this.tabs + const currentIndex = this.currentTab + else if (tabs[val] && !tabs[val].disabled) { + this.currentTab = val + } else if (tabs[currentIndex] && !tabs[currentIndex].disabled]) { + // Stick with current tab, so update-v-model + this.$emit('input', this.currentTab) + } else { + // Tab not available + this.currentTab = -1 } - // Moving left or right? - const direction = val < old ? -1 : 1 - this.setTab(val, false, direction) } }, + created () { + // For SSR and to make sure only a single tab is shown on mount + this.updateTabs() + }, mounted () { + // In case tabs have changed before mount this.updateTabs() // Observe Child changes so we can notify tabs change observeDom(this.$refs.tabsContainer, this.updateTabs.bind(this), { @@ -190,148 +214,158 @@ export default { }) }, methods: { - // Return the sign of a number (as -1, 0, or 1) - sign (x) { - return x === 0 ? 0 : x > 0 ? 1 : -1 + // Update list of b-tab children + updateTabs () { + // Probe tabs + const tabs = (this.$slots.default || []) + .map(vnode => vnode.componentInstance) + .filter(tab => tab && tab._istab) + + // 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.currentTab + if (currentTab >= tabs.length) { + // Handle last tab being removed, so find the last non-disabled tab + tabIndex = tabs.indexOf(tabs.slice().reverse().find(tab => !tab.disabled)) + } 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(tab => !tabs.disabled)) + } + + // Set the current tab state to active + tabs.forEach((tab, idx) => { + tab.localActive = idx === tabIndex && !tab.disabled + }) + + // update the array of tab children + this.tabs = tabs + // Set teh currentTab index (can be -1 if no non-disabled tabs) + this.currentTab = tabIndex + }, + // Force a button to re-render it's content, given a b-tab instance + // Called by b-tab on update() + updateButton(tab) { + const index = this.tabs.indexOf(tab) + const button = this.$refs.buttons[index] + if (button) { + button.$forceUpdate() + } + }, + focusButton(index) { + // Focus a tab button given it's index + const button = this.$refs.buttons[index] + if (button && button.focus) { + button.focus() + } }, // handle keyboard navigation onKeynav (evt) { - if (this.noKeyNav) { - return - } const key = evt.keyCode const shift = evt.shiftKey function stop () { evt.preventDefault() evt.stopPropagation() } - if (key === KeyCodes.UP || key === KeyCodes.LEFT) { + if (key === KeyCodes.UP || key === KeyCodes.LEFT || key === KeyCodes.HOME) { stop() - if (shift) { - this.setTab(0, false, 1) + if (shift || key === KeyCodes.HOME) { + this.firstTab(true) } else { - this.previousTab() + this.previousTab(true) } - } else if (key === KeyCodes.DOWN || key === KeyCodes.RIGHT) { + } else if (key === KeyCodes.DOWN || key === KeyCodes.RIGHT || key === KeyCodes.END) { stop() - if (shift) { - this.setTab(this.tabs.length - 1, false, -1) + if (shift || key === KeyCodes.END) { + this.lastTab(true) } else { - this.nextTab() + this.nextTab(true) } } }, - // Move to next tab - nextTab () { - this.setTab(this.currentTab + 1, false, 1) - }, - // Move to previous tab - previousTab () { - this.setTab(this.currentTab - 1, false, -1) - }, - // Set active tab on the tabs collection and the child 'tab' component - // Index is the tab we want to activate. Direction is the direction we are moving - // so if the tab we requested is disabled, we can skip over it. - // Force is used by updateTabs to ensure we have cleared any previous active tabs. - setTab (index, force, direction) { - direction = this.sign(direction || 0) - index = index || 0 - // Prevent setting same tab and infinite loops! - if (!force && index === this.currentTab) { - return - } - const tab = this.tabs[index] - // Don't go beyond indexes! - if (!tab) { - // Reset the v-model to the current Tab - this.$emit('input', this.currentTab) - return + // Move to first non-disabled tab + firstTab (focus) { + const tabs = this.tabs + const index = tabs.indexOf(tabs.find(tab => !tab.disabled)) + this.currentTab = index + if (focus) { + this.focusButton(index) } - // Ignore or Skip disabled - if (tab.disabled) { - if (direction) { - // Skip to next non disabled tab in specified direction (recursive) - this.setTab(index + direction, force, direction) - } - return + }, + // Move to previous non-disabled tab + previousTab (focus) { + const currentIndex = Math.max(this.currentTab, 0) + const tabs = this.tabs.slice(0, currentIndex) + const index = tabs.lastIndexOf(tabs.find(tab => !tab.disabled)) + this.currentTab = currentIndex - 1 - index + if (focus) { + this.focusButton(index) } - // Activate requested current tab, and deactivte any old tabs - this.tabs.forEach(t => { - if (t === tab) { - // Set new tab as active - this.$set(t, 'localActive', true) - } else { - // Ensure non current tabs are not active - this.$set(t, 'localActive', false) - } - }) - // Update currentTab - this.currentTab = index }, - // Dynamically update tabs list - updateTabs () { - // Probe tabs - this.tabs = stableSort(this.$children.filter(child => child._isTab), (a, b) => { - return a.computedOrder - b.computedOrder - }) - // Set initial active tab - let tabIndex = null - // Find *last* active non-dsabled tab in current tabs - // We trust tab state over currentTab - this.tabs.forEach((tab, index) => { - if (tab.localActive && !tab.disabled) { - tabIndex = index - } - }) - // Else try setting to currentTab - if (tabIndex === null) { - if (this.currentTab >= this.tabs.length) { - // Handle last tab being removed - this.setTab(this.tabs.length - 1, true, -1) - return - } else if ( - this.tabs[this.currentTab] && - !this.tabs[this.currentTab].disabled - ) { - tabIndex = this.currentTab - } + // Move to next non-disabled tab + nextTab (focus) { + const currentIndex = Math.max(this.currentTab, -1) + const tabs = this.tabs + const index = tabs.indexOf(tabs.find(tab => !tab.disabled), currentIndex + 1) + this.currentTab = index + if (focus) { + this.focusButton(index) } - // Else find *first* non-disabled tab in current tabs - if (tabIndex === null) { - this.tabs.forEach((tab, index) => { - if (!tab.disabled && tabIndex === null) { - tabIndex = index - } - }) + }, + // Move to last non-disabled tab + lastTab (focus) { + const tabs = this.tabs + const index = tabs.lastIndexOf(tabs.find(tab => !tab.disabled)) + this.currentTab = index + if (focus) { + this.focusButton(index) } - this.setTab(tabIndex || 0, true, 0) } }, render (h) { const tabs = this.tabs // Navigation 'buttons' const buttons = tabs.map((tab, index) => { - return h(BTabButtonHelper, { - key: index, - props: { - content: tab.$slots.title || tab.title, - href: tab.href, - id: tab.controlledBy || this.safeId(`_BV_tab_${index + 1}_`), - active: tab.localActive, - disabled: tab.disabled, - setSize: tabs.length, - posInSet: index + 1, - controls: this.safeId('_BV_tab_container_'), - linkClass: tab.titleLinkClass, - itemClass: tab.titleItemClass, - noKeyNav: this.noKeyNav - }, - on: { - click: evt => { - this.setTab(index) + const buttonId = tab.controlledBy || this.safeId(`_BV_tab_${index + 1}_`) + return h( + BTabButtonHelper, + { + key: buttonId || index, + ref: 'buttons', + props: { + href: tab.href, // To be deprecated to be always '#' + id: buttonId, + active: tab.localActive && !tab.disabled, + disabled: tab.disabled, + setSize: tabs.length, + posInSet: index + 1, + controls: this.safeId('_BV_tab_container_'), + linkClass: tab.titleLinkClass, + itemClass: tab.titleItemClass, + noKeyNav: this.noKeyNav + }, + on: { + click: evt => { + this.currentTab = index + }, + keydown: evt => { + if (!this.noKeyNav && tab.localActive && !tab.disabled) { + this.onKeyNav(evt) + } + } } - } - }) + }, + [tab.$slots.title || tab.title] + ) }) // Nav 'button' wrapper @@ -354,10 +388,8 @@ export default { ], attrs: { role: 'tablist', - tabindex: this.noKeyNav ? null : '0', id: this.safeId('_BV_tab_controls_') - }, - on: { keydown: this.onKeynav } + } }, [buttons, this.$slots.tabs] ) @@ -382,7 +414,7 @@ export default { } else { empty = h( 'div', - { class: ['tab-pane', 'active', { 'card-body': this.card }] }, + { key: 'empty-tab', class: ['tab-pane', 'active', { 'card-body': this.card }] }, this.$slots.empty ) } @@ -392,7 +424,8 @@ export default { 'div', { ref: 'tabsContainer', - class: ['tab-content', { col: this.vertical }, this.contentClass], + staticClass: 'tab-content', + class: [{ col: this.vertical }, this.contentClass], attrs: { id: this.safeId('_BV_tab_container_') } }, [this.$slots.default, empty] @@ -402,10 +435,8 @@ export default { return h( this.tag, { - class: [ - 'tabs', - { row: this.vertical, 'no-gutters': this.vertical && this.card } - ], + staticClass: 'tabs', + class: { row: this.vertical, 'no-gutters': this.vertical && this.card }, attrs: { id: this.safeId() } }, [ From cc7765bd3d946ac1994d1dbfd0fb6fb7df1042ac Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 00:48:47 -0400 Subject: [PATCH 12/83] lint --- 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 161f886af1e..0f5e85b5f5c 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -190,7 +190,7 @@ export default { val = parseInt(val, 10) const tabs = this.tabs const currentIndex = this.currentTab - else if (tabs[val] && !tabs[val].disabled) { + if (tabs[val] && !tabs[val].disabled) { this.currentTab = val } else if (tabs[currentIndex] && !tabs[currentIndex].disabled]) { // Stick with current tab, so update-v-model From 6741bee600dede9f6a2a5a407c8a03e07c1f943c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 00:54:08 -0400 Subject: [PATCH 13/83] 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 0f5e85b5f5c..76e9fb94828 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -192,7 +192,7 @@ export default { const currentIndex = this.currentTab if (tabs[val] && !tabs[val].disabled) { this.currentTab = val - } else if (tabs[currentIndex] && !tabs[currentIndex].disabled]) { + } else if (tabs[currentIndex] && !tabs[currentIndex].disabled) { // Stick with current tab, so update-v-model this.$emit('input', this.currentTab) } else { From 2bb30a87839ff35611ff68453020df55fa12322d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 00:56:55 -0400 Subject: [PATCH 14/83] lint --- src/components/tabs/tabs.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 76e9fb94828..a720992a1d9 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -29,7 +29,7 @@ const BTabButtonHelper = { { active: this.active, disabled: this.disabled }, this.linkClass ], - props: { + props: { href: this.href, // To be deprecated to always be '#' disabled: this.disabled, active: this.active @@ -241,7 +241,7 @@ export default { if (tabIndex < 0) { tabIndex = tabs.indexOf(tabs.find(tab => !tabs.disabled)) } - + // Set the current tab state to active tabs.forEach((tab, idx) => { tab.localActive = idx === tabIndex && !tab.disabled @@ -254,14 +254,14 @@ export default { }, // Force a button to re-render it's content, given a b-tab instance // Called by b-tab on update() - updateButton(tab) { + updateButton (tab) { const index = this.tabs.indexOf(tab) const button = this.$refs.buttons[index] if (button) { button.$forceUpdate() } }, - focusButton(index) { + focusButton (index) { // Focus a tab button given it's index const button = this.$refs.buttons[index] if (button && button.focus) { From 8a4a297c8927e014af27bf85688438422668c17d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 01:09:37 -0400 Subject: [PATCH 15/83] Update tab.js --- src/components/tabs/tab.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index b77d2be2c53..2f8f68a0ad8 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -64,10 +64,6 @@ export default { lazy: { type: Boolean, default: false - }, - order: { - type: [Number, String], - default: 0 } }, data () { @@ -95,9 +91,6 @@ export default { computedLazy () { return this.bTabs.lazy || this.lazy }, - computedOrder () { - return parseInt(this.order, 10) || 0 - }, _isTab () { // For parent sniffing of child return true From aec4417bf9d4b6dd9fc00529e79d067c1e6f8b5a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 01:14:08 -0400 Subject: [PATCH 16/83] debugging stuff --- src/components/tabs/tabs.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index a720992a1d9..7b480999df2 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -218,8 +218,9 @@ export default { updateTabs () { // Probe tabs const tabs = (this.$slots.default || []) - .map(vnode => vnode.componentInstance) + .map(vnode => vnode.componentInstance ) .filter(tab => tab && tab._istab) + console.log('UpdatTabs:', tabs, this.$slots.default) // Find *last* active non-disabled tab in current tabs // We trust tab state over currentTab, in case tabs were added/removed/re-ordered From 34dc892d9bd6cf23c5a32d44450dda13b8468819 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 01:16:43 -0400 Subject: [PATCH 17/83] Update tabs.js --- src/components/tabs/tabs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 7b480999df2..82ee87ed944 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -219,8 +219,7 @@ export default { // Probe tabs const tabs = (this.$slots.default || []) .map(vnode => vnode.componentInstance ) - .filter(tab => tab && tab._istab) - console.log('UpdatTabs:', tabs, this.$slots.default) + .filter(tab => tab && tab._isTab) // Find *last* active non-disabled tab in current tabs // We trust tab state over currentTab, in case tabs were added/removed/re-ordered From f520cf27680ac7ffb8209116258252d779dab43f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 01:18:08 -0400 Subject: [PATCH 18/83] lint --- 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 82ee87ed944..1482590be53 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -218,7 +218,7 @@ export default { updateTabs () { // Probe tabs const tabs = (this.$slots.default || []) - .map(vnode => vnode.componentInstance ) + .map(vnode => vnode.componentInstance) .filter(tab => tab && tab._isTab) // Find *last* active non-disabled tab in current tabs From 56b78d683258108d3a307be1cef5867f3b07049c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 01:35:00 -0400 Subject: [PATCH 19/83] Update tabs.js --- src/components/tabs/tabs.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 1482590be53..7fbd0a3fe4b 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -87,9 +87,7 @@ export default { name: 'BTabs', mixins: [idMixin], provide () { - return { - tabs: this - } + return { bTabs: this } }, props: { tag: { @@ -359,7 +357,7 @@ export default { }, keydown: evt => { if (!this.noKeyNav && tab.localActive && !tab.disabled) { - this.onKeyNav(evt) + this.onKeynav(evt) } } } From b4e7bce528d2d169272d9cd13978558f45eacd63 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 02:07:13 -0400 Subject: [PATCH 20/83] Update tabs.js --- src/components/tabs/tabs.js | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 7fbd0a3fe4b..da90143e720 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -31,8 +31,7 @@ const BTabButtonHelper = { ], props: { href: this.href, // To be deprecated to always be '#' - disabled: this.disabled, - active: this.active + disabled: this.disabled }, attrs: { role: 'tab', @@ -71,10 +70,13 @@ const BTabButtonHelper = { if (this.disabled) { return } - if (evt.type === 'click' || (evt.keyCode === KeyCodes.SPACE && !this.noKeyNav)) { + if ( + evt.type === 'click' || + (!this.noKeyNav && evt.type === 'keydown' && evt.keyCode === KeyCodes.SPACE) + ) { stop() this.$emit('click', evt) - } else if (evt.type === 'keydown' && this.active && !this.noKeyNav) { + } else if (evt.type === 'keydown' && !this.noKeyNav) { // for keyboard navigation this.$emit('keydown', evt) } @@ -268,6 +270,9 @@ export default { }, // handle keyboard navigation onKeynav (evt) { + if (!this.nokeyNav) { + return + } const key = evt.keyCode const shift = evt.shiftKey function stop () { @@ -304,25 +309,29 @@ export default { const currentIndex = Math.max(this.currentTab, 0) const tabs = this.tabs.slice(0, currentIndex) const index = tabs.lastIndexOf(tabs.find(tab => !tab.disabled)) - this.currentTab = currentIndex - 1 - index - if (focus) { - this.focusButton(index) + if (index > -1) { + this.currentTab = currentIndex - 1 - index + if (focus) { + this.focusButton(index) + } } }, // Move to next non-disabled tab nextTab (focus) { const currentIndex = Math.max(this.currentTab, -1) const tabs = this.tabs - const index = tabs.indexOf(tabs.find(tab => !tab.disabled), currentIndex + 1) - this.currentTab = index - if (focus) { - this.focusButton(index) + const index = tabs.indexOf(tabs.slice(currentIndex + 1).find(tab => !tab.disabled)) + if (index > -1) { + this.currentTab = index + if (focus) { + this.focusButton(index) + } } }, // Move to last non-disabled tab lastTab (focus) { const tabs = this.tabs - const index = tabs.lastIndexOf(tabs.find(tab => !tab.disabled)) + const index = tabs.indexOf(tabs.slice().reverse().find(tab => !tab.disabled)) this.currentTab = index if (focus) { this.focusButton(index) @@ -356,7 +365,7 @@ export default { this.currentTab = index }, keydown: evt => { - if (!this.noKeyNav && tab.localActive && !tab.disabled) { + if (!this.noKeyNav && !tab.disabled) { this.onKeynav(evt) } } From d4a699dfda629628fc8f3956e2eaa71c7a84d875 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 02:43:54 -0400 Subject: [PATCH 21/83] Update tabs.js --- src/components/tabs/tabs.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index da90143e720..a566aa5c235 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -188,16 +188,18 @@ export default { return } val = parseInt(val, 10) + old = parseInt(old, 10) || 0 const tabs = this.tabs const currentIndex = this.currentTab if (tabs[val] && !tabs[val].disabled) { this.currentTab = val - } else if (tabs[currentIndex] && !tabs[currentIndex].disabled) { - // Stick with current tab, so update-v-model - this.$emit('input', this.currentTab) } else { - // Tab not available - this.currentTab = -1 + // Try next/prev tabs + if (val < old) { + this.nextTab() + } else { + this.previousTab() + } } } }, @@ -270,7 +272,7 @@ export default { }, // handle keyboard navigation onKeynav (evt) { - if (!this.nokeyNav) { + if (this.nokeyNav) { return } const key = evt.keyCode @@ -443,7 +445,10 @@ export default { this.tag, { staticClass: 'tabs', - class: { row: this.vertical, 'no-gutters': this.vertical && this.card }, + class: { + row: this.vertical, + 'no-gutters': this.vertical && this.card + }, attrs: { id: this.safeId() } }, [ From bcdbc4096c07f3a980dc3cb8a40cae84c967620b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 02:55:32 -0400 Subject: [PATCH 22/83] 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 a566aa5c235..c1e184ecd11 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -37,7 +37,7 @@ const BTabButtonHelper = { role: 'tab', id: this.id, // Roving tab index when keynav enabled - tabindex: this.noKeyNav ? null : (this.active ? '0' : '-1'), + tabindex: this.noKeyNav ? null : (this.active ? null : '-1'), 'aria-selected': this.active ? 'true' : 'false', 'aria-setsize': this.setSize, 'aria-posinset': this.posInSet, From e1ec16e2d8ba3b4aaf0cfff8723ade3d056e95a7 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 03:36:06 -0400 Subject: [PATCH 23/83] Update tabs.js --- src/components/tabs/tabs.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index c1e184ecd11..c4998b6f9d1 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -15,6 +15,7 @@ const BTabButtonHelper = { id: { type: String, default: null }, active: { type: Boolean, default: false }, disabled: { type: Boolean, default: false }, + tabIndex: { type: Boolean, default: null }, linkClass: { default: null }, itemClass: { default: null }, noKeyNav: { type: Boolean, default: false } @@ -37,7 +38,7 @@ const BTabButtonHelper = { role: 'tab', id: this.id, // Roving tab index when keynav enabled - tabindex: this.noKeyNav ? null : (this.active ? null : '-1'), + tabindex: this.tabIndex, 'aria-selected': this.active ? 'true' : 'false', 'aria-setsize': this.setSize, 'aria-posinset': this.posInSet, @@ -343,8 +344,21 @@ export default { render (h) { const tabs = this.tabs // Navigation 'buttons' + let activeTab = tabs.find(tab => tab.localActive && !tab.disabled) + const fallbackTab = tabs.find(tab => !tab.disabled) + // For each b-tab found const buttons = tabs.map((tab, index) => { const buttonId = tab.controlledBy || this.safeId(`_BV_tab_${index + 1}_`) + let tabindex = null + // Ensure at least one tab button is focusable when keynav enabled (if possible) + if (!this.noKeyNav) { + // buttons are not in tab index unless active, or a fallback tab + tabindex = '-1' + if (activeTab === tab || (!activeTab && fallbackTab === tab)) { + // Place tab button in tab sequence + tabindex = null + } + } return h( BTabButtonHelper, { @@ -353,6 +367,7 @@ export default { props: { href: tab.href, // To be deprecated to be always '#' id: buttonId, + tabIndex: tabindex, active: tab.localActive && !tab.disabled, disabled: tab.disabled, setSize: tabs.length, From 41f47fdacf01eb2b8c40bea8b877e4edff5390a6 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 03:39:09 -0400 Subject: [PATCH 24/83] lint --- src/components/tabs/tabs.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index c4998b6f9d1..c3bcb127889 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -191,7 +191,6 @@ export default { val = parseInt(val, 10) old = parseInt(old, 10) || 0 const tabs = this.tabs - const currentIndex = this.currentTab if (tabs[val] && !tabs[val].disabled) { this.currentTab = val } else { @@ -461,7 +460,7 @@ export default { { staticClass: 'tabs', class: { - row: this.vertical, + row: this.vertical, 'no-gutters': this.vertical && this.card }, attrs: { id: this.safeId() } From ac59727c8f64e864b87f8718ad8c16fda125f97e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 03:59:06 -0400 Subject: [PATCH 25/83] Update tabs.js --- src/components/tabs/tabs.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index c3bcb127889..3f5e869a5c7 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -196,9 +196,9 @@ export default { } else { // Try next/prev tabs if (val < old) { - this.nextTab() - } else { this.previousTab() + } else { + this.nextTab() } } } @@ -312,10 +312,12 @@ export default { const tabs = this.tabs.slice(0, currentIndex) const index = tabs.lastIndexOf(tabs.find(tab => !tab.disabled)) if (index > -1) { - this.currentTab = currentIndex - 1 - index + this.currentTab = index if (focus) { this.focusButton(index) } + } else { + this.$emit('input', this.currentTab) } }, // Move to next non-disabled tab @@ -328,6 +330,8 @@ export default { if (focus) { this.focusButton(index) } + } else { + this.$emit('input', this.currentTab) } }, // Move to last non-disabled tab From ea02b02b143624281a84b0ec795db30a7bd594c4 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 04:54:05 -0400 Subject: [PATCH 26/83] Update tabs.js --- src/components/tabs/tabs.js | 57 ++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 3f5e869a5c7..6f35ad450fe 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -257,15 +257,29 @@ export default { // Force a button to re-render it's content, given a b-tab instance // Called by b-tab on update() updateButton (tab) { - const index = this.tabs.indexOf(tab) - const button = this.$refs.buttons[index] + const button = this.$refs.buttons[this.tabs.indexOf(tab)] if (button) { button.$forceUpdate() } }, - focusButton (index) { - // Focus a tab button given it's index - const button = this.$refs.buttons[index] + // Activate a tab given a b-tab instance + // Also accessed by b-tab + activateTab (tab) { + if (tab) { + if (tab.disabled) { + return false + } else { + const index = this.tabs.indexOf(tab) + this.currentTab = index + return index > -1 + } + } else { + return false + } + }, + // Focus a tab button given it's b-tab instance + focusButton (tab) { + const button = this.$refs.buttons[this.tabs.indexOf(tab)] if (button && button.focus) { button.focus() } @@ -299,22 +313,21 @@ export default { }, // Move to first non-disabled tab firstTab (focus) { - const tabs = this.tabs - const index = tabs.indexOf(tabs.find(tab => !tab.disabled)) - this.currentTab = index + const tab = this.tabs.find(tab => !tab.disabled) + this.activateTab(tab) if (focus) { - this.focusButton(index) + this.focusButton(tab) } }, // Move to previous non-disabled tab previousTab (focus) { const currentIndex = Math.max(this.currentTab, 0) - const tabs = this.tabs.slice(0, currentIndex) - const index = tabs.lastIndexOf(tabs.find(tab => !tab.disabled)) - if (index > -1) { - this.currentTab = index + const tabs = this.tabs.slice(0, currentIndex).reverse() + const tab = tabs.find(tab => !tab.disabled) + if (tab) { + this.activateTab(tab) if (focus) { - this.focusButton(index) + this.focusButton(tab) } } else { this.$emit('input', this.currentTab) @@ -323,12 +336,11 @@ export default { // Move to next non-disabled tab nextTab (focus) { const currentIndex = Math.max(this.currentTab, -1) - const tabs = this.tabs - const index = tabs.indexOf(tabs.slice(currentIndex + 1).find(tab => !tab.disabled)) - if (index > -1) { - this.currentTab = index + const tab = this.tabs.slice(currentIndex + 1).find(tab => !tab.disabled) + if (tab) { + this.activateTab(tab) if (focus) { - this.focusButton(index) + this.focusButton(tab) } } else { this.$emit('input', this.currentTab) @@ -336,11 +348,10 @@ export default { }, // Move to last non-disabled tab lastTab (focus) { - const tabs = this.tabs - const index = tabs.indexOf(tabs.slice().reverse().find(tab => !tab.disabled)) - this.currentTab = index + const tab = this.tabs.slice().reverse().find(tab => !tab.disabled) + this.activateTab(tab) if (focus) { - this.focusButton(index) + this.focusButton(tab) } } }, From 9055b78f085f5703490ef3c5e46e9c3588eb1be1 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 04:57:51 -0400 Subject: [PATCH 27/83] Update tab.js --- src/components/tabs/tab.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 2f8f68a0ad8..3974f053a81 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -107,6 +107,15 @@ export default { // } }, methods: { + // Public methods + activate () { + if (this.bTabs.activateTab && !this.disabled) { + return this.bTabs.activateTab(this) + } else { + return false + } + }, + // Transition handlers beforeEnter () { // change opacity (add 'show' class) 1 frame after display // otherwise css transition won't happen From 4fe2a863b08e58eb8de57e04d9486b347b38c205 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 05:26:45 -0400 Subject: [PATCH 28/83] Update tab.js --- src/components/tabs/tab.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 3974f053a81..abd8349dba8 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -102,9 +102,9 @@ export default { }, updated () { // Force the tab buton content to update (since slots are not reactive) - // if (this.bTabs.updateButton) { - // this.bTabs.updateButton(this) - // } + if (this.bTabs.updateButton) { + this.bTabs.updateButton(this) + } }, methods: { // Public methods From 7a15ec2afbf1af66296992a57320c24e146fe9a1 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 12:53:47 -0400 Subject: [PATCH 29/83] Update tab.js --- src/components/tabs/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index abd8349dba8..1f8af1e191d 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -101,7 +101,7 @@ export default { this.show = this.localActive }, updated () { - // Force the tab buton content to update (since slots are not reactive) + // Force the tab button content to update (since slots are not reactive) if (this.bTabs.updateButton) { this.bTabs.updateButton(this) } From f48deb61601c6f93513c7d6b30b95e740a028d1b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 12:56:30 -0400 Subject: [PATCH 30/83] Update tabs.js --- src/components/tabs/tabs.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 6f35ad450fe..ec8de4c69d7 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -78,7 +78,7 @@ const BTabButtonHelper = { stop() this.$emit('click', evt) } else if (evt.type === 'keydown' && !this.noKeyNav) { - // for keyboard navigation + // For keyboard navigation this.$emit('keydown', evt) } } @@ -249,9 +249,9 @@ export default { tab.localActive = idx === tabIndex && !tab.disabled }) - // update the array of tab children + // Update the array of tab children this.tabs = tabs - // Set teh currentTab index (can be -1 if no non-disabled tabs) + // Set the currentTab index (can be -1 if no non-disabled tabs) this.currentTab = tabIndex }, // Force a button to re-render it's content, given a b-tab instance @@ -366,7 +366,7 @@ export default { let tabindex = null // Ensure at least one tab button is focusable when keynav enabled (if possible) if (!this.noKeyNav) { - // buttons are not in tab index unless active, or a fallback tab + // Buttons are not in tab index unless active, or a fallback tab tabindex = '-1' if (activeTab === tab || (!activeTab && fallbackTab === tab)) { // Place tab button in tab sequence From b13d5f8aebb484d70c5a57694b6e49997f8e875e Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 13:28:51 -0400 Subject: [PATCH 31/83] Update tabs.js --- src/components/tabs/tabs.js | 98 +++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index ec8de4c69d7..38393543cca 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -7,19 +7,41 @@ import idMixin from '../../mixins/id' const BTabButtonHelper = { name: 'BTabButtonHelper', props: { - content: { type: [String, Array], default: '' }, - href: { type: String, default: '#' }, // To be deprecated - posInSet: { type: Number, default: null }, - setSize: { type: Number, default: null }, - controls: { type: String, default: null }, + // Reference to the child b-tab instance + tab: { default: null, required: true }, id: { type: String, default: null }, - active: { type: Boolean, default: false }, - disabled: { type: Boolean, default: false }, + controls: { type: String, default: null }, tabIndex: { type: Boolean, default: null }, - linkClass: { default: null }, - itemClass: { default: null }, + posInSet: { type: Number, default: null }, + setSize: { type: Number, default: null }, noKeyNav: { type: Boolean, default: false } }, + methods: { + focus () { + if (this.$refs.link && this.refs.link.focus) { + this.$refs.link.focus() + } + }, + handleClick (evt) { + function stop () { + evt.preventDefault() + evt.stopPropagation() + } + if (this.tab.disabled) { + return + } + if ( + evt.type === 'click' || + (!this.noKeyNav && evt.type === 'keydown' && evt.keyCode === KeyCodes.SPACE) + ) { + stop() + this.$emit('click', evt) // Could call this.tab.activate() instead + } else if (evt.type === 'keydown' && !this.noKeyNav) { + // For keyboard navigation + this.$emit('keydown', evt) + } + } + }, render (h) { const link = h( BLink, @@ -27,19 +49,22 @@ const BTabButtonHelper = { ref: 'link', staticClass: 'nav-link', class: [ - { active: this.active, disabled: this.disabled }, - this.linkClass + { + active: this.tab.localActive && !this.tab.disabled, + disabled: this.tab.disabled + }, + this.tab.linkClass ], props: { - href: this.href, // To be deprecated to always be '#' - disabled: this.disabled + href: this.tab.href, // To be deprecated to always be '#' + disabled: this.tab.disabled }, attrs: { role: 'tab', id: this.id, // Roving tab index when keynav enabled tabindex: this.tabIndex, - 'aria-selected': this.active ? 'true' : 'false', + 'aria-selected': this.tab.localActive && !this.tab.disabled ? 'true' : 'false', 'aria-setsize': this.setSize, 'aria-posinset': this.posInSet, 'aria-controls': this.controls @@ -49,39 +74,13 @@ const BTabButtonHelper = { keydown: this.handleClick } }, - this.$slots.default || this.content + [this.tab.$slots.title || this.tab.title] ) return h( 'li', - { class: ['nav-item', this.itemClass], attrs: { role: 'presentation' } }, + { class: ['nav-item', this.tab.itemClass], attrs: { role: 'presentation' } }, [link] ) - }, - methods: { - focus () { - if (this.$refs.link && this.refs.link.focus) { - this.$refs.link.focus() - } - }, - handleClick (evt) { - function stop () { - evt.preventDefault() - evt.stopPropagation() - } - if (this.disabled) { - return - } - if ( - evt.type === 'click' || - (!this.noKeyNav && evt.type === 'keydown' && evt.keyCode === KeyCodes.SPACE) - ) { - stop() - this.$emit('click', evt) - } else if (evt.type === 'keydown' && !this.noKeyNav) { - // For keyboard navigation - this.$emit('keydown', evt) - } - } } } @@ -379,30 +378,23 @@ export default { key: buttonId || index, ref: 'buttons', props: { - href: tab.href, // To be deprecated to be always '#' + tab: tab, id: buttonId, + controls: this.safeId('_BV_tab_container_'), tabIndex: tabindex, - active: tab.localActive && !tab.disabled, - disabled: tab.disabled, setSize: tabs.length, posInSet: index + 1, - controls: this.safeId('_BV_tab_container_'), - linkClass: tab.titleLinkClass, - itemClass: tab.titleItemClass, noKeyNav: this.noKeyNav }, on: { - click: evt => { - this.currentTab = index - }, + click: evt => { this.activateTab(tab) }, keydown: evt => { if (!this.noKeyNav && !tab.disabled) { this.onKeynav(evt) } } } - }, - [tab.$slots.title || tab.title] + } ) }) From e6ff08be02972e1e14c27040c8ef8905afff97c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Sat, 12 Jan 2019 13:29:29 -0400 Subject: [PATCH 32/83] Update src/components/tabs/tab.js Co-Authored-By: tmorehouse --- src/components/tabs/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 1f8af1e191d..6daf6f43612 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -130,7 +130,7 @@ export default { raf(() => { this.show = true }) }, beforeLeave () { - // remove the 'show' class + // Remove the 'show' class this.show = false } }, From 65409f55c861b8fe54ddc20c6d8f621d4c7fd6a2 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 14:14:14 -0400 Subject: [PATCH 33/83] Update tabs.js --- src/components/tabs/tabs.js | 72 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 38393543cca..df5f5d0ef24 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -22,7 +22,7 @@ const BTabButtonHelper = { this.$refs.link.focus() } }, - handleClick (evt) { + handleEvt (evt) { function stop () { evt.preventDefault() evt.stopPropagation() @@ -30,15 +30,33 @@ const BTabButtonHelper = { if (this.tab.disabled) { return } - if ( - evt.type === 'click' || - (!this.noKeyNav && evt.type === 'keydown' && evt.keyCode === KeyCodes.SPACE) - ) { + const type = evt.type + const key = evt.keyCode + const shift = evt.shiftKey + if (type === 'click') { + stop() + this.$emit('click', evt) // Could call this.tab.activate() instead + } else if (type === 'keydown' && !this.noKeyNav && key === KeyCodes.SPACE) { + // In keyNav mode, SAPCE press will also trigger a click/select stop() this.$emit('click', evt) // Could call this.tab.activate() instead - } else if (evt.type === 'keydown' && !this.noKeyNav) { + } else if (type === 'keydown' && !this.noKeyNav) { // For keyboard navigation - this.$emit('keydown', evt) + if (key === KeyCodes.UP || key === KeyCodes.LEFT || key === KeyCodes.HOME) { + stop() + if (shift || key === KeyCodes.HOME) { + this.$emit('first', evt) + } else { + this.$emit('prev', evt) + } + } else if (key === KeyCodes.DOWN || key === KeyCodes.RIGHT || key === KeyCodes.END) { + stop() + if (shift || key === KeyCodes.END) { + this.$emit('last', evt) + } else { + this.$emit('next', evt) + } + } } } }, @@ -70,8 +88,8 @@ const BTabButtonHelper = { 'aria-controls': this.controls }, on: { - click: this.handleClick, - keydown: this.handleClick + click: this.handleEvt, + keydown: this.handleEvt } }, [this.tab.$slots.title || this.tab.title] @@ -283,33 +301,6 @@ export default { button.focus() } }, - // handle keyboard navigation - onKeynav (evt) { - if (this.nokeyNav) { - return - } - const key = evt.keyCode - const shift = evt.shiftKey - function stop () { - evt.preventDefault() - evt.stopPropagation() - } - if (key === KeyCodes.UP || key === KeyCodes.LEFT || key === KeyCodes.HOME) { - stop() - if (shift || key === KeyCodes.HOME) { - this.firstTab(true) - } else { - this.previousTab(true) - } - } else if (key === KeyCodes.DOWN || key === KeyCodes.RIGHT || key === KeyCodes.END) { - stop() - if (shift || key === KeyCodes.END) { - this.lastTab(true) - } else { - this.nextTab(true) - } - } - }, // Move to first non-disabled tab firstTab (focus) { const tab = this.tabs.find(tab => !tab.disabled) @@ -388,11 +379,10 @@ export default { }, on: { click: evt => { this.activateTab(tab) }, - keydown: evt => { - if (!this.noKeyNav && !tab.disabled) { - this.onKeynav(evt) - } - } + first: this.firstTab, + prev: this.previousTab, + next: this.nextTab, + last: this.lastTab } } ) From 87116fd71cdeaf4322cb8dfbfa2f9e17abe5e7dd Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 14:22:32 -0400 Subject: [PATCH 34/83] 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 df5f5d0ef24..c9ef3004a07 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -67,7 +67,7 @@ const BTabButtonHelper = { ref: 'link', staticClass: 'nav-link', class: [ - { + { active: this.tab.localActive && !this.tab.disabled, disabled: this.tab.disabled }, From 1ef91fdd48f79811a08c69006299c94bb7ff27cc Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 18:23:50 -0400 Subject: [PATCH 35/83] debug title slot reactivity --- src/components/tabs/tab.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 6daf6f43612..79fb81fc3b9 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -103,6 +103,7 @@ export default { updated () { // Force the tab button content to update (since slots are not reactive) if (this.bTabs.updateButton) { + console.log('b-tab calling updateButton', this) this.bTabs.updateButton(this) } }, From d589a5f01fbfe20ab564f52574c45135814d1165 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 18:24:51 -0400 Subject: [PATCH 36/83] Update tab.js --- src/components/tabs/tab.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 79fb81fc3b9..ccff376b085 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -102,6 +102,7 @@ export default { }, updated () { // Force the tab button content to update (since slots are not reactive) + console.log('b-tab update...') if (this.bTabs.updateButton) { console.log('b-tab calling updateButton', this) this.bTabs.updateButton(this) From 456d198e5dca97cbba403fcad2db1fd2a58fce68 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 18:29:21 -0400 Subject: [PATCH 37/83] more debug code --- src/components/tabs/tabs.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index c9ef3004a07..b1ed6b5dba2 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -274,8 +274,10 @@ export default { // Force a button to re-render it's content, given a b-tab instance // Called by b-tab on update() updateButton (tab) { + console.log('b-tabs received button update request for tab', tab) const button = this.$refs.buttons[this.tabs.indexOf(tab)] - if (button) { + console.log('b-tabs update button', button) + if (button && button.$forceUpdate) { button.$forceUpdate() } }, From 158c2a4ee7032d8bca25e50c6776d39be3418fa7 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 18:48:35 -0400 Subject: [PATCH 38/83] 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 b1ed6b5dba2..8eba9d284db 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -370,6 +370,8 @@ export default { { key: buttonId || index, ref: 'buttons', + // Needed to make this.$refs.buttons an array + refInFor: true, props: { tab: tab, id: buttonId, From 21cb01d6c2610196204a1081e1bfdf7e82609498 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 19:19:37 -0400 Subject: [PATCH 39/83] Update tabs.js --- src/components/tabs/tabs.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 8eba9d284db..ed3bf8ff712 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -102,6 +102,9 @@ const BTabButtonHelper = { } } +// Filter function to filter out disabled tabs +const notDisabled = tab => !tab.disabled + // @vue/component export default { name: 'BTabs', @@ -249,7 +252,7 @@ export default { const currentTab = this.currentTab if (currentTab >= tabs.length) { // Handle last tab being removed, so find the last non-disabled tab - tabIndex = tabs.indexOf(tabs.slice().reverse().find(tab => !tab.disabled)) + tabIndex = tabs.indexOf(tabs.slice().reverse().find(notDisabled)) } else if (tabs[currentTab] && !tabs[currentTab].disabled) { // current tab is not disabled tabIndex = currentTab @@ -258,7 +261,7 @@ export default { // Else find *first* non-disabled tab in current tabs if (tabIndex < 0) { - tabIndex = tabs.indexOf(tabs.find(tab => !tabs.disabled)) + tabIndex = tabs.indexOf(tabs.find(notDisabled)) } // Set the current tab state to active @@ -271,12 +274,15 @@ export default { // Set the currentTab index (can be -1 if no non-disabled tabs) this.currentTab = tabIndex }, + // Find a button taht controls a tab, given the tab reference + // Returns the button vm instance + getButtonForTab (tab) { + return = (this.$refs.buttons || []).find(btn => btn.tab === tab) + }, // Force a button to re-render it's content, given a b-tab instance // Called by b-tab on update() updateButton (tab) { - console.log('b-tabs received button update request for tab', tab) - const button = this.$refs.buttons[this.tabs.indexOf(tab)] - console.log('b-tabs update button', button) + const button = this.getButtonForTab(tab) if (button && button.$forceUpdate) { button.$forceUpdate() } @@ -298,14 +304,14 @@ export default { }, // Focus a tab button given it's b-tab instance focusButton (tab) { - const button = this.$refs.buttons[this.tabs.indexOf(tab)] + const button = this.getButtonForTab(tab) if (button && button.focus) { button.focus() } }, // Move to first non-disabled tab firstTab (focus) { - const tab = this.tabs.find(tab => !tab.disabled) + const tab = this.tabs.find(notDisabled) this.activateTab(tab) if (focus) { this.focusButton(tab) @@ -315,7 +321,7 @@ export default { previousTab (focus) { const currentIndex = Math.max(this.currentTab, 0) const tabs = this.tabs.slice(0, currentIndex).reverse() - const tab = tabs.find(tab => !tab.disabled) + const tab = tabs.find(notDisabled) if (tab) { this.activateTab(tab) if (focus) { @@ -328,7 +334,7 @@ export default { // Move to next non-disabled tab nextTab (focus) { const currentIndex = Math.max(this.currentTab, -1) - const tab = this.tabs.slice(currentIndex + 1).find(tab => !tab.disabled) + const tab = this.tabs.slice(currentIndex + 1).find(notDisabled) if (tab) { this.activateTab(tab) if (focus) { @@ -340,7 +346,7 @@ export default { }, // Move to last non-disabled tab lastTab (focus) { - const tab = this.tabs.slice().reverse().find(tab => !tab.disabled) + const tab = this.tabs.slice().reverse().find(notDisabled) this.activateTab(tab) if (focus) { this.focusButton(tab) From f6269a28851e85aef47cdfedc3ed5552770aa91d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 19:21:59 -0400 Subject: [PATCH 40/83] Update tab.js --- src/components/tabs/tab.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index ccff376b085..06c226e399b 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -102,9 +102,7 @@ export default { }, updated () { // Force the tab button content to update (since slots are not reactive) - console.log('b-tab update...') if (this.bTabs.updateButton) { - console.log('b-tab calling updateButton', this) this.bTabs.updateButton(this) } }, @@ -127,6 +125,7 @@ export default { window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || + /* istanbul ignore next */ function (cb) { setTimeout(cb, 16) } raf(() => { this.show = true }) From e1837d926ce2029b539864b41edd37aeac25c053 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 19:22:58 -0400 Subject: [PATCH 41/83] 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 ed3bf8ff712..f4203c835f8 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -195,6 +195,7 @@ export default { }, watch: { currentTab (val, old) { + /* istanbul ignore if */ if (val === old) { return } @@ -205,6 +206,7 @@ export default { this.$emit('input', val) }, value (val, old) { + /* istanbul ignore if */ if (val === old) { return } From 52a0fa027fc38257185a6159926ad118de18c5c5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 19:39:04 -0400 Subject: [PATCH 42/83] lint --- 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 f4203c835f8..af919d650fc 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -279,7 +279,7 @@ export default { // Find a button taht controls a tab, given the tab reference // Returns the button vm instance getButtonForTab (tab) { - return = (this.$refs.buttons || []).find(btn => btn.tab === tab) + return (this.$refs.buttons || []).find(btn => btn.tab === tab) }, // Force a button to re-render it's content, given a b-tab instance // Called by b-tab on update() From 00502b052fa0e588d93a80f16af208d82465bbdf Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 19:50:26 -0400 Subject: [PATCH 43/83] b-tab: make active prop syncable Supports activating a tab post mount via the `active` prop. `active` prop can use the `.sync` modifier, allowing it to reflect the current visibility of the tab. Note making a tab deactivate with `active` prop is not available --- src/components/tabs/tab.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 06c226e399b..168fbf07e35 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -96,6 +96,23 @@ export default { return true } }, + watch: { + localActive (newVal, oldVal) { + // Make 'active' prop work with `.sync` modifier + this.$emit('update:active', newVal) + }, + active (newVal, oldVal) { + if (newVal !== oldVal) { + if (newVal) { + // If activated post mount + this.activate() + } else { + // We don't have a method to deactivate a tab yet + this.$emit('update:active', this.localActive) + } + } + } + }, mounted () { // Initially show on mount if active and not disabled this.show = this.localActive From b7781e3c0b1bde4f556a0dd823d01350af0b575b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:06:16 -0400 Subject: [PATCH 44/83] Update tabs.js --- src/components/tabs/tabs.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index af919d650fc..c17403e9443 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -199,7 +199,7 @@ export default { if (val === old) { return } - // Ensure only one tab is active + // Ensure only one tab is active at most this.tabs.forEach((tab, idx) => { tab.localActive = val === idx && !tab.disabled }) @@ -304,6 +304,21 @@ export default { return false } }, + // Deactivate a tab given a b-tab instance + // Accessed by b-tab + deactivateTab (tab) { + if (tab) { + // Find first non-disabled tab that isn't the one being deactivated + const newTab = this.tabs.filter(t => t !== tab).find(notDisabled) + if (newTab) { + // activate found tab + this.activateTab(newTab) + } else { + // No tab to show + this.currentTab = -1 + } + } + }, // Focus a tab button given it's b-tab instance focusButton (tab) { const button = this.getButtonForTab(tab) From 7669b5f32b2b4f9e3c8ec0cc48b3689b4151b6bb Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:08:27 -0400 Subject: [PATCH 45/83] Update tab.js --- src/components/tabs/tab.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 168fbf07e35..d3bc6ae80f7 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -124,14 +124,6 @@ export default { } }, methods: { - // Public methods - activate () { - if (this.bTabs.activateTab && !this.disabled) { - return this.bTabs.activateTab(this) - } else { - return false - } - }, // Transition handlers beforeEnter () { // change opacity (add 'show' class) 1 frame after display @@ -150,7 +142,22 @@ export default { beforeLeave () { // Remove the 'show' class this.show = false - } + }, + // Public methods + activate () { + if (this.bTabs.activateTab && !this.disabled) { + return this.bTabs.activateTab(this) + } else { + return false + } + }, + deactivate () { + if (this.bTabs.deactivateTab) { + return this.bTabs.deactivateTab(this) + } else { + return false + } + }, }, render (h) { let content = h( From 86b1c289ec6efa4310c52a552c4c218127e7ba1f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:10:09 -0400 Subject: [PATCH 46/83] Update tabs.js --- src/components/tabs/tabs.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index c17403e9443..093868e31bb 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -311,11 +311,12 @@ export default { // Find first non-disabled tab that isn't the one being deactivated const newTab = this.tabs.filter(t => t !== tab).find(notDisabled) if (newTab) { - // activate found tab + // Activate found tab (which will deactivate calling tab) this.activateTab(newTab) + return true } else { - // No tab to show - this.currentTab = -1 + // No tab to fall back to + return false } } }, From e8e10fbf7ea20481ea0e246756a43fd962dab678 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:21:34 -0400 Subject: [PATCH 47/83] Update tab.js --- src/components/tabs/tab.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index d3bc6ae80f7..d8841ebc8a2 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -107,8 +107,11 @@ export default { // If activated post mount this.activate() } else { - // We don't have a method to deactivate a tab yet - this.$emit('update:active', this.localActive) + if (!this.deactivate()) { + // Tab couldn't be deactivated, so we reset the synced active prop + // Deactivation will fail if no other tabs to activate. + this.$emit('update:active', this.localActive) + } } } } @@ -148,6 +151,7 @@ export default { if (this.bTabs.activateTab && !this.disabled) { return this.bTabs.activateTab(this) } else { + // Not inside a b-tabs component return false } }, @@ -155,6 +159,7 @@ export default { if (this.bTabs.deactivateTab) { return this.bTabs.deactivateTab(this) } else { + // Not inside a b-tabs component return false } }, From 8dc7ed29011186eaf29b041104c92308c51ee0ba Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:32:16 -0400 Subject: [PATCH 48/83] Update tab.js --- src/components/tabs/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index d8841ebc8a2..a3df513ebd4 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -162,7 +162,7 @@ export default { // Not inside a b-tabs component return false } - }, + } }, render (h) { let content = h( From 035bae96a72889429d45577ab2bf27084c0d88b3 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:35:08 -0400 Subject: [PATCH 49/83] Update tabs.js --- src/components/tabs/tabs.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 093868e31bb..e444fafda03 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -18,9 +18,12 @@ const BTabButtonHelper = { }, methods: { focus () { - if (this.$refs.link && this.refs.link.focus) { - this.$refs.link.focus() - } + // wrap in nextTick to ensure DOM has completed rendering + this.$nextTick(() => { + if (this.$refs && this.$refs.link && this.refs.link.focus) { + this.$refs.link.focus() + } + }) }, handleEvt (evt) { function stop () { From 62756f32c56800f3d64b1b5ddc9264f07338d7be Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:38:55 -0400 Subject: [PATCH 50/83] Update tab.js --- src/components/tabs/tab.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index a3df513ebd4..5ed240affb9 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -122,7 +122,8 @@ export default { }, updated () { // Force the tab button content to update (since slots are not reactive) - if (this.bTabs.updateButton) { + // Only done if we have a title slot, as the title prop is reactive + if (this.$slots.title && this.bTabs.updateButton) { this.bTabs.updateButton(this) } }, From 0650d530031758229f8a55e6b2912f4ff15b0765 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 20:44:29 -0400 Subject: [PATCH 51/83] Update tab.js --- src/components/tabs/tab.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 5ed240affb9..f78c4dbd7d4 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -152,15 +152,15 @@ export default { if (this.bTabs.activateTab && !this.disabled) { return this.bTabs.activateTab(this) } else { - // Not inside a b-tabs component + // Not inside a b-tabs component or tab is disabled return false } }, deactivate () { - if (this.bTabs.deactivateTab) { + if (this.bTabs.deactivateTab && this.localActive) { return this.bTabs.deactivateTab(this) } else { - // Not inside a b-tabs component + // Not inside a b-tabs component or not active to begin with return false } } From 2cc62b85689855978189b9cc48f82550e354d422 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 22:56:52 -0400 Subject: [PATCH 52/83] Update tabs.js --- src/components/tabs/tabs.js | 138 ++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 75 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index e444fafda03..efa0ab8c3d2 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -18,12 +18,9 @@ const BTabButtonHelper = { }, methods: { focus () { - // wrap in nextTick to ensure DOM has completed rendering - this.$nextTick(() => { - if (this.$refs && this.$refs.link && this.refs.link.focus) { - this.$refs.link.focus() - } - }) + if (this.$refs && this.$refs.link && this.refs.link.focus) { + this.$refs.link.focus() + } }, handleEvt (evt) { function stop () { @@ -106,7 +103,9 @@ const BTabButtonHelper = { } // Filter function to filter out disabled tabs -const notDisabled = tab => !tab.disabled +function notDisabled (tab) { + return !tab.disabled +} // @vue/component export default { @@ -128,10 +127,6 @@ export default { type: Boolean, default: false }, - value: { - type: Number, - default: null - }, pills: { type: Boolean, default: false @@ -177,6 +172,11 @@ export default { navWrapperClass: { type: [String, Array, Object], default: null + }, + value: { + // v-model + type: Number, + default: null } }, data () { @@ -198,32 +198,33 @@ export default { }, watch: { currentTab (val, old) { - /* istanbul ignore if */ - if (val === old) { - return - } + let index = -1 // Ensure only one tab is active at most this.tabs.forEach((tab, idx) => { - tab.localActive = val === idx && !tab.disabled + if (val === idx && !tab.disabled) { + tab.localActive = true + index = idx + } else { + tab.localActive = false + } }) - this.$emit('input', val) + // update the v-model + this.$emit('input', index) }, value (val, old) { - /* istanbul ignore if */ - if (val === old) { - return - } - val = parseInt(val, 10) - old = parseInt(old, 10) || 0 - const tabs = this.tabs - if (tabs[val] && !tabs[val].disabled) { - this.currentTab = val - } else { - // Try next/prev tabs - if (val < old) { - this.previousTab() + if (val !== old) { + val = parseInt(val, 10) + old = parseInt(old, 10) || 0 + const tabs = this.tabs + if (tabs[val] && !tabs[val].disabled) { + this.currentTab = val } else { - this.nextTab() + // Try next or prev tabs + if (val < old) { + this.previousTab() + } else { + this.nextTab() + } } } } @@ -235,7 +236,7 @@ export default { mounted () { // In case tabs have changed before mount this.updateTabs() - // Observe Child changes so we can notify tabs change + // Observe Child changes so we can update list of tabs observeDom(this.$refs.tabsContainer, this.updateTabs.bind(this), { subtree: false }) @@ -295,91 +296,78 @@ export default { // Activate a tab given a b-tab instance // Also accessed by b-tab activateTab (tab) { + let result = false if (tab) { - if (tab.disabled) { - return false - } else { - const index = this.tabs.indexOf(tab) + const index = this.tabs.indexOf(tab) + if (!tab.disabled && index > -1) { + result = true this.currentTab = index - return index > -1 } - } else { - return false } + this.$emit('input', this.currentTab) + return result }, // Deactivate a tab given a b-tab instance // Accessed by b-tab deactivateTab (tab) { if (tab) { // Find first non-disabled tab that isn't the one being deactivated - const newTab = this.tabs.filter(t => t !== tab).find(notDisabled) - if (newTab) { - // Activate found tab (which will deactivate calling tab) - this.activateTab(newTab) - return true - } else { - // No tab to fall back to - return false - } + // If no available tabs, then don't deactivate current tab + return this.activateTab(this.tabs.filter(t => t !== tab).find(notDisabled)) + } else { + // No tab specified + reutrn false } }, // Focus a tab button given it's b-tab instance focusButton (tab) { - const button = this.getButtonForTab(tab) - if (button && button.focus) { - button.focus() - } + // Wrap in nextTick to ensure DOM has completed rendering/updating before focusing + this.$nextTick(() => { + const button = this.getButtonForTab(tab) + if (button && button.focus) { + button.focus() + } + }) }, // Move to first non-disabled tab firstTab (focus) { const tab = this.tabs.find(notDisabled) - this.activateTab(tab) - if (focus) { + if (this.activateTab(tab) && focus) { this.focusButton(tab) } }, // Move to previous non-disabled tab previousTab (focus) { const currentIndex = Math.max(this.currentTab, 0) - const tabs = this.tabs.slice(0, currentIndex).reverse() - const tab = tabs.find(notDisabled) - if (tab) { - this.activateTab(tab) - if (focus) { - this.focusButton(tab) - } - } else { - this.$emit('input', this.currentTab) + const tab = this.tabs.slice(0, currentIndex).reverse().find(notDisabled) + if (this.activateTab(tab) && focus) { + this.focusButton(tab) } }, // Move to next non-disabled tab nextTab (focus) { const currentIndex = Math.max(this.currentTab, -1) const tab = this.tabs.slice(currentIndex + 1).find(notDisabled) - if (tab) { - this.activateTab(tab) - if (focus) { - this.focusButton(tab) - } - } else { - this.$emit('input', this.currentTab) + if (this.activateTab(tab) && focus) { + this.focusButton(tab) } }, // Move to last non-disabled tab lastTab (focus) { const tab = this.tabs.slice().reverse().find(notDisabled) - this.activateTab(tab) - if (focus) { + if (this.activateTab(tab) && focus) { this.focusButton(tab) } } }, render (h) { const tabs = this.tabs - // Navigation 'buttons' + // Currently active tab let activeTab = tabs.find(tab => tab.localActive && !tab.disabled) + // Tab button to allow focusing when no actgive tab found (keynav only) const fallbackTab = tabs.find(tab => !tab.disabled) - // For each b-tab found + + // For each b-tab found create teh tab buttons const buttons = tabs.map((tab, index) => { const buttonId = tab.controlledBy || this.safeId(`_BV_tab_${index + 1}_`) let tabindex = null @@ -395,7 +383,7 @@ export default { return h( BTabButtonHelper, { - key: buttonId || index, + key: tab._uid || buttonId || index, ref: 'buttons', // Needed to make this.$refs.buttons an array refInFor: true, From 180acc0d9fc9985e7977d21bd0f8bd9035dff8ef Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 23:15:15 -0400 Subject: [PATCH 53/83] lint --- 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 efa0ab8c3d2..04ee9583358 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -316,7 +316,7 @@ export default { return this.activateTab(this.tabs.filter(t => t !== tab).find(notDisabled)) } else { // No tab specified - reutrn false + return false } }, // Focus a tab button given it's b-tab instance From 367dde3f7a4a26ed52acd9dcfac25b41d45fe82c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sat, 12 Jan 2019 23:19:28 -0400 Subject: [PATCH 54/83] lint --- src/components/tabs/tabs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tabs.js b/src/components/tabs/tabs.js index 04ee9583358..baba9013b64 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -103,7 +103,7 @@ const BTabButtonHelper = { } // Filter function to filter out disabled tabs -function notDisabled (tab) { +function notDisabled (tab) { return !tab.disabled } @@ -332,7 +332,7 @@ export default { // Move to first non-disabled tab firstTab (focus) { const tab = this.tabs.find(notDisabled) - if (this.activateTab(tab) && focus) { + if (this.activateTab(tab) && focus) { this.focusButton(tab) } }, From 895925f5d62a8b8d49e69dc1f0283efd56c625e4 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 00:14:15 -0400 Subject: [PATCH 55/83] debugging --- 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 baba9013b64..1144e24a9fa 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -18,7 +18,9 @@ const BTabButtonHelper = { }, methods: { focus () { + console.log('Btn Focus...', this) if (this.$refs && this.$refs.link && this.refs.link.focus) { + console.log('Btn Focus $refs', this.$refs) this.$refs.link.focus() } }, From 6befabdcdb4cb3b4d814a327ab0e70bf6fd9d4f6 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 00:37:09 -0400 Subject: [PATCH 56/83] 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 1144e24a9fa..8373340b448 100644 --- a/src/components/tabs/tabs.js +++ b/src/components/tabs/tabs.js @@ -18,9 +18,7 @@ const BTabButtonHelper = { }, methods: { focus () { - console.log('Btn Focus...', this) - if (this.$refs && this.$refs.link && this.refs.link.focus) { - console.log('Btn Focus $refs', this.$refs) + if (this.$refs && this.$refs.link && this.$refs.link.focus) { this.$refs.link.focus() } }, From e4309269a882f0d6e839037341e07a2a67016b76 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 01:22:30 -0400 Subject: [PATCH 57/83] Create tab.spec.js --- src/components/tabs/tab.spec.js | 190 ++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 src/components/tabs/tab.spec.js diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js new file mode 100644 index 00000000000..6febc568a83 --- /dev/null +++ b/src/components/tabs/tab.spec.js @@ -0,0 +1,190 @@ +import Tab from './tab' +import { mount } from '@vue/test-utils' + +describe('tab', async () => { + it('default has expected classes, attributes and structure', async () => { + const wrapper = mount(Tab) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('tab-pane') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + expect(wrapper.attributes('role')).toBe('tabpanel') + expect(wrapper.attributes('aria-hidden')).toBe('true') + expect(wrapper.attributes('aria-expanded')).toBe('false') + expect(wrapper.attributes('labelledby')).not.toBeDefined() + expect(wrapper.attributes('tabindex')).not.toBeDefined() + expect(wrapper.attributes('id')).toBeDefined() + }) + + it('default has expected data state', async () => { + const wrapper = mount(Tab) + + expect(wrapper.vm._isTab).toBe(true) + expect(wrapper.vm.localActive).toBe(false) + expect(wrapper.vm.show).toBe(false) + }) + + it('has class disabled when disabled=true', async () => { + const wrapper = mount(Tab, { + propsData: { disabled: true } + }) + + expect(wrapper.classes()).toContain('disabled') + expect(wrapper.classes()).toContain('tab-pane') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + }) + + it('has class active when active=true', async () => { + const wrapper = mount(Tab, { + propsData: { active: true } + }) + + expect(wrapper.classes()).toContain('active') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + }) + + it('does not have class active when active=true and disabled=true', async () => { + const wrapper = mount(Tab, { + propsData: { + active: true, + disabled: true + } + }) + + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).toContain('disabled') + expect(wrapper.classes()).toContain('tab-pane') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + }) + + it('has class active when localActive becomes true', async () => { + const wrapper = mount(Tab) + + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + + wrapper.setData({ localActive: true }) + + expect(wrapper.classes()).toContain('active') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + }) + + it('emits event `update:active' when localActive becomes true', async () => { + const wrapper = mount(Tab) + + let called = false + let value = null + wrapper.$on('update:active', (val) => { + called = true + value = val + }) + + expect(called).toBe(false) + expect(value).toBe(null) + + wrapper.setData({ localActive: true }) + + expect(called).toBe(true) + expect(value).toBe(true) + }) + + it('has class fade when parent has fade=true', async () => { + const wrapper = mount(Tab, { + inject: { + bTabs: { + fade: true, + lazy: false, + card: false, + noKeyNav: true + } + } + }) + + expect(wrapper.classes()).toContain('fade') + expect(wrapper.classes()).toContain('tab-pane') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('card-body') + }) + + it('has class card-body when parent has card=true', async () => { + const wrapper = mount(Tab, { + inject: { + bTabs: { + fade: false, + lazy: false, + card: true, + noKeyNav: true + } + } + }) + + expect(wrapper.classes()).toContain('card-body') + expect(wrapper.classes()).toContain('tab-pane') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + }) + + it('does not have class card-body when parent has card=true and prop no-body is set', async () => { + const wrapper = mount(Tab, { + inject: { + bTabs: { + fade: false, + lazy: false, + card: true, + noKeyNav: true + } + }, + propsData: { + noCard: true + } + }) + + expect(wrapper.classes()).not.toContain('card-body') + expect(wrapper.classes()).toContain('tab-pane') + expect(wrapper.classes()).not.toContain('fade') + expect(wrapper.classes()).not.toContain('disabled') + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('fade') + }) + + it('has attribute tabindex="0" when parent has keynav enabled', async () => { + const wrapper = mount(Tab, { + inject: { + bTabs: { + fade: false, + lazy: false, + card: false, + noKeyNav: false + } + } + }) + + expect(wrapper.attributes('tabindex')).toBeDefined() + expect(wrapper.attributes('tabindex')).toBe('0') + }) +}) From 4fd39998df819ca1b610516f7917805cdf3ac148 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 01:25:37 -0400 Subject: [PATCH 58/83] lint --- src/components/tabs/tab.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 6febc568a83..3d068eda5c8 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -87,7 +87,7 @@ describe('tab', async () => { expect(wrapper.classes()).not.toContain('card-body') }) - it('emits event `update:active' when localActive becomes true', async () => { + it('emits event "update:active" when localActive becomes true', async () => { const wrapper = mount(Tab) let called = false From 92102b89ab98a6ceb4ea19de23e932a151aa993c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 01:32:05 -0400 Subject: [PATCH 59/83] lint tests --- src/components/tabs/tab.spec.js | 64 ++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 3d068eda5c8..8746e1e25b9 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -45,7 +45,7 @@ describe('tab', async () => { const wrapper = mount(Tab, { propsData: { active: true } }) - + expect(wrapper.classes()).toContain('active') expect(wrapper.classes()).not.toContain('disabled') expect(wrapper.classes()).not.toContain('show') @@ -60,7 +60,7 @@ describe('tab', async () => { disabled: true } }) - + expect(wrapper.classes()).not.toContain('active') expect(wrapper.classes()).toContain('disabled') expect(wrapper.classes()).toContain('tab-pane') @@ -71,7 +71,7 @@ describe('tab', async () => { it('has class active when localActive becomes true', async () => { const wrapper = mount(Tab) - + expect(wrapper.classes()).not.toContain('active') expect(wrapper.classes()).not.toContain('disabled') expect(wrapper.classes()).not.toContain('show') @@ -89,7 +89,7 @@ describe('tab', async () => { it('emits event "update:active" when localActive becomes true', async () => { const wrapper = mount(Tab) - + let called = false let value = null wrapper.$on('update:active', (val) => { @@ -108,12 +108,14 @@ describe('tab', async () => { it('has class fade when parent has fade=true', async () => { const wrapper = mount(Tab, { - inject: { - bTabs: { - fade: true, - lazy: false, - card: false, - noKeyNav: true + provide () { + return { + bTabs: { + fade: true, + lazy: false, + card: false, + noKeyNav: true + } } } }) @@ -129,12 +131,14 @@ describe('tab', async () => { it('has class card-body when parent has card=true', async () => { const wrapper = mount(Tab, { - inject: { - bTabs: { - fade: false, - lazy: false, - card: true, - noKeyNav: true + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: true, + noKeyNav: true + } } } }) @@ -150,12 +154,14 @@ describe('tab', async () => { it('does not have class card-body when parent has card=true and prop no-body is set', async () => { const wrapper = mount(Tab, { - inject: { - bTabs: { - fade: false, - lazy: false, - card: true, - noKeyNav: true + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: true, + noKeyNav: true + } } }, propsData: { @@ -174,12 +180,14 @@ describe('tab', async () => { it('has attribute tabindex="0" when parent has keynav enabled', async () => { const wrapper = mount(Tab, { - inject: { - bTabs: { - fade: false, - lazy: false, - card: false, - noKeyNav: false + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: false, + noKeyNav: false + } } } }) From bc991f7a67877399815ffc7976f0ca0703cf77f9 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 01:40:05 -0400 Subject: [PATCH 60/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 8746e1e25b9..83a185412be 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -195,4 +195,34 @@ describe('tab', async () => { expect(wrapper.attributes('tabindex')).toBeDefined() expect(wrapper.attributes('tabindex')).toBe('0') }) + + it('calls parent\'s updateButton() when title slot provided', async () => { + let called = false + let vm = null + const wrapper = mount(Tab, { + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: false, + noKeyNav: false, + updateButton(tab) { + called = true + vm = tab + return true + } + } + } + }, + slots: { + default: 'foobar' + } + }) + + wrapper.setData({ localActive: true }) + + expect(called).toBe(true) + expect(vm).toEqual(wrapper.vm) + }) }) From c0a853138d44a32f33b8a3dea4dc33b18c460b80 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 01:47:43 -0400 Subject: [PATCH 61/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 83a185412be..856594dc717 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -5,6 +5,10 @@ describe('tab', async () => { it('default has expected classes, attributes and structure', async () => { const wrapper = mount(Tab) + expect(wrapper).toBeDefined() + + await wrapper.vm.$nextTick() + expect(wrapper.is('div')).toBe(true) expect(wrapper.classes()).toContain('tab-pane') expect(wrapper.classes()).not.toContain('disabled') @@ -92,7 +96,7 @@ describe('tab', async () => { let called = false let value = null - wrapper.$on('update:active', (val) => { + wrapper.vm.$on('update:active', (val) => { called = true value = val }) @@ -125,7 +129,6 @@ describe('tab', async () => { expect(wrapper.classes()).not.toContain('disabled') expect(wrapper.classes()).not.toContain('active') expect(wrapper.classes()).not.toContain('show') - expect(wrapper.classes()).not.toContain('fade') expect(wrapper.classes()).not.toContain('card-body') }) @@ -165,7 +168,7 @@ describe('tab', async () => { } }, propsData: { - noCard: true + noBody: true } }) @@ -178,7 +181,7 @@ describe('tab', async () => { expect(wrapper.classes()).not.toContain('fade') }) - it('has attribute tabindex="0" when parent has keynav enabled', async () => { + it('has attribute tabindex="0" when parent has keynav enabled and active', async () => { const wrapper = mount(Tab, { provide () { return { @@ -189,7 +192,8 @@ describe('tab', async () => { noKeyNav: false } } - } + }, + propsData: { active: true } }) expect(wrapper.attributes('tabindex')).toBeDefined() @@ -207,7 +211,7 @@ describe('tab', async () => { lazy: false, card: false, noKeyNav: false, - updateButton(tab) { + updateButton (tab) { called = true vm = tab return true @@ -216,7 +220,7 @@ describe('tab', async () => { } }, slots: { - default: 'foobar' + title: 'foobar' } }) From bcaa2e05cdef90d0e1563df4c4a4a69c4a48c826 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:07:57 -0400 Subject: [PATCH 62/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 70 +++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 856594dc717..a1eb47ef97e 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -229,4 +229,74 @@ describe('tab', async () => { expect(called).toBe(true) expect(vm).toEqual(wrapper.vm) }) + + it('emits event "update:active" when prop active changes and calls de/activateTab', async () => { + let updateCalled = false + let value = null + let activateCalled = false + let activateVm = null + let deactivateCalled = false + let deactivateVm = null + + const wrapper = mount(Tab, { + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: false, + noKeyNav: false, + activateTab (tab) { + called = true + vm = tab + return true + }, + deactivateTab (tab) { + called = true + vm = tab + return true + } + } + } + } + }) + + wrapper.vm.$on('update:active', (val) => { + updateCalled = true + value = val + }) + + expect(updateCalled).toBe(false) + expect(value).toBe(null) + expect(activateCalled).toBe(false) + expect(activateVm).toBe(null) + expect(deactivateCalled).toBe(false) + expect(deactivateVm).toBe(null) + expect(wrapper.vm.localActive).toBe(false) + + wrapper.setProps({ active: true }) + + expect(updateCalled).toBe(true) + expect(value).toBe(true) + expect(activateCalled).toBe(true) + expect(activateVm).toBe(wrapper.vm) + expect(deactivateCalled).toBe(false) + expect(deactivateVm).toBe(null) + expect(wrapper.vm.localActive).toBe(true) + + updateCalled = false + value = null + activateCalled = false + activateVm = null + + wrapper.setProps({ active: false }) + + expect(updateCalled).toBe(true) + expect(value).toBe(false) + expect(activateCalled).toBe(false) + expect(activateVm).toBe(null) + expect(deactivateCalled).toBe(true) + expect(deactivateVm).toBe(wrapper.vm) + expect(wrapper.vm.localActive).toBe(false) + }) }) From 54a32b0db18de9e76a747a9c2f397a4e4e6c630b Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:10:12 -0400 Subject: [PATCH 63/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index a1eb47ef97e..fc85cc0f3dd 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -247,13 +247,13 @@ describe('tab', async () => { card: false, noKeyNav: false, activateTab (tab) { - called = true - vm = tab + activateCalled = true + activateVm = tab return true }, deactivateTab (tab) { - called = true - vm = tab + deactivateCalled = true + deactivateVm = tab return true } } @@ -283,7 +283,7 @@ describe('tab', async () => { expect(deactivateCalled).toBe(false) expect(deactivateVm).toBe(null) expect(wrapper.vm.localActive).toBe(true) - + updateCalled = false value = null activateCalled = false From 5232e5481fd229731cdee6f0fecf349fc8c1558c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:15:09 -0400 Subject: [PATCH 64/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index fc85cc0f3dd..4637339f282 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -230,7 +230,7 @@ describe('tab', async () => { expect(vm).toEqual(wrapper.vm) }) - it('emits event "update:active" when prop active changes and calls de/activateTab', async () => { + it('calls parent de/activateTab() when prop active changes', async () => { let updateCalled = false let value = null let activateCalled = false @@ -276,7 +276,7 @@ describe('tab', async () => { wrapper.setProps({ active: true }) - expect(updateCalled).toBe(true) + expect(updateCalled).toBe(false) expect(value).toBe(true) expect(activateCalled).toBe(true) expect(activateVm).toBe(wrapper.vm) @@ -291,7 +291,7 @@ describe('tab', async () => { wrapper.setProps({ active: false }) - expect(updateCalled).toBe(true) + expect(updateCalled).toBe(false) expect(value).toBe(false) expect(activateCalled).toBe(false) expect(activateVm).toBe(null) From 9698e6c6e0e7aaa41255f46e770809aeaea0fd9d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:20:47 -0400 Subject: [PATCH 65/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 4637339f282..bc6ac8ea4d0 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -277,7 +277,7 @@ describe('tab', async () => { wrapper.setProps({ active: true }) expect(updateCalled).toBe(false) - expect(value).toBe(true) + expect(value).toBe(null) expect(activateCalled).toBe(true) expect(activateVm).toBe(wrapper.vm) expect(deactivateCalled).toBe(false) @@ -292,7 +292,7 @@ describe('tab', async () => { wrapper.setProps({ active: false }) expect(updateCalled).toBe(false) - expect(value).toBe(false) + expect(value).toBe(null) expect(activateCalled).toBe(false) expect(activateVm).toBe(null) expect(deactivateCalled).toBe(true) From e84a9c8b77bd4ff3f992ab86a0f883e69f270c8a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:22:45 -0400 Subject: [PATCH 66/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index bc6ac8ea4d0..0da1bdd236a 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -272,7 +272,6 @@ describe('tab', async () => { expect(activateVm).toBe(null) expect(deactivateCalled).toBe(false) expect(deactivateVm).toBe(null) - expect(wrapper.vm.localActive).toBe(false) wrapper.setProps({ active: true }) @@ -282,7 +281,6 @@ describe('tab', async () => { expect(activateVm).toBe(wrapper.vm) expect(deactivateCalled).toBe(false) expect(deactivateVm).toBe(null) - expect(wrapper.vm.localActive).toBe(true) updateCalled = false value = null @@ -297,6 +295,5 @@ describe('tab', async () => { expect(activateVm).toBe(null) expect(deactivateCalled).toBe(true) expect(deactivateVm).toBe(wrapper.vm) - expect(wrapper.vm.localActive).toBe(false) }) }) From 4420bfa1725e16668188e6f98c82ff3c8f15da7d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:26:27 -0400 Subject: [PATCH 67/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 0da1bdd236a..078c6aa8786 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -231,8 +231,6 @@ describe('tab', async () => { }) it('calls parent de/activateTab() when prop active changes', async () => { - let updateCalled = false - let value = null let activateCalled = false let activateVm = null let deactivateCalled = false @@ -266,8 +264,6 @@ describe('tab', async () => { value = val }) - expect(updateCalled).toBe(false) - expect(value).toBe(null) expect(activateCalled).toBe(false) expect(activateVm).toBe(null) expect(deactivateCalled).toBe(false) @@ -275,22 +271,18 @@ describe('tab', async () => { wrapper.setProps({ active: true }) - expect(updateCalled).toBe(false) - expect(value).toBe(null) expect(activateCalled).toBe(true) expect(activateVm).toBe(wrapper.vm) expect(deactivateCalled).toBe(false) expect(deactivateVm).toBe(null) - updateCalled = false - value = null activateCalled = false activateVm = null + deactivateCalled = false + deactivateVm = null wrapper.setProps({ active: false }) - expect(updateCalled).toBe(false) - expect(value).toBe(null) expect(activateCalled).toBe(false) expect(activateVm).toBe(null) expect(deactivateCalled).toBe(true) From 58a9c26ca90409b23c8d4b4a82f145d7df0839e5 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:27:47 -0400 Subject: [PATCH 68/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 078c6aa8786..ae32ee07783 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -259,11 +259,6 @@ describe('tab', async () => { } }) - wrapper.vm.$on('update:active', (val) => { - updateCalled = true - value = val - }) - expect(activateCalled).toBe(false) expect(activateVm).toBe(null) expect(deactivateCalled).toBe(false) From d364914cc608ce280c1ac5d66c87c84254892f33 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:31:30 -0400 Subject: [PATCH 69/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index ae32ee07783..7fb43d1f0bb 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -247,11 +247,13 @@ describe('tab', async () => { activateTab (tab) { activateCalled = true activateVm = tab + tab.localActive = true return true }, deactivateTab (tab) { deactivateCalled = true deactivateVm = tab + tab.localActive = false return true } } From ddf4b93297940fd8742df8f7ebbf117a04183927 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 02:42:39 -0400 Subject: [PATCH 70/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 7fb43d1f0bb..4897f6db091 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -285,4 +285,70 @@ describe('tab', async () => { expect(deactivateCalled).toBe(true) expect(deactivateVm).toBe(wrapper.vm) }) + + it('does not call parent activateTab() when prop active changes and disabled=true', async () => { + let activateCalled = false + let activateVm = null + + const wrapper = mount(Tab, { + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: false, + noKeyNav: false, + activateTab (tab) { + activateCalled = true + activateVm = tab + tab.localActive = true + return true + } + } + } + }, + propsData: { disabled: true } + }) + + expect(activateCalled).toBe(false) + expect(activateVm).toBe(null) + + wrapper.setProps({ active: true }) + + expect(activateCalled).toBe(false) + expect(activateVm).toBe(null) + }) + + it('does not call parent deactivateTab() when deactivate() called and not active', async () => { + let deactivateCalled = false + let deactivateVm = null + + const wrapper = mount(Tab, { + provide () { + return { + bTabs: { + fade: false, + lazy: false, + card: false, + noKeyNav: false, + deactivateTab (tab) { + deactivateCalled = true + deactivateVm = tab + tab.localActive = false + return true + } + } + } + } + }) + + expect(deactivateCalled).toBe(false) + expect(deactivateVm).toBe(null) + + const result = wrapper.vm.deactivate() + + expect(deactivateCalled).toBe(false) + expect(deactivateVm).toBe(null) + expect(result).toBe(false) + }) }) From bb4d7cbddd35f8e9bed772418a8939b84a626e0c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:00:53 -0400 Subject: [PATCH 71/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 4897f6db091..d6f91bbe813 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -351,4 +351,31 @@ describe('tab', async () => { expect(deactivateVm).toBe(null) expect(result).toBe(false) }) + + it('has class show when localActive becomes true', async () => { + const wrapper = mount(Tab) + + expect(wrapper.classes()).not.toContain('active') + expect(wrapper.classes()).not.toContain('show') + + wrapper.setData({ localActive: true }) + + expect(wrapper.classes()).toContain('active') + expect(wrapper.classes()).not.toContain('show') + + // JSDOM doesnt support requestAnimationFrame + // So it falls back to setTimeout. So we advance the time + jest.runAllTimers() + await wrapper.vm.$nextTick() + + expect(wrapper.classes()).toContain('show') + expect(wrapper.classes()).toContain('active') + + wrapper.setData({ localActive: false }) + jest.runAllTimers() + await wrapper.vm.$nextTick() + + expect(wrapper.classes()).not.toContain('show') + expect(wrapper.classes()).not.toContain('active') + }) }) From 93a44734fc9d5d15aec4fdd588230d3d39130e99 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:07:24 -0400 Subject: [PATCH 72/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index d6f91bbe813..98490feb38c 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -353,6 +353,7 @@ describe('tab', async () => { }) it('has class show when localActive becomes true', async () => { + jest.useFakeTimers() const wrapper = mount(Tab) expect(wrapper.classes()).not.toContain('active') @@ -365,14 +366,14 @@ describe('tab', async () => { // JSDOM doesnt support requestAnimationFrame // So it falls back to setTimeout. So we advance the time - jest.runAllTimers() + jest.runAllPendingTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).toContain('show') expect(wrapper.classes()).toContain('active') wrapper.setData({ localActive: false }) - jest.runAllTimers() + jest.runAllPendingTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).not.toContain('show') From f56f6a61d1a41c047c2bbd59f2210d8b283d3322 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:10:07 -0400 Subject: [PATCH 73/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 98490feb38c..24f0ed3ff6d 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -366,14 +366,14 @@ describe('tab', async () => { // JSDOM doesnt support requestAnimationFrame // So it falls back to setTimeout. So we advance the time - jest.runAllPendingTimers() + jest.runAllTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).toContain('show') expect(wrapper.classes()).toContain('active') wrapper.setData({ localActive: false }) - jest.runAllPendingTimers() + jest.runAllTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).not.toContain('show') From c76125f56f671f9f9927186cf934ddb6a9ebcd3c Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:20:33 -0400 Subject: [PATCH 74/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 24f0ed3ff6d..8742421470a 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -353,27 +353,24 @@ describe('tab', async () => { }) it('has class show when localActive becomes true', async () => { - jest.useFakeTimers() - const wrapper = mount(Tab) + jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { + cb() + }) + + const wrapper = mount(Tab, { + attachToDocument: true + }) expect(wrapper.classes()).not.toContain('active') expect(wrapper.classes()).not.toContain('show') wrapper.setData({ localActive: true }) - - expect(wrapper.classes()).toContain('active') - expect(wrapper.classes()).not.toContain('show') - - // JSDOM doesnt support requestAnimationFrame - // So it falls back to setTimeout. So we advance the time - jest.runAllTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).toContain('show') expect(wrapper.classes()).toContain('active') wrapper.setData({ localActive: false }) - jest.runAllTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).not.toContain('show') From 95d70d24fb4cdf210a5fd6c3fe9ec1385d78d18a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:26:44 -0400 Subject: [PATCH 75/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index 8742421470a..bb3bdfd54f4 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -1,6 +1,8 @@ import Tab from './tab' import { mount } from '@vue/test-utils' +jest.useFakeTimers() + describe('tab', async () => { it('default has expected classes, attributes and structure', async () => { const wrapper = mount(Tab) @@ -358,19 +360,28 @@ describe('tab', async () => { }) const wrapper = mount(Tab, { - attachToDocument: true + attachToDocument: true, + provide () { + return { + bTabs: { + fade: true + } + } + } }) expect(wrapper.classes()).not.toContain('active') expect(wrapper.classes()).not.toContain('show') wrapper.setData({ localActive: true }) + jest.runAllTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).toContain('show') expect(wrapper.classes()).toContain('active') wrapper.setData({ localActive: false }) + jest.runAllTimers() await wrapper.vm.$nextTick() expect(wrapper.classes()).not.toContain('show') From ba297a75731cad7eea40d63ded57c97870140e94 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:34:23 -0400 Subject: [PATCH 76/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index bb3bdfd54f4..b5934179f5d 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -355,9 +355,7 @@ describe('tab', async () => { }) it('has class show when localActive becomes true', async () => { - jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => { - cb() - }) + window.requestAnimationFrame = (cb) => { setTimeout(cb, 0) } const wrapper = mount(Tab, { attachToDocument: true, @@ -374,6 +372,7 @@ describe('tab', async () => { expect(wrapper.classes()).not.toContain('show') wrapper.setData({ localActive: true }) + await wrapper.vm.$nextTick() jest.runAllTimers() await wrapper.vm.$nextTick() @@ -381,6 +380,7 @@ describe('tab', async () => { expect(wrapper.classes()).toContain('active') wrapper.setData({ localActive: false }) + await wrapper.vm.$nextTick() jest.runAllTimers() await wrapper.vm.$nextTick() From 95653b9eb14518f2908cdc548382b38e056ad99a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:39:48 -0400 Subject: [PATCH 77/83] Update tab.spec.js --- src/components/tabs/tab.spec.js | 34 --------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/components/tabs/tab.spec.js b/src/components/tabs/tab.spec.js index b5934179f5d..29732e07226 100644 --- a/src/components/tabs/tab.spec.js +++ b/src/components/tabs/tab.spec.js @@ -353,38 +353,4 @@ describe('tab', async () => { expect(deactivateVm).toBe(null) expect(result).toBe(false) }) - - it('has class show when localActive becomes true', async () => { - window.requestAnimationFrame = (cb) => { setTimeout(cb, 0) } - - const wrapper = mount(Tab, { - attachToDocument: true, - provide () { - return { - bTabs: { - fade: true - } - } - } - }) - - expect(wrapper.classes()).not.toContain('active') - expect(wrapper.classes()).not.toContain('show') - - wrapper.setData({ localActive: true }) - await wrapper.vm.$nextTick() - jest.runAllTimers() - await wrapper.vm.$nextTick() - - expect(wrapper.classes()).toContain('show') - expect(wrapper.classes()).toContain('active') - - wrapper.setData({ localActive: false }) - await wrapper.vm.$nextTick() - jest.runAllTimers() - await wrapper.vm.$nextTick() - - expect(wrapper.classes()).not.toContain('show') - expect(wrapper.classes()).not.toContain('active') - }) }) From 2064ade51d3fad28d942310bfd989c3b83b29087 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:41:16 -0400 Subject: [PATCH 78/83] Update tab.js --- src/components/tabs/tab.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index f78c4dbd7d4..016d27f04e8 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -129,7 +129,7 @@ export default { }, methods: { // Transition handlers - beforeEnter () { + beforeEnter () /* instanbul ignore next: difficult to test rAF in JSDOM */ { // change opacity (add 'show' class) 1 frame after display // otherwise css transition won't happen // TODO: Move raf method into utils/dom.js @@ -143,7 +143,7 @@ export default { raf(() => { this.show = true }) }, - beforeLeave () { + beforeLeave () /* instanbul ignore next: difficult to test rAF in JSDOM */ { // Remove the 'show' class this.show = false }, From 173603febd1ee15cd1fcaf46b1acb64d408ddd26 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 03:52:36 -0400 Subject: [PATCH 79/83] Update tab.js --- src/components/tabs/tab.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index 016d27f04e8..d0fe818bb13 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -114,6 +114,14 @@ export default { } } } + }, + disabled (newVal, oldVal) { + if (newVal !== oldVal) { + if (!newVal && this.localActive && this.bTabs.firstTab) { + this.localActive = false + this.bTabs.firstTab() + } + } } }, mounted () { From c2070887b22eb8f7795fcd3fa2305c4c9e8bbe92 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 04:25:21 -0400 Subject: [PATCH 80/83] Update tab.js --- src/components/tabs/tab.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tabs/tab.js b/src/components/tabs/tab.js index d0fe818bb13..3a941119741 100644 --- a/src/components/tabs/tab.js +++ b/src/components/tabs/tab.js @@ -117,7 +117,7 @@ export default { }, disabled (newVal, oldVal) { if (newVal !== oldVal) { - if (!newVal && this.localActive && this.bTabs.firstTab) { + if (newVal && this.localActive && this.bTabs.firstTab) { this.localActive = false this.bTabs.firstTab() } From 4bdca69ed6a7bd50e45dee0985099b27aa53f0b3 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 13 Jan 2019 04:35:43 -0400 Subject: [PATCH 81/83] Update README.md --- src/components/tabs/README.md | 88 +++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 29 deletions(-) diff --git a/src/components/tabs/README.md b/src/components/tabs/README.md index 4d3659d6a4a..85e0c27d275 100644 --- a/src/components/tabs/README.md +++ b/src/components/tabs/README.md @@ -20,14 +20,15 @@ For navigation based tabs, use the [``](/docs/components/nav) component a ``` **Tip:** You should supply each child `` component a unique `key` value if dynamically -adding, removing, showing, or hiding `` components. The `key` attribute is a special Vue -attribute, see https://vuejs.org/v2/api/#key). +adding or removing `` components (i.e. `v-if` or for loops). The `key` attribute is a +special Vue attribute, see https://vuejs.org/v2/api/#key). + ## Cards Integration Tabs support integrating with bootstrap cards. Just add the `card` property to ``. and place -it inside a `` component. Note that you should add `no-body` prop on `` component in -order to propertly decorate the card header and remove the extra padding introduced by `card-body`. +it inside a `` component. Note that you should add `no-body` prop on the `` component +in order to propertly decorate the card header and remove the extra padding introduced by `card-body`. ```html @@ -72,9 +73,10 @@ When `` is in `card` mode, each `` sub-component will automatical ``` -Setting the `no-body` prop on `` will have no affect when `` is not in `card` mode +**Note:** Setting the `no-body` prop on `` will have no affect when `` is not in `card` mode (as the `card-body` class is only set when in `card` mode). + ## Pills variant Tabs use the `tabs` styling by default. Just add `pills` property to `` for the pill style @@ -174,9 +176,7 @@ additional custom styling._ ## Fade animation -Fade is enabled by default when changing tabs. It can disabled with `no-fade` property. Note you -should use the `` component when adding contentless-tabs to maintain correct sizing and -alignment. See the advanced usage examples below for an example. +Fade is enabled by default when changing tabs. It can disabled with `no-fade` property. ## Add Tabs without content @@ -193,25 +193,38 @@ If you want to add extra tabs that do not have any content, you can put them in ``` +**Note:** extra (contentless) tabs should be a `` or have the class `nav-item` +with a root element of `
  • ` for proper rendering. + + ## Add custom content to tab title -If you want to add custom content to tab title, like HTML code, icons, or another Vue component, -this possible by using `title` slot +If you want to add custom content to tab title, like HTML code, icons, or another +non-interactive Vue component, this possible by using `title` slot ```html - - + + +

    Tab Contents 1

    +
    + - Tab Contents 1 +

    Tab Contents 2

    ``` +**Do not** place inteactive elements/components inside the title slot. The tab button is a +link which does not support child interactive elements per the HTML5 spec. + + ## Apply custom classes to the generated nav-tabs or pills The tab selectors are based on Boostrap V4's `nav` markup ( i.e. @@ -258,31 +271,46 @@ need to accomodate your custom classes for this._ ## Keyboard Navigation -Keyboard navigation is enabled by default. +Keyboard navigation is enabled by default for ARIA compliance with tablists. -| Keypress | Action | -| --------------------------------------------------------------------- | ---------------------------------------- | -| LEFT or UP | Move to the previous non-disabled tab | -| RIGHT or DOWN | Move to the next non-disabled tab | -| SHIFT+LEFT or SHIFT+UP | Move to the first non-disabled tab | -| SHIFT+RIGHT or SHIFT+DOWN | Move to the last non-disabled tab | -| TAB | Move to the next control on the page | -| SHIFT+TAB | Move to the previous control on the page | +| Keypress | Action | +| --------------------------------------------------------------------- | ----------------------------------------- | +| LEFT or UP | Activate the previous non-disabled tab | +| RIGHT or DOWN | Activate the next non-disabled tab | +| SHIFT+LEFT or SHIFT+UP | Activate the first non-disabled tab | +| HOME | Activate the first non-disabled tab | +| SHIFT+RIGHT or SHIFT+DOWN | Activate the last non-disabled tab | +| END | Activate the last non-disabled tab | +| TAB | Move focus to the active tab content | +| SHIFT+TAB | Move to the previous control on the page | -Disable it by setting the prop `no-key-nav`. Behavior will now default to standard browser +Disable keyboard navigation by setting the prop `no-key-nav`. Behavior will now default to standard browser navigation with TAB key. | Keypress | Action | | ------------------------------- | ----------------------------------------------- | | TAB | Move to the next tab or control on the page | | SHIFT+TAB | Move to the previous tab or control on the page | +| ENTER | Activate current focused tab | + + +## Dynamically activating and deactivating tabs + +Use the `` v-model to control which tab is active by setting the v-model to +the index (zero-based) of the tab to be shown. + +Alternatively, you can use the `active` prop on each `` with the `.sync` modifier +to activate the tab, or detect if a particular tab is active. + +Each `` instance also provides two public methods to activate or deactivate +the tab. The methods are `.activate()` and `.deactivate()`, respectively. If activation +or deactivaton fails (i.e. a tab is disabled or no tab is available to move activation +to), then the currently active tab will remain active and the method will return `false`. -**Caution:** If you have text or text-like inputs in your tabs, leave keyboard navigation off, as it -is not possble to use key presses to jump out of a text (or test-like) inputs. ## Advanced Examples -### External controls +### External controls using v-model ```html