Skip to content

Commit ad0a23d

Browse files
authored
Merge pull request #10 from microcipcip/feature/useMouseLeavePage
feat(useMouseLeavePage): Adding useMouseLeavePage function
2 parents eb01041 + 5e1747c commit ad0a23d

File tree

12 files changed

+263
-4
lines changed

12 files changed

+263
-4
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ Vue.use(VueCompositionAPI);
8484
[![Demo](https://img.shields.io/badge/advanced_demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouse--advanced-demo)
8585
- [`useMouseElement`](./src/components/useMouseElement/stories/useMouseElement.md) — tracks the mouse position relative to given element.
8686
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseelement--demo)
87+
- [`useMouseLeavePage`](./src/components/useMouseLeavePage/stories/useMouseLeavePage.md) — tracks when mouse leaves page boundaries.
88+
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usemouseleavepage--demo)
8789
- [`useSearchParams`](./src/components/useSearchParams/stories/useSearchParams.md) — tracks browser's location search params.
8890
[![Demo](https://img.shields.io/badge/demo-🚀-yellow.svg)](https://microcipcip.github.io/vue-use-kit/?path=/story/sensors-usesearchparams--demo)
8991
- Animations

src/components/useGeolocation/stories/useGeolocation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function useGeolocation(
3939
### Returns
4040

4141
- `geo: Ref<UseGeolocation>` the geolocation object
42-
- `isTracking: Ref<boolean>` whether the function is tracking the user's location or not
42+
- `isTracking: Ref<boolean>` whether this function events are running or not
4343
- `start: Function` the function used for starting the geolocation tracking
4444
- `stop: Function` the function used for stopping the geolocation tracking
4545

src/components/useIdle/stories/useIdle.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ function useIdle(
2626
### Returns
2727

2828
- `isIdle: Ref<boolean>` it is `true` when the user is idle, `false` otherwise
29-
- `isTracking: Ref<boolean>` whether the function is tracking the user idle state or not
29+
- `isTracking: Ref<boolean>` whether this function events are running or not
3030
- `start: Function` the function used for start tracking the user's idle state
3131
- `stop: Function` the function used for stop tracking the user's idle state
3232

src/components/useMediaDevices/stories/useMediaDevices.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ function useMediaDevices(
3030
### Returns
3131

3232
- `devicesState: Ref<UseMediaDevicesState[]>` the list of connected media devices
33-
- `isTracking: Ref<boolean>` whether the function is tracking the connected media devices or not
33+
- `isTracking: Ref<boolean>` whether this function events are running or not
3434
- `isTracked: Ref<boolean>` whether the connected devices have been successfully tracked
3535
- `start: Function` the function used to start tracking the connected media devices
3636
- `stop: Function` the function used to stop tracking the connected media devices
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './useMouseLeavePage'
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<template>
2+
<div class="wrapper">
3+
<table class="table is-fullwidth">
4+
<thead>
5+
<tr>
6+
<th>Prop</th>
7+
<th>Value</th>
8+
</tr>
9+
</thead>
10+
<tbody>
11+
<tr>
12+
<td>hasLeftPage</td>
13+
<td>{{ hasLeftPage }}</td>
14+
</tr>
15+
<tr>
16+
<td colspan="2">
17+
<button class="button is-primary" @click="start" v-if="!isTracking">
18+
Start tracking mouse leave event
19+
</button>
20+
<button class="button is-danger" @click="stop" v-else>
21+
Stop tracking mouse leave event
22+
</button>
23+
</td>
24+
</tr>
25+
</tbody>
26+
</table>
27+
</div>
28+
</template>
29+
30+
<script lang="ts">
31+
import Vue from 'vue'
32+
import { useMouseLeavePage } from '@src/vue-use-kit'
33+
34+
export default Vue.extend({
35+
name: 'UseMouseLeavePageDemo',
36+
setup() {
37+
const { hasLeftPage, isTracking, start, stop } = useMouseLeavePage()
38+
return { hasLeftPage, isTracking, start, stop }
39+
}
40+
})
41+
</script>
42+
43+
<style scoped>
44+
.wrapper {
45+
min-height: 100vh;
46+
}
47+
</style>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# useMouseLeavePage
2+
3+
Vue function that tracks when mouse leaves page boundaries.
4+
5+
## Reference
6+
7+
```typescript
8+
function useMouseLeavePage(
9+
runOnMount?: boolean
10+
): {
11+
hasLeftPage: Ref<boolean>
12+
isTracking: Ref<boolean>
13+
start: () => void
14+
stop: () => void
15+
}
16+
```
17+
18+
### Parameters
19+
20+
- `runOnMount: boolean` whether to check mouse leaves page boundaries tracking on mount, `true` by default
21+
22+
### Returns
23+
24+
- `hasLeftPage: Ref<boolean>` whether the mouse has left the page or not
25+
- `isTracking: Ref<boolean>` whether this function events are running or not
26+
- `start: Function` the function used to start tracking when the mouse leaves the page boundaries
27+
- `stop: Function` the function used to stop tracking when the mouse leaves the page boundaries
28+
29+
## Usage
30+
31+
```html
32+
<template>
33+
<div>
34+
<div>
35+
hasLeftPage:
36+
<pre>{{ hasLeftPage }}</pre>
37+
</div>
38+
<div>
39+
<button @click="start" v-if="!isTracking">
40+
Start tracking mouse leave event
41+
</button>
42+
<button @click="stop" v-else>Stop tracking mouse leave event</button>
43+
</div>
44+
</div>
45+
</template>
46+
47+
<script lang="ts">
48+
import Vue from 'vue'
49+
import { useMouseLeavePage } from 'vue-use-kit'
50+
51+
export default Vue.extend({
52+
name: 'UseMouseLeavePageDemo',
53+
setup() {
54+
const { hasLeftPage, isTracking, start, stop } = useMouseLeavePage()
55+
return { hasLeftPage, isTracking, start, stop }
56+
}
57+
})
58+
</script>
59+
```
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { storiesOf } from '@storybook/vue'
2+
import path from 'path'
3+
import StoryTitle from '@src/helpers/StoryTitle.vue'
4+
import UseMouseLeavePageDemo from './UseMouseLeavePageDemo.vue'
5+
6+
const functionName = 'useMouseLeavePage'
7+
const functionPath = path.resolve(__dirname, '..')
8+
const notes = require(`./${functionName}.md`).default
9+
10+
const basicDemo = () => ({
11+
components: { StoryTitle, demo: UseMouseLeavePageDemo },
12+
template: `
13+
<div class="container">
14+
<story-title
15+
function-path="${functionPath}"
16+
source-name="${functionName}"
17+
demo-name="UseMouseLeavePageDemo.vue"
18+
>
19+
<template v-slot:title></template>
20+
<template v-slot:intro>
21+
<p>
22+
<strong>Try moving the mouse outside the page boundaries</strong> to see the
23+
hasLeftPage variable change on the fly.
24+
</p>
25+
<p>
26+
Please note that because this demo is within an iframe
27+
<strong>it will seem that the callback is firing when you are still within the page.</strong>
28+
Click the "Open canvas in new tab" button on the
29+
top right hand corner to see the demo without iframe.
30+
</p>
31+
</template>
32+
</story-title>
33+
<demo />
34+
</div>`
35+
})
36+
37+
storiesOf('sensors|useMouseLeavePage', module)
38+
.addParameters({ notes })
39+
.add('Demo', basicDemo)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { mount } from '@src/helpers/test'
2+
import { useMouseLeavePage } from '@src/vue-use-kit'
3+
4+
afterEach(() => {
5+
jest.clearAllMocks()
6+
})
7+
8+
const testComponent = (onMount = true) => ({
9+
template: `
10+
<div>
11+
<div id="isTracking" v-if="isTracking"></div>
12+
<div id="hasLeftPage" v-if="hasLeftPage"></div>
13+
<button id="start" @click="start"></button>
14+
<button id="stop" @click="stop"></button>
15+
</div>
16+
`,
17+
setup() {
18+
const { hasLeftPage, isTracking, start, stop } = useMouseLeavePage(onMount)
19+
return { hasLeftPage, isTracking, start, stop }
20+
}
21+
})
22+
23+
describe('useMouseLeavePage', () => {
24+
const eventName = 'mouseout'
25+
it('should call mouseout onMounted', async () => {
26+
const addEventListenerSpy = jest.spyOn(document, 'addEventListener')
27+
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener')
28+
const wrapper = mount(testComponent())
29+
await wrapper.vm.$nextTick()
30+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
31+
expect(addEventListenerSpy).toBeCalledWith(eventName, expect.any(Function))
32+
33+
// Destroy instance to check if the remove event listener is being called
34+
wrapper.destroy()
35+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
36+
expect(removeEventListenerSpy).toBeCalledWith(
37+
eventName,
38+
expect.any(Function)
39+
)
40+
})
41+
42+
it('should call document.addEventListener again when start is called', async () => {
43+
const addEventListenerSpy = jest.spyOn(document, 'addEventListener')
44+
const wrapper = mount(testComponent())
45+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1)
46+
wrapper.find('#stop').trigger('click')
47+
48+
// Wait for Vue to append #start in the DOM
49+
await wrapper.vm.$nextTick()
50+
wrapper.find('#start').trigger('click')
51+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1 * 2)
52+
})
53+
54+
it('should call document.removeEventListener when stop is called', async () => {
55+
const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener')
56+
const wrapper = mount(testComponent())
57+
wrapper.find('#stop').trigger('click')
58+
59+
// Wait for Vue to append #start in the DOM
60+
await wrapper.vm.$nextTick()
61+
expect(removeEventListenerSpy).toHaveBeenCalledTimes(1)
62+
})
63+
64+
it('should show #isTracking when onMount is true', async () => {
65+
const wrapper = mount(testComponent(true))
66+
await wrapper.vm.$nextTick()
67+
expect(wrapper.find('#isTracking').exists()).toBe(true)
68+
})
69+
70+
it('should not show #isTracking when onMount is false', async () => {
71+
const wrapper = mount(testComponent(false))
72+
await wrapper.vm.$nextTick()
73+
expect(wrapper.find('#isTracking').exists()).toBe(false)
74+
})
75+
76+
it('should not show #hasLeftPage when onMount is false', async () => {
77+
const wrapper = mount(testComponent(false))
78+
await wrapper.vm.$nextTick()
79+
expect(wrapper.find('#hasLeftPage').exists()).toBe(false)
80+
})
81+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { ref, onMounted, onUnmounted, Ref } from '@vue/composition-api'
2+
3+
export function useMouseLeavePage(runOnMount = true) {
4+
const isTracking = ref(false)
5+
const hasLeftPage = ref(false)
6+
7+
const handleMouseOut = (e: any) => {
8+
const from = e.relatedTarget || e.toElement
9+
const mouseHasLeftPage = !from || from.nodeName === 'HTML'
10+
hasLeftPage.value = mouseHasLeftPage
11+
}
12+
13+
const start = () => {
14+
if (isTracking.value) return
15+
document.addEventListener('mouseout', handleMouseOut)
16+
isTracking.value = true
17+
}
18+
19+
const stop = () => {
20+
if (!isTracking.value) return
21+
document.removeEventListener('mouseout', handleMouseOut)
22+
isTracking.value = false
23+
}
24+
25+
onMounted(() => runOnMount && start())
26+
onUnmounted(stop)
27+
28+
return { hasLeftPage, isTracking, start, stop }
29+
}

src/components/useSearchParams/stories/useSearchParams.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function useSearchParams(
2424
### Returns
2525

2626
- `searchParams: Ref<object>` the object containing the search parameters as key value pairs
27-
- `isTracking: Ref<boolean>` whether the function is tracking the user's location search parameters or not
27+
- `isTracking: Ref<boolean>` whether this function events are running or not
2828
- `start: Function` the function used to start tracking the location search parameters
2929
- `stop: Function` the function used to stop tracking the location search parameters
3030

src/vue-use-kit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './components/useMedia'
1212
export * from './components/useMediaDevices'
1313
export * from './components/useMouse'
1414
export * from './components/useMouseElement'
15+
export * from './components/useMouseLeavePage'
1516
export * from './components/useSearchParams'
1617

1718
export * from './components/useIntervalFn'

0 commit comments

Comments
 (0)