@office-iss/react-native-win32 0.0.0-canary.275 → 0.0.0-canary.277

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 (30) hide show
  1. package/.flowconfig +2 -1
  2. package/CHANGELOG.json +31 -1
  3. package/CHANGELOG.md +20 -4
  4. package/Libraries/Animated/useAnimatedProps.js +2 -14
  5. package/Libraries/Components/Keyboard/KeyboardAvoidingView.js +2 -0
  6. package/Libraries/Components/View/ViewPropTypes.js +0 -3
  7. package/Libraries/Components/View/ViewPropTypes.win32.js +0 -3
  8. package/Libraries/Core/ExceptionsManager.js +6 -0
  9. package/Libraries/Core/ReactNativeVersion.js +2 -2
  10. package/Libraries/Image/Image.android.js +0 -2
  11. package/Libraries/Image/ImageViewNativeComponent.js +3 -1
  12. package/Libraries/LayoutAnimation/LayoutAnimation.js +2 -2
  13. package/Libraries/NewAppScreen/components/HermesBadge.js +1 -1
  14. package/Libraries/StyleSheet/processBackgroundImage.js +87 -110
  15. package/Libraries/TurboModule/TurboModuleRegistry.js +5 -5
  16. package/Libraries/Utilities/Appearance.js +3 -1
  17. package/jest/setup.js +5 -1
  18. package/overrides.json +3 -3
  19. package/package.json +11 -11
  20. package/src/private/featureflags/ReactNativeFeatureFlags.js +15 -15
  21. package/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +2 -2
  22. package/src/private/setup/setUpDOM.js +14 -6
  23. package/src/private/setup/setUpMutationObserver.js +5 -0
  24. package/src/private/webapis/dom/nodes/ReactNativeElement.js +48 -6
  25. package/src/private/webapis/dom/nodes/ReadOnlyNode.js +3 -1
  26. package/src/private/webapis/intersectionobserver/IntersectionObserver.js +96 -11
  27. package/src/private/webapis/intersectionobserver/IntersectionObserverEntry.js +26 -0
  28. package/src/private/webapis/intersectionobserver/IntersectionObserverManager.js +1 -0
  29. package/src/private/webapis/intersectionobserver/specs/NativeIntersectionObserver.js +1 -0
  30. package/src/private/webapis/intersectionobserver/specs/__mocks__/NativeIntersectionObserver.js +9 -0
package/.flowconfig CHANGED
@@ -107,6 +107,7 @@ flow/
107
107
 
108
108
  [options]
109
109
  enums=true
110
+ as_const=true
110
111
  casting_syntax=both
111
112
 
112
113
  emoji=true
@@ -164,4 +165,4 @@ untyped-import
164
165
  untyped-type-import
165
166
 
166
167
  [version]
167
- ^0.253.0
168
+ ^0.255.0
package/CHANGELOG.json CHANGED
@@ -2,7 +2,37 @@
2
2
  "name": "@office-iss/react-native-win32",
3
3
  "entries": [
4
4
  {
5
- "date": "Sat, 14 Dec 2024 06:28:25 GMT",
5
+ "date": "Fri, 20 Dec 2024 06:21:41 GMT",
6
+ "version": "0.0.0-canary.277",
7
+ "tag": "@office-iss/react-native-win32_v0.0.0-canary.277",
8
+ "comments": {
9
+ "prerelease": [
10
+ {
11
+ "author": "email not defined",
12
+ "package": "@office-iss/react-native-win32",
13
+ "commit": "9372e929083b78c3d8c05ee713a57537f864cdd9",
14
+ "comment": "integrate 0.78.0-nightly-20241201-91e217ff5"
15
+ }
16
+ ]
17
+ }
18
+ },
19
+ {
20
+ "date": "Wed, 18 Dec 2024 06:22:23 GMT",
21
+ "version": "0.0.0-canary.276",
22
+ "tag": "@office-iss/react-native-win32_v0.0.0-canary.276",
23
+ "comments": {
24
+ "prerelease": [
25
+ {
26
+ "author": "email not defined",
27
+ "package": "@office-iss/react-native-win32",
28
+ "commit": "d669d78450c97decf9f9f5acc09e526ddf8dd911",
29
+ "comment": "integrate RN-Nightly 0.77.0-nightly-20241125-4cffff35e"
30
+ }
31
+ ]
32
+ }
33
+ },
34
+ {
35
+ "date": "Sat, 14 Dec 2024 06:29:12 GMT",
6
36
  "version": "0.0.0-canary.275",
7
37
  "tag": "@office-iss/react-native-win32_v0.0.0-canary.275",
8
38
  "comments": {
package/CHANGELOG.md CHANGED
@@ -1,17 +1,33 @@
1
1
  # Change Log - @office-iss/react-native-win32
2
2
 
3
- <!-- This log was last generated on Sat, 14 Dec 2024 06:28:25 GMT and should not be manually modified. -->
3
+ <!-- This log was last generated on Fri, 20 Dec 2024 06:21:41 GMT and should not be manually modified. -->
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
- ## 0.0.0-canary.275
7
+ ## 0.0.0-canary.277
8
8
 
9
- Sat, 14 Dec 2024 06:28:25 GMT
9
+ Fri, 20 Dec 2024 06:21:41 GMT
10
10
 
11
11
  ### Changes
12
12
 
13
- - integrate RN Nightly 0.77.0-nightly-20241118-3986eefed (tatianakapos@microsoft.com)
13
+ - integrate 0.78.0-nightly-20241201-91e217ff5 (email not defined)
14
14
 
15
+ ## 0.0.0-canary.276
16
+
17
+ Wed, 18 Dec 2024 06:22:23 GMT
18
+
19
+ ### Changes
20
+
21
+ - integrate RN-Nightly 0.77.0-nightly-20241125-4cffff35e (email not defined)
22
+
23
+ ## 0.0.0-canary.275
24
+
25
+ Sat, 14 Dec 2024 06:29:12 GMT
26
+
27
+ ### Changes
28
+
29
+ - integrate RN Nightly 0.77.0-nightly-20241118-3986eefed (tatianakapos@microsoft.com)
30
+
15
31
  ## 0.0.0-canary.274
16
32
 
17
33
  Fri, 06 Dec 2024 06:22:27 GMT
@@ -73,8 +73,6 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
73
73
 
74
74
  const useNativePropsInFabric =
75
75
  ReactNativeFeatureFlags.shouldUseSetNativePropsInFabric();
76
- const useSetNativePropsInNativeAnimationsInFabric =
77
- ReactNativeFeatureFlags.shouldUseSetNativePropsInNativeAnimationsInFabric();
78
76
 
79
77
  const useAnimatedPropsLifecycle =
80
78
  ReactNativeFeatureFlags.useInsertionEffectsForAnimations()
@@ -119,12 +117,7 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
119
117
  if (isFabricNode) {
120
118
  // Call `scheduleUpdate` to synchronise Fiber and Shadow tree.
121
119
  // Must not be called in Paper.
122
- if (useSetNativePropsInNativeAnimationsInFabric) {
123
- // $FlowFixMe[incompatible-use]
124
- instance.setNativeProps(node.__getAnimatedValue());
125
- } else {
126
- scheduleUpdate();
127
- }
120
+ scheduleUpdate();
128
121
  }
129
122
  return;
130
123
  }
@@ -201,12 +194,7 @@ export default function useAnimatedProps<TProps: {...}, TInstance>(
201
194
  }
202
195
  };
203
196
  },
