@granite-js/react-native 1.0.31 → 1.0.33
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/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 +3 -3
- 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
|
@@ -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
|
+
});
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useVisibility } from '../visibility';
|
|
3
|
+
import {
|
|
4
|
+
BackEventActionsContext,
|
|
5
|
+
BackEventStateContext,
|
|
6
|
+
type BackEventActionsContextValue,
|
|
7
|
+
type BackEventStateContextValue,
|
|
8
|
+
type PrivateBackEventControls,
|
|
9
|
+
} from './BackEventContext';
|
|
10
|
+
import { useBackEventActionsContext } from './useBackEventActionsContext';
|
|
11
|
+
import type { BackHandlerCallback } from './useBackHandler';
|
|
3
12
|
|
|
4
|
-
export
|
|
5
|
-
addEventListener: (...handlers: Array<() => void>) => void;
|
|
6
|
-
removeEventListener: (...handlers: Array<() => void>) => void;
|
|
7
|
-
}
|
|
13
|
+
export type BackEventSource = 'backButton' | 'iosSwipeGesture' | 'androidHardwareBackPress';
|
|
8
14
|
|
|
9
|
-
interface
|
|
10
|
-
|
|
11
|
-
hasBackEvent: boolean;
|
|
12
|
-
onBack: () => void;
|
|
15
|
+
export interface BackEvent {
|
|
16
|
+
source: BackEventSource;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
|
|
19
|
+
export type BackEventHandler = () => void;
|
|
20
|
+
|
|
21
|
+
export interface BackEventControls {
|
|
22
|
+
addEventListener: (...handlers: Array<BackEventHandler>) => void;
|
|
23
|
+
removeEventListener: (...handlers: Array<BackEventHandler>) => void;
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* @component
|
|
@@ -41,7 +50,37 @@ const BackEventContext = createContext<PrivateBackEventControls | null>(null);
|
|
|
41
50
|
export function BackEventProvider({ children }: { children: ReactNode }) {
|
|
42
51
|
const backEventState = useBackEventState();
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
const actions = useMemo((): BackEventActionsContextValue => {
|
|
54
|
+
return {
|
|
55
|
+
addEventListener: backEventState.addEventListener,
|
|
56
|
+
removeEventListener: backEventState.removeEventListener,
|
|
57
|
+
addBackHandler: backEventState.addBackHandler,
|
|
58
|
+
removeBackHandler: backEventState.removeBackHandler,
|
|
59
|
+
onBack: backEventState.onBack,
|
|
60
|
+
onBackHandler: backEventState.onBackHandler,
|
|
61
|
+
};
|
|
62
|
+
}, [
|
|
63
|
+
backEventState.addBackHandler,
|
|
64
|
+
backEventState.addEventListener,
|
|
65
|
+
backEventState.onBack,
|
|
66
|
+
backEventState.onBackHandler,
|
|
67
|
+
backEventState.removeBackHandler,
|
|
68
|
+
backEventState.removeEventListener,
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
const state = useMemo((): BackEventStateContextValue => {
|
|
72
|
+
return {
|
|
73
|
+
handlersRef: backEventState.handlersRef,
|
|
74
|
+
hasBackEvent: backEventState.hasBackEvent,
|
|
75
|
+
hasBackHandler: backEventState.hasBackHandler,
|
|
76
|
+
};
|
|
77
|
+
}, [backEventState.handlersRef, backEventState.hasBackEvent, backEventState.hasBackHandler]);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<BackEventActionsContext.Provider value={actions}>
|
|
81
|
+
<BackEventStateContext.Provider value={state}>{children}</BackEventStateContext.Provider>
|
|
82
|
+
</BackEventActionsContext.Provider>
|
|
83
|
+
);
|
|
45
84
|
}
|
|
46
85
|
|
|
47
86
|
/**
|
|
@@ -70,11 +109,26 @@ export function BackEventProvider({ children }: { children: ReactNode }) {
|
|
|
70
109
|
* ```
|
|
71
110
|
*/
|
|
72
111
|
export function useBackEventState() {
|
|
73
|
-
const handlersRef = useRef<Set<
|
|
112
|
+
const handlersRef = useRef<Set<BackEventHandler>>(new Set()).current;
|
|
113
|
+
const backHandlersRef = useRef<Set<BackHandlerCallback>>(new Set()).current;
|
|
74
114
|
const [hasBackEvent, setHasBackEvent] = useState(false);
|
|
115
|
+
const [hasBackHandler, setHasBackHandler] = useState(false);
|
|
116
|
+
|
|
117
|
+
const removeEventListener = useCallback(
|
|
118
|
+
(...handlers: Array<BackEventHandler>) => {
|
|
119
|
+
for (const handler of handlers) {
|
|
120
|
+
handlersRef.delete(handler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (handlersRef.size === 0) {
|
|
124
|
+
setHasBackEvent(false);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
[handlersRef]
|
|
128
|
+
);
|
|
75
129
|
|
|
76
130
|
const addEventListener = useCallback(
|
|
77
|
-
(...handlers: Array<
|
|
131
|
+
(...handlers: Array<BackEventHandler>) => {
|
|
78
132
|
for (const handler of handlers) {
|
|
79
133
|
handlersRef.add(handler);
|
|
80
134
|
}
|
|
@@ -86,17 +140,53 @@ export function useBackEventState() {
|
|
|
86
140
|
[handlersRef]
|
|
87
141
|
);
|
|
88
142
|
|
|
89
|
-
const
|
|
90
|
-
(...handlers: Array<
|
|
143
|
+
const removeBackHandler = useCallback(
|
|
144
|
+
(...handlers: Array<BackHandlerCallback>) => {
|
|
91
145
|
for (const handler of handlers) {
|
|
92
|
-
|
|
146
|
+
backHandlersRef.delete(handler);
|
|
93
147
|
}
|
|
94
148
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
149
|
+
if (backHandlersRef.size === 0) {
|
|
150
|
+
setHasBackHandler(false);
|
|
97
151
|
}
|
|
98
152
|
},
|
|
99
|
-
[
|
|
153
|
+
[backHandlersRef]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const addBackHandler = useCallback(
|
|
157
|
+
(handler: BackHandlerCallback) => {
|
|
158
|
+
backHandlersRef.add(handler);
|
|
159
|
+
|
|
160
|
+
if (backHandlersRef.size > 0) {
|
|
161
|
+
setHasBackHandler(true);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
remove: () => removeBackHandler(handler),
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
[backHandlersRef, removeBackHandler]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const onBack = useCallback(() => {
|
|
172
|
+
handlersRef.forEach((handler) => handler());
|
|
173
|
+
}, [handlersRef]);
|
|
174
|
+
|
|
175
|
+
const onBackHandler = useCallback(
|
|
176
|
+
(event: BackEvent) => {
|
|
177
|
+
const orderedBackHandlers = Array.from(backHandlersRef).reverse();
|
|
178
|
+
|
|
179
|
+
for (const handler of orderedBackHandlers) {
|
|
180
|
+
const didHandleBackEvent = handler(event) === true;
|
|
181
|
+
|
|
182
|
+
if (didHandleBackEvent) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
},
|
|
189
|
+
[backHandlersRef]
|
|
100
190
|
);
|
|
101
191
|
|
|
102
192
|
const backEvent = useMemo((): PrivateBackEventControls => {
|
|
@@ -105,11 +195,24 @@ export function useBackEventState() {
|
|
|
105
195
|
removeEventListener,
|
|
106
196
|
handlersRef,
|
|
107
197
|
hasBackEvent,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
198
|
+
hasBackHandler,
|
|
199
|
+
addBackHandler,
|
|
200
|
+
removeBackHandler,
|
|
201
|
+
onBack,
|
|
202
|
+
onBackHandler,
|
|
111
203
|
};
|
|
112
|
-
}, [
|
|
204
|
+
}, [
|
|
205
|
+
addEventListener,
|
|
206
|
+
addBackHandler,
|
|
207
|
+
backHandlersRef,
|
|
208
|
+
handlersRef,
|
|
209
|
+
hasBackEvent,
|
|
210
|
+
hasBackHandler,
|
|
211
|
+
onBack,
|
|
212
|
+
onBackHandler,
|
|
213
|
+
removeBackHandler,
|
|
214
|
+
removeEventListener,
|
|
215
|
+
]);
|
|
113
216
|
|
|
114
217
|
return backEvent;
|
|
115
218
|
}
|
|
@@ -180,36 +283,32 @@ export function useBackEventState() {
|
|
|
180
283
|
* ```
|
|
181
284
|
*/
|
|
182
285
|
export function useBackEvent() {
|
|
183
|
-
const context =
|
|
184
|
-
const handlersRef = useRef<Set<
|
|
286
|
+
const context = useBackEventActionsContext();
|
|
287
|
+
const handlersRef = useRef<Set<BackEventHandler>>(new Set()).current;
|
|
185
288
|
|
|
186
289
|
const isVisible = useVisibility();
|
|
187
290
|
|
|
188
|
-
if (context == null) {
|
|
189
|
-
throw new Error('useBackEvent must be used within a BackEventProvider');
|
|
190
|
-
}
|
|
191
|
-
|
|
192
291
|
const contextAddEventListener = context.addEventListener;
|
|
193
292
|
const contextRemoveEventListener = context.removeEventListener;
|
|
194
293
|
|
|
195
|
-
const
|
|
196
|
-
(...handlers: Array<
|
|
294
|
+
const removeEventListener = useCallback(
|
|
295
|
+
(...handlers: Array<BackEventHandler>) => {
|
|
197
296
|
for (const handler of handlers) {
|
|
198
|
-
handlersRef.
|
|
199
|
-
|
|
297
|
+
handlersRef.delete(handler);
|
|
298
|
+
contextRemoveEventListener(handler);
|
|
200
299
|
}
|
|
201
300
|
},
|
|
202
|
-
[
|
|
301
|
+
[contextRemoveEventListener, handlersRef]
|
|
203
302
|
);
|
|
204
303
|
|
|
205
|
-
const
|
|
206
|
-
(...handlers: Array<
|
|
304
|
+
const addEventListener = useCallback(
|
|
305
|
+
(...handlers: Array<BackEventHandler>) => {
|
|
207
306
|
for (const handler of handlers) {
|
|
208
|
-
handlersRef.
|
|
209
|
-
|
|
307
|
+
handlersRef.add(handler);
|
|
308
|
+
contextAddEventListener(handler);
|
|
210
309
|
}
|
|
211
310
|
},
|
|
212
|
-
[
|
|
311
|
+
[contextAddEventListener, handlersRef]
|
|
213
312
|
);
|
|
214
313
|
|
|
215
314
|
/**
|
|
@@ -242,12 +341,3 @@ export function useBackEvent() {
|
|
|
242
341
|
|
|
243
342
|
return backEvent;
|
|
244
343
|
}
|
|
245
|
-
|
|
246
|
-
export function useBackEventContext() {
|
|
247
|
-
const context = useContext(BackEventContext);
|
|
248
|
-
if (context == null) {
|
|
249
|
-
throw new Error('useBackEvent must be used within a BackEventProvider');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return context;
|
|
253
|
-
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { BackEventActionsContext } from './BackEventContext';
|
|
3
|
+
|
|
4
|
+
export function useBackEventActionsContext() {
|
|
5
|
+
const context = useContext(BackEventActionsContext);
|
|
6
|
+
if (context == null) {
|
|
7
|
+
throw new Error('useBackEvent must be used within a BackEventProvider');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return context;
|
|
11
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useContext, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BackEventActionsContext,
|
|
4
|
+
BackEventStateContext,
|
|
5
|
+
type PrivateBackEventControls,
|
|
6
|
+
} from './BackEventContext';
|
|
7
|
+
|
|
8
|
+
export function useBackEventContext() {
|
|
9
|
+
const actions = useContext(BackEventActionsContext);
|
|
10
|
+
const state = useContext(BackEventStateContext);
|
|
11
|
+
|
|
12
|
+
const context = useMemo((): PrivateBackEventControls | null => {
|
|
13
|
+
if (actions == null || state == null) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
...actions,
|
|
19
|
+
...state,
|
|
20
|
+
};
|
|
21
|
+
}, [actions, state]);
|
|
22
|
+
|
|
23
|
+
if (context == null) {
|
|
24
|
+
throw new Error('useBackEvent must be used within a BackEventProvider');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return context;
|
|
28
|
+
}
|