@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.
Files changed (217) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +24 -0
  3. package/bin/cli.js +3 -0
  4. package/cli.d.ts +1 -0
  5. package/cli.js +4 -0
  6. package/config.d.ts +2 -0
  7. package/config.js +5 -0
  8. package/dist/app/App/index.android.d.ts +2 -0
  9. package/dist/app/App/index.ios.d.ts +6 -0
  10. package/dist/app/AppRoot.d.ts +1 -0
  11. package/dist/app/Granite.d.ts +61 -0
  12. package/dist/app/HostAppRoot.d.ts +1 -0
  13. package/dist/app/index.d.ts +2 -0
  14. package/dist/async-bridges.d.ts +2 -0
  15. package/dist/blur/BlurView.d.ts +78 -0
  16. package/dist/blur/ReactNativeBlurModule.d.ts +6 -0
  17. package/dist/blur/constants.d.ts +1 -0
  18. package/dist/blur/index.d.ts +1 -0
  19. package/dist/constant-bridges.d.ts +1 -0
  20. package/dist/constants.d.ts +1 -0
  21. package/dist/dev-entrypoint/index.d.ts +2 -0
  22. package/dist/event/abstract.d.ts +42 -0
  23. package/dist/event/index.d.ts +2 -0
  24. package/dist/event/useGraniteEvent.d.ts +14 -0
  25. package/dist/impression-area/ImpressionArea.d.ts +231 -0
  26. package/dist/impression-area/index.d.ts +1 -0
  27. package/dist/index.d.ts +21 -0
  28. package/dist/initial-props/InitialProps.d.ts +92 -0
  29. package/dist/initial-props/index.d.ts +1 -0
  30. package/dist/intersection-observer/IOContext.d.ts +10 -0
  31. package/dist/intersection-observer/IOFlatList.d.ts +55 -0
  32. package/dist/intersection-observer/IOManager.d.ts +24 -0
  33. package/dist/intersection-observer/IOScrollView.d.ts +59 -0
  34. package/dist/intersection-observer/InView.d.ts +107 -0
  35. package/dist/intersection-observer/IntersectionObserver.d.ts +67 -0
  36. package/dist/intersection-observer/index.d.ts +8 -0
  37. package/dist/intersection-observer/withIO.d.ts +20 -0
  38. package/dist/jest/index.d.ts +1 -0
  39. package/dist/jest/index.js +32 -0
  40. package/dist/keyboard/KeyboardAboveView.d.ts +40 -0
  41. package/dist/keyboard/index.d.ts +2 -0
  42. package/dist/keyboard/useKeyboardAnimatedHeight.d.ts +20 -0
  43. package/dist/native-event-emitter/eventEmitters/index.d.ts +2 -0
  44. package/dist/native-event-emitter/eventEmitters/types.d.ts +4 -0
  45. package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +10 -0
  46. package/dist/native-event-emitter/index.d.ts +1 -0
  47. package/dist/native-event-emitter/nativeEventEmitter.d.ts +15 -0
  48. package/dist/native-modules/core/GraniteCoreModule.d.ts +8 -0
  49. package/dist/native-modules/index.d.ts +3 -0
  50. package/dist/native-modules/natives/GraniteModule.d.ts +7 -0
  51. package/dist/native-modules/natives/closeView.d.ts +21 -0
  52. package/dist/native-modules/natives/getSchemeUri.d.ts +23 -0
  53. package/dist/native-modules/natives/index.d.ts +3 -0
  54. package/dist/native-modules/natives/openURL.d.ts +36 -0
  55. package/dist/react/index.d.ts +1 -0
  56. package/dist/react/useWaitForReturnNavigator.d.ts +39 -0
  57. package/dist/rn-polyfills/index.d.ts +1 -0
  58. package/dist/rn-polyfills/symbol-asynciterator/index.d.ts +9 -0
  59. package/dist/rn-polyfills/url/index.d.ts +1 -0
  60. package/dist/router/Router.d.ts +59 -0
  61. package/dist/router/components/BackButton.d.ts +7 -0
  62. package/dist/router/components/CanGoBackGuard.d.ts +6 -0
  63. package/dist/router/components/RouterBackButton.d.ts +9 -0
  64. package/dist/router/components/StackNavigator.d.ts +54 -0
  65. package/dist/router/constants.d.ts +2 -0
  66. package/dist/router/createRoute.d.ts +39 -0
  67. package/dist/router/createRoute.test-d.d.ts +9 -0
  68. package/dist/router/hooks/useInitialRouteName.d.ts +1 -0
  69. package/dist/router/hooks/useIsInitialScreen.d.ts +1 -0
  70. package/dist/router/hooks/useRouterControls.d.ts +11 -0
  71. package/dist/router/index.d.ts +3 -0
  72. package/dist/router/types/RequireContext.d.ts +7 -0
  73. package/dist/router/types/RouteScreen.d.ts +16 -0
  74. package/dist/router/types/Screen.d.ts +23 -0
  75. package/dist/router/types/index.d.ts +3 -0
  76. package/dist/router/types/screen-option.d.ts +4 -0
  77. package/dist/router/utils/createParentRouteScreenMap.d.ts +8 -0
  78. package/dist/router/utils/defaultParserParams.d.ts +9 -0
  79. package/dist/router/utils/index.d.ts +2 -0
  80. package/dist/router/utils/matchers.d.ts +2 -0
  81. package/dist/router/utils/mergeParentLayoutScreen.d.ts +18 -0
  82. package/dist/router/utils/path.d.ts +53 -0
  83. package/dist/router/utils/screen.d.ts +37 -0
  84. package/dist/scroll-view-inertial-background/ScrollViewInertialBackground.d.ts +49 -0
  85. package/dist/scroll-view-inertial-background/index.d.ts +1 -0
  86. package/dist/status-bar/StatusBar.android.d.ts +3 -0
  87. package/dist/status-bar/StatusBar.ios.d.ts +3 -0
  88. package/dist/status-bar/index.d.ts +2 -0
  89. package/dist/status-bar/types.d.ts +20 -0
  90. package/dist/status-bar/utils.d.ts +3 -0
  91. package/dist/types/global.d.ts +14 -0
  92. package/dist/use-back-event/index.d.ts +1 -0
  93. package/dist/use-back-event/useBackEvent.d.ts +135 -0
  94. package/dist/utils/noop.d.ts +1 -0
  95. package/dist/utils/usePreservedCallback.d.ts +1 -0
  96. package/dist/video/Video.d.ts +67 -0
  97. package/dist/video/index.d.ts +1 -0
  98. package/dist/video/instance.d.ts +9 -0
  99. package/dist/visibility/VisibilityProvider.d.ts +27 -0
  100. package/dist/visibility/index.d.ts +6 -0
  101. package/dist/visibility/react-navigation/index.d.ts +2 -0
  102. package/dist/visibility/react-navigation/useIsFocusedSafely.d.ts +20 -0
  103. package/dist/visibility/react-navigation/useNavigationSafely.d.ts +19 -0
  104. package/dist/visibility/useIsAppForeground.d.ts +39 -0
  105. package/dist/visibility/useVisibility.d.ts +35 -0
  106. package/dist/visibility/useVisibilityChange.d.ts +51 -0
  107. package/dist/visibility/useVisibilityChanged.d.ts +41 -0
  108. package/dist/visibility/utils/usePrevious.d.ts +15 -0
  109. package/jest.d.ts +1 -0
  110. package/package.json +94 -0
  111. package/presets.d.ts +1 -0
  112. package/src/app/App/index.android.tsx +6 -0
  113. package/src/app/App/index.d.ts +6 -0
  114. package/src/app/App/index.ios.tsx +13 -0
  115. package/src/app/AppRoot.tsx +39 -0
  116. package/src/app/Granite.tsx +128 -0
  117. package/src/app/HostAppRoot.tsx +19 -0
  118. package/src/app/index.ts +2 -0
  119. package/src/async-bridges.ts +2 -0
  120. package/src/blur/BlurView.tsx +103 -0
  121. package/src/blur/ReactNativeBlurModule.ts +19 -0
  122. package/src/blur/constants.ts +3 -0
  123. package/src/blur/index.ts +1 -0
  124. package/src/constant-bridges.ts +1 -0
  125. package/src/constants.ts +1 -0
  126. package/src/dev-entrypoint/index.tsx +17 -0
  127. package/src/event/abstract.ts +130 -0
  128. package/src/event/index.ts +2 -0
  129. package/src/event/useGraniteEvent.ts +34 -0
  130. package/src/impression-area/ImpressionArea.tsx +341 -0
  131. package/src/impression-area/index.ts +1 -0
  132. package/src/index.ts +24 -0
  133. package/src/initial-props/InitialProps.ts +95 -0
  134. package/src/initial-props/index.ts +1 -0
  135. package/src/intersection-observer/IOContext.ts +16 -0
  136. package/src/intersection-observer/IOFlatList.ts +72 -0
  137. package/src/intersection-observer/IOManager.ts +73 -0
  138. package/src/intersection-observer/IOScrollView.ts +69 -0
  139. package/src/intersection-observer/InView.tsx +205 -0
  140. package/src/intersection-observer/IntersectionObserver.ts +212 -0
  141. package/src/intersection-observer/index.ts +24 -0
  142. package/src/intersection-observer/withIO.tsx +151 -0
  143. package/src/jest/index.ts +1 -0
  144. package/src/keyboard/KeyboardAboveView.tsx +62 -0
  145. package/src/keyboard/index.ts +2 -0
  146. package/src/keyboard/useKeyboardAnimatedHeight.tsx +81 -0
  147. package/src/native-event-emitter/eventEmitters/index.ts +3 -0
  148. package/src/native-event-emitter/eventEmitters/types.ts +4 -0
  149. package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +11 -0
  150. package/src/native-event-emitter/index.ts +1 -0
  151. package/src/native-event-emitter/nativeEventEmitter.ts +18 -0
  152. package/src/native-modules/core/GraniteCoreModule.ts +9 -0
  153. package/src/native-modules/index.ts +3 -0
  154. package/src/native-modules/natives/GraniteModule.ts +8 -0
  155. package/src/native-modules/natives/closeView.ts +25 -0
  156. package/src/native-modules/natives/getSchemeUri.ts +27 -0
  157. package/src/native-modules/natives/index.ts +3 -0
  158. package/src/native-modules/natives/openURL.ts +40 -0
  159. package/src/react/index.ts +1 -0
  160. package/src/react/useWaitForReturnNavigator.ts +75 -0
  161. package/src/rn-polyfills/index.ts +7 -0
  162. package/src/rn-polyfills/symbol-asynciterator/index.ts +15 -0
  163. package/src/rn-polyfills/url/index.ts +1 -0
  164. package/src/router/Router.tsx +164 -0
  165. package/src/router/components/BackButton.tsx +58 -0
  166. package/src/router/components/CanGoBackGuard.tsx +31 -0
  167. package/src/router/components/RouterBackButton.tsx +32 -0
  168. package/src/router/components/StackNavigator.tsx +12 -0
  169. package/src/router/constants.ts +3 -0
  170. package/src/router/createRoute.test-d.ts +52 -0
  171. package/src/router/createRoute.ts +161 -0
  172. package/src/router/hooks/useInitialRouteName.tsx +22 -0
  173. package/src/router/hooks/useIsInitialScreen.ts +7 -0
  174. package/src/router/hooks/useRouterControls.tsx +72 -0
  175. package/src/router/index.ts +3 -0
  176. package/src/router/types/RequireContext.ts +7 -0
  177. package/src/router/types/RouteScreen.ts +17 -0
  178. package/src/router/types/Screen.tsx +24 -0
  179. package/src/router/types/index.ts +3 -0
  180. package/src/router/types/screen-option.ts +23 -0
  181. package/src/router/utils/createParentRouteScreenMap.spec.ts +166 -0
  182. package/src/router/utils/createParentRouteScreenMap.ts +136 -0
  183. package/src/router/utils/defaultParserParams.spec.ts +46 -0
  184. package/src/router/utils/defaultParserParams.ts +19 -0
  185. package/src/router/utils/index.ts +2 -0
  186. package/src/router/utils/matchers.ts +5 -0
  187. package/src/router/utils/mergeParentLayoutScreen.spec.tsx +112 -0
  188. package/src/router/utils/mergeParentLayoutScreen.tsx +43 -0
  189. package/src/router/utils/path.spec.ts +135 -0
  190. package/src/router/utils/path.ts +105 -0
  191. package/src/router/utils/screen.tsx +95 -0
  192. package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
  193. package/src/scroll-view-inertial-background/index.ts +1 -0
  194. package/src/status-bar/StatusBar.android.tsx +36 -0
  195. package/src/status-bar/StatusBar.d.ts +4 -0
  196. package/src/status-bar/StatusBar.ios.tsx +34 -0
  197. package/src/status-bar/index.ts +2 -0
  198. package/src/status-bar/types.ts +21 -0
  199. package/src/status-bar/utils.ts +20 -0
  200. package/src/types/global.ts +21 -0
  201. package/src/use-back-event/index.ts +1 -0
  202. package/src/use-back-event/useBackEvent.tsx +260 -0
  203. package/src/utils/noop.ts +1 -0
  204. package/src/utils/usePreservedCallback.ts +16 -0
  205. package/src/video/Video.tsx +104 -0
  206. package/src/video/index.ts +1 -0
  207. package/src/video/instance.tsx +28 -0
  208. package/src/visibility/VisibilityProvider.tsx +36 -0
  209. package/src/visibility/index.ts +6 -0
  210. package/src/visibility/react-navigation/index.ts +2 -0
  211. package/src/visibility/react-navigation/useIsFocusedSafely.tsx +58 -0
  212. package/src/visibility/react-navigation/useNavigationSafely.tsx +30 -0
  213. package/src/visibility/useIsAppForeground.tsx +73 -0
  214. package/src/visibility/useVisibility.tsx +54 -0
  215. package/src/visibility/useVisibilityChange.ts +69 -0
  216. package/src/visibility/useVisibilityChanged.tsx +69 -0
  217. package/src/visibility/utils/usePrevious.tsx +24 -0
