@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
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
|
+
|
|
18
|
+
## 1.0.31
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- f8aee14: fix: impression area debounced handlers
|
|
23
|
+
- @granite-js/blur-view@1.0.31
|
|
24
|
+
- @granite-js/brownfield-module@1.0.31
|
|
25
|
+
- @granite-js/cli@1.0.31
|
|
26
|
+
- @granite-js/jest@1.0.31
|
|
27
|
+
- @granite-js/mpack@1.0.31
|
|
28
|
+
- @granite-js/native@1.0.31
|
|
29
|
+
- @granite-js/plugin-core@1.0.31
|
|
30
|
+
- @granite-js/style-utils@1.0.31
|
|
31
|
+
- @granite-js/video@1.0.31
|
|
32
|
+
|
|
3
33
|
## 1.0.30
|
|
4
34
|
|
|
5
35
|
### 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
|
};
|
|
@@ -39,11 +39,13 @@ export interface IntersectionObserverOptions {
|
|
|
39
39
|
*/
|
|
40
40
|
root: Root;
|
|
41
41
|
rootMargin?: RootMargin;
|
|
42
|
-
|
|
42
|
+
onLayoutThreshold?: number;
|
|
43
|
+
onScrollThreshold?: number;
|
|
43
44
|
}
|
|
44
45
|
export type IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => void;
|
|
45
46
|
export declare const defaultRootMargin: RootMargin;
|
|
46
|
-
export declare const
|
|
47
|
+
export declare const defaultOnLayoutThreshold = 300;
|
|
48
|
+
export declare const defaultOnScrollThreshold = 100;
|
|
47
49
|
/**
|
|
48
50
|
* @kind class
|
|
49
51
|
* @name IntersectionObserver
|
|
@@ -57,10 +59,12 @@ declare class IntersectionObserver {
|
|
|
57
59
|
protected callback: IntersectionObserverCallback;
|
|
58
60
|
protected options: IntersectionObserverOptions;
|
|
59
61
|
protected targets: Element[];
|
|
62
|
+
protected onLayoutHandler: () => void;
|
|
63
|
+
protected onScrollHandler: () => void;
|
|
60
64
|
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions);
|
|
61
65
|
protected measureTarget: (target: Element) => void;
|
|
62
|
-
protected
|
|
63
|
-
protected
|
|
66
|
+
protected createOnLayoutHandler(): () => void;
|
|
67
|
+
protected createOnScrollHandler(): import("es-toolkit").ThrottledFunction<() => void>;
|
|
64
68
|
observe(target: Element): void;
|
|
65
69
|
unobserve(target: Element): void;
|
|
66
70
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ComponentProps } from 'react';
|
|
2
2
|
import { ScrollView } from 'react-native';
|
|
3
|
-
import { RootMargin } from './IntersectionObserver';
|
|
3
|
+
import type { IntersectionObserverOptions, RootMargin } from './IntersectionObserver';
|
|
4
4
|
export interface IOComponentProps {
|
|
5
5
|
rootMargin?: RootMargin;
|
|
6
6
|
}
|
|
@@ -16,5 +16,5 @@ export interface IOComponentProps {
|
|
|
16
16
|
* @param {string[]} [methods] - List of event handler names from BaseComponent to be bound.
|
|
17
17
|
* @returns {React.ComponentType} - Returns a wrapped component that can use Intersection Observer functionality.
|
|
18
18
|
*/
|
|
19
|
-
declare function withIO<CompProps extends Pick<ComponentProps<typeof ScrollView>, 'horizontal' | 'scrollEventThrottle' | 'onContentSizeChange' | 'onLayout' | 'onScroll'>>(BaseComponent: React.ComponentType<CompProps>, methods: string[]): typeof BaseComponent;
|
|
19
|
+
declare function withIO<CompProps extends Pick<ComponentProps<typeof ScrollView>, 'horizontal' | 'scrollEventThrottle' | 'onContentSizeChange' | 'onLayout' | 'onScroll'>>(BaseComponent: React.ComponentType<CompProps>, methods: string[], options?: Omit<IntersectionObserverOptions, 'root' | 'rootMargin'>): typeof BaseComponent;
|
|
20
20
|
export default withIO;
|
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}
|
|
@@ -45,7 +45,8 @@ export interface IntersectionObserverOptions {
|
|
|
45
45
|
*/
|
|
46
46
|
root: Root;
|
|
47
47
|
rootMargin?: RootMargin;
|
|
48
|
-
|
|
48
|
+
onLayoutThreshold?: number;
|
|
49
|
+
onScrollThreshold?: number;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
export type IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => void;
|
|
@@ -57,7 +58,8 @@ export const defaultRootMargin: RootMargin = {
|
|
|
57
58
|
bottom: 0,
|
|
58
59
|
};
|
|
59
60
|
|
|
60
|
-
export const
|
|
61
|
+
export const defaultOnLayoutThreshold = 300;
|
|
62
|
+
export const defaultOnScrollThreshold = 100;
|
|
61
63
|
|
|
62
64
|
/**
|
|
63
65
|
* @kind class
|
|
@@ -72,13 +74,15 @@ class IntersectionObserver {
|
|
|
72
74
|
protected callback: IntersectionObserverCallback;
|
|
73
75
|
protected options: IntersectionObserverOptions;
|
|
74
76
|
protected targets: Element[];
|
|
77
|
+
protected onLayoutHandler: () => void;
|
|
78
|
+
protected onScrollHandler: () => void;
|
|
75
79
|
|
|
76
80
|
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions) {
|
|
77
81
|
this.callback = callback;
|
|
78
82
|
this.options = options;
|
|
79
83
|
this.targets = [];
|
|
80
|
-
this.options.root.onLayout = this.
|
|
81
|
-
this.options.root.onScroll = this.
|
|
84
|
+
this.onLayoutHandler = this.options.root.onLayout = this.createOnLayoutHandler();
|
|
85
|
+
this.onScrollHandler = this.options.root.onScroll = this.createOnScrollHandler();
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
protected measureTarget = (target: Element) => {
|
|
@@ -91,111 +95,117 @@ class IntersectionObserver {
|
|
|
91
95
|
width,
|
|
92
96
|
height,
|
|
93
97
|
};
|
|
94
|
-
this.
|
|
98
|
+
this.onScrollHandler();
|
|
95
99
|
});
|
|
96
100
|
}
|
|
97
101
|
};
|
|
98
102
|
|
|
99
|
-
protected
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
protected createOnLayoutHandler(): () => void {
|
|
104
|
+
const onLayoutThreshold = this.options?.onLayoutThreshold || defaultOnLayoutThreshold;
|
|
105
|
+
return throttle(
|
|
106
|
+
() => {
|
|
107
|
+
for (let index = 0; index < this.targets.length; index += 1) {
|
|
108
|
+
const target = this.targets[index];
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
},
|
|
109
|
-
300,
|
|
110
|
-
{ edges: ['trailing'] }
|
|
111
|
-
) as () => void;
|
|
112
|
-
|
|
113
|
-
protected handleScroll = throttle(
|
|
114
|
-
() => {
|
|
115
|
-
const rootMargin = this.options?.rootMargin || defaultRootMargin;
|
|
116
|
-
|
|
117
|
-
const {
|
|
118
|
-
horizontal,
|
|
119
|
-
current: { contentOffset, contentSize, layoutMeasurement },
|
|
120
|
-
} = this.options.root;
|
|
121
|
-
if (
|
|
122
|
-
contentSize.width <= 0 ||
|
|
123
|
-
contentSize.height <= 0 ||
|
|
124
|
-
layoutMeasurement.width <= 0 ||
|
|
125
|
-
layoutMeasurement.height <= 0
|
|
126
|
-
) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const contentOffsetWithLayout = horizontal
|
|
130
|
-
? contentOffset.x + layoutMeasurement.width
|
|
131
|
-
: contentOffset.y + layoutMeasurement.height;
|
|
132
|
-
const changedTargets: IntersectionObserverEntry[] = [];
|
|
133
|
-
for (let index = 0; index < this.targets.length; index += 1) {
|
|
134
|
-
const target = this.targets[index];
|
|
135
|
-
|
|
136
|
-
if (target == null) {
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const targetLayout = target.layout;
|
|
141
|
-
if (!targetLayout || targetLayout.width === 0 || targetLayout.height === 0) {
|
|
142
|
-
continue;
|
|
110
|
+
if (target != null) {
|
|
111
|
+
this.measureTarget(target);
|
|
112
|
+
}
|
|
143
113
|
}
|
|
114
|
+
},
|
|
115
|
+
onLayoutThreshold,
|
|
116
|
+
{ edges: ['leading', 'trailing'] }
|
|
117
|
+
);
|
|
118
|
+
}
|
|
144
119
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
} else {
|
|
163
|
-
const visibleTargetMinY = Math.max(contentOffset.y - (rootMargin.top || 0), targetLayout.y);
|
|
164
|
-
const visibleTargetMaxY = Math.min(
|
|
165
|
-
contentOffsetWithLayout + (rootMargin.bottom || 0),
|
|
166
|
-
targetLayout.y + targetLayout.height
|
|
167
|
-
);
|
|
168
|
-
const visibleHeight = Math.max(visibleTargetMaxY - visibleTargetMinY, 0);
|
|
169
|
-
|
|
170
|
-
intersectionRatio = visibleHeight / targetLayout.height;
|
|
171
|
-
isIntersecting =
|
|
172
|
-
contentOffsetWithLayout + (rootMargin.bottom || 0) >= targetLayout.y &&
|
|
173
|
-
contentOffset.y - (rootMargin.top || 0) <= targetLayout.y + targetLayout.height;
|
|
120
|
+
protected createOnScrollHandler() {
|
|
121
|
+
const onScrollThreshold = this.options?.onScrollThreshold || defaultOnScrollThreshold;
|
|
122
|
+
return throttle(
|
|
123
|
+
() => {
|
|
124
|
+
const rootMargin = this.options?.rootMargin || defaultRootMargin;
|
|
125
|
+
|
|
126
|
+
const {
|
|
127
|
+
horizontal,
|
|
128
|
+
current: { contentOffset, contentSize, layoutMeasurement },
|
|
129
|
+
} = this.options.root;
|
|
130
|
+
if (
|
|
131
|
+
contentSize.width <= 0 ||
|
|
132
|
+
contentSize.height <= 0 ||
|
|
133
|
+
layoutMeasurement.width <= 0 ||
|
|
134
|
+
layoutMeasurement.height <= 0
|
|
135
|
+
) {
|
|
136
|
+
return;
|
|
174
137
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
target
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
138
|
+
const contentOffsetWithLayout = horizontal
|
|
139
|
+
? contentOffset.x + layoutMeasurement.width
|
|
140
|
+
: contentOffset.y + layoutMeasurement.height;
|
|
141
|
+
const changedTargets: IntersectionObserverEntry[] = [];
|
|
142
|
+
for (let index = 0; index < this.targets.length; index += 1) {
|
|
143
|
+
const target = this.targets[index];
|
|
144
|
+
|
|
145
|
+
if (target == null) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const targetLayout = target.layout;
|
|
150
|
+
if (!targetLayout || targetLayout.width === 0 || targetLayout.height === 0) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const previousIntersectionRatio = target.intersectionRatio;
|
|
155
|
+
|
|
156
|
+
let isIntersecting = false;
|
|
157
|
+
let intersectionRatio = previousIntersectionRatio;
|
|
158
|
+
|
|
159
|
+
if (horizontal) {
|
|
160
|
+
const visibleTargetMinX = Math.max(contentOffset.x - (rootMargin.left || 0), targetLayout.x);
|
|
161
|
+
const visibleTargetMaxX = Math.min(
|
|
162
|
+
contentOffsetWithLayout + (rootMargin.left || 0),
|
|
163
|
+
targetLayout.x + targetLayout.width
|
|
164
|
+
);
|
|
165
|
+
const visibleHeight = Math.max(visibleTargetMaxX - visibleTargetMinX, 0);
|
|
166
|
+
|
|
167
|
+
intersectionRatio = visibleHeight / targetLayout.height;
|
|
168
|
+
isIntersecting =
|
|
169
|
+
contentOffsetWithLayout + (rootMargin.right || 0) >= targetLayout.x &&
|
|
170
|
+
contentOffset.x - (rootMargin.left || 0) <= targetLayout.x + targetLayout.width;
|
|
171
|
+
} else {
|
|
172
|
+
const visibleTargetMinY = Math.max(contentOffset.y - (rootMargin.top || 0), targetLayout.y);
|
|
173
|
+
const visibleTargetMaxY = Math.min(
|
|
174
|
+
contentOffsetWithLayout + (rootMargin.bottom || 0),
|
|
175
|
+
targetLayout.y + targetLayout.height
|
|
176
|
+
);
|
|
177
|
+
const visibleHeight = Math.max(visibleTargetMaxY - visibleTargetMinY, 0);
|
|
178
|
+
|
|
179
|
+
intersectionRatio = visibleHeight / targetLayout.height;
|
|
180
|
+
isIntersecting =
|
|
181
|
+
contentOffsetWithLayout + (rootMargin.bottom || 0) >= targetLayout.y &&
|
|
182
|
+
contentOffset.y - (rootMargin.top || 0) <= targetLayout.y + targetLayout.height;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
intersectionRatio = Math.floor(intersectionRatio * 100) / 100;
|
|
186
|
+
|
|
187
|
+
if (target.inView !== isIntersecting || target.intersectionRatio !== intersectionRatio) {
|
|
188
|
+
target.inView = isIntersecting;
|
|
189
|
+
target.intersectionRatio = intersectionRatio;
|
|
190
|
+
|
|
191
|
+
changedTargets.push({
|
|
192
|
+
target,
|
|
193
|
+
isIntersecting,
|
|
194
|
+
intersectionRatio,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
187
197
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
this.callback(changedTargets);
|
|
199
|
+
},
|
|
200
|
+
onScrollThreshold,
|
|
201
|
+
{ edges: ['leading', 'trailing'] }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
194
204
|
|
|
195
205
|
public observe(target: Element) {
|
|
196
206
|
const index = this.targets.indexOf(target);
|
|
197
207
|
if (index < 0) {
|
|
198
|
-
target.onLayout = this.
|
|
208
|
+
target.onLayout = this.onLayoutHandler;
|
|
199
209
|
this.targets.push(target);
|
|
200
210
|
}
|
|
201
211
|
}
|
|
@@ -2,7 +2,7 @@ import { type ComponentProps, PureComponent, RefObject, createRef } from 'react'
|
|
|
2
2
|
import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, View } from 'react-native';
|
|
3
3
|
import IOContext, { IOContextValue } from './IOContext';
|
|
4
4
|
import IOManager from './IOManager';
|
|
5
|
-
import { Root, RootMargin } from './IntersectionObserver';
|
|
5
|
+
import type { IntersectionObserverOptions, Root, RootMargin } from './IntersectionObserver';
|
|
6
6
|
|
|
7
7
|
export interface IOComponentProps {
|
|
8
8
|
rootMargin?: RootMargin;
|
|
@@ -25,7 +25,11 @@ function withIO<
|
|
|
25
25
|
ComponentProps<typeof ScrollView>,
|
|
26
26
|
'horizontal' | 'scrollEventThrottle' | 'onContentSizeChange' | 'onLayout' | 'onScroll'
|
|
27
27
|
>,
|
|
28
|
-
>(
|
|
28
|
+
>(
|
|
29
|
+
BaseComponent: React.ComponentType<CompProps>,
|
|
30
|
+
methods: string[],
|
|
31
|
+
options?: Omit<IntersectionObserverOptions, 'root' | 'rootMargin'>
|
|
32
|
+
) {
|
|
29
33
|
type ScrollableComponentProps = CompProps & IOComponentProps;
|
|
30
34
|
const IOScrollableComponent = class extends PureComponent<ScrollableComponentProps> {
|
|
31
35
|
protected node: any;
|
|
@@ -75,6 +79,7 @@ function withIO<
|
|
|
75
79
|
get rootMargin() {
|
|
76
80
|
return self.props.rootMargin;
|
|
77
81
|
},
|
|
82
|
+
...options,
|
|
78
83
|
});
|
|
79
84
|
this.manager = manager;
|
|
80
85
|
this.contextValue = {
|