Skip to content

Commit ea9f877

Browse files
committed
Feat: add inactiveSeconds option ✨
1 parent 3b1dd5e commit ea9f877

File tree

6 files changed

+115
-17
lines changed

6 files changed

+115
-17
lines changed

Diff for: README.md

+15-10
Original file line numberDiff line numberDiff line change
@@ -77,16 +77,17 @@ const {
7777

7878
| Key | Default Value | Type | Required |
7979
| ----------------------------- | ----------------- | ------- | -------- |
80-
| **repeatAfterDays** | 7 | Number | false |
81-
| **scrollPercentageToTrigger** | 0 | Number | false |
82-
| **delaySecondsAndTrigger** | 0 | Number | false |
83-
| **triggerOnExitIntent** | true | Boolean | false |
84-
| **touchDeviceSensitivity** | 15 | Number | false |
85-
| **scrollDebounceMillis** | 300 | Number | false |
86-
| **triggerOnPageLoad** | false | Boolean | false |
87-
| **handleScrollBars** | false | Boolean | false |
88-
| **LSItemKey** | 'vue-exit-intent' | String | false |
89-
| **setupBeforeMount** | false | Boolean | false |
80+
| **repeatAfterDays** | 7 | number | false |
81+
| **scrollPercentageToTrigger** | 0 | number | false |
82+
| **delaySecondsAndTrigger** | 0 | number | false |
83+
| **triggerOnExitIntent** | true | boolean | false |
84+
| **touchDeviceSensitivity** | 15 | number | false |
85+
| **scrollDebounceMillis** | 300 | number | false |
86+
| **triggerOnPageLoad** | false | boolean | false |
87+
| **handleScrollBars** | false | boolean | false |
88+
| **LSItemKey** | 'vue-exit-intent' | string | false |
89+
| **setupBeforeMount** | false | boolean | false |
90+
| **inactiveSeconds** | 0 | number | false |
9091

9192
### Options Description
9293

@@ -137,6 +138,10 @@ const {
137138
Determines whether the initialization of the composable occurs during the `onBeforeMount` lifecycle hook instead of the default `onMounted` hook.
138139
This options allows you to set up the exit intent before your component is mounted.
139140

141+
- **inactiveSeconds**
142+
Delay, in seconds, before activating mouse, touch, and scroll listeners to track user behavior and potentially trigger the popup after an exit intent is detected (mouse leaves the viewport, scroll percentage reached, or fast touch scroll up). This delay helps prevent the immediate display of the popup, ensuring it only appears if the user wants to leave the page after the specified time. Set to 0 to disable this delay.
143+
**This option DELAYS adding mouse, scroll and touch listeners**
144+
140145
## Contribute
141146

142147
Feel free to contribute, message me for your ideas.

Diff for: src/App.vue

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import { useVueExitIntent } from '@/composables/useVueExitIntent.js';
33
44
const options = {
5-
handleScrollBars: true
5+
handleScrollBars: true,
6+
inactiveSeconds: 3
67
};
78
89
const {
@@ -19,8 +20,10 @@ const {
1920
<div id="demo-page">
2021
<h1>Vue Exit Intent Demo Page</h1>
2122
<p>
22-
<b>Desktop</b>: Move your mouse outside the document.<br />
23-
<b>Touch Device</b>: After you scroll down the document, scroll up fast.
23+
<b>Desktop</b>: Wait 3 seconds and move your mouse outside the
24+
document.<br />
25+
<b>Touch Device</b>: Wait 3 seconds and after you scroll down the
26+
document, scroll up fast.
2427
</p>
2528
<p>
2629
Do you want to unsubscribe from this popup, and to not trigger in the
@@ -33,6 +36,8 @@ const {
3336
<button @click="resetState">Reset State</button>
3437
</p>
3538

39+
<pre>options: {{ options }}</pre>
40+
3641
<div class="current-state">
3742
<p><strong>Current State:</strong></p>
3843
<p>isShowing: {{ isShowing }}</p>

Diff for: src/composables/useVueExitIntent.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,20 @@ export function useVueExitIntent(userOptions: Partial<Options> = {}) {
6464
isLocalStorageExpired(options) && !isUnsubscribed.value;
6565
};
6666

67-
const initialize = () => {
67+
const addListeners = () => {
6868
if (options.triggerOnExitIntent) {
6969
if (options.touchDeviceSensitivity && isTouchDevice()) {
7070
addTouchListeners();
7171
} else {
7272
addMouseListener();
7373
}
7474
}
75+
if (options.scrollPercentageToTrigger) {
76+
addScrollListener();
77+
}
78+
};
79+
80+
const initialize = () => {
7581
if (options.delaySecondsAndTrigger) {
7682
setTimeout(() => {
7783
fire();
@@ -80,8 +86,13 @@ export function useVueExitIntent(userOptions: Partial<Options> = {}) {
8086
if (options.triggerOnPageLoad) {
8187
fire();
8288
}
83-
if (options.scrollPercentageToTrigger) {
84-
addScrollListener();
89+
90+
if (options.inactiveSeconds) {
91+
setTimeout(() => {
92+
addListeners();
93+
}, options.inactiveSeconds * 1000);
94+
} else {
95+
addListeners();
8596
}
8697
};
8798

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { test, describe, expect, afterEach, vi } from 'vitest';
2+
import { useVueExitIntent } from '@/composables/useVueExitIntent';
3+
import { shallowMount, mount } from '@vue/test-utils';
4+
import * as utils from '@/utils';
5+
6+
const { defaultOptions } = utils;
7+
8+
describe('Respects option inactiveSeconds', () => {
9+
afterEach(() => {
10+
localStorage.clear();
11+
vi.clearAllTimers();
12+
});
13+
14+
test('Triggers immediately if triggerOnPageLoad is true', async () => {
15+
const userOptions = {
16+
...defaultOptions,
17+
triggerOnPageLoad: true,
18+
inactiveSeconds: 5
19+
};
20+
21+
const App = {
22+
template: `<div></div>`,
23+
setup() {
24+
const { isShowing, close } = useVueExitIntent(userOptions);
25+
26+
return {
27+
isShowing,
28+
close
29+
};
30+
}
31+
};
32+
33+
const wrapper = shallowMount(App);
34+
await wrapper.vm.$nextTick();
35+
expect(wrapper.vm.isShowing).toBe(true);
36+
37+
wrapper.vm.close();
38+
expect(wrapper.vm.isShowing).toBe(false);
39+
});
40+
41+
test('Triggers on mouseleave after inactiveSeconds', async () => {
42+
vi.useFakeTimers();
43+
vi.spyOn(utils, 'isTouchDevice').mockReturnValue(false);
44+
45+
const userOptions = {
46+
...defaultOptions,
47+
inactiveSeconds: 2
48+
};
49+
50+
const App = {
51+
template: `<div></div>`,
52+
setup() {
53+
const { isShowing, close } = useVueExitIntent(userOptions);
54+
55+
return {
56+
isShowing,
57+
close
58+
};
59+
}
60+
};
61+
62+
const wrapper = mount(App);
63+
await wrapper.vm.$nextTick();
64+
expect(wrapper.vm.isShowing).toBe(false);
65+
66+
vi.advanceTimersByTime(1000);
67+
expect(wrapper.vm.isShowing).toBe(false);
68+
69+
vi.advanceTimersByTime(1000);
70+
document.documentElement.dispatchEvent(new MouseEvent('mouseleave'));
71+
expect(wrapper.vm.isShowing).toBe(true);
72+
73+
vi.useRealTimers();
74+
});
75+
});

Diff for: src/types/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type Options = {
99
handleScrollBars: boolean;
1010
LSItemKey: string;
1111
setupBeforeMount: Boolean;
12+
inactiveSeconds: number;
1213
};
1314

1415
export type MouseHandler = {

Diff for: src/utils/defaultOptions.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export const defaultOptions: Options = {
1313
triggerOnPageLoad: false,
1414
handleScrollBars: false,
1515
LSItemKey: 'vue-exit-intent',
16-
setupBeforeMount: false
16+
setupBeforeMount: false,
17+
inactiveSeconds: 0
1718
};

0 commit comments

Comments
 (0)