Skip to content

Commit 40466a6

Browse files
committed
feat: Adding useClickAway function and tests for useHover function
1 parent 0ac460b commit 40466a6

File tree

10 files changed

+235
-6
lines changed

10 files changed

+235
-6
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ Please check the [documentation](https://microcipcip.github.io/vue-use-kit/) to
2121
- Sensors
2222
- [`useHover`](./src/components/useHover/stories/useHover.md) — tracks mouse hover state of a given element. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usehover--demo)
2323
- [`useMedia`](./src/components/useMedia/stories/useMedia.md) — tracks state of a CSS media query. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemedia--demo)
24-
- [`useMouse`](./src/components/useMouse/stories/useMouse.md) — tracks the mouse's position. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--demo)
25-
- [`useMouseElement`](./src/components/useMouseElement/stories/useMouseElement.md) — tracks the mouse's position relative to given element. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseelement--demo)
24+
- [`useMouse`](./src/components/useMouse/stories/useMouse.md) — tracks the mouse position. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--demo)
25+
- [`useMouseElement`](./src/components/useMouseElement/stories/useMouseElement.md) — tracks the mouse position relative to given element. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseelement--demo)
2626
- Animations
27-
- [`useTimeout`](./src/components/useTimeout/stories/useTimeout.md) — returns `isReady` prop as `true` when timer is completed. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-usetimeout--demo)
27+
- [`useTimeout`](./src/components/useTimeout/stories/useTimeout.md) — returns `isReady` true when timer is completed. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-usetimeout--demo)
2828
- [`useTimeoutFn`](./src/components/useTimeoutFn/stories/useTimeoutFn.md) — calls function when timer is completed. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/animations-usetimeoutfn--demo)
29+
- UI
30+
- [`useClickAway`](./src/components/useClickAway/stories/useClickAway.md) — triggers callback when user clicks outside target area. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/ui-useclickaway--demo)
2931
- Utils
3032
- [`getQuery`](./src/components/getQuery/stories/getQuery.md) — get a CSS media query string. [![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/utils-getquery--demo)
3133

src/components/useClickAway/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useClickAway'
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<template>
2+
<div
3+
class="dropdown"
4+
ref="dropdownRef"
5+
:class="{ 'is-active': isDropdownOpen }"
6+
>
7+
<div class="dropdown-trigger">
8+
<button class="button" @click="openDropdownHandler">
9+
<span>Click me!</span>
10+
<span class="icon is-small"><i class="fas fa-angle-down"></i></span>
11+
</button>
12+
</div>
13+
<div class="dropdown-menu">
14+
<div class="dropdown-content">
15+
<a href="#" class="dropdown-item" @click.prevent="closeDropdownHandler"
16+
>Home</a
17+
>
18+
<a href="#" class="dropdown-item" @click.prevent="closeDropdownHandler"
19+
>About</a
20+
>
21+
</div>
22+
</div>
23+
</div>
24+
</template>
25+
26+
<script lang="ts">
27+
import Vue from 'vue'
28+
import { ref } from '../../../api'
29+
import { useClickAway } from '../../../vue-use-kit'
30+
31+
export default Vue.extend({
32+
name: 'UseClickAwayDemo',
33+
setup() {
34+
const dropdownRef = ref(null)
35+
const isDropdownOpen = ref(false)
36+
const clickAwayHandler = () => {
37+
isDropdownOpen.value = false
38+
}
39+
40+
useClickAway(dropdownRef, clickAwayHandler)
41+
42+
const openDropdownHandler = () => {
43+
isDropdownOpen.value = true
44+
}
45+
46+
const closeDropdownHandler = () => {
47+
isDropdownOpen.value = false
48+
}
49+
50+
return {
51+
dropdownRef,
52+
isDropdownOpen,
53+
openDropdownHandler,
54+
closeDropdownHandler
55+
}
56+
}
57+
})
58+
</script>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# useClickAway
2+
3+
Vue function that triggers callback when user clicks outside of target area.
4+
5+
## Reference
6+
7+
```typescript
8+
useClickAway(
9+
element: Ref<null | Element>,
10+
onClickAway: (e: Event) => void,
11+
events?: string[]
12+
): void;
13+
```
14+
15+
### Parameters
16+
17+
- `element: string` the element to checked for click away events
18+
- `onClickAway: string` the callback to run when triggering a click away
19+
- `events: string` list of events to listen to, defaults to `['mousedown', 'touchstart']`
20+
21+
## Usage
22+
23+
```html
24+
<template>
25+
<div ref="targetRef">
26+
<span>Target area</span>
27+
<div v-if="isDropdownOpen">Dropdown nav</div>
28+
</div>
29+
</template>
30+
31+
<script lang="ts">
32+
import Vue from 'vue'
33+
import { ref } from '../../../api'
34+
import { useClickAway } from 'vue-use-kit'
35+
36+
export default Vue.extend({
37+
name: 'UseClickAwayDemo',
38+
setup() {
39+
const targetRef = ref(null)
40+
const isDropdownOpen = ref(true)
41+
const clickAwayHandler = () => {
42+
isDropdownOpen.value = false
43+
}
44+
useClickAway(targetRef, clickAwayHandler)
45+
46+
return { targetRef, isDropdownOpen }
47+
}
48+
})
49+
</script>
50+
```
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import StoryTitle from '../../../helpers/StoryTitle.vue'
3+
import UseClickAwayDemo from './UseClickAwayDemo.vue'
4+
5+
const storiesPath = __dirname
6+
const notes = require('./useClickAway.md').default
7+
8+
const basicDemo = () => ({
9+
components: { StoryTitle, demo: UseClickAwayDemo },
10+
template: `
11+
<div class="container">
12+
<story-title stories-path="${storiesPath}" file-name="UseClickAwayDemo.vue">
13+
<template v-slot:title></template>
14+
<template v-slot:intro>
15+
<p>
16+
Open the dropdown below then <strong>click anywhere outside</strong> to close it.
17+
</p>
18+
</template>
19+
</story-title>
20+
<demo />
21+
</div>`
22+
})
23+
24+
storiesOf('ui|useClickAway', module)
25+
.addParameters({ notes })
26+
.add('Demo', basicDemo)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { mount } from '../../helpers/test'
2+
import { ref } from '../../api'
3+
import { useClickAway } from '../../vue-use-kit'
4+
5+
const testComponent = () => ({
6+
template: `
7+
<div id="outside-el">
8+
<div ref="dropdownRef" id="dropdown-trigger"></div>
9+
<div id="dropdown" v-if="isDropdownOpen"></div>
10+
</div>
11+
`,
12+
setup() {
13+
const dropdownRef = ref(null)
14+
const isDropdownOpen = ref(false)
15+
16+
useClickAway(dropdownRef, () => {
17+
isDropdownOpen.value = true
18+
})
19+
20+
return { dropdownRef, isDropdownOpen }
21+
}
22+
})
23+
24+
describe('useClickAway', () => {
25+
it('should call document.addEventListener', async () => {
26+
const addEventListenerSpy = jest.spyOn(document, 'addEventListener')
27+
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener')
28+
expect(addEventListenerSpy).not.toHaveBeenCalled()
29+
const wrapper = mount(testComponent())
30+
await wrapper.vm.$nextTick()
31+
expect(addEventListenerSpy).toHaveBeenCalled()
32+
33+
// Destroy instance to check if the remove event listener is being called
34+
wrapper.destroy()
35+
expect(removeEventListenerSpy).toHaveBeenCalled()
36+
addEventListenerSpy.mockClear()
37+
})
38+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { onMounted, onUnmounted, Ref } from '../../api'
2+
3+
const defaultEvents = ['mousedown', 'touchstart']
4+
5+
export function useClickAway(
6+
element: Ref<null | Element>,
7+
onClickAway: (e: Event) => void,
8+
events = defaultEvents
9+
) {
10+
const handler = (e: Event) => {
11+
if (element.value && !element.value.contains(e.target as Node)) {
12+
onClickAway(e)
13+
}
14+
}
15+
16+
onMounted(() => {
17+
events.forEach(evtName => document.addEventListener(evtName, handler))
18+
})
19+
20+
onUnmounted(() => {
21+
events.forEach(evtName => document.removeEventListener(evtName, handler))
22+
})
23+
}
Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
1+
import { mount } from '../../helpers/test'
2+
import { ref } from '../../api'
3+
import { useHover } from '../../vue-use-kit'
4+
5+
const elementContainer: any = { ref: { value: null } }
6+
const testComponent = () => ({
7+
template: `
8+
<div id="isHovered" v-if="isHovered"></div>
9+
`,
10+
setup() {
11+
const elRef = ref(document.body as any)
12+
const isHovered = useHover(elRef)
13+
return { isHovered }
14+
}
15+
})
16+
117
describe('useHover', () => {
2-
it('should do something', () => {
3-
// Add test here
18+
it('should call document.body hover events', () => {
19+
const addEventListenerSpy = jest.spyOn(document.body, 'addEventListener')
20+
const removeEventListenerSpy = jest.spyOn(
21+
document.body,
22+
'removeEventListener'
23+
)
24+
const wrapper = mount(testComponent())
25+
expect(addEventListenerSpy).toHaveBeenCalled()
26+
expect(removeEventListenerSpy).not.toHaveBeenCalled()
27+
wrapper.destroy()
28+
expect(removeEventListenerSpy).toHaveBeenCalled()
29+
})
30+
31+
it('should return isHovered false by default', () => {
32+
const wrapper = mount(testComponent())
33+
expect(wrapper.find('#isHovered').exists()).toBe(false)
434
})
535
})

src/components/useSampleComponent/stories/useSampleComponent.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Vue function that...
55
## Reference
66

77
```typescript
8-
// const valueReturned = useSampleComponent()
8+
// useSampleComponent()
99
```
1010

1111
### Parameters

src/vue-use-kit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './components/getQuery'
2+
export * from './components/useClickAway'
23
export * from './components/useMedia'
34
export * from './components/useMouse'
45
export * from './components/useMouseElement'

0 commit comments

Comments
 (0)