Skip to content

Commit

Permalink
Feat: add inactiveSeconds option ✨
Browse files Browse the repository at this point in the history
  • Loading branch information
nickap committed Aug 10, 2024
1 parent 3b1dd5e commit ea9f877
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 17 deletions.
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,17 @@ const {

| Key | Default Value | Type | Required |
| ----------------------------- | ----------------- | ------- | -------- |
| **repeatAfterDays** | 7 | Number | false |
| **scrollPercentageToTrigger** | 0 | Number | false |
| **delaySecondsAndTrigger** | 0 | Number | false |
| **triggerOnExitIntent** | true | Boolean | false |
| **touchDeviceSensitivity** | 15 | Number | false |
| **scrollDebounceMillis** | 300 | Number | false |
| **triggerOnPageLoad** | false | Boolean | false |
| **handleScrollBars** | false | Boolean | false |
| **LSItemKey** | 'vue-exit-intent' | String | false |
| **setupBeforeMount** | false | Boolean | false |
| **repeatAfterDays** | 7 | number | false |
| **scrollPercentageToTrigger** | 0 | number | false |
| **delaySecondsAndTrigger** | 0 | number | false |
| **triggerOnExitIntent** | true | boolean | false |
| **touchDeviceSensitivity** | 15 | number | false |
| **scrollDebounceMillis** | 300 | number | false |
| **triggerOnPageLoad** | false | boolean | false |
| **handleScrollBars** | false | boolean | false |
| **LSItemKey** | 'vue-exit-intent' | string | false |
| **setupBeforeMount** | false | boolean | false |
| **inactiveSeconds** | 0 | number | false |

### Options Description

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

- **inactiveSeconds**
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.
**This option DELAYS adding mouse, scroll and touch listeners**

## Contribute

Feel free to contribute, message me for your ideas.
Expand Down
11 changes: 8 additions & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import { useVueExitIntent } from '@/composables/useVueExitIntent.js';
const options = {
handleScrollBars: true
handleScrollBars: true,
inactiveSeconds: 3
};
const {
Expand All @@ -19,8 +20,10 @@ const {
<div id="demo-page">
<h1>Vue Exit Intent Demo Page</h1>
<p>
<b>Desktop</b>: Move your mouse outside the document.<br />
<b>Touch Device</b>: After you scroll down the document, scroll up fast.
<b>Desktop</b>: Wait 3 seconds and move your mouse outside the
document.<br />
<b>Touch Device</b>: Wait 3 seconds and after you scroll down the
document, scroll up fast.
</p>
<p>
Do you want to unsubscribe from this popup, and to not trigger in the
Expand All @@ -33,6 +36,8 @@ const {
<button @click="resetState">Reset State</button>
</p>

<pre>options: {{ options }}</pre>

<div class="current-state">
<p><strong>Current State:</strong></p>
<p>isShowing: {{ isShowing }}</p>
Expand Down
17 changes: 14 additions & 3 deletions src/composables/useVueExitIntent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,20 @@ export function useVueExitIntent(userOptions: Partial<Options> = {}) {
isLocalStorageExpired(options) && !isUnsubscribed.value;
};

const initialize = () => {
const addListeners = () => {
if (options.triggerOnExitIntent) {
if (options.touchDeviceSensitivity && isTouchDevice()) {
addTouchListeners();
} else {
addMouseListener();
}
}
if (options.scrollPercentageToTrigger) {
addScrollListener();
}
};

const initialize = () => {
if (options.delaySecondsAndTrigger) {
setTimeout(() => {
fire();
Expand All @@ -80,8 +86,13 @@ export function useVueExitIntent(userOptions: Partial<Options> = {}) {
if (options.triggerOnPageLoad) {
fire();
}
if (options.scrollPercentageToTrigger) {
addScrollListener();

if (options.inactiveSeconds) {
setTimeout(() => {
addListeners();
}, options.inactiveSeconds * 1000);
} else {
addListeners();
}
};

Expand Down
75 changes: 75 additions & 0 deletions src/tests/composables/useVueExitIntent/inactiveSeconds.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { test, describe, expect, afterEach, vi } from 'vitest';
import { useVueExitIntent } from '@/composables/useVueExitIntent';
import { shallowMount, mount } from '@vue/test-utils';
import * as utils from '@/utils';

const { defaultOptions } = utils;

describe('Respects option inactiveSeconds', () => {
afterEach(() => {
localStorage.clear();
vi.clearAllTimers();
});

test('Triggers immediately if triggerOnPageLoad is true', async () => {
const userOptions = {
...defaultOptions,
triggerOnPageLoad: true,
inactiveSeconds: 5
};

const App = {
template: `<div></div>`,
setup() {
const { isShowing, close } = useVueExitIntent(userOptions);

return {
isShowing,
close
};
}
};

const wrapper = shallowMount(App);
await wrapper.vm.$nextTick();
expect(wrapper.vm.isShowing).toBe(true);

wrapper.vm.close();
expect(wrapper.vm.isShowing).toBe(false);
});

test('Triggers on mouseleave after inactiveSeconds', async () => {
vi.useFakeTimers();
vi.spyOn(utils, 'isTouchDevice').mockReturnValue(false);

const userOptions = {
...defaultOptions,
inactiveSeconds: 2
};

const App = {
template: `<div></div>`,
setup() {
const { isShowing, close } = useVueExitIntent(userOptions);

return {
isShowing,
close
};
}
};

const wrapper = mount(App);
await wrapper.vm.$nextTick();
expect(wrapper.vm.isShowing).toBe(false);

vi.advanceTimersByTime(1000);
expect(wrapper.vm.isShowing).toBe(false);

vi.advanceTimersByTime(1000);
document.documentElement.dispatchEvent(new MouseEvent('mouseleave'));
expect(wrapper.vm.isShowing).toBe(true);

vi.useRealTimers();
});
});
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Options = {
handleScrollBars: boolean;
LSItemKey: string;
setupBeforeMount: Boolean;
inactiveSeconds: number;
};

export type MouseHandler = {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/defaultOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export const defaultOptions: Options = {
triggerOnPageLoad: false,
handleScrollBars: false,
LSItemKey: 'vue-exit-intent',
setupBeforeMount: false
setupBeforeMount: false,
inactiveSeconds: 0
};

0 comments on commit ea9f877

Please sign in to comment.