@@ -0,0 +1,72 @@
1
+ import { RefAttributes } from 'react';
2
+ import { FlatList, FlatListProps } from 'react-native';
3
+ import withIO, { IOComponentProps } from './withIO';
4
+
5
+ export type IOFlatListController = FlatList;
6
+
7
+ export type IOFlatListProps<ItemT = any> = IOComponentProps & FlatListProps<ItemT>;
8
+
9
+ /**
10
+ * @public
11
+ * @category Screen Control
12
+ * @name IOFlatList
13
+ * @description
14
+ * `IOFlatList` is a `FlatList` component with added Intersection Observer functionality to detect when specific elements become visible or disappear from the screen during scrolling. Using this component, you can easily check and handle whether each item in the list is visible on the screen.
15
+ *
16
+ * When used with `InView`, you can check the exposure status of each element. The [InView](/reference/react-native/Screen%20Control/InView) component included as a child element detects whether the element is visible on the screen through the observation functionality of `IOFlatList` and triggers events based on the exposure status.
17
+ *
18
+ * @example
19
+ *
20
+ * You can check whether each item in the list appears on the screen using `IOFlatList`.
21
+ * When each item in the list appears on the screen, the `InView` component changes to the `visible` state.
22
+ *
23
+ * ```tsx
24
+ * import { ReactNode, useState } from 'react';
25
+ * import { StyleSheet, Text, View } from 'react-native';
26
+ * import { InView, IOFlatList } from '@granite-js/react-native';
27
+ *
28
+ * const mockData = Array.from({ length: 30 }, (_, i) => ({ key: String(i) }));
29
+ *
30
+ * export default function FlatListPage() {
31
+ * return <IOFlatList data={mockData} renderItem={({ item }) => <InViewItem>{item.key}</InViewItem>} />;
32
+ * }
33
+ *
34
+ * function InViewItem({ children }: { children: ReactNode }) {
35
+ * const [visible, setVisible] = useState(false);
36
+ *
37
+ * return (
38
+ * <InView onChange={setVisible}>
39
+ * <View style={styles.item}>
40
+ * <Text>{children}</Text>
41
+ * <Text>{visible ? 'visible' : ''}</Text>
42
+ * </View>
43
+ * </InView>
44
+ * );
45
+ * }
46
+ *
47
+ * const styles = StyleSheet.create({
48
+ * item: {
49
+ * padding: 16,
50
+ * borderBottomWidth: 1,
51
+ * borderBottomColor: '#ddd',
52
+ * },
53
+ * });
54
+ * ```
55
+ */
56
+ const IOFlatList = withIO(FlatList, [
57
+ 'flashScrollIndicators',
58
+ 'getNativeScrollRef',
59
+ 'getScrollResponder',
60
+ 'getScrollableNode',
61
+ 'scrollToEnd',
62
+ 'scrollToIndex',
63
+ 'scrollToItem',
64
+ 'scrollToOffset',
65
+ ]) as unknown as typeof IOFlatListFunction;
66
+
67
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
68
+ declare function IOFlatListFunction<ItemT = any>(
69
+ props: IOFlatListProps<ItemT> & RefAttributes<IOFlatListController>
70
+ ): JSX.Element;
71
+
72
+ export default IOFlatList;
@@ -0,0 +1,73 @@
1
+ import IntersectionObserver, {
2
+ IntersectionObserverOptions,
3
+ IntersectionObserverEntry,
4
+ Element,
5
+ } from './IntersectionObserver';
6
+
7
+ export type ObserverInstanceCallback = (inView: boolean, intersectionRatio: number) => void;
8
+
9
+ export interface ObserverInstance {
10
+ readonly callback: ObserverInstanceCallback;
11
+ readonly element: Element;
12
+ readonly observerId: number;
13
+ readonly observer: IntersectionObserver;
14
+ }
15
+
16
+ /**
17
+ * @kind class
18
+ * @name IOManager
19
+ * @description A class that tracks the visibility of DOM elements using `IntersectionObserver` instances and executes callbacks when elements enter or leave the viewport.
20
+ * This class makes it easy to manage multiple elements and execute custom logic based on the visibility status of each element.
21
+ */
22
+ class IOManager {
23
+ io: IntersectionObserver;
24
+ observerId: number;
25
+ instanceMap: Map<Element, ObserverInstance> = new Map();
26
+
27
+ constructor(options: IntersectionObserverOptions) {
28
+ this.io = new IntersectionObserver(this.handleChange, options);
29
+ this.observerId = 0;
30
+ }
31
+
32
+ handleChange = (entries: IntersectionObserverEntry[]) => {
33
+ for (let index = 0; index < entries.length; index += 1) {
34
+ const entry = entries[index];
35
+
36
+ if (entry == null) {
37
+ continue;
38
+ }
39
+
40
+ const { target, isIntersecting, intersectionRatio } = entry;
41
+ const instance = this.instanceMap.get(target);
42
+ if (instance) {
43
+ instance.callback(isIntersecting, intersectionRatio);
44
+ }
45
+ }
46
+ };
47
+
48
+ observe(element: Element, callback: ObserverInstanceCallback): ObserverInstance {
49
+ const existInstance = this.instanceMap.get(element);
50
+ if (existInstance) {
51
+ return existInstance;
52
+ }
53
+ this.observerId += 1;
54
+ const instance: ObserverInstance = {
55
+ callback,
56
+ element,
57
+ observerId: this.observerId,
58
+ observer: this.io,
59
+ };
60
+ this.instanceMap.set(element, instance);
61
+ this.io.observe(element);
62
+ return instance;
63
+ }
64
+
65
+ unobserve(element: any) {
66
+ if (this.instanceMap.has(element)) {
67
+ this.instanceMap.delete(element);
68
+ this.io.unobserve(element);
69
+ }
70
+ }
71
+ }
72
+
73
+ export default IOManager;
@@ -0,0 +1,69 @@
1
+ import { ForwardRefExoticComponent, RefAttributes } from 'react';
2
+ import { ScrollView, ScrollViewProps } from 'react-native';
3
+ import withIO, { IOComponentProps } from './withIO';
4
+
5
+ export type IOScrollViewController = ScrollView;
6
+
7
+ export type IOScrollViewProps = IOComponentProps & ScrollViewProps;
8
+
9
+ /**
10
+ * @public
11
+ * @category Screen Control
12
+ * @name IOScrollView
13
+ * @description
14
+ * `IOScrollView` is a [ScrollView](https://reactnative.dev/docs/scrollview) component with added `Intersection Observer` functionality. It can detect when specific elements become visible or disappear from the screen during scrolling.
15
+ * By utilizing this functionality with the `InView` component as a child element, you can easily check whether elements are exposed on the screen.
16
+ *
17
+ * @example
18
+ *
19
+ * You can check whether each item in the list appears on the screen using `IOScrollView`.
20
+ * When each item in the list appears on the screen, the `InView` component changes to the `visible` state.
21
+ *
22
+ * ```tsx
23
+ * import { ReactNode, useState } from 'react';
24
+ * import { StyleSheet, Text, View } from 'react-native';
25
+ * import { InView, IOScrollView } from '@granite-js/react-native';
26
+ *
27
+ * const mockData = Array.from({ length: 30 }, (_, i) => ({ key: String(i) }));
28
+ *
29
+ * export default function IOScrollViewPage() {
30
+ * return (
31
+ * <IOScrollView>
32
+ * {mockData.map((data) => (
33
+ * <InViewItem key={data.key}>{data.key}</InViewItem>
34
+ * ))}
35
+ * </IOScrollView>
36
+ * );
37
+ * }
38
+ *
39
+ * function InViewItem({ children }: { children: ReactNode }) {
40
+ * const [visible, setVisible] = useState(false);
41
+ *
42
+ * return (
43
+ * <InView onChange={setVisible}>
44
+ * <View style={styles.item}>
45
+ * <Text>{children}</Text>
46
+ * <Text>{visible ? 'visible' : ''}</Text>
47
+ * </View>
48
+ * </InView>
49
+ * );
50
+ * }
51
+ *
52
+ * const styles = StyleSheet.create({
53
+ * item: {
54
+ * padding: 16,
55
+ * borderBottomWidth: 1,
56
+ * borderBottomColor: '#ddd',
57
+ * },
58
+ * });
59
+ * ```
60
+ */
61
+ const IOScrollView = withIO(ScrollView, [
62
+ 'scrollTo',
63
+ 'scrollToEnd',
64
+ 'getScrollResponder',
65
+ 'getScrollableNode',
66
+ 'getInnerViewNode',
67
+ ]) as unknown as ForwardRefExoticComponent<IOScrollViewProps & RefAttributes<IOScrollViewController>>;
68
+
69
+ export default IOScrollView;
@@ -0,0 +1,205 @@
1
+ import { ComponentType, PureComponent, ReactElement, ReactNode, RefObject } from 'react';
2
+ import { LayoutChangeEvent, View, ViewProps } from 'react-native';
3
+ import IOContext, { IOContextValue } from './IOContext';
4
+ import { ObserverInstance } from './IOManager';
5
+ import { Element } from './IntersectionObserver';
6
+
7
+ export interface RenderProps {
8
+ inView: boolean;
9
+ onChange: (inView: boolean) => void;
10
+ }
11
+
12
+ export interface Props {
13
+ [key: string]: any;
14
+ }
15
+
16
+ export type InViewProps<T = Props> = T & {
17
+ children: ReactNode | ((fields: RenderProps) => ReactElement<View>);
18
+ as?: ComponentType<any>;
19
+ triggerOnce?: boolean;
20
+ onLayout?: (event: LayoutChangeEvent) => void;
21
+ onChange?: (inView: boolean, areaThreshold: number) => void;
22
+ };
23
+
24
+ export type InViewWrapper = ComponentType<{
25
+ ref?: RefObject<any> | ((ref: any) => void);
26
+ onLayout?: (event: LayoutChangeEvent) => void;
27
+ }>;
28
+
29
+ /**
30
+ * @public
31
+ * @category Screen Control
32
+ * @name InView
33
+ * @description
34
+ * The `InView` component detects when an element starts to become visible on the screen or disappears from the screen.
35
+ * When an element starts to become visible on the screen, the `onChanged` handler is called with `true` as the first argument. Conversely, when the element disappears from the screen, `false` is passed.
36
+ * The second argument of the `onChanged` handler receives the exposure ratio of the element on the screen. The exposure ratio value ranges from `0` to `1.0`. For example, if `0.2` is passed, it means the component is 20% exposed on the screen.
37
+ *
38
+ ::: warning Note
39
+
40
+ `InView` must be used inside [IOScrollView](/reference/react-native/Screen%20Control/InView.md) or [IOFlatList](/reference/react-native/Screen%20Control/IOFlatList.md) that includes `IOContext`.
41
+ If used outside of `IOContext`, an `IOProviderMissingError` will occur.
42
+
43
+ :::
44
+
45
+ * @param {Object} props - Props object passed to the component.
46
+ * @param {React.ReactNode} props.children - Child components to be rendered under the component.
47
+ * @param {React.ComponentType} [prop.as=View] - Specifies the component to actually render. Default is the [View](https://reactnative.dev/docs/view) component.
48
+ * @param {boolean} [triggerOnce=false] - Use this option if you want to call the `onChange` callback only once when the element first becomes visible.
49
+ * @param {(event: LayoutChangeEvent) => void} [onLayout] - Callback function called when there is a change in the layout.
50
+ * @param {(inView: boolean, areaThreshold: number) => void} [onChange] - Callback function called when an element appears or disappears from the screen. The first argument receives the visibility status, and the second argument receives the exposure ratio.
51
+ *
52
+ * @example
53
+ *
54
+ * ### Detecting the `10%` point of an element using the `InView` component
55
+ *
56
+ * ```tsx
57
+ * import { LayoutChangeEvent, View, Text, Dimensions } from 'react-native';
58
+ * import { InView, IOScrollView } from '@granite-js/react-native';
59
+ *
60
+ * export function InViewExample() {
61
+ * const handleLayout = (event: LayoutChangeEvent) => {
62
+ * console.log('Layout changed', event.nativeEvent.layout);
63
+ * };
64
+ *
65
+ * const handleChange = (inView: boolean, areaThreshold: number) => {
66
+ * if (inView) {
67
+ * console.log(`Element is visible at ${areaThreshold * 100}% ratio`);
68
+ * } else {
69
+ * console.log('Element is not visible');
70
+ * }
71
+ * };
72
+ *
73
+ * return (
74
+ * <IOScrollView>
75
+ * <View style={{ height: HEIGHT, width: '100%', backgroundColor: 'blue' }}>
76
+ * <Text style={{ color: 'white' }}>Please scroll down</Text>
77
+ * </View>
78
+ * <InView onLayout={handleLayout} onChange={handleChange}>
79
+ * <View style={{ width: 100, height: 300, backgroundColor: 'yellow' }}>
80
+ * <View style={{ position: 'absolute', top: 30, width: 100, height: 1, borderWidth: 1 }}>
81
+ * <Text style={{ position: 'absolute', top: 0 }}>10% point</Text>
82
+ * </View>
83
+ * </View>
84
+ * </InView>
85
+ * </IOScrollView>
86
+ * );
87
+ * }
88
+ * ```
89
+ */
90
+ class InView<T = ViewProps> extends PureComponent<InViewProps<T>> {
91
+ static contextType = IOContext;
92
+ static defaultProps: Partial<InViewProps> = {
93
+ triggerOnce: false,
94
+ as: View,
95
+ };
96
+
97
+ context: undefined | IOContextValue = undefined;
98
+ mounted = false;
99
+
100
+ protected element: Element;
101
+ protected instance: undefined | ObserverInstance;
102
+ protected view: any;
103
+
104
+ constructor(props: InViewProps<T>) {
105
+ super(props);
106
+
107
+ this.element = {
108
+ inView: false,
109
+ intersectionRatio: 0,
110
+ layout: {
111
+ x: 0,
112
+ y: 0,
113
+ width: 0,
114
+ height: 0,
115
+ },
116
+ measureLayout: this.measureLayout,
117
+ };
118
+ }
119
+
120
+ componentDidMount() {
121
+ this.mounted = true;
122
+ if (this.context?.manager) {
123
+ this.instance = this.context.manager.observe(this.element, this.handleChange);
124
+ }
125
+ }
126
+
127
+ componentWillUnmount() {
128
+ this.mounted = false;
129
+ if (this.context?.manager && this.instance) {
130
+ this.context.manager.unobserve(this.element);
131
+ }
132
+ }
133
+
134
+ protected handleChange = (inView: boolean, areaThreshold: number) => {
135
+ if (this.mounted) {
136
+ const { triggerOnce, onChange } = this.props;
137
+ if (inView && triggerOnce) {
138
+ if (this.context?.manager) {
139
+ this.context?.manager.unobserve(this.element);
140
+ }
141
+ }
142
+ if (onChange) {
143
+ onChange(inView, areaThreshold);
144
+ }
145
+ }
146
+ };
147
+
148
+ protected handleRef = (ref: any) => {
149
+ this.view = ref;
150
+ };
151
+
152
+ protected handleLayout = (event: LayoutChangeEvent) => {
153
+ const {
154
+ nativeEvent: { layout },
155
+ } = event;
156
+ if (layout.width !== this.element.layout.width || layout.height !== this.element.layout.height) {
157
+ if (this.element.onLayout) {
158
+ this.element.onLayout();
159
+ }
160
+ }
161
+ const { onLayout } = this.props;
162
+ if (onLayout) {
163
+ onLayout(event);
164
+ }
165
+ };
166
+
167
+ measure = (...args: any) => {
168
+ this.view.measure(...args);
169
+ };
170
+
171
+ measureInWindow = (...args: any) => {
172
+ this.view.measureInWindow(...args);
173
+ };
174
+
175
+ measureLayout = (...args: any) => {
176
+ this.view.measureLayout(...args);
177
+ };
178
+
179
+ setNativeProps = (...args: any) => {
180
+ this.view.setNativeProps(...args);
181
+ };
182
+
183
+ focus = (...args: any) => {
184
+ this.view.focus(...args);
185
+ };
186
+
187
+ blur = (...args: any) => {
188
+ this.view.blur(...args);
189
+ };
190
+
191
+ render() {
192
+ const { as, children, ...props } = this.props;
193
+ if (typeof children === 'function') {
194
+ return null;
195
+ }
196
+ const ViewComponent: InViewWrapper = (as || View) as InViewWrapper;
197
+ return (
198
+ <ViewComponent {...props} ref={this.handleRef} onLayout={this.handleLayout}>
199
+ {children}
200
+ </ViewComponent>
201
+ );
202
+ }
203
+ }
204
+
205
+ export default InView;
@@ -0,0 +1,212 @@
1
+ import { throttle } from 'es-toolkit';
2
+ import { LayoutRectangle, NativeScrollEvent } from 'react-native';
3
+
4
+ export interface Root {
5
+ /**
6
+ * NodeHandle of the target component
7
+ */
8
+ node: any;
9
+ /**
10
+ * Whether horizontal scroll is enabled
11
+ */
12
+ horizontal: boolean;
13
+ /**
14
+ * Scroll event of the target component
15
+ */
16
+ current: NativeScrollEvent;
17
+ onLayout?: () => void;
18
+ onScroll?: (event: NativeScrollEvent) => void;
19
+ }
20
+
21
+ export interface Element {
22
+ inView: boolean;
23
+ intersectionRatio: number;
24
+ layout: LayoutRectangle;
25
+ measureLayout: (node: any, callback: (x: number, y: number, width: number, height: number) => void) => void;
26
+ onLayout?: () => void;
27
+ }
28
+
29
+ export interface IntersectionObserverEntry {
30
+ target: Element;
31
+ isIntersecting: boolean;
32
+ intersectionRatio: number;
33
+ }
34
+
35
+ export interface RootMargin {
36
+ left?: number;
37
+ right?: number;
38
+ top?: number;
39
+ bottom?: number;
40
+ }
41
+
42
+ export interface IntersectionObserverOptions {
43
+ /**
44
+ * Information about the component that wraps the element
45
+ */
46
+ root: Root;
47
+ rootMargin?: RootMargin;
48
+ threshold?: number | number[];
49
+ }
50
+
51
+ export type IntersectionObserverCallback = (entries: IntersectionObserverEntry[]) => void;
52
+
53
+ export const defaultRootMargin: RootMargin = {
54
+ left: 0,
55
+ right: 0,
56
+ top: 0,
57
+ bottom: 0,
58
+ };
59
+
60
+ export const defaultThreshold = 0;
61
+
62
+ /**
63
+ * @kind class
64
+ * @name IntersectionObserver
65
+ * @description
66
+ * IntersectionObserver implemented for React Native environment.
67
+ *
68
+ * @param {IntersectionObserverCallback} callback - Callback function that is called when the visibility state of the target element changes.
69
+ * @param {IntersectionObserverOptions} options - Options object that controls the behavior of IntersectionObserver.
70
+ */
71
+ class IntersectionObserver {
72
+ protected callback: IntersectionObserverCallback;
73
+ protected options: IntersectionObserverOptions;
74
+ protected targets: Element[];
75
+
76
+ constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions) {
77
+ this.callback = callback;
78
+ this.options = options;
79
+ this.targets = [];
80
+ this.options.root.onLayout = this.handleLayout;
81
+ this.options.root.onScroll = this.handleScroll;
82
+ }
83
+
84
+ protected measureTarget = (target: Element) => {
85
+ const rootNode = this.options.root.node;
86
+ if (rootNode) {
87
+ target.measureLayout(rootNode, (x, y, width, height) => {
88
+ target.layout = {
89
+ x,
90
+ y,
91
+ width,
92
+ height,
93
+ };
94
+ this.handleScroll();
95
+ });
96
+ }
97
+ };
98
+
99
+ protected handleLayout = throttle(
100
+ () => {
101
+ for (let index = 0; index < this.targets.length; index += 1) {
102
+ const target = this.targets[index];
103
+
104
+ if (target != null) {
105
+ this.measureTarget(target);
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;
143
+ }
144
+
145
+ const previousIntersectionRatio = target.intersectionRatio;
146
+
147
+ let isIntersecting = false;
148
+ let intersectionRatio = previousIntersectionRatio;
149
+
150
+ if (horizontal) {
151
+ const visibleTargetMinX = Math.max(contentOffset.x - (rootMargin.left || 0), targetLayout.x);
152
+ const visibleTargetMaxX = Math.min(
153
+ contentOffsetWithLayout + (rootMargin.left || 0),
154
+ targetLayout.x + targetLayout.width
155
+ );
156
+ const visibleHeight = Math.max(visibleTargetMaxX - visibleTargetMinX, 0);
157
+
158
+ intersectionRatio = visibleHeight / targetLayout.height;
159
+ isIntersecting =
160
+ contentOffsetWithLayout + (rootMargin.right || 0) >= targetLayout.x &&
161
+ contentOffset.x - (rootMargin.left || 0) <= targetLayout.x + targetLayout.width;
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;
174
+ }
175
+
176
+ intersectionRatio = Math.floor(intersectionRatio * 100) / 100;
177
+
178
+ if (target.inView !== isIntersecting || target.intersectionRatio !== intersectionRatio) {
179
+ target.inView = isIntersecting;
180
+ target.intersectionRatio = intersectionRatio;
181
+
182
+ changedTargets.push({
183
+ target,
184
+ isIntersecting,
185
+ intersectionRatio,
186
+ });
187
+ }
188
+ }
189
+ this.callback(changedTargets);
190
+ },
191
+ 100,
192
+ { edges: ['trailing'] }
193
+ ) as () => void;
194
+
195
+ public observe(target: Element) {
196
+ const index = this.targets.indexOf(target);
197
+ if (index < 0) {
198
+ target.onLayout = this.handleLayout;
199
+ this.targets.push(target);
200
+ }
201
+ }
202
+
203
+ public unobserve(target: Element) {
204
+ const index = this.targets.indexOf(target);
205
+ if (index >= 0) {
206
+ target.onLayout = undefined;
207
+ this.targets.splice(index, 1);
208
+ }
209
+ }
210
+ }
211
+
212
+ export default IntersectionObserver;
@@ -0,0 +1,24 @@
1
+ import IOContext from './IOContext';
2
+ import IOFlatList, { type IOFlatListController, type IOFlatListProps } from './IOFlatList';
3
+ import IOScrollView, { type IOScrollViewController, type IOScrollViewProps } from './IOScrollView';
4
+ import InView, { type InViewProps } from './InView';
5
+ import {
6
+ type IntersectionObserverEntry,
7
+ type IntersectionObserverOptions,
8
+ type RootMargin,
9
+ } from './IntersectionObserver';
10
+ import type { IOComponentProps } from './withIO';
11
+
12
+ export type {
13
+ IntersectionObserverEntry,
14
+ IntersectionObserverOptions,
15
+ RootMargin,
16
+ InViewProps,
17
+ IOComponentProps,
18
+ IOFlatListController,
19
+ IOFlatListProps,
20
+ IOScrollViewController,
21
+ IOScrollViewProps,
22
+ };
23
+
24
+ export { InView, IOContext, IOFlatList, IOScrollView };