@cleartrip/ct-design-intersection-observer 5.30.0 → 5.32.0-SNAPSHOT-native-main.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.
@@ -1 +1 @@
1
- {"version":3,"file":"IntersectionObserver.native.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/IntersectionObserver.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,EAAE,yBAAyB,EAAE,MAAM,QAAQ,CAAC;AAEnD,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAyC7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"IntersectionObserver.native.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/IntersectionObserver.native.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,EAAE,yBAAyB,EAAE,MAAM,QAAQ,CAAC;AAEnD,QAAA,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CAqD7D,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
package/dist/type.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface IntersectionObserverProps {
12
12
  triggerOnce?: boolean;
13
13
  onUnmount?: () => void;
14
14
  observeOnScroll?: boolean;
15
+ observeWithScrollOffset?: boolean;
15
16
  testID?: string;
16
17
  }
17
18
  //# sourceMappingURL=type.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,MAAM,WAAW,yBAAyB;IAExC,QAAQ,EAAE,SAAS,CAAC;IAGpB,cAAc,EAAE,MAAM,IAAI,CAAC;IAG3B,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAGpC,0BAA0B,CAAC,EAAE,MAAM,IAAI,CAAC;IAGxC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAGrB,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAGvE,6BAA6B,CAAC,EAAE,OAAO,CAAC;IAGxC,WAAW,CAAC,EAAE,OAAO,CAAC;IAKtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IAKvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAM1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/type.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,MAAM,WAAW,yBAAyB;IAExC,QAAQ,EAAE,SAAS,CAAC;IAGpB,cAAc,EAAE,MAAM,IAAI,CAAC;IAG3B,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAGpC,0BAA0B,CAAC,EAAE,MAAM,IAAI,CAAC;IAGxC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAGrB,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAGvE,6BAA6B,CAAC,EAAE,OAAO,CAAC;IAGxC,WAAW,CAAC,EAAE,OAAO,CAAC;IAKtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IAKvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAK1B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAMlC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -23,6 +23,7 @@ export interface IntersectionOptions extends IntersectionObserverInit {
23
23
  observeOnScroll?: boolean;
24
24
  onUnmount?: () => void;
25
25
  checkFullVisibility?: boolean;
26
+ observeWithScrollOffset?: boolean;
26
27
  }
