@granite-js/react-native 0.0.1

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 (189) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/LICENSE +202 -0
  3. package/README.md +24 -0
  4. package/bin/cli.js +3 -0
  5. package/cli.d.ts +1 -0
  6. package/cli.js +4 -0
  7. package/config.d.ts +2 -0
  8. package/config.js +5 -0
  9. package/dist/app/App/index.android.d.ts +2 -0
  10. package/dist/app/App/index.ios.d.ts +6 -0
  11. package/dist/app/Granite.d.ts +60 -0
  12. package/dist/app/index.d.ts +2 -0
  13. package/dist/app/registerPage.d.ts +20 -0
  14. package/dist/async-bridges.d.ts +2 -0
  15. package/dist/constant-bridges.d.ts +1 -0
  16. package/dist/dev-entrypoint/index.d.ts +2 -0
  17. package/dist/event/abstract.d.ts +42 -0
  18. package/dist/event/index.d.ts +2 -0
  19. package/dist/event/useGraniteEvent.d.ts +14 -0
  20. package/dist/impression-area/ImpressionArea.d.ts +231 -0
  21. package/dist/impression-area/index.d.ts +1 -0
  22. package/dist/index.d.ts +17 -0
  23. package/dist/initial-props/InitialProps.d.ts +127 -0
  24. package/dist/initial-props/index.d.ts +1 -0
  25. package/dist/intersection-observer/IOContext.d.ts +10 -0
  26. package/dist/intersection-observer/IOFlatList.d.ts +55 -0
  27. package/dist/intersection-observer/IOManager.d.ts +24 -0
  28. package/dist/intersection-observer/IOScrollView.d.ts +59 -0
  29. package/dist/intersection-observer/InView.d.ts +107 -0
  30. package/dist/intersection-observer/IntersectionObserver.d.ts +67 -0
  31. package/dist/intersection-observer/index.d.ts +8 -0
  32. package/dist/intersection-observer/withIO.d.ts +20 -0
  33. package/dist/jest/index.d.ts +1 -0
  34. package/dist/jest/index.js +32 -0
  35. package/dist/keyboard/KeyboardAboveView.d.ts +40 -0
  36. package/dist/keyboard/index.d.ts +1 -0
  37. package/dist/keyboard/useKeyboardAnimatedHeight.d.ts +20 -0
  38. package/dist/native-event-emitter/eventEmitters/index.d.ts +2 -0
  39. package/dist/native-event-emitter/eventEmitters/types.d.ts +4 -0
  40. package/dist/native-event-emitter/eventEmitters/visibilityChanged.d.ts +10 -0
  41. package/dist/native-event-emitter/index.d.ts +1 -0
  42. package/dist/native-event-emitter/nativeEventEmitter.d.ts +15 -0
  43. package/dist/native-modules/core/GraniteCoreModule.d.ts +8 -0
  44. package/dist/native-modules/index.d.ts +3 -0
  45. package/dist/native-modules/natives/GraniteModule.d.ts +7 -0
  46. package/dist/native-modules/natives/closeView.d.ts +21 -0
  47. package/dist/native-modules/natives/getSchemeUri.d.ts +23 -0
  48. package/dist/native-modules/natives/index.d.ts +3 -0
  49. package/dist/native-modules/natives/openURL.d.ts +36 -0
  50. package/dist/react/index.d.ts +1 -0
  51. package/dist/react/useWaitForReturnNavigator.d.ts +39 -0
  52. package/dist/rn-polyfills/index.d.ts +1 -0
  53. package/dist/rn-polyfills/symbol-asynciterator/index.d.ts +9 -0
  54. package/dist/rn-polyfills/url/index.d.ts +1 -0
  55. package/dist/router/Router.d.ts +59 -0
  56. package/dist/router/components/BackButton.d.ts +7 -0
  57. package/dist/router/components/CanGoBackGuard.d.ts +6 -0
  58. package/dist/router/components/RouterBackButton.d.ts +9 -0
  59. package/dist/router/components/StackNavigator.d.ts +54 -0
  60. package/dist/router/constants.d.ts +2 -0
  61. package/dist/router/createRoute.d.ts +39 -0
  62. package/dist/router/createRoute.test-d.d.ts +9 -0
  63. package/dist/router/hooks/useInitialRouteName.d.ts +1 -0
  64. package/dist/router/hooks/useIsInitialScreen.d.ts +1 -0
  65. package/dist/router/hooks/useRouterControls.d.ts +11 -0
  66. package/dist/router/index.d.ts +3 -0
  67. package/dist/router/types/RequireContext.d.ts +7 -0
  68. package/dist/router/types/RouteScreen.d.ts +16 -0
  69. package/dist/router/types/Screen.d.ts +23 -0
  70. package/dist/router/types/index.d.ts +3 -0
  71. package/dist/router/types/screen-option.d.ts +4 -0
  72. package/dist/router/utils/createParentRouteScreenMap.d.ts +8 -0
  73. package/dist/router/utils/defaultParserParams.d.ts +9 -0
  74. package/dist/router/utils/index.d.ts +2 -0
  75. package/dist/router/utils/matchers.d.ts +2 -0
  76. package/dist/router/utils/mergeParentLayoutScreen.d.ts +18 -0
  77. package/dist/router/utils/path.d.ts +53 -0
  78. package/dist/router/utils/screen.d.ts +53 -0
  79. package/dist/scroll-view-inertial-background/ScrollViewInertialBackground.d.ts +49 -0
  80. package/dist/scroll-view-inertial-background/index.d.ts +1 -0
  81. package/dist/types/global.d.ts +18 -0
  82. package/dist/use-back-event/index.d.ts +1 -0
  83. package/dist/use-back-event/useBackEvent.d.ts +135 -0
  84. package/dist/utils/noop.d.ts +1 -0
  85. package/dist/utils/usePreservedCallback.d.ts +1 -0
  86. package/dist/visibility/VisibilityProvider.d.ts +27 -0
  87. package/dist/visibility/index.d.ts +6 -0
  88. package/dist/visibility/react-navigation/index.d.ts +2 -0
  89. package/dist/visibility/react-navigation/useIsFocusedSafely.d.ts +20 -0
  90. package/dist/visibility/react-navigation/useNavigationSafely.d.ts +19 -0
  91. package/dist/visibility/useIsAppForeground.d.ts +39 -0
  92. package/dist/visibility/useVisibility.d.ts +35 -0
  93. package/dist/visibility/useVisibilityChange.d.ts +51 -0
  94. package/dist/visibility/useVisibilityChanged.d.ts +41 -0
  95. package/dist/visibility/utils/usePrevious.d.ts +15 -0
  96. package/jest.d.ts +1 -0
  97. package/package.json +92 -0
  98. package/presets.d.ts +1 -0
  99. package/src/app/App/index.android.tsx +6 -0
  100. package/src/app/App/index.d.ts +6 -0
  101. package/src/app/App/index.ios.tsx +13 -0
  102. package/src/app/Granite.tsx +130 -0
  103. package/src/app/index.ts +2 -0
  104. package/src/app/registerPage.ts +29 -0
  105. package/src/async-bridges.ts +2 -0
  106. package/src/constant-bridges.ts +1 -0
  107. package/src/dev-entrypoint/index.tsx +21 -0
  108. package/src/event/abstract.ts +130 -0
  109. package/src/event/index.ts +2 -0
  110. package/src/event/useGraniteEvent.ts +34 -0
  111. package/src/impression-area/ImpressionArea.tsx +341 -0
  112. package/src/impression-area/index.ts +1 -0
  113. package/src/index.ts +19 -0
  114. package/src/initial-props/InitialProps.ts +144 -0
  115. package/src/initial-props/index.ts +1 -0
  116. package/src/intersection-observer/IOContext.ts +16 -0
  117. package/src/intersection-observer/IOFlatList.ts +72 -0
  118. package/src/intersection-observer/IOManager.ts +73 -0
  119. package/src/intersection-observer/IOScrollView.ts +69 -0
  120. package/src/intersection-observer/InView.tsx +205 -0
  121. package/src/intersection-observer/IntersectionObserver.ts +212 -0
  122. package/src/intersection-observer/index.ts +24 -0
  123. package/src/intersection-observer/withIO.tsx +151 -0
  124. package/src/jest/index.ts +1 -0
  125. package/src/keyboard/KeyboardAboveView.tsx +62 -0
  126. package/src/keyboard/index.ts +1 -0
  127. package/src/keyboard/useKeyboardAnimatedHeight.tsx +81 -0
  128. package/src/native-event-emitter/eventEmitters/index.ts +3 -0
  129. package/src/native-event-emitter/eventEmitters/types.ts +4 -0
  130. package/src/native-event-emitter/eventEmitters/visibilityChanged.ts +11 -0
  131. package/src/native-event-emitter/index.ts +1 -0
  132. package/src/native-event-emitter/nativeEventEmitter.ts +18 -0
  133. package/src/native-modules/core/GraniteCoreModule.ts +9 -0
  134. package/src/native-modules/index.ts +3 -0
  135. package/src/native-modules/natives/GraniteModule.ts +8 -0
  136. package/src/native-modules/natives/closeView.ts +25 -0
  137. package/src/native-modules/natives/getSchemeUri.ts +27 -0
  138. package/src/native-modules/natives/index.ts +3 -0
  139. package/src/native-modules/natives/openURL.ts +40 -0
  140. package/src/react/index.ts +1 -0
  141. package/src/react/useWaitForReturnNavigator.ts +75 -0
  142. package/src/rn-polyfills/index.ts +7 -0
  143. package/src/rn-polyfills/symbol-asynciterator/index.ts +15 -0
  144. package/src/rn-polyfills/url/index.ts +1 -0
  145. package/src/router/Router.tsx +164 -0
  146. package/src/router/components/BackButton.tsx +58 -0
  147. package/src/router/components/CanGoBackGuard.tsx +31 -0
  148. package/src/router/components/RouterBackButton.tsx +32 -0
  149. package/src/router/components/StackNavigator.tsx +12 -0
  150. package/src/router/constants.ts +3 -0
  151. package/src/router/createRoute.test-d.ts +52 -0
  152. package/src/router/createRoute.ts +161 -0
  153. package/src/router/hooks/useInitialRouteName.tsx +22 -0
  154. package/src/router/hooks/useIsInitialScreen.ts +7 -0
  155. package/src/router/hooks/useRouterControls.tsx +72 -0
  156. package/src/router/index.ts +3 -0
  157. package/src/router/types/RequireContext.ts +7 -0
  158. package/src/router/types/RouteScreen.ts +17 -0
  159. package/src/router/types/Screen.tsx +24 -0
  160. package/src/router/types/index.ts +3 -0
  161. package/src/router/types/screen-option.ts +23 -0
  162. package/src/router/utils/createParentRouteScreenMap.spec.ts +166 -0
  163. package/src/router/utils/createParentRouteScreenMap.ts +136 -0
  164. package/src/router/utils/defaultParserParams.spec.ts +46 -0
  165. package/src/router/utils/defaultParserParams.ts +19 -0
  166. package/src/router/utils/index.ts +2 -0
  167. package/src/router/utils/matchers.ts +5 -0
  168. package/src/router/utils/mergeParentLayoutScreen.spec.tsx +112 -0
  169. package/src/router/utils/mergeParentLayoutScreen.tsx +43 -0
  170. package/src/router/utils/path.spec.ts +135 -0
  171. package/src/router/utils/path.ts +105 -0
  172. package/src/router/utils/screen.tsx +111 -0
  173. package/src/scroll-view-inertial-background/ScrollViewInertialBackground.tsx +99 -0
  174. package/src/scroll-view-inertial-background/index.ts +1 -0
  175. package/src/types/global.ts +31 -0
  176. package/src/use-back-event/index.ts +1 -0
  177. package/src/use-back-event/useBackEvent.tsx +260 -0
  178. package/src/utils/noop.ts +1 -0
  179. package/src/utils/usePreservedCallback.ts +16 -0
  180. package/src/visibility/VisibilityProvider.tsx +36 -0
  181. package/src/visibility/index.ts +6 -0
  182. package/src/visibility/react-navigation/index.ts +2 -0
  183. package/src/visibility/react-navigation/useIsFocusedSafely.tsx +58 -0
  184. package/src/visibility/react-navigation/useNavigationSafely.tsx +30 -0
  185. package/src/visibility/useIsAppForeground.tsx +73 -0
  186. package/src/visibility/useVisibility.tsx +54 -0
  187. package/src/visibility/useVisibilityChange.ts +69 -0
  188. package/src/visibility/useVisibilityChanged.tsx +69 -0
  189. package/src/visibility/utils/usePrevious.tsx +24 -0
