@granite-js/react-native 0.0.0-dev-20250725013859
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/LICENSE +202 -0
- package/README.md +24 -0
- package/bin/cli.js +3 -0
- package/cli.d.ts +1 -0
- package/cli.js +4 -0
- package/config.d.ts +2 -0
- package/config.js +5 -0
- package/dist/app/App/index.android.d.ts +2 -0
- package/dist/app/App/index.ios.d.ts +6 -0
- package/dist/app/AppRoot.d.ts +1 -0
- package/dist/app/Granite.d.ts +61 -0
- package/dist/app/HostAppRoot.d.ts +1 -0
- package/dist/app/index.d.ts +2 -0
- package/dist/async-bridges.d.ts +2 -0
- package/dist/blur/BlurView.d.ts +78 -0
- package/dist/blur/ReactNativeBlurModule.d.ts +6 -0
- package/dist/blur/constants.d.ts +1 -0
- package/dist/blur/index.d.ts +1 -0
- package/dist/constant-bridges.d.ts +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/dev-entrypoint/index.d.ts +2 -0
- package/dist/event/abstract.d.ts +42 -0
- package/dist/event/index.d.ts +2 -0
- package/dist/event/useGraniteEvent.d.ts +14 -0
- package/dist/impression-area/ImpressionArea.d.ts +231 -0
- package/dist/impression-area/index.d.ts +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/initial-props/InitialProps.d.ts +92 -0
- package/dist/initial-props/index.d.ts +1 -0
- package/dist/intersection-observer/IOContext.d.ts +10 -0
- package/dist/intersection-observer/IOFlatList.d.ts +55 -0
- package/dist/intersection-observer/IOManager.d.ts +24 -0
- package/dist/intersection-observer/IOScrollView.d.ts +59 -0
- package/dist/intersection-observer/InView.d.ts +107 -0
- package/dist/intersection-observer/IntersectionObserver.d.ts +67 -0
- package/dist/intersection-observer/index.d.ts +8 -0
- package/dist/intersection-observer/withIO.d.ts +20 -0
- package/dist/jest/index.d.ts +1 -0
- package/dist/jest/index.js +32 -0
- package/dist/keyboard/KeyboardAboveView.d.ts +40 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/useKeyboardAnimatedHeight.d.ts +20 -0
- package/dist/native-event-emitter/eventEmitters/index.d.ts +2 -0
- package/dist/native-event-emitter/eventEmitters/types.d.ts +4 -0
- package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +10 -0
- package/dist/native-event-emitter/index.d.ts +1 -0
- package/dist/native-event-emitter/nativeEventEmitter.d.ts +15 -0
- package/dist/native-modules/core/GraniteCoreModule.d.ts +8 -0
- package/dist/native-modules/index.d.ts +3 -0
- package/dist/native-modules/natives/GraniteModule.d.ts +7 -0
- package/dist/native-modules/natives/closeView.d.ts +21 -0
- package/dist/native-modules/natives/getSchemeUri.d.ts +23 -0
- package/dist/native-modules/natives/index.d.ts +3 -0
- package/dist/native-modules/natives/openURL.d.ts +36 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/useWaitForReturnNavigator.d.ts +39 -0
- package/dist/rn-polyfills/index.d.ts +1 -0
- package/dist/rn-polyfills/symbol-asynciterator/index.d.ts +9 -0
- package/dist/rn-polyfills/url/index.d.ts +1 -0
- package/dist/router/Router.d.ts +59 -0
- package/dist/router/components/BackButton.d.ts +7 -0
- package/dist/router/components/CanGoBackGuard.d.ts +6 -0
- package/dist/router/components/RouterBackButton.d.ts +9 -0
- package/dist/router/components/StackNavigator.d.ts +54 -0
- package/dist/router/constants.d.ts +2 -0
- package/dist/router/createRoute.d.ts +39 -0
- package/dist/router/createRoute.test-d.d.ts +9 -0
- package/dist/router/hooks/useInitialRouteName.d.ts +1 -0
- package/dist/router/hooks/useIsInitialScreen.d.ts +1 -0
- package/dist/router/hooks/useRouterControls.d.ts +11 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/types/RequireContext.d.ts +7 -0
- package/dist/router/types/RouteScreen.d.ts +16 -0
- package/dist/router/types/Screen.d.ts +23 -0
- package/dist/router/types/index.d.ts +3 -0
- package/dist/router/types/screen-option.d.ts +4 -0
- package/dist/router/utils/createParentRouteScreenMap.d.ts +8 -0
- package/dist/router/utils/defaultParserParams.d.ts +9 -0
- package/dist/router/utils/index.d.ts +2 -0
- package/dist/router/utils/matchers.d.ts +2 -0
- package/dist/router/utils/mergeParentLayoutScreen.d.ts +18 -0
- package/dist/router/utils/path.d.ts +53 -0
- package/dist/router/utils/screen.d.ts +37 -0
- package/dist/scroll-view-inertial-background/ScrollViewInertialBackground.d.ts +49 -0
- package/dist/scroll-view-inertial-background/index.d.ts +1 -0
- package/dist/status-bar/StatusBar.android.d.ts +3 -0
- package/dist/status-bar/StatusBar.ios.d.ts +3 -0
- package/dist/status-bar/index.d.ts +2 -0
- package/dist/status-bar/types.d.ts +20 -0
- package/dist/status-bar/utils.d.ts +3 -0
- package/dist/types/global.d.ts +14 -0
- package/dist/use-back-event/index.d.ts +1 -0
- package/dist/use-back-event/useBackEvent.d.ts +135 -0
- package/dist/utils/noop.d.ts +1 -0
- package/dist/utils/usePreservedCallback.d.ts +1 -0
- package/dist/video/Video.d.ts +67 -0
- package/dist/video/index.d.ts +1 -0
- package/dist/video/instance.d.ts +9 -0
- package/dist/visibility/VisibilityProvider.d.ts +27 -0
- package/dist/visibility/index.d.ts +6 -0
- package/dist/visibility/react-navigation/index.d.ts +2 -0
- package/dist/visibility/react-navigation/useIsFocusedSafely.d.ts +20 -0
- package/dist/visibility/react-navigation/useNavigationSafely.d.ts +19 -0
- package/dist/visibility/useIsAppForeground.d.ts +39 -0
- package/dist/visibility/useVisibility.d.ts +35 -0
- package/dist/visibility/useVisibilityChange.d.ts +51 -0
- package/dist/visibility/useVisibilityChanged.d.ts +41 -0
- package/dist/visibility/utils/usePrevious.d.ts +15 -0
- package/jest.d.ts +1 -0
- package/package.json +94 -0
- package/presets.d.ts +1 -0
- package/src/app/App/index.android.tsx +6 -0
- package/src/app/App/index.d.ts +6 -0
- package/src/app/App/index.ios.tsx +13 -0
- package/src/app/AppRoot.tsx +39 -0
- package/src/app/Granite.tsx +128 -0
- package/src/app/HostAppRoot.tsx +19 -0
- package/src/app/index.ts +2 -0
- package/src/async-bridges.ts +2 -0
- package/src/blur/BlurView.tsx +103 -0
- package/src/blur/ReactNativeBlurModule.ts +19 -0
- package/src/blur/constants.ts +3 -0
- package/src/blur/index.ts +1 -0
- package/src/constant-bridges.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/dev-entrypoint/index.tsx +17 -0
- package/src/event/abstract.ts +130 -0
- package/src/event/index.ts +2 -0
- package/src/event/useGraniteEvent.ts +34 -0
- package/src/impression-area/ImpressionArea.tsx +341 -0
- package/src/impression-area/index.ts +1 -0
- package/src/index.ts +24 -0
- package/src/initial-props/InitialProps.ts +95 -0
- package/src/initial-props/index.ts +1 -0
- package/src/intersection-observer/IOContext.ts +16 -0
- package/src/intersection-observer/IOFlatList.ts +72 -0
- package/src/intersection-observer/IOManager.ts +73 -0
- package/src/intersection-observer/IOScrollView.ts +69 -0
- package/src/intersection-observer/InView.tsx +205 -0
- package/src/intersection-observer/IntersectionObserver.ts +212 -0
- package/src/intersection-observer/index.ts +24 -0
- package/src/intersection-observer/withIO.tsx +151 -0
- package/src/jest/index.ts +1 -0
- package/src/keyboard/KeyboardAboveView.tsx +62 -0
- package/src/keyboard/index.ts +2 -0
- package/src/keyboard/useKeyboardAnimatedHeight.tsx +81 -0
- package/src/native-event-emitter/eventEmitters/index.ts +3 -0
- package/src/native-event-emitter/eventEmitters/types.ts +4 -0
- package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +11 -0
- package/src/native-event-emitter/index.ts +1 -0
- package/src/native-event-emitter/nativeEventEmitter.ts +18 -0
- package/src/native-modules/core/GraniteCoreModule.ts +9 -0
- package/src/native-modules/index.ts +3 -0
- package/src/native-modules/natives/GraniteModule.ts +8 -0
- package/src/native-modules/natives/closeView.ts +25 -0
- package/src/native-modules/natives/getSchemeUri.ts +27 -0
- package/src/native-modules/natives/index.ts +3 -0
- package/src/native-modules/natives/openURL.ts +40 -0
- package/src/react/index.ts +1 -0
- package/src/react/useWaitForReturnNavigator.ts +75 -0
- package/src/rn-polyfills/index.ts +7 -0
- package/src/rn-polyfills/symbol-asynciterator/index.ts +15 -0
- package/src/rn-polyfills/url/index.ts +1 -0
- package/src/router/Router.tsx +164 -0
- package/src/router/components/BackButton.tsx +58 -0
- package/src/router/components/CanGoBackGuard.tsx +31 -0
- package/src/router/components/RouterBackButton.tsx +32 -0
- package/src/router/components/StackNavigator.tsx +12 -0
- package/src/router/constants.ts +3 -0
- package/src/router/createRoute.test-d.ts +52 -0
- package/src/router/createRoute.ts +161 -0
- package/src/router/hooks/useInitialRouteName.tsx +22 -0
- package/src/router/hooks/useIsInitialScreen.ts +7 -0
- package/src/router/hooks/useRouterControls.tsx +72 -0
- package/src/router/index.ts +3 -0
- package/src/router/types/RequireContext.ts +7 -0
- package/src/router/types/RouteScreen.ts +17 -0
- package/src/router/types/Screen.tsx +24 -0
- package/src/router/types/index.ts +3 -0
- package/src/router/types/screen-option.ts +23 -0
- package/src/router/utils/createParentRouteScreenMap.spec.ts +166 -0
- package/src/router/utils/createParentRouteScreenMap.ts +136 -0
- package/src/router/utils/defaultParserParams.spec.ts +46 -0
- package/src/router/utils/defaultParserParams.ts +19 -0
- package/src/router/utils/index.ts +2 -0
- package/src/router/utils/matchers.ts +5 -0
- package/src/router/utils/mergeParentLayoutScreen.spec.tsx +112 -0
- package/src/router/utils/mergeParentLayoutScreen.tsx +43 -0
- package/src/router/utils/path.spec.ts +135 -0
- package/src/router/utils/path.ts +105 -0
- package/src/router/utils/screen.tsx +95 -0
- package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
- package/src/scroll-view-inertial-background/index.ts +1 -0
- package/src/status-bar/StatusBar.android.tsx +36 -0
- package/src/status-bar/StatusBar.d.ts +4 -0
- package/src/status-bar/StatusBar.ios.tsx +34 -0
- package/src/status-bar/index.ts +2 -0
- package/src/status-bar/types.ts +21 -0
- package/src/status-bar/utils.ts +20 -0
- package/src/types/global.ts +21 -0
- package/src/use-back-event/index.ts +1 -0
- package/src/use-back-event/useBackEvent.tsx +260 -0
- package/src/utils/noop.ts +1 -0
- package/src/utils/usePreservedCallback.ts +16 -0
- package/src/video/Video.tsx +104 -0
- package/src/video/index.ts +1 -0
- package/src/video/instance.tsx +28 -0
- package/src/visibility/VisibilityProvider.tsx +36 -0
- package/src/visibility/index.ts +6 -0
- package/src/visibility/react-navigation/index.ts +2 -0
- package/src/visibility/react-navigation/useIsFocusedSafely.tsx +58 -0
- package/src/visibility/react-navigation/useNavigationSafely.tsx +30 -0
- package/src/visibility/useIsAppForeground.tsx +73 -0
- package/src/visibility/useVisibility.tsx +54 -0
- package/src/visibility/useVisibilityChange.ts +69 -0
- package/src/visibility/useVisibilityChanged.tsx +69 -0
- package/src/visibility/utils/usePrevious.tsx +24 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export abstract class GraniteEventDefinition<O = undefined, R = void> {
|
|
2
|
+
// Event name (literal type recommended)
|
|
3
|
+
abstract name: string;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Method to implement event logic.
|
|
7
|
+
* @param options - Event options (undefined if not used)
|
|
8
|
+
* @param onEvent - Callback called when the event is triggered.
|
|
9
|
+
* @param onError - Callback called on error (error type: unknown)
|
|
10
|
+
*/
|
|
11
|
+
abstract listener(options: O, onEvent: (response: R) => void, onError: (error: unknown) => void): void;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Additional logic to execute when removing the event listener.
|
|
15
|
+
* If needed in the plugin, override this method and it will be called
|
|
16
|
+
* when the listener is removed.
|
|
17
|
+
*/
|
|
18
|
+
abstract remove(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GraniteEvent class registers event definition instances passed to the constructor.
|
|
23
|
+
* The addEventListener method only allows registered event names, and the types of passed
|
|
24
|
+
* options/callbacks are automatically inferred.
|
|
25
|
+
*
|
|
26
|
+
* Each subscription is uniquely identified by (event name + options).
|
|
27
|
+
*/
|
|
28
|
+
export class GraniteEvent<Defs extends GraniteEventDefinition<unknown, unknown>> {
|
|
29
|
+
private definitions = new Map<Defs['name'], Defs>();
|
|
30
|
+
private subscriptionGroups = new Map<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
eventName: Defs['name'];
|
|
34
|
+
options: Extract<Defs, { name: Defs['name'] }> extends GraniteEventDefinition<infer O, any> ? O : never;
|
|
35
|
+
callbacks: Array<{
|
|
36
|
+
onEvent: (response: any) => void;
|
|
37
|
+
onError?: (error: unknown) => void;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
>();
|
|
41
|
+
|
|
42
|
+
constructor(defs: Defs[]) {
|
|
43
|
+
for (const def of defs) {
|
|
44
|
+
this.definitions.set(def.name, def);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
addEventListener<E extends Defs['name']>(
|
|
49
|
+
eventName: E,
|
|
50
|
+
listener: Extract<Defs, { name: E }> extends GraniteEventDefinition<infer O, infer R>
|
|
51
|
+
? [O] extends [undefined]
|
|
52
|
+
? { onEvent: (response: R) => void; onError?: (error: unknown) => void }
|
|
53
|
+
: { options: O; onEvent: (response: R) => void; onError?: (error: unknown) => void }
|
|
54
|
+
: never
|
|
55
|
+
): () => void {
|
|
56
|
+
const def = this.definitions.get(eventName);
|
|
57
|
+
if (!def) {
|
|
58
|
+
throw new Error(`Event "${String(eventName)}" is not registered.`);
|
|
59
|
+
}
|
|
60
|
+
const opts = (listener as any).options;
|
|
61
|
+
const key = this._getKey(String(eventName), opts);
|
|
62
|
+
let group = this.subscriptionGroups.get(key);
|
|
63
|
+
|
|
64
|
+
if (!group) {
|
|
65
|
+
group = {
|
|
66
|
+
eventName,
|
|
67
|
+
options: opts,
|
|
68
|
+
callbacks: [],
|
|
69
|
+
};
|
|
70
|
+
this.subscriptionGroups.set(key, group);
|
|
71
|
+
|
|
72
|
+
const aggregatedOnEvent = (response: any) => {
|
|
73
|
+
for (const cb of group!.callbacks) {
|
|
74
|
+
cb.onEvent(response);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const aggregatedOnError = (error: unknown) => {
|
|
78
|
+
for (const cb of group!.callbacks) {
|
|
79
|
+
if (cb.onError) {
|
|
80
|
+
cb.onError(error);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
def.listener(group.options, aggregatedOnEvent, aggregatedOnError);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
group.callbacks.push({ onEvent: listener.onEvent, onError: listener.onError });
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
const grp = this.subscriptionGroups.get(key);
|
|
92
|
+
if (!grp) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
grp.callbacks = grp.callbacks.filter((cb) => cb.onEvent !== listener.onEvent);
|
|
96
|
+
if (grp.callbacks.length === 0) {
|
|
97
|
+
def.remove();
|
|
98
|
+
this.subscriptionGroups.delete(key);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
emit<E extends Defs['name']>(
|
|
104
|
+
eventName: E,
|
|
105
|
+
data: Extract<Defs, { name: E }> extends GraniteEventDefinition<unknown, infer R> ? R : never
|
|
106
|
+
) {
|
|
107
|
+
const def = this.definitions.get(eventName);
|
|
108
|
+
if (!def) {
|
|
109
|
+
throw new Error(`Event "${String(eventName)}" is not registered.`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const group of this.subscriptionGroups.values()) {
|
|
113
|
+
if (group.eventName === eventName) {
|
|
114
|
+
for (const callback of group.callbacks) {
|
|
115
|
+
try {
|
|
116
|
+
callback.onEvent(data);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (callback.onError) {
|
|
119
|
+
callback.onError(error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _getKey(eventName: string, options: unknown): string {
|
|
128
|
+
return `${eventName}:${options ? JSON.stringify(options) : ''}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { GraniteEvent, GraniteEventDefinition } from './abstract';
|
|
3
|
+
import { BackEventControls, useBackEvent } from '../use-back-event/useBackEvent';
|
|
4
|
+
|
|
5
|
+
class BackEvent extends GraniteEventDefinition<void, void> {
|
|
6
|
+
name = 'backEvent' as const;
|
|
7
|
+
|
|
8
|
+
ref = {
|
|
9
|
+
remove: () => {},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
constructor(private backEventControls: BackEventControls) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
remove() {
|
|
17
|
+
this.ref.remove();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
listener(_: void, onEvent: (response: void) => void): void {
|
|
21
|
+
this.backEventControls.addEventListener(onEvent);
|
|
22
|
+
this.ref.remove = () => this.backEventControls.removeEventListener(onEvent);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useGraniteEvent() {
|
|
27
|
+
const backEvent = useBackEvent();
|
|
28
|
+
|
|
29
|
+
const event = useMemo(() => {
|
|
30
|
+
return new GraniteEvent([new BackEvent(backEvent)]);
|
|
31
|
+
}, [backEvent]);
|
|
32
|
+
|
|
33
|
+
return event;
|
|
34
|
+
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { debounce } from 'es-toolkit';
|
|
2
|
+
import { PropsWithChildren, ReactElement, ReactNode, useContext, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { StyleProp, View, ViewStyle } from 'react-native';
|
|
4
|
+
import { InView, IOContext } from '../intersection-observer';
|
|
5
|
+
import { noop } from '../utils/noop';
|
|
6
|
+
import { usePreservedCallback } from '../utils/usePreservedCallback';
|
|
7
|
+
import { useVisibility } from '../visibility';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
onImpressionStart?: () => void;
|
|
11
|
+
onImpressionEnd?: () => void;
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
UNSAFE__impressFallbackOnMount?: boolean;
|
|
14
|
+
style?: StyleProp<ViewStyle>;
|
|
15
|
+
areaThreshold?: number;
|
|
16
|
+
timeThreshold?: number;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @public
|
|
22
|
+
* @category Screen Control
|
|
23
|
+
* @name ImpressionArea
|
|
24
|
+
* @description
|
|
25
|
+
* A component that detects whether a specific component is visible on the screen and notifies the outside. Using this component, you can easily implement features like collecting logs or running animations when a specific component becomes visible on the screen.
|
|
26
|
+
* The visibility is detected using the return value of `useVisibility` and the `IOScrollView` and `InView` components that indicate whether the component is displayed within the viewport. When using `ScrollView` in React, even if a view is not visible on the screen, using `ImpressionArea` allows you to trigger events only when the view is actually visible on the screen.
|
|
27
|
+
|
|
28
|
+
::: info Note
|
|
29
|
+
|
|
30
|
+
`ImpressionArea` must be used inside `IOScrollView`. If you need to use it outside of `IOScrollView`, you can set the `UNSAFE__impressFallbackOnMount` property to `true` to detect based on when the component is mounted. If this property is set to `false` and used outside of `IOScrollView`, an `IOProviderMissingError` will occur.
|
|
31
|
+
|
|
32
|
+
:::
|
|
33
|
+
*
|
|
34
|
+
* @param {() => void} [onImpressionStart] Callback function that is executed when the child component becomes visible on the screen.
|
|
35
|
+
* @param {() => void} [onImpressionEnd] Callback function that is executed when the child component is hidden from the screen.
|
|
36
|
+
* @param {boolean} [enabled=true] Value that directly controls the condition for visibility. Default value is `true`. If passed as `false`, the `onImpressionStart` callback function will not be executed even if the component is visible.
|
|
37
|
+
* @param {number} [areaThreshold=0] Value that sets the ratio of the visible area. If the component appears on the screen with a ratio greater than this value, `onImpressionStart` is called.
|
|
38
|
+
* The value should be set between 0 and 1. Setting it to 0 triggers the event when even 1px of the component is visible. Conversely, setting it to 1 only triggers the event when the component is 100% visible on the screen.
|
|
39
|
+
* @param {number} [timeThreshold=0] Sets the time in milliseconds before `onImpressionStart` is called after this component becomes visible on the screen. Default value is `0` milliseconds (0 seconds).
|
|
40
|
+
* @param {ViewStyle} [style] `style` value to be applied to the `InView` component. Default value is `undefined`, used when you want to specify a style.
|
|
41
|
+
* @param {boolean} [UNSAFE__impressFallbackOnMount=false] Whether to consider the component as visible immediately when mounted. Default value is `false`.
|
|
42
|
+
* Useful when you cannot determine if the component is in the viewport without using `IOScrollView`. For example, a component located outside of `IOScrollView` will be considered visible at mount time if set to `true`.
|
|
43
|
+
*
|
|
44
|
+
* @returns {ReactElement} - Returns a component that can detect visibility on the screen.
|
|
45
|
+
* @example
|
|
46
|
+
*
|
|
47
|
+
* ### Basic Usage Example
|
|
48
|
+
*
|
|
49
|
+
* ```tsx
|
|
50
|
+
* import { useState } from 'react';
|
|
51
|
+
* import { Button, Dimensions, Text, View } from 'react-native';
|
|
52
|
+
* import { ImpressionArea, IOScrollView } from '@granite-js/react-native';
|
|
53
|
+
*
|
|
54
|
+
* export default function ImpressionAreaExample() {
|
|
55
|
+
* const [isImpressionStart, setIsImpressionStart] = useState(false);
|
|
56
|
+
*
|
|
57
|
+
* return (
|
|
58
|
+
* <>
|
|
59
|
+
* <Text>{isImpressionStart ? 'Impression Start' : 'Impression End'}</Text>
|
|
60
|
+
* <IOScrollView
|
|
61
|
+
* style={{
|
|
62
|
+
* flex: 1,
|
|
63
|
+
* margin: 16,
|
|
64
|
+
* backgroundColor: 'white',
|
|
65
|
+
* }}
|
|
66
|
+
* >
|
|
67
|
+
* <View
|
|
68
|
+
* style={{
|
|
69
|
+
* height: Dimensions.get('screen').height,
|
|
70
|
+
* borderWidth: 1,
|
|
71
|
+
* borderColor: 'black',
|
|
72
|
+
* }}
|
|
73
|
+
* >
|
|
74
|
+
* <Text>Scroll to here</Text>
|
|
75
|
+
* </View>
|
|
76
|
+
*
|
|
77
|
+
* <ImpressionArea
|
|
78
|
+
* onImpressionStart={() => setIsImpressionStart(true)}
|
|
79
|
+
* onImpressionEnd={() => setIsImpressionStart(false)}
|
|
80
|
+
* >
|
|
81
|
+
* <Button title="Button" />
|
|
82
|
+
* </ImpressionArea>
|
|
83
|
+
* </IOScrollView>
|
|
84
|
+
* </>
|
|
85
|
+
* );
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* ### Example of Detection at Mount Time
|
|
90
|
+
*
|
|
91
|
+
* When `ImpressionArea` is not located inside a component like `IOScrollView`, setting `UNSAFE__impressFallbackOnMount` to `true` will consider the component as visible when it is mounted.
|
|
92
|
+
*
|
|
93
|
+
* ```tsx
|
|
94
|
+
* import { useState } from 'react';
|
|
95
|
+
* import { Button, Dimensions, ScrollView, Text, View } from 'react-native';
|
|
96
|
+
* import { ImpressionArea } from '@granite-js/react-native';
|
|
97
|
+
*
|
|
98
|
+
* export default function ImpressionArea2Example() {
|
|
99
|
+
* const [isImpressionStart, setIsImpressionStart] = useState(false);
|
|
100
|
+
*
|
|
101
|
+
* return (
|
|
102
|
+
* <>
|
|
103
|
+
* <Text>{isImpressionStart ? 'Impression Start' : 'Impression End'}</Text>
|
|
104
|
+
* <ScrollView
|
|
105
|
+
* style={{
|
|
106
|
+
* flex: 1,
|
|
107
|
+
* margin: 16,
|
|
108
|
+
* backgroundColor: 'white',
|
|
109
|
+
* }}
|
|
110
|
+
* >
|
|
111
|
+
* <View
|
|
112
|
+
* style={{
|
|
113
|
+
* height: Dimensions.get('screen').height,
|
|
114
|
+
* borderWidth: 1,
|
|
115
|
+
* borderColor: 'black',
|
|
116
|
+
* }}
|
|
117
|
+
* >
|
|
118
|
+
* <Text>Scroll to here</Text>
|
|
119
|
+
* </View>
|
|
120
|
+
*
|
|
121
|
+
* <ImpressionArea
|
|
122
|
+
* UNSAFE__impressFallbackOnMount={true}
|
|
123
|
+
* onImpressionStart={() => setIsImpressionStart(true)}
|
|
124
|
+
* onImpressionEnd={() => setIsImpressionStart(false)}
|
|
125
|
+
* >
|
|
126
|
+
* <Button title="Button" />
|
|
127
|
+
* </ImpressionArea>
|
|
128
|
+
* </ScrollView>
|
|
129
|
+
* </>
|
|
130
|
+
* );
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export function ImpressionArea(props: Props): ReactElement {
|
|
135
|
+
const context = useContext(IOContext);
|
|
136
|
+
|
|
137
|
+
if (context?.manager == null && props.UNSAFE__impressFallbackOnMount) {
|
|
138
|
+
return <ImpressionAreaOnMount {...props} />;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return <ImpressionAreaWithIntersectionObserver {...props} />;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export class IOProviderMissingError extends Error {
|
|
145
|
+
message =
|
|
146
|
+
'ImpressionArea가 IOContext.Provider 밖에서 사용되었습니다. ' +
|
|
147
|
+
'최상위 ScrollView가 @granite-js/react-native의 IOScrollView인지 확인해주세요.';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @category Components
|
|
152
|
+
* @kind function
|
|
153
|
+
* @name ImpressionAreaOnMount
|
|
154
|
+
* @description
|
|
155
|
+
* A component that calls `onImpressionStart` and `onImpressionEnd` when the component is mounted on the screen.
|
|
156
|
+
* Used when `UNSAFE__impressFallbackOnMount` is `true` in `ImpressionArea`.
|
|
157
|
+
* This component can be used when `ImpressionArea` is needed outside of `IOScrollView`.
|
|
158
|
+
*
|
|
159
|
+
* @param {() => void} [onImpressionStart] - Callback function that is called when the component is mounted.
|
|
160
|
+
* @param {() => void} [onImpressionEnd] - Callback function that is called when the component is unmounted.
|
|
161
|
+
* @param {StyleProp<ViewStyle>} [style] - Sets the style of the top-level container of the `ImpressionAreaOnMount` component.
|
|
162
|
+
* @param {ReactNode} [children] - Sets the child components to be wrapped by `ImpressionAreaOnMount`.
|
|
163
|
+
* @returns {ReactElement} - Returns a component that detects mount status
|
|
164
|
+
* @example
|
|
165
|
+
* ### Example of Detection at Mount Time
|
|
166
|
+
* ```tsx
|
|
167
|
+
* import { ImpressionAreaOnMount } from '@granite-js/react-native';
|
|
168
|
+
*
|
|
169
|
+
* export default function AlwaysImpressionAreaPage() {
|
|
170
|
+
* // isImpressionStart is set to true when AlwaysImpressionAreaPage is mounted
|
|
171
|
+
* const [isImpressionStart, setIsImpressionStart] = useState(false);
|
|
172
|
+
*
|
|
173
|
+
* return (
|
|
174
|
+
* <ScrollView
|
|
175
|
+
* style={{
|
|
176
|
+
* flex: 1,
|
|
177
|
+
* padding: 16,
|
|
178
|
+
* backgroundColor: 'white',
|
|
179
|
+
* }}
|
|
180
|
+
* >
|
|
181
|
+
* <TestTitle title="ImpressionArea" description="@granite-js/impression-area" />
|
|
182
|
+
* <Button label={'Scroll to origin'} onPress={() => {}} />
|
|
183
|
+
* <Code style={{ width, height }} json={{ isImpressionStart }} />
|
|
184
|
+
* <ImpressionAreaOnMount
|
|
185
|
+
* onImpressionStart={() => setIsImpressionStart(true)}
|
|
186
|
+
* onImpressionEnd={() => setIsImpressionStart(false)}
|
|
187
|
+
* >
|
|
188
|
+
* <Button label={'Scroll to here'} onPress={() => {}} />
|
|
189
|
+
* </ImpressionArea>
|
|
190
|
+
* </ScrollView>
|
|
191
|
+
* );
|
|
192
|
+
* }
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
export function ImpressionAreaOnMount({
|
|
196
|
+
onImpressionStart: _onImpressionStart = noop,
|
|
197
|
+
onImpressionEnd: _onImpressionEnd = noop,
|
|
198
|
+
style,
|
|
199
|
+
children,
|
|
200
|
+
}: PropsWithChildren<Props>): ReactElement {
|
|
201
|
+
const visible = useVisibility();
|
|
202
|
+
|
|
203
|
+
const isImpressed = visible;
|
|
204
|
+
|
|
205
|
+
const onImpressionStart = usePreservedCallback(_onImpressionStart);
|
|
206
|
+
const onImpressionEnd = usePreservedCallback(_onImpressionEnd);
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
if (isImpressed) {
|
|
210
|
+
onImpressionStart();
|
|
211
|
+
} else {
|
|
212
|
+
onImpressionEnd();
|
|
213
|
+
}
|
|
214
|
+
}, [isImpressed, onImpressionStart, onImpressionEnd]);
|
|
215
|
+
|
|
216
|
+
return <View style={style}>{children}</View>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @category Components
|
|
221
|
+
* @kind function
|
|
222
|
+
* @name ImpressionAreaWithIntersectionObserver
|
|
223
|
+
* @description
|
|
224
|
+
* A component that calls `onImpressionStart` and `onImpressionEnd` based on whether the component is visible on the user's screen.
|
|
225
|
+
* This is a component that can be used in `IOScrollView` and can notify the outside about its visibility status.
|
|
226
|
+
*
|
|
227
|
+
* @param {() => void} [onImpressionStart] - Callback function that is called when the component becomes visible on the screen.
|
|
228
|
+
* @param {() => void} [onImpressionEnd] - Callback function that is called when the component disappears from the screen.
|
|
229
|
+
* @param {boolean} [enabled=true] - Sets whether to enable `ImpressionArea`.
|
|
230
|
+
* @param {StyleProp<ViewStyle>} [style] - Sets the style of the top-level container of the `ImpressionAreaWithIntersectionObserver` component.
|
|
231
|
+
* @param {number} [areaThreshold=0] - Considers the component visible when its visible area exceeds the set ratio.
|
|
232
|
+
* @param {number} [timeThreshold=0] - Sets the time in milliseconds before `onImpressionStart` and `onImpressionEnd` are called after the component becomes visible on the screen. Default value is `0` milliseconds.
|
|
233
|
+
* @param {ReactNode} [children] - Sets the child components to be wrapped by `ImpressionAreaWithIntersectionObserver`.
|
|
234
|
+
* @returns {ReactElement} - Returns a component that can detect visibility on the screen.
|
|
235
|
+
* @example
|
|
236
|
+
* ### Example using IntersectionObserver
|
|
237
|
+
* ```tsx
|
|
238
|
+
* import { ImpressionAreaWithIntersectionObserver, IOScrollView } from '@granite-js/react-native';
|
|
239
|
+
*
|
|
240
|
+
* export default function ImpressionAreaPage() {
|
|
241
|
+
* const [isImpressionStart, setIsImpressionStart] = useState(false);
|
|
242
|
+
*
|
|
243
|
+
* return (
|
|
244
|
+
* <IOScrollView
|
|
245
|
+
* style={{
|
|
246
|
+
* flex: 1,
|
|
247
|
+
* margin: 16,
|
|
248
|
+
* backgroundColor: 'white',
|
|
249
|
+
* }}
|
|
250
|
+
* >
|
|
251
|
+
* <TestTitle title="ImpressionArea" description="@granite-js/impression-area" />
|
|
252
|
+
* <Button label={'Scroll to origin'} onPress={() => {}} />
|
|
253
|
+
* <Code style={{ width, height }} textStyle={{ flex: 1, verticalAlign: 'middle' }} json={{ isImpressionStart }} />
|
|
254
|
+
* <ImpressionAreaWithIntersectionObserver
|
|
255
|
+
* onImpressionStart={() => setIsImpressionStart(true)}
|
|
256
|
+
* onImpressionEnd={() => setIsImpressionStart(false)}
|
|
257
|
+
* >
|
|
258
|
+
* <Button label={'Scroll to here'} onPress={() => {}} />
|
|
259
|
+
* </ImpressionArea>
|
|
260
|
+
* </IOScrollView>
|
|
261
|
+
* );
|
|
262
|
+
* }
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
export function ImpressionAreaWithIntersectionObserver({
|
|
266
|
+
onImpressionStart: _onImpressionStart = noop,
|
|
267
|
+
onImpressionEnd: _onImpressionEnd = noop,
|
|
268
|
+
timeThreshold = 0,
|
|
269
|
+
...props
|
|
270
|
+
}: PropsWithChildren<{
|
|
271
|
+
onImpressionStart?: () => void;
|
|
272
|
+
onImpressionEnd?: () => void;
|
|
273
|
+
enabled?: boolean;
|
|
274
|
+
style?: StyleProp<ViewStyle>;
|
|
275
|
+
areaThreshold?: number;
|
|
276
|
+
timeThreshold?: number;
|
|
277
|
+
}>): ReactElement {
|
|
278
|
+
const [isImpressed, setIsImpressed] = useState(false);
|
|
279
|
+
|
|
280
|
+
const onImpressionChange = useMemo(() => {
|
|
281
|
+
if (timeThreshold === 0) {
|
|
282
|
+
return setIsImpressed;
|
|
283
|
+
} else {
|
|
284
|
+
return debounce(setIsImpressed, timeThreshold);
|
|
285
|
+
}
|
|
286
|
+
}, [setIsImpressed, timeThreshold]);
|
|
287
|
+
|
|
288
|
+
const onImpressionStart = usePreservedCallback(_onImpressionStart);
|
|
289
|
+
const onImpressionEnd = usePreservedCallback(_onImpressionEnd);
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (isImpressed) {
|
|
293
|
+
onImpressionStart?.();
|
|
294
|
+
} else {
|
|
295
|
+
onImpressionEnd?.();
|
|
296
|
+
}
|
|
297
|
+
}, [isImpressed, onImpressionStart, onImpressionEnd]);
|
|
298
|
+
|
|
299
|
+
return <ImpressionAreaImpl {...props} onImpressionChange={onImpressionChange} />;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function ImpressionAreaImpl({
|
|
303
|
+
children,
|
|
304
|
+
onImpressionChange: _onImpressionChange = noop,
|
|
305
|
+
enabled = true,
|
|
306
|
+
areaThreshold = 0,
|
|
307
|
+
style,
|
|
308
|
+
}: PropsWithChildren<{
|
|
309
|
+
onImpressionChange?: (isImpressed: boolean) => void;
|
|
310
|
+
enabled?: boolean;
|
|
311
|
+
style?: StyleProp<ViewStyle>;
|
|
312
|
+
areaThreshold?: number;
|
|
313
|
+
}>) {
|
|
314
|
+
const visible = useVisibility();
|
|
315
|
+
const [inviewImpressed, setInviewImpressed] = useState(false);
|
|
316
|
+
|
|
317
|
+
const context = useContext(IOContext);
|
|
318
|
+
|
|
319
|
+
if (context?.manager == null) {
|
|
320
|
+
throw new IOProviderMissingError();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const impressed = visible && inviewImpressed && enabled;
|
|
324
|
+
const onImpressionChange = usePreservedCallback(_onImpressionChange);
|
|
325
|
+
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
onImpressionChange?.(impressed);
|
|
328
|
+
}, [impressed, onImpressionChange]);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<InView
|
|
332
|
+
onChange={(inView, currentThreshold) => {
|
|
333
|
+
const isVisible = inView && currentThreshold >= areaThreshold;
|
|
334
|
+
setInviewImpressed(isVisible);
|
|
335
|
+
}}
|
|
336
|
+
style={style}
|
|
337
|
+
>
|
|
338
|
+
{children}
|
|
339
|
+
</InView>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './ImpressionArea';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import './types/global';
|
|
2
|
+
|
|
3
|
+
export { Granite } from './app';
|
|
4
|
+
export * from '@granite-js/style-utils';
|
|
5
|
+
export * from '@granite-js/image';
|
|
6
|
+
export * from '@granite-js/lottie';
|
|
7
|
+
|
|
8
|
+
export * from './dev-entrypoint';
|
|
9
|
+
export * from './native-modules/natives';
|
|
10
|
+
export * from './visibility';
|
|
11
|
+
export * from './use-back-event';
|
|
12
|
+
export * from './keyboard';
|
|
13
|
+
export * from './intersection-observer';
|
|
14
|
+
export * from './impression-area';
|
|
15
|
+
export * from './scroll-view-inertial-background';
|
|
16
|
+
export * from './react';
|
|
17
|
+
export * from './router/createRoute';
|
|
18
|
+
export * from './event';
|
|
19
|
+
export * from './video';
|
|
20
|
+
export * from './status-bar';
|
|
21
|
+
export * from './blur';
|
|
22
|
+
|
|
23
|
+
export type { InitialProps, ColorPreference } from './initial-props';
|
|
24
|
+
export type { GraniteProps } from './app';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @public
|
|
3
|
+
* @category UI
|
|
4
|
+
* @name ColorPreference
|
|
5
|
+
* @description
|
|
6
|
+
* Type representing the color mode of the current device. It is a string representing light mode and dark mode.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {'light' | 'dark'} ColorPreference
|
|
9
|
+
*/
|
|
10
|
+
export type ColorPreference = 'light' | 'dark';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @category Types
|
|
14
|
+
* @name BaseInitialProps
|
|
15
|
+
* @description
|
|
16
|
+
* Interface representing the base initial properties.
|
|
17
|
+
*
|
|
18
|
+
* @interface
|
|
19
|
+
* @property {'ios' | 'android'} platform - Platform type
|
|
20
|
+
* @property {ColorPreference} initialColorPreference - Initial color
|
|
21
|
+
* @property {string} [scheme] - Executed scheme
|
|
22
|
+
*/
|
|
23
|
+
type BaseInitialProps = {
|
|
24
|
+
platform: 'ios' | 'android';
|
|
25
|
+
initialColorPreference: ColorPreference;
|
|
26
|
+
scheme?: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @category Types
|
|
31
|
+
* @name AndroidInitialProps
|
|
32
|
+
* @description
|
|
33
|
+
* Values passed from Android to React Native.
|
|
34
|
+
*
|
|
35
|
+
* @interface
|
|
36
|
+
* @augments BaseInitialProps
|
|
37
|
+
* @property {'android'} platform - Platform name (Android)
|
|
38
|
+
*/
|
|
39
|
+
export type AndroidInitialProps = BaseInitialProps & {
|
|
40
|
+
platform: 'android';
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @category Types
|
|
45
|
+
* @name IOSInitialProps
|
|
46
|
+
* @description
|
|
47
|
+
* Interface representing iOS initial properties.
|
|
48
|
+
*
|
|
49
|
+
* @interface
|
|
50
|
+
* @augments BaseInitialProps
|
|
51
|
+
* @property {'ios'} platform - Platform (iOS)
|
|
52
|
+
*/
|
|
53
|
+
export type IOSInitialProps = BaseInitialProps & {
|
|
54
|
+
platform: 'ios';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @public
|
|
59
|
+
* @category Core
|
|
60
|
+
* @name InitialProps
|
|
61
|
+
* @description
|
|
62
|
+
* Provides the initial data type that native platforms (Android/iOS) pass to the app when a user enters a specific screen in a React Native app.
|
|
63
|
+
* The initial data contains important information used for screen initialization, and the required data types differ by native platform.
|
|
64
|
+
*
|
|
65
|
+
* The data type provided by Android is `AndroidInitialProps`, and the data type provided by iOS is `IOSInitialProps`.
|
|
66
|
+
*
|
|
67
|
+
* @property {'ios' | 'android'} platform - The platform on which the app is currently running. Has a value of either `ios` or `android`.
|
|
68
|
+
* @property {ColorPreference} initialColorPreference - The initial color theme. Represents the color theme set by the user.
|
|
69
|
+
* @property {string} [scheme] - The URL scheme used to enter the current screen.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
*
|
|
73
|
+
* ### Example using `InitialProps`
|
|
74
|
+
*
|
|
75
|
+
* ::: code-group
|
|
76
|
+
* ```tsx [_app.tsx]
|
|
77
|
+
* import { PropsWithChildren } from 'react';
|
|
78
|
+
* import { Granite, InitialProps } from '@granite-js/react-native';
|
|
79
|
+
* import { context } from '../require.context';
|
|
80
|
+
*
|
|
81
|
+
* const APP_NAME = 'my-app-name';
|
|
82
|
+
*
|
|
83
|
+
* function AppContainer({ children, ...initialProps }: PropsWithChildren<InitialProps>) {
|
|
84
|
+
* console.log({ initialProps });
|
|
85
|
+
* return <>{children}</>;
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* export default Granite.registerApp(AppContainer, {
|
|
89
|
+
* appName: APP_NAME,
|
|
90
|
+
* context,
|
|
91
|
+
* });
|
|
92
|
+
* ```
|
|
93
|
+
* :::
|
|
94
|
+
*/
|
|
95
|
+
export type InitialProps = AndroidInitialProps | IOSInitialProps;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './InitialProps';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import IOManager from './IOManager';
|
|
3
|
+
|
|
4
|
+
export interface IOContextValue {
|
|
5
|
+
manager: null | IOManager;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @name IOContext
|
|
10
|
+
* @description Context that shares the IOManager instance.
|
|
11
|
+
*/
|
|
12
|
+
const IOContext = createContext<IOContextValue>({
|
|
13
|
+
manager: null,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export default IOContext;
|