Skip to content

Commit fe636b0

Browse files
✨ to handle multiple prevent exit
1 parent 7d9655a commit fe636b0

File tree

13 files changed

+195
-56
lines changed

13 files changed

+195
-56
lines changed

configs/test-utils/src/__mocks__/@monkvision/common.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,5 @@ export = {
118118
useAsyncInterval: jest.fn(),
119119
useSearchParams: jest.fn(() => ({ get: jest.fn(() => null) })),
120120
useObjectMemo: jest.fn((obj) => obj),
121-
usePreventExit: jest.fn(),
121+
usePreventExit: jest.fn(() => ({ allowRedirect: jest.fn() })),
122122
};

packages/common/README/HOOKS.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,16 @@ function TestComponent() {
137137
Custom hook used to get a translation function tObj that translates TranslationObjects.
138138

139139
### usePreventExit
140-
```tsx
140+
TODO: need to add
141+
```ts
141142
import { usePreventExit } from '@monkvision/common';
142143

143-
function TestComponent() {
144-
usePreventExit();
145-
return <>...</>;
144+
function MyComponent() {
145+
const { forceOut } = usePreventExit(true);// commonly it should be a expression.
146+
function anyEvent(){
147+
forceOut();
148+
// Possible Navigation
149+
}
146150
}
147151
```
148152
This hook is used to prevent the user from leaving the page by displaying a confirmation dialog when the user tries to
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { useEffect, useMemo } from 'react';
2+
import { createPreventExitListener } from './store';
3+
4+
/**
5+
* Custom hook that allows you
6+
* to access the PreventExit Context methods inside a component.
7+
*
8+
* Note : If this hook is called inside a component
9+
* that is not a child of a PreventExit component,
10+
* it will throw an error.
11+
*
12+
* @example
13+
* ```tsx
14+
* function MyComponent() {
15+
* const { forceOut } = usePreventExit(true);// commonly it should be a expression.
16+
* return <div onClick={() => forceOut()}>My Component</div>;
17+
* }
18+
* ```
19+
*/
20+
export function usePreventExit(preventExit: boolean) {
21+
const { cleanup, setPreventExit, allowRedirect } = useMemo(createPreventExitListener, []);
22+
useMemo(() => setPreventExit(preventExit), [preventExit]);
23+
useEffect(() => cleanup, []);
24+
return { allowRedirect };
25+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './hooks';
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const keys: Array<symbol> = [];
2+
const allPreventExitState: Record<symbol, boolean> = {};
3+
4+
function checkNoMorePreventExit() {
5+
if (keys.map((key) => allPreventExitState[key]).every((i) => i === false)) {
6+
window.onbeforeunload = null;
7+
return true;
8+
}
9+
return false;
10+
}
11+
function publish(id: symbol, preventExit: boolean) {
12+
allPreventExitState[id] = preventExit;
13+
if (!checkNoMorePreventExit())
14+
window.onbeforeunload = (e) => {
15+
e.preventDefault();
16+
return 'Confirm Alert appears';
17+
};
18+
}
19+
/**
20+
* Creates a listener function that manages the preventExit state of a component.
21+
*/
22+
export function createPreventExitListener() {
23+
const key = Symbol('PreventExitListener');
24+
allPreventExitState[key] = true;
25+
keys.push(key);
26+
return {
27+
/**
28+
* To change the preventExit state of the component.
29+
*/
30+
setPreventExit: (preventExit: boolean) => {
31+
publish(key, preventExit);
32+
},
33+
/**
34+
* Allows the user to leave the page without confirmation temporarily.
35+
* This should be used when the developer wants to explicitly allow navigation.
36+
*/
37+
allowRedirect: () => {
38+
window.onbeforeunload = null;
39+
},
40+
/**
41+
* Performs garbage collection by removing the preventExit state associated with the component.
42+
* This should be used when the component is unmounted.
43+
*/
44+
cleanup: () => {
45+
delete allPreventExitState[key];
46+
checkNoMorePreventExit();
47+
},
48+
};
49+
}

packages/common/src/hooks/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,3 @@ export * from './useSearchParams';
1010
export * from './useInterval';
1111
export * from './useAsyncInterval';
1212
export * from './useObjectMemo';
13-
export * from './usePreventExit';

packages/common/src/hooks/usePreventExit.ts

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

packages/common/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
export * from './PreventExit';
2+
export * from './apps';
3+
export * from './hooks';
14
export * from './i18n';
25
export * from './state';
36
export * from './theme';
47
export * from './utils';
5-
export * from './hooks';
6-
export * from './apps';
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import { usePreventExit } from '../../src/PreventExit/hooks';
3+
import { createPreventExitListener } from '../../src/PreventExit/store';
4+
5+
jest.mock('../../src/PreventExit/store', () => ({
6+
createPreventExitListener: jest.fn(() => ({
7+
cleanup: jest.fn(),
8+
setPreventExit: jest.fn(),
9+
allowRedirect: jest.fn(),
10+
})),
11+
}));
12+
describe('PreventExit hook usePreventExit', () => {
13+
let spyCreatePreventExitListener: jest.SpyInstance<typeof createPreventExitListener>;
14+
beforeEach(() => {
15+
spyCreatePreventExitListener = jest.spyOn(
16+
require('../../src/PreventExit/store'),
17+
'createPreventExitListener',
18+
);
19+
});
20+
afterEach(() => jest.clearAllMocks());
21+
22+
it('should clean up when unmount', () => {
23+
const { unmount } = renderHook(() => usePreventExit(true));
24+
unmount();
25+
expect(spyCreatePreventExitListener.mock.results.at(-1)?.value.cleanup).toHaveBeenCalledTimes(
26+
1,
27+
);
28+
});
29+
30+
it('should set preventExit when value changes', async () => {
31+
const { rerender } = renderHook((props) => usePreventExit(props), {
32+
initialProps: true,
33+
});
34+
expect(
35+
spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
36+
).toHaveBeenCalledTimes(1);
37+
rerender(false);
38+
expect(
39+
spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
40+
).toHaveBeenCalledTimes(2);
41+
rerender(false);
42+
expect(
43+
spyCreatePreventExitListener.mock.results.at(-1)?.value.setPreventExit,
44+
).toHaveBeenCalledTimes(2);
45+
});
46+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { createPreventExitListener } from '../../src/PreventExit/store';
2+
3+
describe('preventExitStore', () => {
4+
let listener1: ReturnType<typeof createPreventExitListener>;
5+
let listener2: ReturnType<typeof createPreventExitListener>;
6+
7+
beforeEach(() => {
8+
listener1 = createPreventExitListener();
9+
listener2 = createPreventExitListener();
10+
});
11+
12+
afterEach(() => {
13+
listener1.cleanup();
14+
listener2.cleanup();
15+
});
16+
17+
it('should prevent exit: listener 1', () => {
18+
listener1.setPreventExit(true);
19+
listener2.setPreventExit(true);
20+
expect(window.onbeforeunload).not.toBe(null);
21+
listener1.setPreventExit(true);
22+
listener2.setPreventExit(false);
23+
expect(window.onbeforeunload).not.toBe(null);
24+
listener1.setPreventExit(false);
25+
listener2.setPreventExit(true);
26+
expect(window.onbeforeunload).not.toBe(null);
27+
listener1.setPreventExit(false);
28+
listener2.setPreventExit(false);
29+
expect(window.onbeforeunload).toBe(null);
30+
});
31+
32+
it('should allow redirect: listener 1', () => {
33+
const preventExit = [true, false];
34+
preventExit.forEach((i) => {
35+
preventExit.forEach((j) => {
36+
listener1.setPreventExit(i);
37+
listener2.setPreventExit(j);
38+
listener1.allowRedirect();
39+
expect(window.onbeforeunload).toBe(null);
40+
});
41+
});
42+
});
43+
44+
it('should allow redirect: lister 2', () => {
45+
listener2.cleanup();
46+
listener1.cleanup();
47+
expect(window.onbeforeunload).toBe(null);
48+
});
49+
});

0 commit comments

Comments
 (0)