27
28
  export declare enum SCROLLING_TYPE {
28
29
  NONE = "none",
@@ -1 +1 @@
1
- {"version":3,"file":"useInViewType.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/useInViewType.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC;AAEjD,MAAM,WAAW,mBAAoB,SAAQ,wBAAwB;IAEnE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAEtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,KAAK,CAAC,EAAE,MAAM,CAAC;IAKf,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAGrC,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAMpC,uBAAuB,CAAC,EAAE;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IAKF,8BAA8B,CAAC,EAAE,OAAO,CAAC;IAKzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,0BAA0B,CAAC,EAAE,MAAM,IAAI,CAAC;IAGxC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAGhC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAEvE,6BAA6B,CAAC,EAAE,OAAO,CAAC;IAMxC,eAAe,CAAC,EAAE,OAAO,CAAC;IAK1B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IAMvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,oBAAY,cAAc;IACxB,IAAI,SAAS;IACb,UAAU,eAAe;IACzB,QAAQ,aAAa;CACtB;AAED,UAAU,UAAU;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb"}
1
+ {"version":3,"file":"useInViewType.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/useInViewType.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,MAAM,MAAM,kBAAkB,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC;AAEjD,MAAM,WAAW,mBAAoB,SAAQ,wBAAwB;IAEnE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAEtB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,IAAI,CAAC,EAAE,OAAO,CAAC;IAGf,KAAK,CAAC,EAAE,MAAM,CAAC;IAKf,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAGrC,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IAMpC,uBAAuB,CAAC,EAAE;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IAKF,8BAA8B,CAAC,EAAE,OAAO,CAAC;IAKzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,cAAc,CAAC,EAAE,MAAM,CAAC;IAGxB,0BAA0B,CAAC,EAAE,MAAM,IAAI,CAAC;IAGxC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAGhC,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAEvE,6BAA6B,CAAC,EAAE,OAAO,CAAC;IAMxC,eAAe,CAAC,EAAE,OAAO,CAAC;IAK1B,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IAMvB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAM9B,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,oBAAY,cAAc;IACxB,IAAI,SAAS;IACb,UAAU,eAAe;IACzB,QAAQ,aAAa;CACtB;AAED,UAAU,UAAU;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;CACb"}
@@ -1,4 +1,8 @@
1
1
  import { IntersectionOptions, InViewHookResponse } from './useInViewType';
2
- declare const useInView: <T>({ threshold, timedThreshold, onChange, onIntersectionComplete, onTimedObservationComplete, minimumObservationTime, observerBox, gestureHorizontallyScrollable, triggerOnce, observeOnScroll, onUnmount, checkFullVisibility, }: IntersectionOptions) => InViewHookResponse<T>;
2
+ export declare const useInViewWithLayout: <T>({ threshold, timedThreshold, onChange, onIntersectionComplete, onTimedObservationComplete, minimumObservationTime, observerBox, gestureHorizontallyScrollable, triggerOnce, observeOnScroll, onUnmount, checkFullVisibility, observeWithScrollOffset, }: IntersectionOptions) => {
3
+ ref: InViewHookResponse<T>;
4
+ onLayout: () => void;
5
+ };
6
+ declare const useInView: <T>(options: IntersectionOptions) => InViewHookResponse<T>;
3
7
  export default useInView;
4
8
  //# sourceMappingURL=useinView.native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useinView.native.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/useinView.native.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAkI1E,QAAA,MAAM,SAAS,GAAI,CAAC,EAAE,gOAanB,mBAAmB,KAAG,kBAAkB,CAAC,CAAC,CAoI5C,CAAC;AAEF,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"useinView.native.d.ts","sourceRoot":"","sources":["../packages/components/InterSectionObserver/src/useinView.native.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AA0L1E,eAAO,MAAM,mBAAmB,GAAI,CAAC,EAAE,yPAcpC,mBAAmB,KAAG;IAAE,GAAG,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;CAsM1E,CAAC;AAEF,QAAA,MAAM,SAAS,GAAI,CAAC,EAAE,SAAS,mBAAmB,KAAG,kBAAkB,CAAC,CAAC,CAAwC,CAAC;AAElH,eAAe,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleartrip/ct-design-intersection-observer",
3
- "version": "5.30.0",
3
+ "version": "5.32.0-SNAPSHOT-native-main.1",
4
4
  "description": "Intersection Observer Component",
5
5
  "types": "dist/index.d.ts",
6
6
  "main": "./dist/ct-design-intersection-observer.cjs.js",
@@ -24,15 +24,15 @@
24
24
  "src"
25
25
  ],
26
26
  "dependencies": {
27
- "@cleartrip/ct-design-scroll-container": "5.30.0",
28
- "@cleartrip/ct-design-container": "5.30.0",
29
- "@cleartrip/ct-design-style-manager": "5.30.0",
30
- "@cleartrip/ct-design-theme": "5.30.0",
31
- "@cleartrip/ct-design-list": "5.30.0",
32
- "@cleartrip/ct-design-common-utils": "5.30.0"
27
+ "@cleartrip/ct-design-container": "5.32.0-SNAPSHOT-native-main.1",
28
+ "@cleartrip/ct-design-scroll-container": "5.32.0-SNAPSHOT-native-main.1",
29
+ "@cleartrip/ct-design-style-manager": "5.32.0-SNAPSHOT-native-main.1",
30
+ "@cleartrip/ct-design-theme": "5.32.0-SNAPSHOT-native-main.1",
31
+ "@cleartrip/ct-design-list": "5.32.0-SNAPSHOT-native-main.1",
32
+ "@cleartrip/ct-design-common-utils": "5.32.0-SNAPSHOT-native-main.1"
33
33
  },
