@granite-js/react-native 1.0.31 → 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 +15 -0
- package/dist/app/AppRoot.d.ts +1 -1
- package/dist/app/Granite.d.ts +8 -1
- 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/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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @granite-js/react-native
|
|
2
2
|
|
|
3
|
+
## 1.0.32
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 20f2781: add opt-in back handler API
|
|
8
|
+
- @granite-js/blur-view@1.0.32
|
|
9
|
+
- @granite-js/brownfield-module@1.0.32
|
|
10
|
+
- @granite-js/cli@1.0.32
|
|
11
|
+
- @granite-js/jest@1.0.32
|
|
12
|
+
- @granite-js/mpack@1.0.32
|
|
13
|
+
- @granite-js/native@1.0.32
|
|
14
|
+
- @granite-js/plugin-core@1.0.32
|
|
15
|
+
- @granite-js/style-utils@1.0.32
|
|
16
|
+
- @granite-js/video@1.0.32
|
|
17
|
+
|
|
3
18
|
## 1.0.31
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/app/AppRoot.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function AppRoot({ appName, context, container: Container, initialProps, initialScheme, router, setIosSwipeGestureEnabled, getInitialUrl, }: AppRootProps): import("react/jsx-runtime").JSX.Element;
|
|
1
|
+
export declare function AppRoot({ appName, context, container: Container, initialProps, initialScheme, router, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl, }: AppRootProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/app/Granite.d.ts
CHANGED
|
@@ -31,6 +31,13 @@ export interface GraniteProps {
|
|
|
31
31
|
setIosSwipeGestureEnabled?: ({ isEnabled }: {
|
|
32
32
|
isEnabled: boolean;
|
|
33
33
|
}) => void;
|
|
34
|
+
/**
|
|
35
|
+
* @description
|
|
36
|
+
* The function to register a handler that runs when the iOS swipe back gesture is detected.
|
|
37
|
+
*/
|
|
38
|
+
setiOSBackPressHandler?: ({ handler }: {
|
|
39
|
+
handler: () => void;
|
|
40
|
+
}) => Promise<void> | void;
|
|
34
41
|
/**
|
|
35
42
|
* @description
|
|
36
43
|
* The function to provide the initial URL to router.
|
|
@@ -74,7 +81,7 @@ export interface GraniteProps {
|
|
|
74
81
|
* ```
|
|
75
82
|
*/
|
|
76
83
|
export declare const Granite: {
|
|
77
|
-
registerApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName, context, router, initialScheme, setIosSwipeGestureEnabled, getInitialUrl }: GraniteProps): (initialProps: InitialProps) => JSX.Element;
|
|
84
|
+
registerApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName, context, router, initialScheme, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl }: GraniteProps): (initialProps: InitialProps) => JSX.Element;
|
|
78
85
|
registerHostApp(AppContainer: ComponentType<PropsWithChildren<InitialProps>>, { appName }: Pick<GraniteProps, "appName">): (initialProps: InitialProps) => React.JSX.Element;
|
|
79
86
|
readonly appName: string;
|
|
80
87
|
};
|
package/dist/router/Router.d.ts
CHANGED
|
@@ -63,5 +63,5 @@ type NavigationContainerProps = Pick<ComponentProps<typeof NavigationContainer>,
|
|
|
63
63
|
* }
|
|
64
64
|
* ```
|
|
65
65
|
*/
|
|
66
|
-
export declare function Router({ prefix, context, container: Container, initialProps, initialScheme, navigationContainerRef, defaultScreenOption, screenContainer, defaultErrorComponent, setIosSwipeGestureEnabled, getInitialUrl, ...navigationContainerProps }: InternalRouterProps & RouterProps): ReactElement;
|
|
66
|
+
export declare function Router({ prefix, context, container: Container, initialProps, initialScheme, navigationContainerRef, defaultScreenOption, screenContainer, defaultErrorComponent, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl, ...navigationContainerProps }: InternalRouterProps & RouterProps): ReactElement;
|
|
67
67
|
export {};
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
2
|
+
import type { BackEvent } from '../../use-back-event';
|
|
3
|
+
type SetIosSwipeGestureEnabled = ({ isEnabled }: {
|
|
4
|
+
isEnabled: boolean;
|
|
5
|
+
}) => Promise<void> | void;
|
|
6
|
+
type SetIOSBackPressHandler = ({ handler }: {
|
|
7
|
+
handler: () => void;
|
|
8
|
+
}) => Promise<void> | void;
|
|
9
|
+
export declare function CanGoBackGuard({ children, canGoBack, hasBackEvent, onBack, isInitialScreen, setIosSwipeGestureEnabled, setiOSBackPressHandler, }: {
|
|
3
10
|
canGoBack: boolean;
|
|
11
|
+
hasBackEvent: boolean;
|
|
4
12
|
isInitialScreen: boolean;
|
|
5
13
|
children: ReactNode;
|
|
6
|
-
onBack?: () => void;
|
|
7
|
-
setIosSwipeGestureEnabled?:
|
|
8
|
-
|
|
9
|
-
}) => void;
|
|
14
|
+
onBack?: (event: BackEvent) => void;
|
|
15
|
+
setIosSwipeGestureEnabled?: SetIosSwipeGestureEnabled;
|
|
16
|
+
setiOSBackPressHandler?: SetIOSBackPressHandler;
|
|
10
17
|
}): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
|
|
1
|
+
import type { NavigationContainerRefWithCurrent } from '@granite-js/native/@react-navigation/native';
|
|
2
|
+
import type { BackEvent } from '../../use-back-event';
|
|
2
3
|
/**
|
|
3
4
|
* @public
|
|
4
5
|
* @name useRouterBackHandler
|
|
@@ -52,6 +53,8 @@ export declare function useInternalRouterBackHandler({ navigationContainerRef, o
|
|
|
52
53
|
onClose?: () => void;
|
|
53
54
|
}): {
|
|
54
55
|
handler: () => void;
|
|
56
|
+
handleBackEvent: (event: BackEvent) => void;
|
|
55
57
|
canGoBack: boolean;
|
|
58
|
+
hasBackEvent: boolean;
|
|
56
59
|
onBack: () => void;
|
|
57
60
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BackEvent, BackEventControls, BackEventHandler } from './useBackEvent';
|
|
2
|
+
import type { BackHandlerCallback, BackHandlerControls } from './useBackHandler';
|
|
3
|
+
export interface PrivateBackEventControls extends BackEventControls {
|
|
4
|
+
handlersRef: Set<BackEventHandler>;
|
|
5
|
+
hasBackEvent: boolean;
|
|
6
|
+
hasBackHandler: boolean;
|
|
7
|
+
addBackHandler: BackHandlerControls['addEventListener'];
|
|
8
|
+
removeBackHandler: (...handlers: Array<BackHandlerCallback>) => void;
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
onBackHandler: (event: BackEvent) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export type BackEventActionsContextValue = Omit<PrivateBackEventControls, 'handlersRef' | 'hasBackEvent' | 'hasBackHandler'>;
|
|
13
|
+
export type BackEventStateContextValue = Pick<PrivateBackEventControls, 'handlersRef' | 'hasBackEvent' | 'hasBackHandler'>;
|
|
14
|
+
export declare const BackEventActionsContext: import("react").Context<BackEventActionsContextValue | null>;
|
|
15
|
+
export declare const BackEventStateContext: import("react").Context<BackEventStateContextValue | null>;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { type PrivateBackEventControls } from './BackEventContext';
|
|
3
|
+
export type BackEventSource = 'backButton' | 'iosSwipeGesture' | 'androidHardwareBackPress';
|
|
4
|
+
export interface BackEvent {
|
|
5
|
+
source: BackEventSource;
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
export type BackEventHandler = () => void;
|
|
8
|
+
export interface BackEventControls {
|
|
9
|
+
addEventListener: (...handlers: Array<BackEventHandler>) => void;
|
|
10
|
+
removeEventListener: (...handlers: Array<BackEventHandler>) => void;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* @component
|
|
@@ -127,5 +128,3 @@ export declare function useBackEventState(): PrivateBackEventControls;
|
|
|
127
128
|
* ```
|
|
128
129
|
*/
|
|
129
130
|
export declare function useBackEvent(): BackEventControls;
|
|
130
|
-
export declare function useBackEventContext(): PrivateBackEventControls;
|
|
131
|
-
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useBackEventActionsContext(): import("./BackEventContext").BackEventActionsContextValue;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BackEvent } from './useBackEvent';
|
|
2
|
+
export type BackHandlerCallback = (event: BackEvent) => boolean | void;
|
|
3
|
+
export interface BackHandlerSubscription {
|
|
4
|
+
remove: () => void;
|
|
5
|
+
}
|
|
6
|
+
export interface BackHandlerControls {
|
|
7
|
+
addEventListener: (handler: BackHandlerCallback) => BackHandlerSubscription;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* @public
|
|
11
|
+
* @category Screen Control
|
|
12
|
+
* @name useBackHandler
|
|
13
|
+
* @description
|
|
14
|
+
* A Hook that registers back handlers. Back handlers run before handlers registered through `useBackEvent`.
|
|
15
|
+
* Return `true` to stop bubbling and prevent default back navigation. Return `false` or return nothing to continue bubbling to the next handler.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
*
|
|
19
|
+
* ### Example of Closing an Overlay First
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { useEffect, useState } from 'react';
|
|
23
|
+
* import { Button, View } from 'react-native';
|
|
24
|
+
* import { useBackHandler } from '@granite-js/react-native';
|
|
25
|
+
*
|
|
26
|
+
* export function OverlayExample() {
|
|
27
|
+
* const backHandler = useBackHandler();
|
|
28
|
+
* const [isOverlayOpen, setIsOverlayOpen] = useState(false);
|
|
29
|
+
*
|
|
30
|
+
* useEffect(() => {
|
|
31
|
+
* const subscription = backHandler.addEventListener(() => {
|
|
32
|
+
* if (isOverlayOpen) {
|
|
33
|
+
* setIsOverlayOpen(false);
|
|
34
|
+
* return true;
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* return undefined;
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* return () => {
|
|
41
|
+
* subscription.remove();
|
|
42
|
+
* };
|
|
43
|
+
* }, [backHandler, isOverlayOpen]);
|
|
44
|
+
*
|
|
45
|
+
* return (
|
|
46
|
+
* <View>
|
|
47
|
+
* <Button title="Open Overlay" onPress={() => setIsOverlayOpen(true)} />
|
|
48
|
+
* </View>
|
|
49
|
+
* );
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function useBackHandler(): BackHandlerControls;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@granite-js/react-native",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "The Granite Framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"granite": "./bin/cli.js"
|
|
@@ -91,7 +91,8 @@
|
|
|
91
91
|
"@babel/core": "7.28.5",
|
|
92
92
|
"@babel/preset-env": "7.28.5",
|
|
93
93
|
"@babel/preset-typescript": "7.28.5",
|
|
94
|
-
"@granite-js/native": "1.0.
|
|
94
|
+
"@granite-js/native": "1.0.32",
|
|
95
|
+
"@granite-js/vitest": "1.0.32",
|
|
95
96
|
"@testing-library/dom": "^10.4.0",
|
|
96
97
|
"@testing-library/react": "^16.1.0",
|
|
97
98
|
"@types/babel__core": "^7",
|
|
@@ -113,21 +114,21 @@
|
|
|
113
114
|
"zod": "^4.1.12"
|
|
114
115
|
},
|
|
115
116
|
"peerDependencies": {
|
|
116
|
-
"@granite-js/native": "1.0.
|
|
117
|
+
"@granite-js/native": "1.0.32",
|
|
117
118
|
"@types/react": "*",
|
|
118
119
|
"brick-module": "*",
|
|
119
120
|
"react": "*",
|
|
120
121
|
"react-native": "*"
|
|
121
122
|
},
|
|
122
123
|
"dependencies": {
|
|
123
|
-
"@granite-js/blur-view": "1.0.
|
|
124
|
-
"@granite-js/brownfield-module": "1.0.
|
|
125
|
-
"@granite-js/cli": "1.0.
|
|
126
|
-
"@granite-js/jest": "1.0.
|
|
127
|
-
"@granite-js/mpack": "1.0.
|
|
128
|
-
"@granite-js/plugin-core": "1.0.
|
|
129
|
-
"@granite-js/style-utils": "1.0.
|
|
130
|
-
"@granite-js/video": "1.0.
|
|
124
|
+
"@granite-js/blur-view": "1.0.32",
|
|
125
|
+
"@granite-js/brownfield-module": "1.0.32",
|
|
126
|
+
"@granite-js/cli": "1.0.32",
|
|
127
|
+
"@granite-js/jest": "1.0.32",
|
|
128
|
+
"@granite-js/mpack": "1.0.32",
|
|
129
|
+
"@granite-js/plugin-core": "1.0.32",
|
|
130
|
+
"@granite-js/style-utils": "1.0.32",
|
|
131
|
+
"@granite-js/video": "1.0.32",
|
|
131
132
|
"@standard-schema/spec": "^1.0.0",
|
|
132
133
|
"es-toolkit": "^1.39.8",
|
|
133
134
|
"react-native-url-polyfill": "3.0.0"
|
package/src/app/AppRoot.tsx
CHANGED
|
@@ -16,6 +16,7 @@ interface AppRootProps extends GraniteProps {
|
|
|
16
16
|
initialProps: InitialProps;
|
|
17
17
|
initialScheme: string;
|
|
18
18
|
setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => void;
|
|
19
|
+
setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;
|
|
19
20
|
getInitialUrl: InternalRouterProps['getInitialUrl'];
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -27,6 +28,7 @@ export function AppRoot({
|
|
|
27
28
|
initialScheme,
|
|
28
29
|
router,
|
|
29
30
|
setIosSwipeGestureEnabled,
|
|
31
|
+
setiOSBackPressHandler,
|
|
30
32
|
getInitialUrl,
|
|
31
33
|
}: AppRootProps) {
|
|
32
34
|
const prefix = getSchemePrefix({
|
|
@@ -47,6 +49,7 @@ export function AppRoot({
|
|
|
47
49
|
container={Container}
|
|
48
50
|
prefix={prefix}
|
|
49
51
|
setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
|
|
52
|
+
setiOSBackPressHandler={setiOSBackPressHandler}
|
|
50
53
|
getInitialUrl={getInitialUrl}
|
|
51
54
|
{...router}
|
|
52
55
|
/>
|
package/src/app/Granite.tsx
CHANGED
|
@@ -39,6 +39,12 @@ export interface GraniteProps {
|
|
|
39
39
|
*/
|
|
40
40
|
setIosSwipeGestureEnabled?: ({ isEnabled }: { isEnabled: boolean }) => void;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* @description
|
|
44
|
+
* The function to register a handler that runs when the iOS swipe back gesture is detected.
|
|
45
|
+
*/
|
|
46
|
+
setiOSBackPressHandler?: ({ handler }: { handler: () => void }) => Promise<void> | void;
|
|
47
|
+
|
|
42
48
|
/**
|
|
43
49
|
* @description
|
|
44
50
|
* The function to provide the initial URL to router.
|
|
@@ -65,7 +71,7 @@ const createApp = () => {
|
|
|
65
71
|
return {
|
|
66
72
|
registerApp(
|
|
67
73
|
AppContainer: ComponentType<PropsWithChildren<InitialProps>>,
|
|
68
|
-
{ appName, context, router, initialScheme, setIosSwipeGestureEnabled, getInitialUrl }: GraniteProps
|
|
74
|
+
{ appName, context, router, initialScheme, setIosSwipeGestureEnabled, setiOSBackPressHandler, getInitialUrl }: GraniteProps
|
|
69
75
|
): (initialProps: InitialProps) => JSX.Element {
|
|
70
76
|
if (appName === ENTRY_BUNDLE_NAME) {
|
|
71
77
|
throw new Error(`Reserved app name 'shared' cannot be used`);
|
|
@@ -80,6 +86,7 @@ const createApp = () => {
|
|
|
80
86
|
initialProps={initialProps}
|
|
81
87
|
initialScheme={initialSchemeValue}
|
|
82
88
|
setIosSwipeGestureEnabled={setIosSwipeGestureEnabled}
|
|
89
|
+
setiOSBackPressHandler={setiOSBackPressHandler}
|
|
83
90
|
getInitialUrl={getInitialUrl}
|
|
84
91
|
appName={appName}
|
|
85
92
|
context={context}
|
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
|
+
});
|