204
- [
205
- node,
206
- useNativePropsInFabric,
207
- useSetNativePropsInNativeAnimationsInFabric,
208
- props,
209
- ],
197
+ [node, useNativePropsInFabric, props],
210
198
  );
211
199
  const callbackRef = useRefEffect<TInstance>(refEffect);
212
200
 
@@ -116,6 +116,8 @@ class KeyboardAvoidingView extends React.Component<Props, State> {
116
116
  };
117
117
 
118
118
  _onLayout = async (event: ViewLayoutEvent) => {
119
+ event.persist();
120
+
119
121
  const oldFrame = this._frame;
120
122
  this._frame = event.nativeEvent.layout;
121
123
  if (!this._initialFrameHeight) {
@@ -567,9 +567,6 @@ export type ViewProps = $ReadOnly<{|
567
567
  * optimization. Set this property to `false` to disable this optimization and
568
568
  * ensure that this `View` exists in the native view hierarchy.
569
569
  *
570
- * @platform android
571
- * In Fabric, this prop is used in ios as well.
572
- *
573
570
  * See https://reactnative.dev/docs/view#collapsable
574
571
  */
575
572
  collapsable?: ?boolean,
@@ -628,9 +628,6 @@ export type ViewProps = $ReadOnly<{|
628
628
  * optimization. Set this property to `false` to disable this optimization and
629
629
  * ensure that this `View` exists in the native view hierarchy.
630
630
  *
631
- * @platform android
632
- * In Fabric, this prop is used in ios as well.
633
- *
634
631
  * See https://reactnative.dev/docs/view#collapsable
635
632
  */
636
633
  collapsable?: ?boolean,
@@ -121,6 +121,12 @@ function reportException(
121
121
  const NativeExceptionsManager =
122
122
  require('./NativeExceptionsManager').default;
123
123
  if (NativeExceptionsManager) {
124
+ if (isFatal) {
125
+ if (global.RN$hasHandledFatalException?.()) {
126
+ return;
127
+ }
128
+ global.RN$notifyOfFatalException?.();
129
+ }
124
130
  NativeExceptionsManager.reportException(data);
125
131
  }
126
132
  }
@@ -15,9 +15,9 @@ const version: $ReadOnly<{
15
15
  prerelease: string | null,
16
16
  }> = {
17
17
  major: 0,
18
- minor: 77,
18
+ minor: 78,
19
19
  patch: 0,
20
- prerelease: 'nightly-20241118-3986eefed',
20
+ prerelease: 'nightly-20241201-91e217ff5',
21
21
  };
22
22
 
23
23
  module.exports = {version};
@@ -133,7 +133,6 @@ let BaseImage: AbstractImageAndroid = React.forwardRef(
133
133
  width: undefined,
134
134
  height: undefined,
135
135
  };
136
- const defaultSource = resolveAssetSource(props.defaultSource);
137
136
  const loadingIndicatorSource = resolveAssetSource(
138
137
  props.loadingIndicatorSource,
139
138
  );
@@ -179,7 +178,6 @@ let BaseImage: AbstractImageAndroid = React.forwardRef(
179
178
  /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
180
179
  * when making Flow check .android.js files. */
181
180
  headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}),
182
- defaultSrc: defaultSource ? defaultSource.uri : null,
183
181
  loadingIndicatorSrc: loadingIndicatorSource
184
182
  ? loadingIndicatorSource.uri
185
183
  : null,
@@ -82,6 +82,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
82
82
  },
83
83
  validAttributes: {
84
84
  blurRadius: true,
85
+ defaultSource: {
86
+ process: require('./resolveAssetSource'),
87
+ },
85
88
  internal_analyticTag: true,
86
89
  resizeMethod: true,
87
90
  resizeMode: true,
@@ -100,7 +103,6 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
100
103
  borderRadius: true,
101
104
  headers: true,
102
105
  shouldNotifyLoadEvents: true,
103
- defaultSrc: true,
104
106
  overlayColor: {
105
107
  process: require('../StyleSheet/processColor').default,
106
108
  },
@@ -121,7 +121,7 @@ const Presets = {
121
121
  'opacity',
122
122
  ): LayoutAnimationConfig),
123
123
  linear: (create(500, 'linear', 'opacity'): LayoutAnimationConfig),
124
- spring: {
124
+ spring: ({
125
125
  duration: 700,
126
126
  create: {
127
127
  type: 'linear',
@@ -135,7 +135,7 @@ const Presets = {
135
135
  type: 'linear',
136
136
  property: 'opacity',
137
137
  },
138
- },
138
+ }: LayoutAnimationConfig),
139
139
  };
140
140
 
141
141
  /**
@@ -40,8 +40,8 @@ const HermesBadge = (): Node => {
40
40
  const styles = StyleSheet.create({
41
41
  badge: {
42
42
  position: 'absolute',
43
- top: 8,
44
43
  right: 12,
44
+ bottom: 8,
45
45
  },
46
46
  badgeText: {
47
47
  fontSize: 14,
@@ -14,25 +14,28 @@ import type {ProcessedColorValue} from './processColor';
14
14
  import type {GradientValue} from './StyleSheetTypes';
15
15
 
16
16
  const processColor = require('./processColor').default;
17
- const DIRECTION_REGEX =
18
- /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/;
17
+ const DIRECTION_KEYWORD_REGEX =
18
+ /^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/i;
19
19
  const ANGLE_UNIT_REGEX = /^([+-]?\d*\.?\d+)(deg|grad|rad|turn)$/i;
20
20
 
21
- const TO_BOTTOM_START_END_POINTS = {
22
- start: {x: 0.5, y: 0},
23
- end: {x: 0.5, y: 1},
24
- };
21
+ type LinearGradientDirection =
22
+ | {type: 'angle', value: number}
23
+ | {type: 'keyword', value: string};
25
24
 
26
25
  type ParsedGradientValue = {
27
26
  type: 'linearGradient',
28
- start: {x: number, y: number},
29
- end: {x: number, y: number},
27
+ direction: LinearGradientDirection,
30
28
  colorStops: $ReadOnlyArray<{
31
29
  color: ProcessedColorValue,
32
30
  position: number,
33
31
  }>,
34
32
  };
35
33
 
34
+ const DEFAULT_DIRECTION: LinearGradientDirection = {
35
+ type: 'angle',
36
+ value: 180,
37
+ };
38
+
36
39
  export default function processBackgroundImage(
37
40
  backgroundImage: ?($ReadOnlyArray<GradientValue> | string),
38
41
  ): $ReadOnlyArray<ParsedGradientValue> {
@@ -76,37 +79,43 @@ export default function processBackgroundImage(
76
79
  }
77
80
  }
78
81
 
79
- let points: {
80
- start: ParsedGradientValue['start'],
81
- end: ParsedGradientValue['end'],
82
- } | null = null;
83
-
84
- if (typeof bgImage.direction === 'undefined') {
85
- points = TO_BOTTOM_START_END_POINTS;
86
- } else if (ANGLE_UNIT_REGEX.test(bgImage.direction)) {
87
- const angle = parseAngle(bgImage.direction);
88
- if (angle != null) {
89
- points = calculateStartEndPointsFromAngle(angle);
90
- }
91
- } else if (DIRECTION_REGEX.test(bgImage.direction)) {
92
- const processedPoints = calculateStartEndPointsFromDirection(
93
- bgImage.direction,
94
- );
95
- if (processedPoints != null) {
96
- points = processedPoints;
82
+ let direction: LinearGradientDirection = DEFAULT_DIRECTION;
83
+ const bgDirection =
84
+ bgImage.direction != null ? bgImage.direction.toLowerCase() : null;
85
+
86
+ if (bgDirection != null) {
87
+ if (ANGLE_UNIT_REGEX.test(bgDirection)) {
88
+ const parsedAngle = getAngleInDegrees(bgDirection);
89
+ if (parsedAngle != null) {
90
+ direction = {
91
+ type: 'angle',
92
+ value: parsedAngle,
93
+ };
94
+ } else {
95
+ // If an angle is invalid, return an empty array and do not apply any gradient. Same as web.
96
+ return [];
97
+ }
98
+ } else if (DIRECTION_KEYWORD_REGEX.test(bgDirection)) {
99
+ const parsedDirection = getDirectionForKeyword(bgDirection);
100
+ if (parsedDirection != null) {
101
+ direction = parsedDirection;
102
+ } else {
103
+ // If a direction is invalid, return an empty array and do not apply any gradient. Same as web.
104
+ return [];
105
+ }
106
+ } else {
107
+ // If a direction is invalid, return an empty array and do not apply any gradient. Same as web.
108
+ return [];
97
109
  }
98
110
  }
99
111
 
100
112
  const fixedColorStops = getFixedColorStops(processedColorStops);
101
113
 
102
- if (points != null) {
103
- result = result.concat({
104
- type: 'linearGradient',
105
- start: points.start,
106
- end: points.end,
107
- colorStops: fixedColorStops,
108
- });
109
- }
114
+ result = result.concat({
115
+ type: 'linearGradient',
116
+ direction,
117
+ colorStops: fixedColorStops,
118
+ });
110
119
  }
111
120
  }
112
121
 
@@ -118,30 +127,39 @@ function parseCSSLinearGradient(
118
127
  ): $ReadOnlyArray<ParsedGradientValue> {
119
128
  const gradients = [];
120
129
  let match;
130
+
131
+ // matches one or more linear-gradient functions in CSS
121
132
  const linearGradientRegex = /linear-gradient\s*\(((?:\([^)]*\)|[^())])*)\)/gi;
122
133
 
123
134
  while ((match = linearGradientRegex.exec(cssString))) {
124
135
  const gradientContent = match[1];
125
136
  const parts = gradientContent.split(',');
126
- let points = TO_BOTTOM_START_END_POINTS;
137
+ let direction: LinearGradientDirection = DEFAULT_DIRECTION;
127
138
  const trimmedDirection = parts[0].trim().toLowerCase();
139
+
140
+ // matches individual color stops in a gradient function
141
+ // supports various color formats: named colors, hex colors, rgb(a), and hsl(a)
142
+ // e.g. "red 20%", "blue 50%", "rgba(0, 0, 0, 0.5) 30% 50%"
143
+ // TODO: does not support color hint syntax yet. It is WIP.
128
144
  const colorStopRegex =
129
145
  /\s*((?:(?:rgba?|hsla?)\s*\([^)]+\))|#[0-9a-fA-F]+|[a-zA-Z]+)(?:\s+(-?[0-9.]+%?)(?:\s+(-?[0-9.]+%?))?)?\s*/gi;
130
146
 
131
147
  if (ANGLE_UNIT_REGEX.test(trimmedDirection)) {
132
- const angle = parseAngle(trimmedDirection);
133
- if (angle != null) {
134
- points = calculateStartEndPointsFromAngle(angle);
148
+ const parsedAngle = getAngleInDegrees(trimmedDirection);
149
+ if (parsedAngle != null) {
150
+ direction = {
151
+ type: 'angle',
152
+ value: parsedAngle,
153
+ };
135
154
  parts.shift();
136
155
  } else {
137
156
  // If an angle is invalid, return an empty array and do not apply any gradient. Same as web.
138
157
  return [];
139
158
  }
140
- } else if (DIRECTION_REGEX.test(trimmedDirection)) {
141
- const parsedPoints =
142
- calculateStartEndPointsFromDirection(trimmedDirection);
143
- if (parsedPoints != null) {
144
- points = parsedPoints;
159
+ } else if (DIRECTION_KEYWORD_REGEX.test(trimmedDirection)) {
160
+ const parsedDirection = getDirectionForKeyword(trimmedDirection);
161
+ if (parsedDirection != null) {
162
+ direction = parsedDirection;
145
163
  parts.shift();
146
164
  } else {
147
165
  // If a direction is invalid, return an empty array and do not apply any gradient. Same as web.
@@ -198,8 +216,7 @@ function parseCSSLinearGradient(
198
216
 
199
217
  gradients.push({
200
218
  type: 'linearGradient',
201
- start: points.start,
202
- end: points.end,
219
+ direction,
203
220
  colorStops: fixedColorStops,
204
221
  });
205
222
  }
@@ -207,83 +224,43 @@ function parseCSSLinearGradient(
207
224
  return gradients;
208
225
  }
209
226
 
210
- function calculateStartEndPointsFromDirection(direction: string): ?{
211
- start: {x: number, y: number},
212
- end: {x: number, y: number},
213
- } {
227
+ function getDirectionForKeyword(direction?: string): ?LinearGradientDirection {
228
+ if (direction == null) {
229
+ return null;
230
+ }
214
231
  // Remove extra whitespace
215
- const normalizedDirection = direction.replace(/\s+/g, ' ');
232
+ const normalized = direction.replace(/\s+/g, ' ').toLowerCase();
216
233
 
217
- switch (normalizedDirection) {
234
+ switch (normalized) {
235
+ case 'to top':
236
+ return {type: 'angle', value: 0};
218
237
  case 'to right':
219
- return {
220
- start: {x: 0, y: 0.5},
221
- end: {x: 1, y: 0.5},
222
- };
223
- case 'to left':
224
- return {
225
- start: {x: 1, y: 0.5},
226
- end: {x: 0, y: 0.5},
227
- };
238
+ return {type: 'angle', value: 90};
228
239
  case 'to bottom':
229
- return TO_BOTTOM_START_END_POINTS;
230
- case 'to top':
231
- return {
232
- start: {x: 0.5, y: 1},
233
- end: {x: 0.5, y: 0},
234
- };
240
+ return {type: 'angle', value: 180};
241
+ case 'to left':
242
+ return {type: 'angle', value: 270};
243
+ case 'to top right':
244
+ case 'to right top':
245
+ return {type: 'keyword', value: 'to top right'};
235
246
  case 'to bottom right':
236
247
  case 'to right bottom':
237
- return {
238
- start: {x: 0, y: 0},
239
- end: {x: 1, y: 1},
240
- };
248
+ return {type: 'keyword', value: 'to bottom right'};
241
249
  case 'to top left':
242
250
  case 'to left top':
243
- return {
244
- start: {x: 1, y: 1},
245
- end: {x: 0, y: 0},
246
- };
251
+ return {type: 'keyword', value: 'to top left'};
247
252
  case 'to bottom left':
248
253
  case 'to left bottom':
249
- return {
250
- start: {x: 1, y: 0},
251
- end: {x: 0, y: 1},
252
- };
253
- case 'to top right':
254
- case 'to right top':
255
- return {
256
- start: {x: 0, y: 1},
257
- end: {x: 1, y: 0},
258
- };
254
+ return {type: 'keyword', value: 'to bottom left'};
259
255
  default:
260
256
  return null;
261
257
  }
262
258
  }
263
259
 
264
- function calculateStartEndPointsFromAngle(angleRadians: number): {
265
- start: {x: number, y: number},
266
- end: {x: number, y: number},
267
- } {
268
- // Normalize angle to be between 0 and 2π
269
- let angleRadiansNormalized = angleRadians % (2 * Math.PI);
270
- if (angleRadiansNormalized < 0) {
271
- angleRadiansNormalized += 2 * Math.PI;
260
+ function getAngleInDegrees(angle?: string): ?number {
261
+ if (angle == null) {
262
+ return null;
272
263
  }
273
-
274
- const endX = 0.5 + 0.5 * Math.sin(angleRadiansNormalized);
275
- const endY = 0.5 - 0.5 * Math.cos(angleRadiansNormalized);
276
-
277
- const startX = 1 - endX;
278
- const startY = 1 - endY;
279
-
280
- return {
281
- start: {x: startX, y: startY},
282
- end: {x: endX, y: endY},
283
- };
284
- }
285
-
286
- function parseAngle(angle: string): ?number {
287
264
  const match = angle.match(ANGLE_UNIT_REGEX);
288
265
  if (!match) {
289
266
  return null;
@@ -294,13 +271,13 @@ function parseAngle(angle: string): ?number {
294
271
  const numericValue = parseFloat(value);
295
272
  switch (unit) {
296
273
  case 'deg':
297
- return (numericValue * Math.PI) / 180;
274
+ return numericValue;
298
275
  case 'grad':
299
- return (numericValue * Math.PI) / 200;
276
+ return numericValue * 0.9; // 1 grad = 0.9 degrees
300
277
  case 'rad':
301
- return numericValue;
278
+ return (numericValue * 180) / Math.PI;
302
279
  case 'turn':
303
- return numericValue * 2 * Math.PI;
280
+ return numericValue * 360; // 1 turn = 360 degrees
304
281
  default:
305
282
  return null;
306
283
  }
@@ -16,9 +16,6 @@ const NativeModules = require('../BatchedBridge/NativeModules');
16
16
 
17
17
  const turboModuleProxy = global.__turboModuleProxy;
18
18
 
19
- const useLegacyNativeModuleInterop =
20
- global.RN$Bridgeless !== true || global.RN$TurboInterop === true;
21
-
22
19
  function requireModule<T: TurboModule>(name: string): ?T {
23
20
  if (turboModuleProxy != null) {
24
21
  const module: ?T = turboModuleProxy(name);
@@ -27,8 +24,11 @@ function requireModule<T: TurboModule>(name: string): ?T {
27
24
  }
28
25
  }
29
26
 
30
- if (useLegacyNativeModuleInterop) {
31
- // Backward compatibility layer during migration.
27
+ if (
28
+ global.RN$Bridgeless !== true ||
29
+ global.RN$TurboInterop === true ||
30
+ global.RN$UnifiedNativeModuleProxy === true
31
+ ) {
32
32
  const legacyModule: ?T = NativeModules[name];
33
33
  if (legacyModule != null) {
34
34
  return legacyModule;
@@ -105,7 +105,9 @@ export function setColorScheme(colorScheme: ?ColorSchemeName): void {
105
105
  const {NativeAppearance} = state;
106
106
  if (NativeAppearance != null) {
107
107
  NativeAppearance.setColorScheme(colorScheme ?? 'unspecified');
108
- state.appearance = {colorScheme};
108
+ state.appearance = {
109
+ colorScheme: toColorScheme(NativeAppearance.getColorScheme()),
110
+ };
109
111
  }
110
112
  }
111
113
 
package/jest/setup.js CHANGED
@@ -448,4 +448,8 @@ jest
448
448
  return jest.requireActual(
449
449
  '../Libraries/ReactNative/RendererImplementation',
450
450
  );
451
- });
451
+ })
452
+ .mock('../Libraries/Utilities/useColorScheme', () => ({
453
+ __esModule: true,
454
+ default: jest.fn().mockReturnValue('light'),
455
+ }));
package/overrides.json CHANGED
@@ -7,13 +7,13 @@
7
7
  "**/__snapshots__/**",
8
8
  "src-win/rntypes/**"
9
9
  ],
10
- "baseVersion": "0.77.0-nightly-20241118-3986eefed",
10
+ "baseVersion": "0.78.0-nightly-20241201-91e217ff5",
11
11
  "overrides": [
12
12
  {
13
13
  "type": "derived",
14
14
  "file": ".flowconfig",
15
15
  "baseFile": ".flowconfig",
16
- "baseHash": "afa414432bb8dae6ce606a148b74f85fcef8a59d"
16
+ "baseHash": "38943f268bd48c91a2ace6ad2a9302a3bb346dbd"
17
17
  },
18
18
  {
19
19
  "type": "derived",
@@ -197,7 +197,7 @@
197
197
  "type": "patch",
198
198
  "file": "src-win/Libraries/Components/View/ViewPropTypes.win32.js",
199
199
  "baseFile": "packages/react-native/Libraries/Components/View/ViewPropTypes.js",
200
- "baseHash": "a742b26e4c96fdefb07779e40bc58cd1cc872675",
200
+ "baseHash": "994aead3d5ab49e6bd34a497094a4bc131a8bdb1",
201
201
  "issue": 6240
202
202
  },
203
203
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-iss/react-native-win32",
3
- "version": "0.0.0-canary.275",
3
+ "version": "0.0.0-canary.277",
4
4
  "description": "Implementation of react native on top of Office's Win32 platform.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,19 +30,19 @@
30
30
  "@react-native-community/cli-platform-android": "15.0.0-alpha.2",
31
31
  "@react-native-community/cli-platform-ios": "15.0.0-alpha.2",
32
32
  "@react-native/assets": "1.0.0",
33
- "@react-native/assets-registry": "0.77.0-nightly-20241118-3986eefed",
34
- "@react-native/codegen": "0.77.0-nightly-20241118-3986eefed",
35
- "@react-native/community-cli-plugin": "0.77.0-nightly-20241118-3986eefed",
36
- "@react-native/gradle-plugin": "0.77.0-nightly-20241118-3986eefed",
37
- "@react-native/js-polyfills": "0.77.0-nightly-20241118-3986eefed",
38
- "@react-native/normalize-colors": "0.77.0-nightly-20241118-3986eefed",
39
- "@react-native/virtualized-lists": "0.77.0-nightly-20241118-3986eefed",
33
+ "@react-native/assets-registry": "0.78.0-nightly-20241201-91e217ff5",
34
+ "@react-native/codegen": "0.78.0-nightly-20241201-91e217ff5",
35
+ "@react-native/community-cli-plugin": "0.78.0-nightly-20241201-91e217ff5",
36
+ "@react-native/gradle-plugin": "0.78.0-nightly-20241201-91e217ff5",
37
+ "@react-native/js-polyfills": "0.78.0-nightly-20241201-91e217ff5",
38
+ "@react-native/normalize-colors": "0.78.0-nightly-20241201-91e217ff5",
39
+ "@react-native/virtualized-lists": "0.78.0-nightly-20241201-91e217ff5",
40
40
  "abort-controller": "^3.0.0",
41
41
  "anser": "^1.4.9",
42
42
  "ansi-regex": "^5.0.0",
43
43
  "art": "^0.10.0",
44
44
  "babel-jest": "^29.7.0",
45
- "babel-plugin-syntax-hermes-parser": "0.24.0",
45
+ "babel-plugin-syntax-hermes-parser": "0.25.1",
46
46
  "base64-js": "^1.5.1",
47
47
  "chalk": "^4.0.0",
48
48
  "commander": "^12.0.0",
@@ -90,14 +90,14 @@
90
90
  "just-scripts": "^1.3.3",
91
91
  "prettier": "2.8.8",
92
92
  "react": "18.3.1",
93
- "react-native": "0.77.0-nightly-20241118-3986eefed",
93
+ "react-native": "0.78.0-nightly-20241201-91e217ff5",
94
94
  "react-native-platform-override": "^1.9.49",
95
95
  "typescript": "5.0.4"
96
96
  },
97
97
  "peerDependencies": {
98
98
  "@types/react": "^18.2.6",
99
99
  "react": "^18.2.0",
100
- "react-native": "0.77.0-nightly-20241118-3986eefed"
100
+ "react-native": "0.78.0-nightly-20241201-91e217ff5"
101
101
  },
102
102
  "beachball": {
103
103
  "defaultNpmTag": "canary",
@@ -4,7 +4,7 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @generated SignedSource<<8334fdc6d34d4a090eb8be73f92ccd0f>>
7
+ * @generated SignedSource<<38ad29621eeb29a9f82735dc187c13d4>>
8
8
  * @flow strict
9
9
  */
10
10
 
@@ -31,17 +31,17 @@ export type ReactNativeFeatureFlagsJsOnly = {
31
31
  animatedShouldDebounceQueueFlush: Getter<boolean>,
32
32
  animatedShouldUseSingleOp: Getter<boolean>,
33
33
  disableInteractionManager: Getter<boolean>,
34
+ disableInteractionManagerInBatchinator: Getter<boolean>,
34
35
  enableAccessToHostTreeInFabric: Getter<boolean>,
35
36
  enableAnimatedAllowlist: Getter<boolean>,
36
37
  enableAnimatedClearImmediateFix: Getter<boolean>,
37
38
  enableAnimatedPropsMemo: Getter<boolean>,
38
- enableOptimisedVirtualizedCells: Getter<boolean>,
39
+ fixVirtualizeListCollapseWindowSize: Getter<boolean>,
39
40
  isLayoutAnimationEnabled: Getter<boolean>,
40
41
  shouldSkipStateUpdatesForLoopingAnimations: Getter<boolean>,
41
42
  shouldUseAnimatedObjectForTransform: Getter<boolean>,
42
43
  shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean>,
43
44
  shouldUseSetNativePropsInFabric: Getter<boolean>,
44
- shouldUseSetNativePropsInNativeAnimationsInFabric: Getter<boolean>,
45
45
  useInsertionEffectsForAnimations: Getter<boolean>,
46
46
  useRefsForTextInputState: Getter<boolean>,
47
47
  };
@@ -68,6 +68,7 @@ export type ReactNativeFeatureFlags = {
68
68
  enableFixForViewCommandRace: Getter<boolean>,
69
69
  enableGranularShadowTreeStateReconciliation: Getter<boolean>,
70
70
  enableIOSViewClipToPaddingBox: Getter<boolean>,
71
+ enableImagePrefetchingAndroid: Getter<boolean>,
71
72
  enableLayoutAnimationsOnAndroid: Getter<boolean>,
72
73
  enableLayoutAnimationsOnIOS: Getter<boolean>,
73
74
  enableLongTaskAPI: Getter<boolean>,
@@ -86,7 +87,6 @@ export type ReactNativeFeatureFlags = {
86
87
  initEagerTurboModulesOnNativeModulesQueueAndroid: Getter<boolean>,
87
88
  lazyAnimationCallbacks: Getter<boolean>,
88
89
  loadVectorDrawablesOnImages: Getter<boolean>,
89
- setAndroidLayoutDirection: Getter<boolean>,
90
90
  traceTurboModulePromiseRejectionsOnAndroid: Getter<boolean>,
91
91
  useAlwaysAvailableJSErrorHandling: Getter<boolean>,
92
92
  useFabricInterop: Getter<boolean>,
@@ -119,6 +119,11 @@ export const animatedShouldUseSingleOp: Getter<boolean> = createJavaScriptFlagGe
119
119
  */
120
120
  export const disableInteractionManager: Getter<boolean> = createJavaScriptFlagGetter('disableInteractionManager', false);
121
121
 
122
+ /**
123
+ * Skips InteractionManager in `Batchinator` and invokes callbacks synchronously.
124
+ */
125
+ export const disableInteractionManagerInBatchinator: Getter<boolean> = createJavaScriptFlagGetter('disableInteractionManagerInBatchinator', false);
126
+
122
127
  /**
123
128
  * Enables access to the host tree in Fabric using DOM-compatible APIs.
124
129
  */
@@ -140,9 +145,9 @@ export const enableAnimatedClearImmediateFix: Getter<boolean> = createJavaScript
140
145
  export const enableAnimatedPropsMemo: Getter<boolean> = createJavaScriptFlagGetter('enableAnimatedPropsMemo', true);
141
146
 
142
147
  /**
143
- * Removing unnecessary rerenders Virtualized cells after any rerenders of Virualized list. Works with strict=true option
148
+ * Fixing an edge case where the current window size is not properly calculated with fast scrolling. Window size collapsed to 1 element even if windowSize more than the current amount of elements
144
149
  */
145
- export const enableOptimisedVirtualizedCells: Getter<boolean> = createJavaScriptFlagGetter('enableOptimisedVirtualizedCells', false);
150
+ export const fixVirtualizeListCollapseWindowSize: Getter<boolean> = createJavaScriptFlagGetter('fixVirtualizeListCollapseWindowSize', false);
146
151
 
147
152
  /**
148
153
  * Function used to enable / disabled Layout Animations in React Native.
@@ -169,11 +174,6 @@ export const shouldUseRemoveClippedSubviewsAsDefaultOnIOS: Getter<boolean> = cre
169
174
  */
170
175
  export const shouldUseSetNativePropsInFabric: Getter<boolean> = createJavaScriptFlagGetter('shouldUseSetNativePropsInFabric', true);
171
176
 
172
- /**
173
- * Enables use of setNativeProps in Native driven animations in Fabric.
174
- */
175
- export const shouldUseSetNativePropsInNativeAnimationsInFabric: Getter<boolean> = createJavaScriptFlagGetter('shouldUseSetNativePropsInNativeAnimationsInFabric', false);
176
-
177
177
  /**
178
178
  * Changes construction of the animation graph to `useInsertionEffect` instead of `useLayoutEffect`.
179
179
  */
@@ -256,6 +256,10 @@ export const enableGranularShadowTreeStateReconciliation: Getter<boolean> = crea
256
256
  * iOS Views will clip to their padding box vs border box
257
257
  */
258
258
  export const enableIOSViewClipToPaddingBox: Getter<boolean> = createNativeFlagGetter('enableIOSViewClipToPaddingBox', false);
259
+ /**
260
+ * When enabled, Andoid will build and initiate image prefetch requests on ImageShadowNode::layout
261
+ */
262
+ export const enableImagePrefetchingAndroid: Getter<boolean> = createNativeFlagGetter('enableImagePrefetchingAndroid', false);
259
263
  /**
260
264
  * When enabled, LayoutAnimations API will animate state changes on Android.
261
265
  */
@@ -328,10 +332,6 @@ export const lazyAnimationCallbacks: Getter<boolean> = createNativeFlagGetter('l
328
332
  * Adds support for loading vector drawable assets in the Image component (only on Android)
329
333
  */
330
334
  export const loadVectorDrawablesOnImages: Getter<boolean> = createNativeFlagGetter('loadVectorDrawablesOnImages', false);
331
- /**
332
- * Propagate layout direction to Android views.
333
- */
334
- export const setAndroidLayoutDirection: Getter<boolean> = createNativeFlagGetter('setAndroidLayoutDirection', true);
335
335
  /**
336
336
  * Enables storing js caller stack when creating promise in native module. This is useful in case of Promise rejection and tracing the cause.
337
337
  */
@@ -4,7 +4,7 @@
4
4
  * This source code is licensed under the MIT license found in the
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  *
7
- * @generated SignedSource<<ced421dd8531e9a074999a1cdb5e4a16>>
7
+ * @generated SignedSource<<17c0e677f7b308795e836c558b15d1d1>>
8
8
  * @flow strict
9
9
  */
10
10
 
@@ -41,6 +41,7 @@ export interface Spec extends TurboModule {
41
41
  +enableFixForViewCommandRace?: () => boolean;
42
42
  +enableGranularShadowTreeStateReconciliation?: () => boolean;
43
43
  +enableIOSViewClipToPaddingBox?: () => boolean;
44
+ +enableImagePrefetchingAndroid?: () => boolean;
44
45
  +enableLayoutAnimationsOnAndroid?: () => boolean;
45
46
  +enableLayoutAnimationsOnIOS?: () => boolean;
46
47
  +enableLongTaskAPI?: () => boolean;
@@ -59,7 +60,6 @@ export interface Spec extends TurboModule {
59
60
  +initEagerTurboModulesOnNativeModulesQueueAndroid?: () => boolean;
60
61
  +lazyAnimationCallbacks?: () => boolean;
61
62
  +loadVectorDrawablesOnImages?: () => boolean;
62
- +setAndroidLayoutDirection?: () => boolean;
63
63
  +traceTurboModulePromiseRejectionsOnAndroid?: () => boolean;
64
64
  +useAlwaysAvailableJSErrorHandling?: () => boolean;
65
65
  +useFabricInterop?: () => boolean;
@@ -8,8 +8,7 @@
8
8
  * @format
9
9
  */
10
10
 
11
- import DOMRect from '../webapis/dom/geometry/DOMRect';
12
- import DOMRectReadOnly from '../webapis/dom/geometry/DOMRectReadOnly';
11
+ import {polyfillGlobal} from '../../../Libraries/Utilities/PolyfillFunctions';
13
12
 
14
13
  let initialized = false;
15
14
 
@@ -20,9 +19,18 @@ export default function setUpDOM() {
20
19
 
21
20
  initialized = true;
22
21
 
23
- // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it
24
- global.DOMRect = DOMRect;
22
+ polyfillGlobal(
23
+ 'DOMRect',
24
+ () => require('../webapis/dom/geometry/DOMRect').default,
25
+ );
25
26
 
26
- // $FlowExpectedError[cannot-write] The global isn't writable anywhere but here, where we define it
27
- global.DOMRectReadOnly = DOMRectReadOnly;
27
+ polyfillGlobal(
28
+ 'DOMRectReadOnly',
29
+ () => require('../webapis/dom/geometry/DOMRectReadOnly').default,
30
+ );
31
+
32
+ polyfillGlobal(
33
+ 'NodeList',
34
+ () => require('../webapis/dom/oldstylecollections/NodeList').default,
35
+ );
28
36
  }
@@ -23,4 +23,9 @@ export default function setUpMutationObserver() {
23
23
  'MutationObserver',
24
24
  () => require('../webapis/mutationobserver/MutationObserver').default,
25
25
  );
26
+
27
+ polyfillGlobal(
28
+ 'MutationRecord',
29
+ () => require('../webapis/mutationobserver/MutationRecord').default,
30
+ );
26
31
  }
@@ -25,7 +25,7 @@ import {getFabricUIManager} from '../../../../../Libraries/ReactNative/FabricUIM
25
25
  import {create as createAttributePayload} from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload';
26
26
  import warnForStyleProps from '../../../../../Libraries/ReactNative/ReactFabricPublicInstance/warnForStyleProps';
27
27
  import ReadOnlyElement, {getBoundingClientRect} from './ReadOnlyElement';
28
- import ReadOnlyNode from './ReadOnlyNode';
28
+ import ReadOnlyNode, {setInstanceHandle} from './ReadOnlyNode';
29
29
  import {
30
30
  getPublicInstanceFromInternalInstanceHandle,
31
31
  getShadowNode,
@@ -35,7 +35,26 @@ import nullthrows from 'nullthrows';
35
35
 
36
36
  const noop = () => {};
37
37
 
38
- export default class ReactNativeElement
38
+ // Ideally, this class would be exported as-is, but this implementation is
39
+ // significantly slower than the existing `ReactFabricHostComponent`.
40
+ // This is a very hot code path (this class is instantiated once per rendered
41
+ // host component in the tree) and we can't regress performance here.
42
+ //
43
+ // This implementation is slower because this is a subclass and we have to call
44
+ // super(), which is a very slow operation the way that Babel transforms it at
45
+ // the moment.
46
+ //
47
+ // The optimization we're doing is using an old-style function constructor,
48
+ // where we're not required to use `super()`, and we make that constructor
49
+ // extend this class so it inherits all the methods and it sets the class
50
+ // hierarchy correctly.
51
+ //
52
+ // An alternative implementation was to implement the constructor as a function
53
+ // returning a manually constructed instance using `Object.create()` but that
54
+ // was slower than this method because the engine has to create an object than
55
+ // we then discard to create a new one.
56
+
57
+ class ReactNativeElementMethods
39
58
  extends ReadOnlyElement
40
59
  implements INativeMethods
41
60
  {
@@ -43,8 +62,10 @@ export default class ReactNativeElement
43
62
  __nativeTag: number;
44
63
  __internalInstanceHandle: InternalInstanceHandle;
45
64
 
46
- #viewConfig: ViewConfig;
65
+ __viewConfig: ViewConfig;
47
66
 
67
+ // This constructor isn't really used. See the `ReactNativeElement` function
68
+ // below.
48
69
  constructor(
49
70
  tag: number,
50
71
  viewConfig: ViewConfig,
@@ -54,7 +75,7 @@ export default class ReactNativeElement
54
75
 
55
76
  this.__nativeTag = tag;
56
77
  this.__internalInstanceHandle = internalInstanceHandle;
57
- this.#viewConfig = viewConfig;
78
+ this.__viewConfig = viewConfig;
58
79
  }
59
80
 
60
81
  get offsetHeight(): number {
@@ -171,12 +192,12 @@ export default class ReactNativeElement
171
192
 
172
193
  setNativeProps(nativeProps: {...}): void {
173
194
  if (__DEV__) {
174
- warnForStyleProps(nativeProps, this.#viewConfig.validAttributes);
195
+ warnForStyleProps(nativeProps, this.__viewConfig.validAttributes);
175
196
  }
176
197
 
177
198
  const updatePayload = createAttributePayload(
178
199
  nativeProps,
179
- this.#viewConfig.validAttributes,
200
+ this.__viewConfig.validAttributes,
180
201
  );
181
202
 
182
203
  const node = getShadowNode(this);
@@ -186,3 +207,24 @@ export default class ReactNativeElement
186
207
  }
187
208
  }
188
209
  }
210
+
211
+ // Alternative constructor just implemented to provide a better performance than
212
+ // calling super() in the original class.
213
+ function ReactNativeElement(
214
+ this: ReactNativeElementMethods,
215
+ tag: number,
216
+ viewConfig: ViewConfig,
217
+ internalInstanceHandle: InternalInstanceHandle,
218
+ ) {
219
+ this.__nativeTag = tag;
220
+ this.__internalInstanceHandle = internalInstanceHandle;
221
+ this.__viewConfig = viewConfig;
222
+ setInstanceHandle(this, internalInstanceHandle);
223
+ }
224
+
225
+ ReactNativeElement.prototype = Object.create(
226
+ ReactNativeElementMethods.prototype,
227
+ );
228
+
229
+ // $FlowExpectedError[prop-missing]
230
+ export default ReactNativeElement as typeof ReactNativeElementMethods;
@@ -26,6 +26,8 @@ let ReadOnlyElementClass: Class<ReadOnlyElement>;
26
26
 
27
27
  export default class ReadOnlyNode {
28
28
  constructor(internalInstanceHandle: InternalInstanceHandle) {
29
+ // This constructor is inlined in `ReactNativeElement` so if you modify
30
+ // this make sure that their implementation stays in sync.
29
31
  setInstanceHandle(this, internalInstanceHandle);
30
32
  }
31
33
 
@@ -293,7 +295,7 @@ export function getInstanceHandle(node: ReadOnlyNode): InternalInstanceHandle {
293
295
  return node[INSTANCE_HANDLE_KEY];
294
296
  }
295
297
 
296
- function setInstanceHandle(
298
+ export function setInstanceHandle(
297
299
  node: ReadOnlyNode,
298
300
  instanceHandle: InternalInstanceHandle,
299
301
  ): void {
@@ -25,6 +25,18 @@ type IntersectionObserverInit = {
25
25
  // root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native.
26
26
  // rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native.
27
27
  threshold?: number | $ReadOnlyArray<number>,
28
+
29
+ /**
30
+ * This is a React Native specific option (not spec compliant) that specifies
31
+ * ratio threshold(s) of the intersection area to the total `root` area.
32
+ *
33
+ * If set, it will either be a singular ratio value between 0-1 (inclusive)
34
+ * or an array of such ratios.
35
+ *
36
+ * Note: If `rn_rootThreshold` is set, and `threshold` is not set,
37
+ * `threshold` will not default to [0] (as per spec)
38
+ */
39
+ rn_rootThreshold?: number | $ReadOnlyArray<number>,
28
40
  };
29
41
 
30
42
  /**
@@ -44,13 +56,16 @@ type IntersectionObserverInit = {
44
56
  * elements with the same observer.
45
57
  *
46
58
  * This implementation only supports the `threshold` option at the moment
47
- * (`root` and `rootMargin` are not supported).
59
+ * (`root` and `rootMargin` are not supported) and provides a React Native specific
60
+ * option `rn_rootThreshold`.
61
+ *
48
62
  */
49
63
  export default class IntersectionObserver {
50
64
  _callback: IntersectionObserverCallback;
51
65
  _thresholds: $ReadOnlyArray<number>;
52
66
  _observationTargets: Set<ReactNativeElement> = new Set();
53
67
  _intersectionObserverId: ?IntersectionObserverId;
68
+ _rootThresholds: $ReadOnlyArray<number> | null;
54
69
 
55
70
  constructor(
56
71
  callback: IntersectionObserverCallback,
@@ -83,7 +98,12 @@ export default class IntersectionObserver {
83
98
  }
84
99
 
85
100
  this._callback = callback;
86
- this._thresholds = normalizeThresholds(options?.threshold);
101
+
102
+ this._rootThresholds = normalizeRootThreshold(options?.rn_rootThreshold);
103
+ this._thresholds = normalizeThreshold(
104
+ options?.threshold,
105
+ this._rootThresholds != null, // only provide default if no rootThreshold
106
+ );
87
107
  }
88
108
 
89
109
  /**
@@ -115,14 +135,27 @@ export default class IntersectionObserver {
115
135
  * A list of thresholds, sorted in increasing numeric order, where each
116
136
  * threshold is a ratio of intersection area to bounding box area of an
117
137
  * observed target.
118
- * Notifications for a target are generated when any of the thresholds are
119
- * crossed for that target.
120
- * If no value was passed to the constructor, `0` is used.
138
+ * Notifications for a target are generated when any of the thresholds specified
139
+ * in `rn_rootThreshold` or `threshold` are crossed for that target.
140
+ *
141
+ * If no value was passed to the constructor, and no `rn_rootThreshold`
142
+ * is set, `0` is used.
121
143
  */
122
144
  get thresholds(): $ReadOnlyArray<number> {
123
145
  return this._thresholds;
124
146
  }
125
147
 
148
+ /**
149
+ * A list of root thresholds, sorted in increasing numeric order, where each
150
+ * threshold is a ratio of intersection area to bounding box area of the specified
151
+ * root view, which defaults to the viewport.
152
+ * Notifications for a target are generated when any of the thresholds specified
153
+ * in `rn_rootThreshold` or `threshold` are crossed for that target.
154
+ */
155
+ get rootThresholds(): $ReadOnlyArray<number> | null {
156
+ return this._rootThresholds;
157
+ }
158
+
126
159
  /**
127
160
  * Adds an element to the set of target elements being watched by the
128
161
  * `IntersectionObserver`.
@@ -221,32 +254,84 @@ export default class IntersectionObserver {
221
254
  * Converts the user defined `threshold` value into an array of sorted valid
222
255
  * threshold options for `IntersectionObserver` (double ∈ [0, 1]).
223
256
  *
257
+ * If `defaultEmpty` is true, then defaults to empty array, otherwise [0].
258
+ *
224
259
  * @example
225
260
  * normalizeThresholds(0.5); // → [0.5]
226
261
  * normalizeThresholds([1, 0.5, 0]); // → [0, 0.5, 1]
227
262
  * normalizeThresholds(['1', '0.5', '0']); // → [0, 0.5, 1]
263
+ * normalizeThresholds(null); // → [0]
264
+ * normalizeThresholds([null, null]); // → [0, 0]
265
+ *
266
+ * normalizeThresholds([null], true); // → [0]
267
+ * normalizeThresholds(null, true); // → []
268
+ * normalizeThresholds([], true); // → []
228
269
  */
229
- function normalizeThresholds(threshold: mixed): $ReadOnlyArray<number> {
270
+ function normalizeThreshold(
271
+ threshold: mixed,
272
+ defaultEmpty: boolean = false,
273
+ ): $ReadOnlyArray<number> {
230
274
  if (Array.isArray(threshold)) {
231
275
  if (threshold.length > 0) {
232
- return threshold.map(normalizeThresholdValue).sort();
276
+ return threshold
277
+ .map(t => normalizeThresholdValue(t, 'threshold'))
278
+ .map(t => t ?? 0)
279
+ .sort();
280
+ } else if (defaultEmpty) {
281
+ return [];
233
282
  } else {
234
283
  return [0];
235
284
  }
236
285
  }
237
286
 
238
- return [normalizeThresholdValue(threshold)];
287
+ const normalized = normalizeThresholdValue(threshold, 'threshold');
288
+ if (normalized == null) {
289
+ return defaultEmpty ? [] : [0];
290
+ }
291
+
292
+ return [normalized];
293
+ }
294
+
295
+ /**
296
+ * Converts the user defined `rn_rootThreshold` value into an array of sorted valid
297
+ * threshold options for `IntersectionObserver` (double ∈ [0, 1]).
298
+ *
299
+ * If invalid array or null, returns null.
300
+ *
301
+ * @example
302
+ * normalizeRootThreshold(0.5); // → [0.5]
303
+ * normalizeRootThresholds([1, 0.5, 0]); // → [0, 0.5, 1]
304
+ * normalizeRootThresholds([null, '0.5', '0']); // → [0, 0.5]
305
+ * normalizeRootThresholds(null); // → null
306
+ * normalizeRootThresholds([null, null]); // → null
307
+ */
308
+ function normalizeRootThreshold(
309
+ rootThreshold: mixed,
310
+ ): null | $ReadOnlyArray<number> {
311
+ if (Array.isArray(rootThreshold)) {
312
+ const normalizedArr = rootThreshold
313
+ .map(rt => normalizeThresholdValue(rt, 'rn_rootThreshold'))
314
+ .filter((rt): rt is number => rt != null)
315
+ .sort();
316
+ return normalizedArr.length === 0 ? null : normalizedArr;
317
+ }
318
+
319
+ const normalized = normalizeThresholdValue(rootThreshold, 'rn_rootThreshold');
320
+ return normalized == null ? null : [normalized];
239
321
  }
240
322
 
241
- function normalizeThresholdValue(threshold: mixed): number {
323
+ function normalizeThresholdValue(
324
+ threshold: mixed,
325
+ property: string,
326
+ ): null | number {
242
327
  if (threshold == null) {
243
- return 0;
328
+ return null;
244
329
  }
245
330
 
246
331
  const thresholdAsNumber = Number(threshold);
247
332
  if (!Number.isFinite(thresholdAsNumber)) {
248
333
  throw new TypeError(
249
- "Failed to read the 'threshold' property from 'IntersectionObserverInit': The provided double value is non-finite.",
334
+ `Failed to read the '${property}' property from 'IntersectionObserverInit': The provided double value is non-finite.`,
250
335
  );
251
336
  }
252
337
 
@@ -74,6 +74,32 @@ export default class IntersectionObserverEntry {
74
74
  return Math.min(ratio, 1);
75
75
  }
76
76
 
77
+ /**
78
+ * Returns the ratio of the `intersectionRect` to the `boundingRootRect`.
79
+ */
80
+ get rn_intersectionRootRatio(): number {
81
+ const intersectionRect = this.intersectionRect;
82
+
83
+ const rootRect = this._nativeEntry.rootRect;
84
+ const boundingRootRect = new DOMRectReadOnly(
85
+ rootRect[0],
86
+ rootRect[1],
87
+ rootRect[2],
88
+ rootRect[3],
89
+ );
90
+
91
+ if (boundingRootRect.width === 0 || boundingRootRect.height === 0) {
92
+ return 0;
93
+ }
94
+
95
+ const ratio =
96
+ (intersectionRect.width * intersectionRect.height) /
97
+ (boundingRootRect.width * boundingRootRect.height);
98
+
99
+ // Prevent rounding errors from making this value greater than 1.
100
+ return Math.min(ratio, 1);
101
+ }
102
+
77
103
  /**
78
104
  * Returns a `DOMRectReadOnly` representing the target's visible area.
79
105
  */
@@ -162,6 +162,7 @@ export function observe({
162
162
  intersectionObserverId,
163
163
  targetShadowNode,
164
164
  thresholds: registeredObserver.observer.thresholds,
165
+ rootThresholds: registeredObserver.observer.rootThresholds,
165
166
  });
166
167
 
167
168
  return true;
@@ -26,6 +26,7 @@ export type NativeIntersectionObserverObserveOptions = {
26
26
  intersectionObserverId: number,
27
27
  targetShadowNode: mixed,
28
28
  thresholds: $ReadOnlyArray<number>,
29
+ rootThresholds?: ?$ReadOnlyArray<number>,
29
30
  };
30
31
 
31
32
  export interface Spec extends TurboModule {
@@ -23,8 +23,10 @@ import nullthrows from 'nullthrows';
23
23
 
24
24
  type ObserverState = {
25
25
  thresholds: $ReadOnlyArray<number>,
26
+ rootThresholds?: ?$ReadOnlyArray<number>,
26
27
  intersecting: boolean,
27
28
  currentThreshold: ?number,
29
+ currentRootThreshold: ?number,
28
30
  };
29
31
 
30
32
  type Observation = {
@@ -74,8 +76,10 @@ const NativeIntersectionObserverMock = {
74
76
  ...options,
75
77
  state: {
76
78
  thresholds: options.thresholds,
79
+ rootThresholds: options.rootThresholds,
77
80
  intersecting: false,
78
81
  currentThreshold: null,
82
+ currentRootThreshold: null,
79
83
  },
80
84
  };
81
85
  observations.push(observation);
@@ -141,9 +145,14 @@ const NativeIntersectionObserverMock = {
141
145
  if (observation.state.intersecting) {
142
146
  observation.state.intersecting = false;
143
147
  observation.state.currentThreshold = null;
148
+ observation.state.currentRootThreshold = null;
144
149
  } else {
145
150
  observation.state.intersecting = true;
146
151
  observation.state.currentThreshold = observation.thresholds[0];
152
+ observation.state.currentRootThreshold =
153
+ observation.rootThresholds != null
154
+ ? observation.rootThresholds[0]
155
+ : null;
147
156
  }
148
157
  pendingRecords.push(createRecordFromObservation(observation));
149
158
  setImmediate(notifyIntersectionObservers);