34
34
  "devDependencies": {
35
- "@cleartrip/ct-design-types": "5.30.0"
35
+ "@cleartrip/ct-design-types": "5.32.0-SNAPSHOT-native-main.1"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "react": ">=16.8.0",
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { View } from 'react-native';
4
- import useInView from './useinView';
4
+ import { useInViewWithLayout } from './useinView.native';
5
5
 
6
6
  import { IntersectionObserverProps } from './type';
7
7
 
@@ -18,9 +18,10 @@ const InterSectionObserver: React.FC<IntersectionObserverProps> = ({
18
18
  onIntersectionComplete,
19
19
  onUnmount,
20
20
  observeOnScroll,
21
+ observeWithScrollOffset,
21
22
  testID,
22
23
  }) => {
23
- const ref = useInView<View>({
24
+ const { ref, onLayout } = useInViewWithLayout<View>({
24
25
  onChange: onIntersection,
25
26
  onTimedObservationComplete,
26
27
  onIntersectionComplete,
@@ -32,17 +33,28 @@ const InterSectionObserver: React.FC<IntersectionObserverProps> = ({
32
33
  triggerOnce,
33
34
  onUnmount,
34
35
  observeOnScroll,
36
+ observeWithScrollOffset,
35
37
  });
36
38
  if (wrapped) {
37
39
  return (
38
- <View ref={ref} testID={testID ? `${testID}-IntersectionObserver` : undefined} collapsable={false}>
40
+ <View
41
+ ref={ref}
42
+ testID={testID ? `${testID}-IntersectionObserver` : undefined}
43
+ collapsable={false}
44
+ onLayout={onLayout}
45
+ >
39
46
  {children}
40
47
  </View>
41
48
  );
42
49
  }
43
50
  return (
44
51
  <>
45
- <View ref={ref} testID={testID ? `${testID}-IntersectionObserver` : undefined} collapsable={false} />
52
+ <View
53
+ ref={ref}
54
+ testID={testID ? `${testID}-IntersectionObserver` : undefined}
55
+ collapsable={false}
56
+ onLayout={onLayout}
57
+ />
46
58
  {children}
47
59
  </>
48
60
  );
package/src/type.ts CHANGED
@@ -41,6 +41,11 @@ export interface IntersectionObserverProps {
41
41
  */
42
42
  observeOnScroll?: boolean;
43
43
 
44
+ /**
45
+ * compute visibility from the cached layout and parent scroll offset instead of measuring on every observe
46
+ */
47
+ observeWithScrollOffset?: boolean;
48
+
44
49
  /**
45
50
  * Identifier used to locate this component in end-to-end tests.
46
51
  * Forwarded to the observed React Native view.
@@ -78,6 +78,12 @@ export interface IntersectionOptions extends IntersectionObserverInit {
78
78
  * @default false
79
79
  */
80
80
  checkFullVisibility?: boolean;
81
+
82
+ /**
83
+ * compute visibility from the cached layout and parent scroll offset instead of measuring on every observe
84
+ * @default false
85
+ */
86
+ observeWithScrollOffset?: boolean;
81
87
  }
82
88
 
83
89
  export enum SCROLLING_TYPE {
@@ -1,7 +1,12 @@
1
- import { useCallback, useEffect, useRef } from 'react';
1
+ import { RefObject, useCallback, useEffect, useRef } from 'react';
2
2
 
3
3
  import { Dimensions, View } from 'react-native';
4
- import { useAddObservableChild, useGetScrolling } from '@cleartrip/ct-design-scroll-container';
4
+ import {
5
+ ObserveOptions,
6
+ useAddObservableChild,
7
+ useGetScrollOffset,
8
+ useGetScrolling,
9
+ } from '@cleartrip/ct-design-scroll-container';
5
10
  import { useListItem } from '@cleartrip/ct-design-list';
6
11
  import { promiseHandler } from '@cleartrip/ct-design-common-utils';
7
12
 
@@ -14,12 +19,27 @@ const { width, height } = Dimensions.get('window');
14
19
  */
15
20
 
16
21
  interface VisibilityState {
17
- visibleRef: React.MutableRefObject<number>;
18
- timeoutVisibleRef: React.MutableRefObject<boolean>;
19
- timeoutVisibleStartRef: React.MutableRefObject<{ start: number; end: number }>;
20
- triggerOnceValue: React.MutableRefObject<boolean>;
22
+ visibleRef: RefObject<number>;
23
+ timeoutVisibleRef: RefObject<boolean>;
24
+ timeoutVisibleStartRef: RefObject<{ start: number; end: number }>;
25
+ triggerOnceValue: RefObject<boolean>;
21
26
  }
22
27
 
28
+ /** window coordinates + size of the element, and the scroll offset at the time it was measured */
29
+ interface CachedLayout {
30
+ pageX: number;
31
+ pageY: number;
32
+ width: number;
33
+ height: number;
34
+ scrollX: number;
35
+ scrollY: number;
36
+ }
37
+
38
+ /** allowed difference (percentage points) between measure and offset based visibility */
39
+ const VISIBILITY_MISMATCH_TOLERANCE = 10;
40
+ /** px difference treated as equal while diagnosing a mismatch */
41
+ const PIXEL_TOLERANCE = 1;
42
+
23
43
  const calculateVisibility = (
24
44
  rectTop: number,
25
45
  rectBottom: number,
@@ -66,6 +86,47 @@ const calculateVisibility = (
66
86
  return { verticalVisibility: clamp(verticalVisibility), horizontalVisibility: clamp(horizontalVisibility) };
67
87
  };
68
88
 
89
+ /**
90
+ * Visibility percentage fed to handleVisibilityChange; 0 when not visible
91
+ */
92
+ const getVisibility = (
93
+ pageX: number,
94
+ pageY: number,
95
+ rectWidth: number,
96
+ rectHeight: number,
97
+ observerBox: [number, number, number | undefined, number | undefined],
98
+ threshold: number[],
99
+ horizontal: boolean,
100
+ gestureHorizontallyScrollable: boolean,
101
+ checkFullVisibility: boolean,
102
+ ): number => {
103
+ const { verticalVisibility, horizontalVisibility } = calculateVisibility(
104
+ pageY,
105
+ pageY + rectHeight,
106
+ rectHeight,
107
+ pageX,
108
+ pageX + rectWidth,
109
+ rectWidth,
110
+ observerBox,
111
+ );
112
+
113
+ const visiblePercentage = threshold[0] * 100;
114
+ const isVisible =
115
+ horizontal || gestureHorizontallyScrollable
116
+ ? horizontalVisibility >= visiblePercentage
117
+ : verticalVisibility >= visiblePercentage;
118
+ const visibility = horizontal ? horizontalVisibility : verticalVisibility;
119
+
120
+ const totalVisibility = (horizontalVisibility * verticalVisibility) / 100;
121
+ if (totalVisibility >= visiblePercentage) {
122
+ return totalVisibility;
123
+ }
124
+ if (!checkFullVisibility && isVisible) {
125
+ return visibility;
126
+ }
127
+ return 0;
128
+ };
129
+
69
130
  const handleVisibilityChange = (
70
131
  visibility: number,
71
132
  threshold: number[],
@@ -135,7 +196,7 @@ const handleVisibilityChange = (
135
196
  }
136
197
  };
137
198
 
138
- const useInView = <T>({
199
+ export const useInViewWithLayout = <T>({
139
200
  threshold = [0.01, 0.5],
140
201
  timedThreshold = 1000,
141
202
  onChange,
@@ -148,8 +209,13 @@ const useInView = <T>({
148
209
  observeOnScroll = false,
149
210
  onUnmount,
150
211
  checkFullVisibility = false,
151
- }: IntersectionOptions): InViewHookResponse<T> => {
212
+ observeWithScrollOffset = false,
213
+ }: IntersectionOptions): { ref: InViewHookResponse<T>; onLayout: () => void } => {
152
214
  const ref = useRef<View>(null);
215
+ const layoutRef = useRef<CachedLayout | null>(null);
216
+ // observe is registered once with the scroll container, so read the latest box from a ref
217
+ const observerBoxRef = useRef(observerBox);
218
+ observerBoxRef.current = observerBox;
153
219
  const visibleRef = useRef(0);
154
220
  const timeoutVisibleRef = useRef(false);
155
221
  const timeoutVisibleStartRef = useRef({ start: 0, end: 0 });
@@ -157,9 +223,27 @@ const useInView = <T>({
157
223
 
158
224
  const { horizontal } = useGetScrolling();
159
225
  const { listId } = useListItem();
226
+ const getScrollOffset = useGetScrollOffset();
227
+
228
+ const cacheLayout = useCallback(
229
+ (pageX: number, pageY: number, layoutWidth: number, layoutHeight: number) => {
230
+ if (!getScrollOffset) {
231
+ return;
232
+ }
233
+ const { x, y } = getScrollOffset();
234
+ layoutRef.current = { pageX, pageY, width: layoutWidth, height: layoutHeight, scrollX: x, scrollY: y };
235
+ },
236
+ [getScrollOffset],
237
+ );
238
+
239
+ const handleLayout = useCallback(() => {
240
+ ref.current?.measure((_x, _y, layoutWidth, layoutHeight, pageX, pageY) => {
241
+ cacheLayout(pageX, pageY, layoutWidth, layoutHeight);
242
+ });
243
+ }, [cacheLayout]);
160
244
 
161
245
  const observerMethod = useCallback(
162
- (visibleItems?: Set<number>, isScrolling?: boolean) => {
246
+ (visibleItems?: Set<number>, isScrolling?: boolean, options?: ObserveOptions) => {
163
247
  if (!observeOnScroll && isScrolling) {
164
248
  return;
165
249
  }
@@ -182,6 +266,48 @@ const useInView = <T>({
182
266
  return;
183
267
  }
184
268
 
269
+ const visibilityOf = (x: number, y: number, w: number, h: number) =>
270
+ getVisibility(
271
+ x,
272
+ y,
273
+ w,
274
+ h,
275
+ observerBoxRef.current,
276
+ threshold,
277
+ !!horizontal,
278
+ gestureHorizontallyScrollable,
279
+ checkFullVisibility,
280
+ );
281
+
282
+ const state = {
283
+ visibleRef,
284
+ timeoutVisibleRef,
285
+ timeoutVisibleStartRef,
286
+ triggerOnceValue,
287
+ };
288
+
289
+ const callbacks = {
290
+ onChange,
291
+ onIntersectionComplete,
292
+ onTimedObservationComplete,
293
+ triggerOnce,
294
+ minimumObservationTime,
295
+ observerMethod,
296
+ };
297
+
298
+ const cachedLayout = layoutRef.current;
299
+ if (observeWithScrollOffset && cachedLayout && options?.scrollOffset && !options.refreshLayout) {
300
+ const { x, y } = options.scrollOffset;
301
+ const visibility = visibilityOf(
302
+ cachedLayout.pageX - (x - cachedLayout.scrollX),
303
+ cachedLayout.pageY - (y - cachedLayout.scrollY),
304
+ cachedLayout.width,
305
+ cachedLayout.height,
306
+ );
307
+ handleVisibilityChange(visibility, threshold, state, callbacks);
308
+ return;
309
+ }
310
+
185
311
  promiseHandler.add(() => {
186
312
  return new Promise<void>((resolve) => {
187
313
  if (!ref.current) {
@@ -189,61 +315,63 @@ const useInView = <T>({
189
315
  return;
190
316
  }
191
317
  ref.current?.measure((_x, _y, width, height, pageX, pageY) => {
192
- const { verticalVisibility, horizontalVisibility } = calculateVisibility(
193
- pageY,
194
- pageY + height,
195
- height,
196
- pageX,
197
- pageX + width,
198
- width,
199
- observerBox,
200
- );
201
-
202
- const visiblePercentage = threshold[0] * 100;
203
- const isVisible =
204
- horizontal || gestureHorizontallyScrollable
205
- ? horizontalVisibility >= visiblePercentage && verticalVisibility > 0
206
- : verticalVisibility >= visiblePercentage;
207
- const visibility = horizontal ? horizontalVisibility : verticalVisibility;
208
-
209
- const totalVisibility = (horizontalVisibility * verticalVisibility) / 100;
210
- const isTotalVisible = totalVisibility >= visiblePercentage;
211
-
212
- const state = {
213
- visibleRef,
214
- timeoutVisibleRef,
215
- timeoutVisibleStartRef,
216
- triggerOnceValue,
217
- };
218
-
219
- const callbacks = {
220
- onChange,
221
- onIntersectionComplete,
222
- onTimedObservationComplete,
223
- triggerOnce,
224
- minimumObservationTime,
225
- observerMethod,
226
- };
227
- if (isTotalVisible) {
228
- handleVisibilityChange(totalVisibility, threshold, state, callbacks);
229
- } else if (!checkFullVisibility && isVisible) {
230
- handleVisibilityChange(visibility, threshold, state, callbacks);
231
- } else {
232
- handleVisibilityChange(0, threshold, state, callbacks);
318
+ const visibility = visibilityOf(pageX, pageY, width, height);
319
+
320
+ // TODO: remove measure based logic once offset based logic is verified
321
+ const cached = layoutRef.current;
322
+ const scrollOffset = options?.scrollOffset;
323
+ if (cached && scrollOffset && !options?.refreshLayout) {
324
+ // measure resolves async, so compare against the offset at resolve time
325
+ const measuredAtOffset = getScrollOffset?.() ?? scrollOffset;
326
+ const offsetPageX = cached.pageX - (measuredAtOffset.x - cached.scrollX);
327
+ const offsetPageY = cached.pageY - (measuredAtOffset.y - cached.scrollY);
328
+ const clamp = (value: number) => Math.min(100, Math.max(0, value));
329
+ const measureVisibility = clamp(visibility);
330
+ const offsetVisibility = clamp(visibilityOf(offsetPageX, offsetPageY, cached.width, cached.height));
331
+ if (Math.abs(offsetVisibility - measureVisibility) > VISIBILITY_MISMATCH_TOLERANCE) {
332
+ const drift = { x: pageX - offsetPageX, y: pageY - offsetPageY };
333
+ const reasons: string[] = [];
334
+ if (
335
+ Math.abs(width - cached.width) > PIXEL_TOLERANCE ||
336
+ Math.abs(height - cached.height) > PIXEL_TOLERANCE
337
+ ) {
338
+ reasons.push('size changed since cache: layout not refreshed');
339
+ }
340
+ if (Math.abs(drift.x) > PIXEL_TOLERANCE || Math.abs(drift.y) > PIXEL_TOLERANCE) {
341
+ reasons.push('position drift not explained by own scroll: parent moved / nested scroll / sticky');
342
+ }
343
+ console.error('[useInView] offset vs measure visibility mismatch', {
344
+ reasons,
345
+ diff: offsetVisibility - measureVisibility,
346
+ drift,
347
+ listId,
348
+ measure: { visibility: measureVisibility, pageX, pageY, width, height },
349
+ offset: { visibility: offsetVisibility, pageX: offsetPageX, pageY: offsetPageY, measuredAtOffset },
350
+ eventScrollOffset: scrollOffset,
351
+ cached,
352
+ });
353
+ }
233
354
  }
355
+ if (options?.refreshLayout) {
356
+ cacheLayout(pageX, pageY, width, height);
357
+ }
358
+
359
+ handleVisibilityChange(visibility, threshold, state, callbacks);
234
360
  resolve();
235
361
  });
236
362
  });
237
363
  }, ref.current);
238
364
  },
239
365
  [
366
+ cacheLayout,
240
367
  checkFullVisibility,
241
368
  gestureHorizontallyScrollable,
369
+ getScrollOffset,
242
370
  horizontal,
243
371
  listId,
244
372
  minimumObservationTime,
245
373
  observeOnScroll,
246
- observerBox,
374
+ observeWithScrollOffset,
247
375
  onChange,
248
376
  onIntersectionComplete,
249
377
  onTimedObservationComplete,
@@ -271,7 +399,7 @@ const useInView = <T>({
271
399
 
272
400
  useEffect(() => {
273
401
  setTimeout(() => {
274
- observerMethod();
402
+ observerMethod(undefined, false, { refreshLayout: true });
275
403
  }, 400);
276
404
  return () => {
277
405
  handleUnmount();
@@ -279,7 +407,9 @@ const useInView = <T>({
279
407
  // eslint-disable-next-line react-hooks/exhaustive-deps
280
408
  }, [handleUnmount]);
281
409
 
282
- return ref as InViewHookResponse<T>;
410
+ return { ref: ref as InViewHookResponse<T>, onLayout: handleLayout };
283
411
  };
284
412
 
413
+ const useInView = <T>(options: IntersectionOptions): InViewHookResponse<T> => useInViewWithLayout<T>(options).ref;
414
+
285
415
  export default useInView;