Skip to content

Commit

Permalink
feat(v-b-hover): new directive for reacting to hover changes (#4771)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmorehouse authored Feb 15, 2020
1 parent 1e02769 commit b7adc6d
Show file tree
Hide file tree
Showing 11 changed files with 243 additions and 160 deletions.
89 changes: 89 additions & 0 deletions src/directives/hover/README.md
Original file line number Diff line number Diff line change
@@ -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.
53 changes: 53 additions & 0 deletions src/directives/hover/hover.js
Original file line number Diff line number Diff line change
@@ -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 })
}
}
61 changes: 61 additions & 0 deletions src/directives/hover/hover.spec.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
11 changes: 11 additions & 0 deletions src/directives/hover/index.d.ts
Original file line number Diff line number Diff line change
@@ -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
8 changes: 8 additions & 0 deletions src/directives/hover/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { VBHover } from './hover'
import { pluginFactory } from '../../utils/plugins'

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

export { VBHoverPlugin, VBHover }
14 changes: 14 additions & 0 deletions src/directives/hover/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
1 change: 1 addition & 0 deletions src/directives/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions src/directives/index.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
61 changes: 0 additions & 61 deletions src/utils/bv-hover-swap.js

This file was deleted.

Loading

0 comments on commit b7adc6d

Please sign in to comment.