@@ -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 };
@@ -0,0 +1,151 @@
1
+ import { type ComponentProps, PureComponent, RefObject, createRef } from 'react';
2
+ import { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, ScrollView, findNodeHandle } from 'react-native';
3
+ import IOContext, { IOContextValue } from './IOContext';
4
+ import IOManager from './IOManager';
5
+ import { Root, RootMargin } from './IntersectionObserver';
6
+
7
+ export interface IOComponentProps {
8
+ rootMargin?: RootMargin;
9
+ }
10
+
11
+ /**
12
+ * @category Functions
13
+ * @kind function
14
+ * @name withIO
15
+ * @description
16
+ * A Higher-Order Component (HoC) that wraps a component with `IOContext` to enable Intersection Observer functionality.
17
+ *
18
+ * @argument
19
+ * @param {React.ComponentType} [BaseComponent] - Callback function that is called when the component is mounted.
20
+ * @param {string[]} [methods] - List of event handler names from BaseComponent to be bound.
21
+ * @returns {React.ComponentType} - Returns a wrapped component that can use Intersection Observer functionality.
22
+ */
23
+ function withIO<
24
+ CompProps extends Pick<
25
+ ComponentProps<typeof ScrollView>,
26
+ 'horizontal' | 'scrollEventThrottle' | 'onContentSizeChange' | 'onLayout' | 'onScroll'
27
+ >,
28
+ >(BaseComponent: React.ComponentType<CompProps>, methods: string[]) {
29
+ type ScrollableComponentProps = CompProps & IOComponentProps;
30
+ const IOScrollableComponent = class extends PureComponent<ScrollableComponentProps> {
31
+ protected node: any;
32
+ protected scroller: RefObject<any>;
33
+ protected root: Root;
34
+ protected manager: IOManager;
35
+ protected contextValue: IOContextValue;
36
+
37
+ constructor(props: ScrollableComponentProps) {
38
+ super(props);
39
+
40
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
41
+ const self = this;
42
+ this.scroller = createRef();
43
+ this.root = {
44
+ get node() {
45
+ return self.node;
46
+ },
47
+ get horizontal() {
48
+ return Boolean(self.props.horizontal);
49
+ },
50
+ current: {
51
+ contentInset: {
52
+ top: 0,
53
+ right: 0,
54
+ bottom: 0,
55
+ left: 0,
56
+ },
57
+ contentOffset: {
58
+ x: 0,
59
+ y: 0,
60
+ },
61
+ contentSize: {
62
+ width: 0,
63
+ height: 0,
64
+ },
65
+ layoutMeasurement: {
66
+ width: 0,
67
+ height: 0,
68
+ },
69
+ zoomScale: 1,
70
+ },
71
+ };
72
+ const manager = new IOManager({
73
+ root: this.root,
74
+ get rootMargin() {
75
+ return self.props.rootMargin;
76
+ },
77
+ });
78
+ this.manager = manager;
79
+ this.contextValue = {
80
+ manager,
81
+ };
82
+ }
83
+
84
+ componentDidMount() {
85
+ this.node = findNodeHandle(this.scroller.current);
86
+ methods.forEach((method) => {
87
+ (this as any)[method] = (...args: any) => {
88
+ this.scroller.current?.[method]?.(...args);
89
+ };
90
+ });
91
+ }
92
+
93
+ protected handleContentSizeChange = (width: number, height: number) => {
94
+ const { contentSize } = this.root.current;
95
+ if (width !== contentSize.width || height !== contentSize.height) {
96
+ this.root.current.contentSize = { width, height };
97
+ if (width > 0 && height > 0 && this.root.onLayout) {
98
+ this.root.onLayout();
99
+ }
100
+ }
101
+ const { onContentSizeChange } = this.props;
102
+ if (onContentSizeChange) {
103
+ onContentSizeChange(width, height);
104
+ }
105
+ };
106
+
107
+ protected handleLayout = (event: LayoutChangeEvent) => {
108
+ const {
109
+ nativeEvent: { layout },
110
+ } = event;
111
+ const { layoutMeasurement } = this.root.current;
112
+ if (layoutMeasurement.width !== layout.width || layoutMeasurement.height !== layout.height) {
113
+ this.root.current.layoutMeasurement = layout;
114
+ }
115
+ const { onLayout } = this.props;
116
+ if (onLayout) {
117
+ onLayout(event);
118
+ }
119
+ };
120
+
121
+ protected handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
122
+ this.root.current = event.nativeEvent;
123
+ if (this.root.onScroll) {
124
+ this.root.onScroll(this.root.current);
125
+ }
126
+ const { onScroll } = this.props;
127
+ if (onScroll) {
128
+ onScroll(event);
129
+ }
130
+ };
131
+
132
+ render() {
133
+ return (
134
+ <IOContext.Provider value={this.contextValue}>
135
+ <BaseComponent
136
+ scrollEventThrottle={16}
137
+ {...this.props}
138
+ ref={this.scroller}
139
+ onContentSizeChange={this.handleContentSizeChange}
140
+ onLayout={this.handleLayout}
141
+ onScroll={this.handleScroll}
142
+ />
143
+ </IOContext.Provider>
144
+ );
145
+ }
146
+ };
147
+
148
+ return IOScrollableComponent as typeof BaseComponent;
149
+ }
150
+
151
+ export default withIO;
@@ -0,0 +1 @@
1
+ export { setup, config } from '@granite-js/jest';