Skip to content

Commit b7adc6d

Browse files
authored
feat(v-b-hover): new directive for reacting to hover changes (bootstrap-vue#4771)
1 parent 1e02769 commit b7adc6d

File tree

11 files changed

+243
-160
lines changed

11 files changed

+243
-160
lines changed

src/directives/hover/README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Hover
2+
3+
> `v-b-hover` is a lightweight directive that allows you to react when an element either becomes
4+
> hovered or unhovered.
5+
6+
The `v-b-hover` directive can be used as an alternative to using custom CSS to handle hover states.
7+
8+
The `v-b-hover` directive was added in version `2.5.0`.
9+
10+
## Overview
11+
12+
- `v-b-hover` will call your callback method with a boolean value indicating if the element is
13+
hovered or not.
14+
- The directive can be placed on almost any element or component.
15+
- Internally, BootstrapVue uses this directive in several components.
16+
17+
## Directive syntax and usage
18+
19+
```html
20+
<div v-b-hover="callback">content</div>
21+
```
22+
23+
Where callback is required:
24+
25+
- A function reference that will be called whenever hover state changes. The callback is passed a
26+
single boolean argument. `true` indicates that the element (or component) is hovered by the users
27+
pointing device, or `false` if the element is not hovered.
28+
29+
The directive has no modifiers.
30+
31+
### Usage example
32+
33+
```html
34+
<template>
35+
<div v-b-hover="hoverHandler"> ... </div>
36+
</template>
37+
38+
<script>
39+
export default {
40+
methods: {
41+
hoverHandler(isHovered) {
42+
if (isHovered) {
43+
// Do something
44+
} else {
45+
// Do something else
46+
}
47+
}
48+
}
49+
}
50+
</script>
51+
```
52+
53+
## Live example
54+
55+
In the following, we are swapping icons and tet color depending on the hover state of the element:
56+
57+
```html
58+
<template>
59+
<div>
60+
<div v-b-hover="handleHover" class="border rounded py-3 px-4">
61+
<b-icon v-if="isHovered" icon="battery-full" scale="2"></b-icon>
62+
<b-icon v-else icon="battery" scale="2"></b-icon>
63+
<span class="ml-2" :class="isHovered ? 'text-danger' : ''">Hover this area</span>
64+
</div>
65+
</div>
66+
</template>
67+
68+
<script>
69+
export default {
70+
data() {
71+
return {
72+
isHovered: false
73+
}
74+
},
75+
methods: {
76+
handleHover(hovered) {
77+
this.isHovered = hovered
78+
}
79+
}
80+
}
81+
</script>
82+
83+
<!-- b-v-hover-example.vue -->
84+
```
85+
86+
## Accessibility concerns
87+
88+
Hover state should not be used to convey special meaning, as screen reader users and keyboard only
89+
users typically ac not typically trigger hover state on elements.

src/directives/hover/hover.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// v-b-hover directive
2+
import { isBrowser } from '../../utils/env'
3+
import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events'
4+
import { isFunction } from '../../utils/inspect'
5+
6+
// --- Constants ---
7+
8+
const PROP = '__BV_hover_handler__'
9+
const MOUSEENTER = 'mouseenter'
10+
const MOUSELEAVE = 'mouseleave'
11+
12+
// --- Utility methods ---
13+
14+
const createListener = handler => {
15+
const listener = evt => {
16+
handler(evt.type === MOUSEENTER, evt)
17+
}
18+
listener.fn = handler
19+
return listener
20+
}
21+
22+
const updateListeners = (on, el, listener) => {
23+
eventOnOff(on, el, MOUSEENTER, listener, EVENT_OPTIONS_NO_CAPTURE)
24+
eventOnOff(on, el, MOUSELEAVE, listener, EVENT_OPTIONS_NO_CAPTURE)
25+
}
26+
27+
// --- Directive bind/unbind/update handler ---
28+
29+
const directive = (el, { value: handler = null }) => {
30+
if (isBrowser) {
31+
const listener = el[PROP]
32+
const hasListener = isFunction(listener)
33+
const handlerChanged = !(hasListener && listener.fn === handler)
34+
if (hasListener && handlerChanged) {
35+
updateListeners(false, el, listener)
36+
delete el[PROP]
37+
}
38+
if (isFunction(handler) && handlerChanged) {
39+
el[PROP] = createListener(handler)
40+
updateListeners(true, el, el[PROP])
41+
}
42+
}
43+
}
44+
45+
// VBHover directive
46+
47+
export const VBHover = {
48+
bind: directive,
49+
componentUpdated: directive,
50+
unbind(el) {
51+
directive(el, { value: null })
52+
}
53+
}

src/directives/hover/hover.spec.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { mount, createLocalVue as CreateLocalVue } from '@vue/test-utils'
2+
import { waitNT } from '../../../tests/utils'
3+
import { VBHover } from './hover'
4+
5+
describe('v-b-hover directive', () => {
6+
it('works', async () => {
7+
const localVue = new CreateLocalVue()
8+
let hovered1 = false
9+
let hovered2 = false
10+
const App = localVue.extend({
11+
data() {
12+
return {
13+
text: 'FOO',
14+
changeHandler: false
15+
}
16+
},
17+
directives: {
18+
BHover: VBHover
19+
},
20+
methods: {
21+
handleHover1(isHovered) {
22+
hovered1 = isHovered
23+
},
24+
handleHover2(isHovered) {
25+
hovered2 = isHovered
26+
}
27+
},
28+
template: `<div v-b-hover="changeHandler ? handleHover2 : handleHover1"><span>{{ text }}</span></div>`
29+
})
30+
const wrapper = mount(App)
31+
32+
expect(wrapper.isVueInstance()).toBe(true)
33+
expect(hovered1).toBe(false)
34+
35+
wrapper.trigger('mouseenter')
36+
await waitNT(wrapper.vm)
37+
38+
expect(hovered1).toBe(true)
39+
40+
wrapper.trigger('mouseleave')
41+
await waitNT(wrapper.vm)
42+
43+
expect(hovered1).toBe(false)
44+
45+
wrapper.setData({ text: 'BAR' })
46+
47+
wrapper.trigger('mouseenter')
48+
await waitNT(wrapper.vm)
49+
50+
expect(hovered1).toBe(true)
51+
52+
wrapper.setData({ changeHandler: true })
53+
54+
wrapper.trigger('mouseenter')
55+
await waitNT(wrapper.vm)
56+
57+
expect(hovered2).toBe(true)
58+
59+
wrapper.destroy()
60+
})
61+
})

src/directives/hover/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// VBHover
3+
//
4+
import Vue, { DirectiveOptions } from 'vue'
5+
import { BvPlugin } from '../../'
6+
7+
// Plugin
8+
export declare const VBHoverPlugin: BvPlugin
9+
10+
// directive: v-b-hover
11+
export declare const VBHover: DirectiveOptions

src/directives/hover/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { VBHover } from './hover'
2+
import { pluginFactory } from '../../utils/plugins'
3+
4+
const VBHoverPlugin = /*#__PURE__*/ pluginFactory({
5+
directives: { VBHover }
6+
})
7+
8+
export { VBHoverPlugin, VBHover }

src/directives/hover/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@bootstrap-vue/v-b-hover",
3+
"version": "1.0.0",
4+
"meta": {
5+
"title": "Hover",
6+
"description": "A lightweight directive that allows you to react when an element either becomes hovered or unhovered",
7+
"directive": "VBHover",
8+
"new": true,
9+
"version": "2.5.0",
10+
"expression": [
11+
"Function"
12+
]
13+
}
14+
}

