@granite-js/react-native 1.0.30 → 1.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/app/AppRoot.d.ts +1 -1
- package/dist/app/Granite.d.ts +8 -1
- package/dist/intersection-observer/IntersectionObserver.d.ts +8 -4
- package/dist/intersection-observer/withIO.d.ts +2 -2
- package/dist/router/Router.d.ts +1 -1
- package/dist/router/components/CanGoBackGuard.d.ts +13 -5
- package/dist/router/components/useRouterBackHandler.d.ts +4 -1
- package/dist/use-back-event/BackEventContext.d.ts +15 -0
- package/dist/use-back-event/index.d.ts +1 -0
- package/dist/use-back-event/useBackEvent.d.ts +8 -9
- package/dist/use-back-event/useBackEventActionsContext.d.ts +1 -0
- package/dist/use-back-event/useBackEventContext.d.ts +2 -0
- package/dist/use-back-event/useBackHandler.d.ts +53 -0
- package/package.json +12 -11
- package/src/app/AppRoot.tsx +3 -0
- package/src/app/Granite.tsx +8 -1
- package/src/intersection-observer/IntersectionObserver.ts +106 -96
- package/src/intersection-observer/withIO.tsx +7 -2
- package/src/router/Router.tsx +6 -2
- package/src/router/components/CanGoBackGuard.spec.tsx +85 -0
- package/src/router/components/CanGoBackGuard.tsx +37 -14
- package/src/router/components/useRouterBackHandler.spec.tsx +129 -0
- package/src/router/components/useRouterBackHandler.tsx +34 -13
- package/src/use-back-event/BackEventContext.ts +26 -0
- package/src/use-back-event/index.ts +1 -0
- package/src/use-back-event/useBackEvent.spec.tsx +107 -0
- package/src/use-back-event/useBackEvent.tsx +138 -48
- package/src/use-back-event/useBackEventActionsContext.ts +11 -0
- package/src/use-back-event/useBackEventContext.ts +28 -0
- package/src/use-back-event/useBackHandler.spec.tsx +113 -0
- package/src/use-back-event/useBackHandler.tsx +114 -0
package/src/router/Router.tsx
CHANGED
|
@@ -65,6 +65,7 @@ export interface InternalRouterProps {
|
|
|
65
65
|
initialProps: InitialProps;
|
|
66
66
|
initialScheme: string;
|
|
67
67
|
setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => Promise<void> | void;
|
|
68
|
+
setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;
|
|
68
69
|
getInitialUrl?: RouterControlsConfig['getInitialUrl'];
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -148,6 +149,7 @@ export function Router({
|
|
|
148
149
|
defaultErrorComponent,
|
|
149
150
|
// Public props (StackNavigator)
|
|
150
151
|
setIosSwipeGestureEnabled,
|
|
152
|
+
setiOSBackPressHandler,
|
|
151
153
|
getInitialUrl,
|
|
152
154
|
...navigationContainerProps
|
|
153
155
|
}: InternalRouterProps & RouterProps): ReactElement {
|
|
@@ -162,7 +164,7 @@ export function Router({
|
|
|
162
164
|
|
|
163
165
|
const ref = useMemo(() => navigationContainerRef ?? createNavigationContainerRef<never>(), [navigationContainerRef]);
|
|
164
166
|
|
|
165
|
-
const { handler, canGoBack,
|
|
167
|
+
const { handler, handleBackEvent, canGoBack, hasBackEvent } = useInternalRouterBackHandler({
|
|
166
168
|
navigationContainerRef: ref,
|
|
167
169
|
onClose: closeView,
|
|
168
170
|
});
|
|
@@ -195,9 +197,11 @@ export function Router({
|
|
|
195
197
|
>
|
|
196
198
|
<CanGoBackGuard
|
|
197
199
|
canGoBack={canGoBack}
|
|
200
|
+
hasBackEvent={hasBackEvent}
|
|
198
201
|
isInitialScreen={isInitialScreen}
|
|
199
|
-
onBack={
|
|
202
|
+
onBack={handleBackEvent}
|
|
200
203
|
setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
|
|
204
|
+
setiOSBackPressHandler={setiOSBackPressHandler}
|
|
201
205
|
>
|
|
202
206
|
<Container {...initialProps}>
|
|
203
207
|
<StackNavigator.Navigator screenOptions={screenOptions}>{Screens}</StackNavigator.Navigator>
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { render } from '@testing-library/react';
|
|
2
|
+
import { BackHandler } from 'react-native';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { CanGoBackGuard } from './CanGoBackGuard';
|
|
5
|
+
|
|
6
|
+
describe('CanGoBackGuard', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.mocked(BackHandler.addEventListener).mockClear();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('passes Android hardware back events to onBack and consumes the native event', () => {
|
|
12
|
+
const onBack = vi.fn();
|
|
13
|
+
|
|
14
|
+
render(
|
|
15
|
+
<CanGoBackGuard canGoBack={false} hasBackEvent={true} isInitialScreen={true} onBack={onBack}>
|
|
16
|
+
<div />
|
|
17
|
+
</CanGoBackGuard>
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const handler = vi.mocked(BackHandler.addEventListener).mock.calls[0]?.[1];
|
|
21
|
+
|
|
22
|
+
expect(BackHandler.addEventListener).toHaveBeenCalledWith('hardwareBackPress', expect.any(Function));
|
|
23
|
+
expect(handler?.()).toBe(true);
|
|
24
|
+
expect(onBack).toHaveBeenCalledWith({ source: 'androidHardwareBackPress' });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not register Android hardware back when no back event exists', () => {
|
|
28
|
+
const onBack = vi.fn();
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<CanGoBackGuard canGoBack={true} hasBackEvent={false} isInitialScreen={true} onBack={onBack}>
|
|
32
|
+
<div />
|
|
33
|
+
</CanGoBackGuard>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(BackHandler.addEventListener).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('registers the iOS swipe back handler while back events exist', () => {
|
|
40
|
+
const onBack = vi.fn();
|
|
41
|
+
const setiOSBackPressHandler = vi.fn();
|
|
42
|
+
|
|
43
|
+
const { unmount } = render(
|
|
44
|
+
<CanGoBackGuard
|
|
45
|
+
canGoBack={true}
|
|
46
|
+
hasBackEvent={true}
|
|
47
|
+
isInitialScreen={true}
|
|
48
|
+
onBack={onBack}
|
|
49
|
+
setiOSBackPressHandler={setiOSBackPressHandler}
|
|
50
|
+
>
|
|
51
|
+
<div />
|
|
52
|
+
</CanGoBackGuard>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
setiOSBackPressHandler.mock.calls[0]?.[0].handler();
|
|
56
|
+
|
|
57
|
+
expect(setiOSBackPressHandler).toHaveBeenCalledWith({ handler: expect.any(Function) });
|
|
58
|
+
expect(onBack).toHaveBeenCalledWith({ source: 'iosSwipeGesture' });
|
|
59
|
+
|
|
60
|
+
unmount();
|
|
61
|
+
|
|
62
|
+
expect(setiOSBackPressHandler).toHaveBeenLastCalledWith({ handler: expect.any(Function) });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('disables iOS swipe when the current state should block default back navigation', () => {
|
|
66
|
+
const setIosSwipeGestureEnabled = vi.fn();
|
|
67
|
+
|
|
68
|
+
const { unmount } = render(
|
|
69
|
+
<CanGoBackGuard
|
|
70
|
+
canGoBack={false}
|
|
71
|
+
hasBackEvent={true}
|
|
72
|
+
isInitialScreen={true}
|
|
73
|
+
setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
|
|
74
|
+
>
|
|
75
|
+
<div />
|
|
76
|
+
</CanGoBackGuard>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(setIosSwipeGestureEnabled).toHaveBeenCalledWith({ isEnabled: false });
|
|
80
|
+
|
|
81
|
+
unmount();
|
|
82
|
+
|
|
83
|
+
expect(setIosSwipeGestureEnabled).toHaveBeenLastCalledWith({ isEnabled: true });
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { ReactNode, useEffect } from 'react';
|
|
2
2
|
import { BackHandler } from 'react-native';
|
|
3
|
+
import type { BackEvent } from '../../use-back-event';
|
|
4
|
+
|
|
5
|
+
type SetIosSwipeGestureEnabled = ({ isEnabled }: { isEnabled: boolean }) => Promise<void> | void;
|
|
6
|
+
type SetIOSBackPressHandler = ({ handler }: { handler: () => void }) => Promise<void> | void;
|
|
3
7
|
|
|
4
8
|
export function CanGoBackGuard({
|
|
5
9
|
children,
|
|
6
10
|
canGoBack,
|
|
11
|
+
hasBackEvent,
|
|
7
12
|
onBack,
|
|
8
13
|
isInitialScreen,
|
|
9
14
|
setIosSwipeGestureEnabled,
|
|
15
|
+
setiOSBackPressHandler,
|
|
10
16
|
}: {
|
|
11
17
|
canGoBack: boolean;
|
|
18
|
+
hasBackEvent: boolean;
|
|
12
19
|
isInitialScreen: boolean;
|
|
13
20
|
children: ReactNode;
|
|
14
|
-
onBack?: () => void;
|
|
15
|
-
setIosSwipeGestureEnabled?:
|
|
21
|
+
onBack?: (event: BackEvent) => void;
|
|
22
|
+
setIosSwipeGestureEnabled?: SetIosSwipeGestureEnabled;
|
|
23
|
+
setiOSBackPressHandler?: SetIOSBackPressHandler;
|
|
16
24
|
}) {
|
|
17
|
-
const shouldBlockGoingBack = !canGoBack;
|
|
18
|
-
|
|
19
25
|
useEffect(() => {
|
|
20
26
|
if (!isInitialScreen || !canGoBack) {
|
|
21
27
|
setIosSwipeGestureEnabled?.({ isEnabled: false });
|
|
@@ -29,19 +35,36 @@ export function CanGoBackGuard({
|
|
|
29
35
|
}, [canGoBack, isInitialScreen, setIosSwipeGestureEnabled]);
|
|
30
36
|
|
|
31
37
|
useEffect(() => {
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return true;
|
|
36
|
-
});
|
|
38
|
+
if (!hasBackEvent) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
43
|
+
onBack?.({ source: 'androidHardwareBackPress' });
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
subscription.remove();
|
|
50
|
+
};
|
|
51
|
+
}, [hasBackEvent, onBack]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!hasBackEvent || setiOSBackPressHandler == null) {
|
|
55
|
+
return;
|
|
41
56
|
}
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
58
|
+
setiOSBackPressHandler({
|
|
59
|
+
handler: () => {
|
|
60
|
+
onBack?.({ source: 'iosSwipeGesture' });
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
setiOSBackPressHandler({ handler: () => {} });
|
|
66
|
+
};
|
|
67
|
+
}, [hasBackEvent, onBack, setiOSBackPressHandler]);
|
|
45
68
|
|
|
46
69
|
return <>{children}</>;
|
|
47
70
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { useInternalRouterBackHandler } from './useRouterBackHandler';
|
|
5
|
+
import { BackEventProvider, useBackEvent, useBackHandler } from '../../use-back-event';
|
|
6
|
+
|
|
7
|
+
vi.mock('../../visibility', () => ({
|
|
8
|
+
useVisibility: () => true,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
function createNavigationContainerRef() {
|
|
12
|
+
return {
|
|
13
|
+
canGoBack: vi.fn(() => true),
|
|
14
|
+
goBack: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function BackEventProviderWrapper({ children }: { children: ReactNode }) {
|
|
19
|
+
return <BackEventProvider>{children}</BackEventProvider>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function useBackHandlerTest({
|
|
23
|
+
navigationContainerRef,
|
|
24
|
+
onClose,
|
|
25
|
+
}: {
|
|
26
|
+
navigationContainerRef: ReturnType<typeof createNavigationContainerRef>;
|
|
27
|
+
onClose?: () => void;
|
|
28
|
+
}) {
|
|
29
|
+
const backEvent = useBackEvent();
|
|
30
|
+
const backHandler = useBackHandler();
|
|
31
|
+
const routerBackHandler = useInternalRouterBackHandler({
|
|
32
|
+
navigationContainerRef: navigationContainerRef as any,
|
|
33
|
+
onClose,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
backEvent,
|
|
38
|
+
backHandler,
|
|
39
|
+
routerBackHandler,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('useInternalRouterBackHandler', () => {
|
|
44
|
+
it('stops when a back handler handles the back event', () => {
|
|
45
|
+
const navigationContainerRef = createNavigationContainerRef();
|
|
46
|
+
const legacyHandler = vi.fn();
|
|
47
|
+
const backHandler = vi.fn(() => true);
|
|
48
|
+
const { result } = renderHook(() => useBackHandlerTest({ navigationContainerRef }), {
|
|
49
|
+
wrapper: BackEventProviderWrapper,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
result.current.backEvent.addEventListener(legacyHandler);
|
|
54
|
+
result.current.backHandler.addEventListener(backHandler);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
act(() => {
|
|
58
|
+
result.current.routerBackHandler.handleBackEvent({ source: 'backButton' });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(backHandler).toHaveBeenCalledWith({ source: 'backButton' });
|
|
62
|
+
expect(legacyHandler).not.toHaveBeenCalled();
|
|
63
|
+
expect(navigationContainerRef.goBack).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('runs every legacy handler when back handlers do not handle the event', () => {
|
|
67
|
+
const navigationContainerRef = createNavigationContainerRef();
|
|
68
|
+
const backHandler = vi.fn(() => false);
|
|
69
|
+
const firstLegacyHandler = vi.fn();
|
|
70
|
+
const secondLegacyHandler = vi.fn();
|
|
71
|
+
const { result } = renderHook(() => useBackHandlerTest({ navigationContainerRef }), {
|
|
72
|
+
wrapper: BackEventProviderWrapper,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
act(() => {
|
|
76
|
+
result.current.backHandler.addEventListener(backHandler);
|
|
77
|
+
result.current.backEvent.addEventListener(firstLegacyHandler, secondLegacyHandler);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
act(() => {
|
|
81
|
+
result.current.routerBackHandler.handleBackEvent({ source: 'iosSwipeGesture' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(backHandler).toHaveBeenCalledWith({ source: 'iosSwipeGesture' });
|
|
85
|
+
expect(firstLegacyHandler).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(secondLegacyHandler).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(navigationContainerRef.goBack).not.toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('runs default back when back handlers return false or undefined and no legacy handler exists', () => {
|
|
91
|
+
const navigationContainerRef = createNavigationContainerRef();
|
|
92
|
+
const backHandler = vi.fn(() => false);
|
|
93
|
+
const passiveHandler = vi.fn();
|
|
94
|
+
const { result } = renderHook(() => useBackHandlerTest({ navigationContainerRef }), {
|
|
95
|
+
wrapper: BackEventProviderWrapper,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
act(() => {
|
|
99
|
+
result.current.backHandler.addEventListener(backHandler);
|
|
100
|
+
result.current.backHandler.addEventListener(passiveHandler);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
act(() => {
|
|
104
|
+
result.current.routerBackHandler.handleBackEvent({ source: 'androidHardwareBackPress' });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(passiveHandler).toHaveBeenCalledWith({ source: 'androidHardwareBackPress' });
|
|
108
|
+
expect(backHandler).toHaveBeenCalledWith({ source: 'androidHardwareBackPress' });
|
|
109
|
+
expect(navigationContainerRef.goBack).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('uses backButton as the source for the header back handler', () => {
|
|
113
|
+
const navigationContainerRef = createNavigationContainerRef();
|
|
114
|
+
const backHandler = vi.fn(() => true);
|
|
115
|
+
const { result } = renderHook(() => useBackHandlerTest({ navigationContainerRef }), {
|
|
116
|
+
wrapper: BackEventProviderWrapper,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
act(() => {
|
|
120
|
+
result.current.backHandler.addEventListener(backHandler);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
act(() => {
|
|
124
|
+
result.current.routerBackHandler.handler();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(backHandler).toHaveBeenCalledWith({ source: 'backButton' });
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
|
|
1
|
+
import type { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
|
|
2
2
|
import { useCallback, useMemo } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import type { BackEvent } from '../../use-back-event';
|
|
4
|
+
import { useBackEventContext } from '../../use-back-event/useBackEventContext';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @public
|
|
@@ -63,29 +64,49 @@ export function useInternalRouterBackHandler({
|
|
|
63
64
|
navigationContainerRef: NavigationContainerRefWithCurrent<never>;
|
|
64
65
|
onClose?: () => void;
|
|
65
66
|
}) {
|
|
66
|
-
const { hasBackEvent, onBack } = useBackEventContext();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const handler = useCallback(() => {
|
|
70
|
-
onBack?.();
|
|
71
|
-
|
|
72
|
-
if (!canGoBack) {
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
67
|
+
const { hasBackEvent, hasBackHandler, onBack, onBackHandler } = useBackEventContext();
|
|
68
|
+
const hasRegisteredBackHandler = hasBackEvent || hasBackHandler;
|
|
69
|
+
const canGoBack = !hasRegisteredBackHandler;
|
|
75
70
|
|
|
71
|
+
const handleDefaultBack = useCallback(() => {
|
|
76
72
|
if (navigationContainerRef.canGoBack()) {
|
|
77
73
|
navigationContainerRef.goBack();
|
|
78
74
|
} else {
|
|
79
75
|
onClose?.();
|
|
80
76
|
}
|
|
81
|
-
}, [
|
|
77
|
+
}, [navigationContainerRef, onClose]);
|
|
78
|
+
|
|
79
|
+
const handleBackEvent = useCallback(
|
|
80
|
+
(event: BackEvent) => {
|
|
81
|
+
const didHandleBackEvent = onBackHandler(event);
|
|
82
|
+
|
|
83
|
+
if (didHandleBackEvent) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onBack();
|
|
88
|
+
|
|
89
|
+
if (hasBackEvent) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
handleDefaultBack();
|
|
94
|
+
},
|
|
95
|
+
[handleDefaultBack, hasBackEvent, onBack, onBackHandler]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const handler = useCallback(() => {
|
|
99
|
+
handleBackEvent({ source: 'backButton' });
|
|
100
|
+
}, [handleBackEvent]);
|
|
82
101
|
|
|
83
102
|
return useMemo(
|
|
84
103
|
() => ({
|
|
85
104
|
handler,
|
|
105
|
+
handleBackEvent,
|
|
86
106
|
canGoBack,
|
|
107
|
+
hasBackEvent: hasRegisteredBackHandler,
|
|
87
108
|
onBack,
|
|
88
109
|
}),
|
|
89
|
-
[canGoBack, handler, onBack]
|
|
110
|
+
[canGoBack, handleBackEvent, handler, hasRegisteredBackHandler, onBack]
|
|
90
111
|
);
|
|
91
112
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type { BackEvent, BackEventControls, BackEventHandler } from './useBackEvent';
|
|
3
|
+
import type { BackHandlerCallback, BackHandlerControls } from './useBackHandler';
|
|
4
|
+
|
|
5
|
+
export interface PrivateBackEventControls extends BackEventControls {
|
|
6
|
+
handlersRef: Set<BackEventHandler>;
|
|
7
|
+
hasBackEvent: boolean;
|
|
8
|
+
hasBackHandler: boolean;
|
|
9
|
+
addBackHandler: BackHandlerControls['addEventListener'];
|
|
10
|
+
removeBackHandler: (...handlers: Array<BackHandlerCallback>) => void;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
onBackHandler: (event: BackEvent) => boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type BackEventActionsContextValue = Omit<
|
|
16
|
+
PrivateBackEventControls,
|
|
17
|
+
'handlersRef' | 'hasBackEvent' | 'hasBackHandler'
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export type BackEventStateContextValue = Pick<
|
|
21
|
+
PrivateBackEventControls,
|
|
22
|
+
'handlersRef' | 'hasBackEvent' | 'hasBackHandler'
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
export const BackEventActionsContext = createContext<BackEventActionsContextValue | null>(null);
|
|
26
|
+
export const BackEventStateContext = createContext<BackEventStateContextValue | null>(null);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { BackEventProvider, useBackEvent, useBackEventState } from './useBackEvent';
|
|
5
|
+
|
|
6
|
+
vi.mock('../visibility', () => ({
|
|
7
|
+
useVisibility: () => true,
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
function BackEventProviderWrapper({ children }: { children: ReactNode }) {
|
|
11
|
+
return <BackEventProvider>{children}</BackEventProvider>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('useBackEventState', () => {
|
|
15
|
+
it('keeps legacy back event handlers variadic and runs every handler', () => {
|
|
16
|
+
const firstHandler = vi.fn();
|
|
17
|
+
const secondHandler = vi.fn();
|
|
18
|
+
const { result } = renderHook(() => useBackEventState());
|
|
19
|
+
|
|
20
|
+
act(() => {
|
|
21
|
+
result.current.addEventListener(firstHandler, secondHandler);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
result.current.onBack();
|
|
25
|
+
|
|
26
|
+
expect(result.current.hasBackEvent).toBe(true);
|
|
27
|
+
expect(firstHandler).toHaveBeenCalledTimes(1);
|
|
28
|
+
expect(secondHandler).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('ignores legacy back event handler return values', () => {
|
|
32
|
+
const handler = vi.fn(() => false);
|
|
33
|
+
const { result } = renderHook(() => useBackEventState());
|
|
34
|
+
|
|
35
|
+
act(() => {
|
|
36
|
+
result.current.addEventListener(handler);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
result.current.onBack();
|
|
40
|
+
|
|
41
|
+
expect(result.current.hasBackEvent).toBe(true);
|
|
42
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('derives legacy back event state from registered handlers', () => {
|
|
46
|
+
const handler = vi.fn();
|
|
47
|
+
const { result } = renderHook(() => useBackEventState());
|
|
48
|
+
|
|
49
|
+
act(() => {
|
|
50
|
+
result.current.addEventListener(handler);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.current.hasBackEvent).toBe(true);
|
|
54
|
+
|
|
55
|
+
act(() => {
|
|
56
|
+
result.current.removeEventListener(handler);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
result.current.onBack();
|
|
60
|
+
|
|
61
|
+
expect(result.current.hasBackEvent).toBe(false);
|
|
62
|
+
expect(handler).not.toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('keeps the legacy state object stable while back event existence does not change', () => {
|
|
66
|
+
const firstHandler = vi.fn();
|
|
67
|
+
const secondHandler = vi.fn();
|
|
68
|
+
const { result } = renderHook(() => useBackEventState());
|
|
69
|
+
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.addEventListener(firstHandler);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const activeBackEvent = result.current;
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
result.current.addEventListener(secondHandler);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
expect(result.current).toBe(activeBackEvent);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('useBackEvent', () => {
|
|
85
|
+
it('keeps the returned back event controls stable when provider state changes', () => {
|
|
86
|
+
const handler = vi.fn();
|
|
87
|
+
const renderCount = vi.fn();
|
|
88
|
+
const { result } = renderHook(
|
|
89
|
+
() => {
|
|
90
|
+
renderCount();
|
|
91
|
+
|
|
92
|
+
return useBackEvent();
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
wrapper: BackEventProviderWrapper,
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
const backEvent = result.current;
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
result.current.addEventListener(handler);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(result.current).toBe(backEvent);
|
|
105
|
+
expect(renderCount).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
});
|