diff --git a/src/utils/bv-hover-swap.js b/src/utils/bv-hover-swap.js new file mode 100644 index 00000000000..b605f17d541 --- /dev/null +++ b/src/utils/bv-hover-swap.js @@ -0,0 +1,66 @@ +import Vue from './vue' +import { eventOn, eventOff } from './dom' + +// --- Constants --- + +const EVENT_OPTIONS = { passive: true } + +// @vue/component +export const BVHoverSwap = /*#__PURE__*/ Vue.extend({ + name: 'BVHoverSwap', + props: { + tag: { + type: String, + default: 'div' + }, + parent: { + type: Boolean, + default: false + } + }, + data() { + return { + isHovered: false + } + }, + watch: { + parent() { + this.listen(true) + } + }, + created() { + // Create non-reactive property + this.$_hoverEl = null + }, + mounted() { + this.$nextTick(() => this.listen(true)) + }, + updated() /* istanbul ignore next */ { + this.$nextTick(() => this.listen(true)) + }, + beforeDestroy() { + this.listen(false) + this.$_hoverEl = null + }, + methods: { + listen(on) { + const el = this.parent ? this.$el.parentElement || this.$el : this.$el + if (on && this.$_hoverEl !== el) { + this.listen(false) + this.$_hoverEl = el + } + const method = on ? eventOn : eventOff + method(this.$_hoverEl, 'mouseenter', this.handleHover, EVENT_OPTIONS) + method(this.$_hoverEl, 'mouseleave', this.handleHover, EVENT_OPTIONS) + }, + handleHover(evt) { + this.isHovered = evt.type === 'mouseenter' + } + }, + render(h) { + const $scoped = this.$scopedSlots + const $default = $scoped.default || (() => h()) + const $hovered = $scoped.hovered || $default + return h(this.tag, [this.isHovered ? $hovered() : $default()]) + } +}) diff --git a/src/utils/bv-hover-swap.spec.js b/src/utils/bv-hover-swap.spec.js new file mode 100644 index 00000000000..484c2554637 --- /dev/null +++ b/src/utils/bv-hover-swap.spec.js @@ -0,0 +1,99 @@ +import { mount } from '@vue/test-utils' +import { waitNT } from '../../tests/utils' +import { BVHoverSwap } from './bv-hover-swap' + +describe('utils/bv-hoverswap', () => { + it('works', async () => { + const wrapper = mount(BVHoverSwap, { + slots: { + default: 'FOO', + hovered: 'BAR' + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.text()).toBe('FOO') + + wrapper.trigger('mouseenter') + await waitNT(wrapper.vm) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.text()).toBe('BAR') + + wrapper.trigger('mouseleave') + await waitNT(wrapper.vm) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.text()).toBe('FOO') + + wrapper.destroy() + }) + + it('works when `parent` is true ', async () => { + const app = { + props: { + parent: { + type: Boolean, + defaut: false + } + }, + methods: { + foo() { + return this.$createElement('span', {}, 'FOO') + }, + bar() { + return this.$createElement('span', {}, 'BAR') + } + }, + render(h) { + const $content = h(BVHoverSwap, { + props: { parent: this.parent }, + scopedSlots: { default: this.foo, hovered: this.bar } + }) + return h('div', {}, [$content]) + } + } + const wrapper = mount(app, { + propsData: { + parent: true + } + }) + + expect(wrapper.isVueInstance()).toBe(true) + await waitNT(wrapper.vm) + expect(wrapper.is('div')).toBe(true) + expect(wrapper.find('div > div').exists()).toBe(true) + expect(wrapper.find('div > div').is(BVHoverSwap)).toBe(true) + expect(wrapper.text()).toBe('FOO') + + wrapper.trigger('mouseenter') + await waitNT(wrapper.vm) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.text()).toBe('BAR') + + wrapper.trigger('mouseleave') + await waitNT(wrapper.vm) + + expect(wrapper.text()).toBe('FOO') + + wrapper.setProps({ + parent: false + }) + await waitNT(wrapper.vm) + + wrapper.trigger('mouseenter') + await waitNT(wrapper.vm) + + expect(wrapper.text()).toBe('FOO') + + wrapper.find('div > div').trigger('mouseenter') + await waitNT(wrapper.vm) + + expect(wrapper.text()).toBe('BAR') + + wrapper.destroy() + }) +}) diff --git a/src/utils/bv-transition.js b/src/utils/bv-transition.js index 320acb80b7e..b74e2d74f2a 100644 --- a/src/utils/bv-transition.js +++ b/src/utils/bv-transition.js @@ -24,6 +24,7 @@ const FADE_PROPS = { leaveActiveClass: 'fade' } +// @vue/component export const BVTransition = /*#__PURE__*/ Vue.extend({ name: 'BVTransition', functional: true,