@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 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
@@ -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;
@@ -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
  };
@@ -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
- export declare function CanGoBackGuard({ children, canGoBack, onBack, isInitialScreen, setIosSwipeGestureEnabled, }: {
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?: ({ isEnabled }: {
8
- isEnabled: boolean;
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 +1,2 @@
1
1
  export * from './useBackEvent';
2
+ export * from './useBackHandler';
@@ -1,12 +1,13 @@
1
1
  import { ReactNode } from 'react';
2
- export interface BackEventControls {
3
- addEventListener: (...handlers: Array<() => void>) => void;
4
- removeEventListener: (...handlers: Array<() => void>) => void;
2
+ import { type PrivateBackEventControls } from './BackEventContext';
3
+ export type BackEventSource = 'backButton' | 'iosSwipeGesture' | 'androidHardwareBackPress';
4
+ export interface BackEvent {
5
+ source: BackEventSource;
5
6
  }
6
- interface PrivateBackEventControls extends BackEventControls {
7
- handlersRef: Set<() => void>;
8
- hasBackEvent: boolean;
9
- onBack: () => void;
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,2 @@
1
+ import { type PrivateBackEventControls } from './BackEventContext';
2
+ export declare function useBackEventContext(): PrivateBackEventControls;
@@ -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.31",
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.31",
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.31",
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.31",
124
- "@granite-js/brownfield-module": "1.0.31",
125
- "@granite-js/cli": "1.0.31",
126
- "@granite-js/jest": "1.0.31",
127
- "@granite-js/mpack": "1.0.31",
128
- "@granite-js/plugin-core": "1.0.31",
129
- "@granite-js/style-utils": "1.0.31",
130
- "@granite-js/video": "1.0.31",
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"
@@ -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
  />
@@ -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}
@@ -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, onBack } = useInternalRouterBackHandler({
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={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?: ({ isEnabled }: { isEnabled: boolean }) => void;
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 (shouldBlockGoingBack) {
33
- const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
34
- onBack?.();
35
- return true;
36
- });
38
+ if (!hasBackEvent) {
39
+ return;
40
+ }
37
41
 
38
- return () => {
39
- subscription.remove();
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
- return;
44
- }, [shouldBlockGoingBack, onBack]);
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
+ });