Skip to content
Permalink
Browse files
feat(v-b-hover): new directive for reacting to hover changes (#4771)
  • Loading branch information
tmorehouse committed Feb 15, 2020
1 parent 1e02769 commit b7adc6dc726f75c0578b3de5208f112bef58b4ad
Showing 11 changed files with 243 additions and 160 deletions.
@@ -0,0 +1,89 @@
# Hover

> `v-b-hover` is a lightweight directive that allows you to react when an element either becomes
> hovered or unhovered.
The `v-b-hover` directive can be used as an alternative to using custom CSS to handle hover states.

The `v-b-hover` directive was added in version `2.5.0`.

## Overview

- `v-b-hover` will call your callback method with a boolean value indicating if the element is
hovered or not.
- The directive can be placed on almost any element or component.
- Internally, BootstrapVue uses this directive in several components.

## Directive syntax and usage

```html
<div v-b-hover="callback">content</div>
```

Where callback is required:

- A function reference that will be called whenever hover state changes. The callback is passed a
single boolean argument. `true` indicates that the element (or component) is hovered by the users
pointing device, or `false` if the element is not hovered.

The directive has no modifiers.

### Usage example

```html
<template>
<div v-b-hover="hoverHandler"> ... </div>
</template>

<script>
export default {
methods: {
hoverHandler(isHovered) {
if (isHovered) {
// Do something
} else {
// Do something else
}
}
}
}
</script>
```

## Live example

In the following, we are swapping icons and tet color depending on the hover state of the element:

```html
<template>
<div>
<div v-b-hover="handleHover" class="border rounded py-3 px-4">
<b-icon v-if="isHovered" icon="battery-full" scale="2"></b-icon>
<b-icon v-else icon="battery" scale="2"></b-icon>
<span class="ml-2" :class="isHovered ? 'text-danger' : ''">Hover this area</span>
</div>
</div>
</template>

<script>
export default {
data() {
return {
isHovered: false
}
},
methods: {
handleHover(hovered) {
this.isHovered = hovered
}
}
}
</script>

<!-- b-v-hover-example.vue -->
```

## Accessibility concerns

Hover state should not be used to convey special meaning, as screen reader users and keyboard only
users typically ac not typically trigger hover state on elements.
@@ -0,0 +1,53 @@
// v-b-hover directive
import { isBrowser } from '../../utils/env'
import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events'
import { isFunction } from '../../utils/inspect'

// --- Constants ---

const PROP = '__BV_hover_handler__'
const MOUSEENTER = 'mouseenter'
const MOUSELEAVE = 'mouseleave'

// --- Utility methods ---

const createListener = handler => {
const listener = evt => {
handler(evt.type === MOUSEENTER, evt)
}
listener.fn = handler
return listener
}

const updateListeners = (on, el, listener) => {
eventOnOff(on, el, MOUSEENTER, listener, EVENT_OPTIONS_NO_CAPTURE)
eventOnOff(on, el, MOUSELEAVE, listener, EVENT_OPTIONS_NO_CAPTURE)
}

// --- Directive bind/unbind/update handler ---

const directive = (el, { value: handler = null }) => {
if (isBrowser) {
const listener = el[PROP]
const hasListener = isFunction(listener)
const handlerChanged = !(hasListener && listener.fn === handler)
if (hasListener && handlerChanged) {
updateListeners(false, el, listener)
delete el[PROP]
}
if (isFunction(handler) && handlerChanged) {
el[PROP] = createListener(handler)
updateListeners(true, el, el[PROP])
}
}
}

// VBHover directive

export const VBHover = {
bind: directive,
componentUpdated: directive,
unbind(el) {
directive(el, { value: null })
}
}
@@ -0,0 +1,61 @@
import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
import { waitNT } from '../../../tests/utils'
import { VBHover } from './hover'

describe('v-b-hover directive', () => {
it('works', async () => {
const localVue = new CreateLocalVue()
let hovered1 = false
let hovered2 = false
const App = localVue.extend({
data() {
return {
text: 'FOO',
changeHandler: false
}
},
directives: {
BHover: VBHover
},
methods: {
handleHover1(isHovered) {
hovered1 = isHovered
},
handleHover2(isHovered) {
hovered2 = isHovered
}
},
template: `<div v-b-hover="changeHandler ? handleHover2 : handleHover1"><span>{{ text }}</span></div>`
})
const wrapper = mount(App)

expect(wrapper.isVueInstance()).toBe(true)
expect(hovered1).toBe(false)

wrapper.trigger('mouseenter')
await waitNT(wrapper.vm)

expect(hovered1).toBe(true)

wrapper.trigger('mouseleave')
await waitNT(wrapper.vm)

expect(hovered1).toBe(false)

wrapper.setData({ text: 'BAR' })

wrapper.trigger('mouseenter')
await waitNT(wrapper.vm)

expect(hovered1).toBe(true)

wrapper.setData({ changeHandler: true })

wrapper.trigger('mouseenter')
await waitNT(wrapper.vm)

expect(hovered2).toBe(true)

wrapper.destroy()
})
})
@@ -0,0 +1,11 @@
//
// VBHover
//
import Vue, { DirectiveOptions } from 'vue'
import { BvPlugin } from '../../'

// Plugin
export declare const VBHoverPlugin: BvPlugin

// directive: v-b-hover
export declare const VBHover: DirectiveOptions
@@ -0,0 +1,8 @@
import { VBHover } from './hover'
import { pluginFactory } from '../../utils/plugins'

const VBHoverPlugin = /*#__PURE__*/ pluginFactory({
directives: { VBHover }
})

export { VBHoverPlugin, VBHover }
@@ -0,0 +1,14 @@
{
"name": "@bootstrap-vue/v-b-hover",
"version": "1.0.0",
"meta": {
"title": "Hover",
"description": "A lightweight directive that allows you to react when an element either becomes hovered or unhovered",
"directive": "VBHover",
"new": true,
"version": "2.5.0",
"expression": [
"Function"
]
}
}
@@ -4,6 +4,7 @@ import { BvPlugin } from '../'
export declare const directivesPlugin: BvPlugin

// Named exports of all directives
export * from './hover'
export * from './modal'
export * from './popover'
export * from './scrollspy'
@@ -1,5 +1,6 @@
import { pluginFactory } from '../utils/plugins'

import { VBHoverPlugin } from './hover'
import { VBModalPlugin } from './modal'
import { VBPopoverPlugin } from './popover'
import { VBScrollspyPlugin } from './scrollspy'
@@ -10,6 +11,7 @@ import { VBVisiblePlugin } from './visible'
// Main plugin for installing all directive plugins
export const directivesPlugin = /*#__PURE__*/ pluginFactory({
plugins: {
VBHoverPlugin,
VBModalPlugin,
VBPopoverPlugin,
VBScrollspyPlugin,
@@ -285,6 +285,10 @@ export { BTooltip } from './components/tooltip/tooltip'
// can be reverted back to `export * from './scrollspy'` when Webpack v5 is released
// https://github.com/webpack/webpack/pull/9203 (available in Webpack v5.0.0-alpha.15)

// export * from './directives/hover'
export { VBHoverPlugin } from './directives/hover'
export { VBHover } from './directives/hover/hover'

// export * from './directives/modal'
export { VBModalPlugin } from './directives/modal'
export { VBModal } from './directives/modal/modal'

This file was deleted.

0 comments on commit b7adc6d

Please sign in to comment.