src/directives/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { BvPlugin } from '../'
44
export declare const directivesPlugin: BvPlugin
55

66
// Named exports of all directives
7+
export * from './hover'
78
export * from './modal'
89
export * from './popover'
910
export * from './scrollspy'

src/directives/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { pluginFactory } from '../utils/plugins'
22

3+
import { VBHoverPlugin } from './hover'
34
import { VBModalPlugin } from './modal'
45
import { VBPopoverPlugin } from './popover'
56
import { VBScrollspyPlugin } from './scrollspy'
@@ -10,6 +11,7 @@ import { VBVisiblePlugin } from './visible'
1011
// Main plugin for installing all directive plugins
1112
export const directivesPlugin = /*#__PURE__*/ pluginFactory({
1213
plugins: {
14+
VBHoverPlugin,
1315
VBModalPlugin,
1416
VBPopoverPlugin,
1517
VBScrollspyPlugin,

src/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ export { BTooltip } from './components/tooltip/tooltip'
285285
// can be reverted back to `export * from './scrollspy'` when Webpack v5 is released
286286
// https://github.com/webpack/webpack/pull/9203 (available in Webpack v5.0.0-alpha.15)
287287

288+
// export * from './directives/hover'
289+
export { VBHoverPlugin } from './directives/hover'
290+
export { VBHover } from './directives/hover/hover'
291+
288292
// export * from './directives/modal'
289293
export { VBModalPlugin } from './directives/modal'
290294
export { VBModal } from './directives/modal/modal'

src/utils/bv-hover-swap.js

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)