react-native-tvos 0.86.0-0rc2 → 0.86.0-0rc3

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 (25) hide show
  1. package/Libraries/Components/ScrollView/ScrollView.d.ts +1 -1
  2. package/Libraries/Components/ScrollView/ScrollView.js +1 -1
  3. package/Libraries/Components/TV/TVViewPropTypes.js +9 -0
  4. package/Libraries/Components/View/View.js +7 -3
  5. package/Libraries/Core/ReactNativeVersion.js +1 -1
  6. package/Libraries/NativeComponent/TVViewConfig.js +1 -0
  7. package/README.md +13 -1
  8. package/React/Base/RCTVersion.m +1 -1
  9. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +35 -15
  10. package/ReactAndroid/gradle.properties +1 -1
  11. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  12. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +14 -4
  13. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +14 -4
  14. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +46 -6
  15. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +1 -0
  16. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +5 -0
  17. package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
  18. package/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +7 -0
  19. package/ReactCommon/react/renderer/components/view/BaseViewProps.h +1 -0
  20. package/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +18 -0
  21. package/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.h +1 -0
  22. package/package.json +10 -10
  23. package/sdks/.hermesv1version +1 -1
  24. package/sdks/hermes-engine/version.properties +1 -1
  25. package/types/public/ReactNativeTVTypes.d.ts +9 -0
@@ -515,7 +515,7 @@ export interface ScrollViewPropsIOS {
515
515
  * - `start` (the default) will align the snap at the left (horizontal) or top (vertical)
516
516
  * - `center` will align the snap in the center
517
517
  * - `end` will align the snap at the right (horizontal) or bottom (vertical)
518
- * - `item` will align the snap according to the value of `scrollSnapAlign` for individual items in the scroll view (TV platforms only).
518
+ * - `item` will align the snap according to each item's own `scrollSnapAlign` or `scrollSnapOffset` prop (TV platforms only).
519
519
  */
520
520
  snapToAlignment?: 'start' | 'center' | 'end' | 'item' | undefined;
521
521
 
@@ -620,7 +620,7 @@ type ScrollViewBaseProps = Readonly<{
620
620
  * - `'start'` (the default) will align the snap at the left (horizontal) or top (vertical)
621
621
  * - `'center'` will align the snap in the center
622
622
  * - `'end'` will align the snap at the right (horizontal) or bottom (vertical)
623
- * - `'item'` will align the snap according to the value of `scrollSnapAlign` for individual items in the scroll view (TV platforms only).
623
+ * - `'item'` will align the snap according to each item's own `scrollSnapAlign` or `scrollSnapOffset` prop (TV platforms only).
624
624
  */
625
625
  snapToAlignment?: ?('start' | 'center' | 'end' | 'item'),
626
626
  /**
@@ -131,4 +131,13 @@ export type TVViewProps = $ReadOnly<{|
131
131
  */
132
132
  scrollSnapAlign?: ?('start' | 'center' | 'end'),
133
133
 
134
+ /**
135
+ * Per-item scroll snap offset in dp/pt. When set on a View inside a
136
+ * ScrollView with `snapToAlignment="item"`, the focus engine lands the
137
+ * View's top at viewport y = scrollSnapOffset. Standalone alternative to
138
+ * `scrollSnapAlign` — set one or the other; if both are set on the same
139
+ * View, `scrollSnapOffset` takes precedence.
140
+ */
141
+ scrollSnapOffset?: ?number,
142
+
134
143
  |}>;
@@ -155,9 +155,13 @@ component View(
155
155
  delete processedProps.isTVSelectable;
156
156
  }
157
157
 
158
- // Views with scrollSnapAlign must not be flattened by Fabric, otherwise
159
- // the prop never reaches the native view and scroll snapping breaks.
160
- if (processedProps.scrollSnapAlign != null) {
158
+ // Views with scrollSnapAlign or scrollSnapOffset must not be flattened by
159
+ // Fabric, otherwise the prop never reaches the native view and scroll
160
+ // snapping breaks.
161
+ if (
162
+ processedProps.scrollSnapAlign != null ||
163
+ processedProps.scrollSnapOffset != null
164
+ ) {
161
165
  processedProps.collapsable = false;
162
166
  }
163
167
 
@@ -29,7 +29,7 @@ export default class ReactNativeVersion {
29
29
  static major: number = 0;
30
30
  static minor: number = 86;
31
31
  static patch: number = 0;
32
- static prerelease: string | null = '0rc2';
32
+ static prerelease: string | null = '0rc3';
33
33
 
34
34
  static getVersionString(): string {
35
35
  return `${this.major}.${this.minor}.${this.patch}${this.prerelease != null ? `-${this.prerelease}` : ''}`;
@@ -26,5 +26,6 @@ export const validAttributesForTVProps = {
26
26
  trapFocusDown: true,
27
27
  trapFocusUp: true,
28
28
  scrollSnapAlign: true,
29
+ scrollSnapOffset: true,
29
30
  };
30
31
 
package/README.md CHANGED
@@ -272,6 +272,7 @@ class Game2048 extends React.Component {
272
272
  | Prop (View) | Value | Description |
273
273
  |---|---|---|
274
274
  | scrollSnapAlign | `'start'` \| `'center'` \| `'end'` | Controls where this item snaps inside its parent ScrollView. Only used when the parent has `snapToAlignment="item"`. |
275
+ | scrollSnapOffset | `number` | Per-item pixel offset (in dp/pt). When set, the focus engine lands this item's leading edge at `scrollSnapOffset` from the viewport origin. Standalone alternative to `scrollSnapAlign` — takes precedence if both are set on the same item. Only used when the parent has `snapToAlignment="item"`. |
275
276
 
276
277
  ```jsx
277
278
  <ScrollView
@@ -291,6 +292,16 @@ class Game2048 extends React.Component {
291
292
  </ScrollView>
292
293
  ```
293
294
 
295
+ `scrollSnapOffset` is useful when different items in the same ScrollView need to land at different positions — e.g. a hero row peeking 27dp from the top while poster rows peek 550dp:
296
+
297
+ ```jsx
298
+ <ScrollView snapToAlignment="item">
299
+ <View scrollSnapOffset={27}><Hero /></View>
300
+ <View scrollSnapOffset={100}><Featured /></View>
301
+ <View scrollSnapOffset={550}><PosterRow /></View>
302
+ </ScrollView>
303
+ ```
304
+
294
305
  - _ScrollView animation control_: The `scrollAnimationEnabled` prop allows disabling scroll animations when focus changes on TV. When set to `false`, the scroll view jumps instantly to the focused item instead of animating. This only affects TV platforms and has no effect on mobile.
295
306
 
296
307
  | Prop (ScrollView) | Value | Description |
@@ -307,7 +318,8 @@ class Game2048 extends React.Component {
307
318
 
308
319
  - _Interaction with existing ScrollView props_: The TV scroll props build on top of existing React Native ScrollView behavior. Here is how they interact:
309
320
 
310
- - `snapToAlignment="item"` does not require `snapToInterval` to be set. It works as a standalone snapping mode where each child's `scrollSnapAlign` determines the snap position. If `snapToInterval` is also set, the interval is applied as an additional constraint after the item offset is computed.
321
+ - `snapToAlignment="item"` does not require `snapToInterval` to be set. It works as a standalone snapping mode where each child's `scrollSnapAlign` or `scrollSnapOffset` determines the snap position. If `snapToInterval` is also set, the interval is applied as an additional constraint after the item offset is computed.
322
+ - `scrollSnapOffset` ignores `snapToItemPadding`. The padding is a global value applied to `scrollSnapAlign` cases; `scrollSnapOffset` is itself a per-item pixel offset that fully specifies where the item lands.
311
323
  - `snapToAlignment="item"` should not be combined with `pagingEnabled`. Both attempt to control scroll positioning independently, which leads to unpredictable behavior. Use one or the other.
312
324
  - `snapToStart` and `snapToEnd` work independently of `snapToAlignment="item"`. They control edge behavior during swipe/drag momentum (whether the scroll view snaps to the first or last position), but do not affect focus driven item snapping.
313
325
  - `scrollAnimationEnabled={false}` disables all scroll animations, including programmatic `scrollTo({animated: true})` calls. When disabled, all scrolling is instant.
@@ -24,7 +24,7 @@ NSDictionary* RCTGetReactNativeVersion(void)
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(86),
26
26
  RCTVersionPatch: @(0),
27
- RCTVersionPrerelease: @"0rc2",
27
+ RCTVersionPrerelease: @"0rc3",
28
28
  };
29
29
  });
30
30
  return __rnVersion;
@@ -1171,13 +1171,20 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1171
1171
  self->_eventEmitter->onBlur();
1172
1172
  }
1173
1173
 
1174
- // Focus marker helper: traverses view hierarchy to find scrollSnapAlign prop
1175
- // Returns the view that has the property via the output parameter
1176
- - (NSString *)findScrollSnapAlignInView:(UIView *)view foundView:(UIView **)outView
1174
+ // Focus marker helper: traverses view hierarchy to find either scrollSnapAlign
1175
+ // or scrollSnapOffset. Returns the view that has the property via the output
1176
+ // parameter and the marker type via outAlign/outOffset (exactly one non-nil on
1177
+ // return). Walk is inner→outer, latest marker wins (same as PR-1068's
1178
+ // scrollSnapAlign behavior). On a single view declaring both, scrollSnapOffset
1179
+ // wins as the more specific config.
1180
+ - (UIView *)findScrollSnapInView:(UIView *)view
1181
+ outAlign:(NSString **)outAlign
1182
+ outOffset:(NSNumber **)outOffset
1177
1183
  {
1178
1184
  UIView *testView = view;
1179
1185
  UIView *snapTarget;
1180
- NSString *marker;
1186
+ NSString *align;
1187
+ NSNumber *offset;
1181
1188
 
1182
1189
  while (testView && testView != self) {
1183
1190
  if (![testView isKindOfClass:RCTViewComponentView.class])
@@ -1188,28 +1195,37 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1188
1195
  RCTViewComponentView *componentView = (RCTViewComponentView *)testView;
1189
1196
 
1190
1197
  const auto &viewProps = static_cast<const facebook::react::BaseViewProps &>(*componentView.props);
1191
- if (viewProps.scrollSnapAlign.has_value() && !viewProps.scrollSnapAlign.value().empty()) {
1192
- marker = [NSString stringWithUTF8String:viewProps.scrollSnapAlign.value().c_str()];
1198
+ if (viewProps.scrollSnapOffset.has_value()) {
1199
+ offset = @(viewProps.scrollSnapOffset.value());
1200
+ align = nil;
1201
+ snapTarget = componentView;
1202
+ } else if (viewProps.scrollSnapAlign.has_value() && !viewProps.scrollSnapAlign.value().empty()) {
1203
+ align = [NSString stringWithUTF8String:viewProps.scrollSnapAlign.value().c_str()];
1204
+ offset = nil;
1193
1205
  snapTarget = componentView;
1194
1206
  }
1195
1207
 
1196
1208
  testView = [testView superview];
1197
1209
  }
1198
- *outView = snapTarget;
1199
- return marker;
1210
+ *outAlign = align;
1211
+ *outOffset = offset;
1212
+ return snapTarget;
1200
1213
  }
1201
1214
 
1202
1215
  - (void)_handleScrollSnapForFocusedView:(UIView *)focusedView
1203
1216
  {
1204
1217
  const auto &scrollProps = static_cast<const ScrollViewProps &>(*_props);
1205
- UIView *snapAlignView = nil;
1206
- NSString *scrollSnapAlign = [self findScrollSnapAlignInView:focusedView foundView:&snapAlignView];
1207
- if (scrollSnapAlign == nil || snapAlignView == nil) {
1218
+
1219
+ NSString *scrollSnapAlign = nil;
1220
+ NSNumber *scrollSnapOffset = nil;
1221
+ UIView *snapTargetView = [self findScrollSnapInView:focusedView
1222
+ outAlign:&scrollSnapAlign
1223
+ outOffset:&scrollSnapOffset];
1224
+ if (snapTargetView == nil || (scrollSnapAlign == nil && scrollSnapOffset == nil)) {
1208
1225
  return;
1209
1226
  }
1210
-
1211
1227
  RCTEnhancedScrollView *scrollView = (RCTEnhancedScrollView *)_scrollView;
1212
- CGRect focusedFrame = [snapAlignView convertRect:snapAlignView.bounds toView:_scrollView];
1228
+ CGRect focusedFrame = [snapTargetView convertRect:snapTargetView.bounds toView:_scrollView];
1213
1229
  CGFloat targetOffset;
1214
1230
  CGFloat snapToItemPadding = scrollProps.snapToItemPadding;
1215
1231
 
@@ -1229,8 +1245,12 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1229
1245
  currentOffset = scrollView.contentOffset.y;
1230
1246
  maxContentSize = scrollView.contentSize.height;
1231
1247
  }
1232
- // Calculate target offset based on scrollSnapAlign (unified for both axes)
1233
- if ([scrollSnapAlign isEqualToString:@"start"]) {
1248
+
1249
+ if (scrollSnapOffset != nil) {
1250
+ // Per-item pixel offset path: land the snap target's leading edge
1251
+ // at viewport coordinate = scrollSnapOffset.
1252
+ targetOffset = focusedOrigin - scrollSnapOffset.doubleValue;
1253
+ } else if ([scrollSnapAlign isEqualToString:@"start"]) {
1234
1254
  targetOffset = focusedOrigin - snapToItemPadding;
1235
1255
  } else if ([scrollSnapAlign isEqualToString:@"center"]) {
1236
1256
  CGFloat viewportCenter = viewportSize / 2;
@@ -1,4 +1,4 @@
1
- VERSION_NAME=0.86.0-0rc2
1
+ VERSION_NAME=0.86.0-0rc3
2
2
  react.internal.publishingGroup=io.github.react-native-tvos
3
3
  react.internal.hermesPublishingGroup=com.facebook.hermes
4
4
 
@@ -15,6 +15,6 @@ public object ReactNativeVersion {
15
15
  "major" to 0,
16
16
  "minor" to 86,
17
17
  "patch" to 0,
18
- "prerelease" to "0rc2"
18
+ "prerelease" to "0rc3"
19
19
  )
20
20
  }
@@ -578,29 +578,39 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
578
578
  }
579
579
 
580
580
  /**
581
- * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
582
- * Returns true if snap scrolling was performed, false otherwise.
581
+ * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign
582
+ * or scrollSnapOffset. Returns true if snap scrolling was performed, false otherwise.
583
583
  */
584
584
  private boolean tryScrollSnapToChild(View focused) {
585
585
  if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
586
586
  return false;
587
587
  }
588
588
 
589
- kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
589
+ kotlin.Triple<View, String, Integer> result =
590
+ ReactScrollViewHelper.findScrollSnap(focused, this);
590
591
  if (result == null) {
591
592
  return false;
592
593
  }
593
594
 
594
595
  View snapTarget = result.getFirst();
595
596
  String alignment = result.getSecond();
597
+ Integer snapOffset = result.getThird();
596
598
 
597
599
  Rect rect = new Rect();
598
600
  snapTarget.getDrawingRect(rect);
599
601
  offsetDescendantRectToMyCoords(snapTarget, rect);
600
602
 
601
- int viewportWidth = getWidth() - getPaddingLeft() - getPaddingRight();
602
603
  int maxScrollX = Math.max(0, computeHorizontalScrollRange() - getWidth());
603
604
 
605
+ if (snapOffset != null) {
606
+ int targetOffset = ReactScrollViewHelper.computeScrollSnapTargetForOffset(
607
+ rect.left, snapOffset, mSnapInterval, maxScrollX);
608
+ reactSmoothScrollTo(targetOffset, getScrollY());
609
+ return true;
610
+ }
611
+
612
+ int viewportWidth = getWidth() - getPaddingLeft() - getPaddingRight();
613
+
604
614
  Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
605
615
  rect.left, rect.right, viewportWidth, alignment, mSnapInterval, mSnapToItemPadding, maxScrollX);
606
616
  if (targetOffset == null) {
@@ -520,29 +520,39 @@ public class ReactScrollView extends ScrollView
520
520
  }
521
521
 
522
522
  /**
523
- * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
524
- * Returns true if snap scrolling was performed, false otherwise.
523
+ * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign
524
+ * or scrollSnapOffset. Returns true if snap scrolling was performed, false otherwise.
525
525
  */
526
526
  private boolean tryScrollSnapToChild(View focused) {
527
527
  if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
528
528
  return false;
529
529
  }
530
530
 
531
- kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
531
+ kotlin.Triple<View, String, Integer> result =
532
+ ReactScrollViewHelper.findScrollSnap(focused, this);
532
533
  if (result == null) {
533
534
  return false;
534
535
  }
535
536
 
536
537
  View snapTarget = result.getFirst();
537
538
  String alignment = result.getSecond();
539
+ Integer snapOffset = result.getThird();
538
540
 
539
541
  Rect rect = new Rect();
540
542
  snapTarget.getDrawingRect(rect);
541
543
  offsetDescendantRectToMyCoords(snapTarget, rect);
542
544
 
543
- int viewportHeight = getHeight() - getPaddingTop() - getPaddingBottom();
544
545
  int maxScrollY = getMaxScrollY();
545
546
 
547
+ if (snapOffset != null) {
548
+ int targetOffset = ReactScrollViewHelper.computeScrollSnapTargetForOffset(
549
+ rect.top, snapOffset, mSnapInterval, maxScrollY);
550
+ reactSmoothScrollTo(getScrollX(), targetOffset);
551
+ return true;
552
+ }
553
+
554
+ int viewportHeight = getHeight() - getPaddingTop() - getPaddingBottom();
555
+
546
556
  Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
547
557
  rect.top, rect.bottom, viewportHeight, alignment, mSnapInterval, mSnapToItemPadding, maxScrollY);
548
558
  if (targetOffset == null) {
@@ -576,27 +576,67 @@ public object ReactScrollViewHelper {
576
576
 
577
577
  /**
578
578
  * Walks up the view hierarchy from the focused view to find a ReactViewGroup with
579
- * scrollSnapAlign set. Returns a Pair of (snapTarget, alignment) or null if not found.
579
+ * either scrollSnapAlign or scrollSnapOffset set. Returns a Triple of (snapTarget,
580
+ * alignment, offsetPx) or null if neither marker was found. Exactly one of
581
+ * alignment/offsetPx is non-null on return.
582
+ *
583
+ * Walk is inner→outer, latest marker wins. On a single view declaring both,
584
+ * scrollSnapOffset wins as the more specific config.
580
585
  *
581
586
  * Shared by [ReactScrollView] and [ReactHorizontalScrollView].
582
587
  */
583
588
  @JvmStatic
584
- public fun findScrollSnapAlign(focused: View, scrollView: ViewGroup): Pair<View, String>? {
589
+ public fun findScrollSnap(focused: View, scrollView: ViewGroup): Triple<View, String?, Int?>? {
585
590
  var view: View? = focused
586
591
  var snapTarget: View? = null
587
592
  var alignment: String? = null
593
+ var offset: Int? = null
588
594
  while (view != null && view !== scrollView) {
589
595
  if (view is ReactViewGroup) {
590
- val snap = view.scrollSnapAlign
591
- if (snap != null) {
592
- alignment = snap
596
+ val o = view.scrollSnapOffset
597
+ val a = view.scrollSnapAlign
598
+ if (o != null) {
599
+ offset = o
600
+ alignment = null
601
+ snapTarget = view
602
+ } else if (a != null) {
603
+ alignment = a
604
+ offset = null
593
605
  snapTarget = view
594
606
  }
595
607
  }
596
608
  val parent = view.parent
597
609
  view = if (parent is View) parent else null
598
610
  }
599
- return if (alignment != null && snapTarget != null) Pair(snapTarget, alignment) else null
611
+ return if (snapTarget != null && (alignment != null || offset != null))
612
+ Triple(snapTarget, alignment, offset) else null
613
+ }
614
+
615
+ /**
616
+ * Computes the target scroll offset for the per-item scrollSnapOffset path:
617
+ * land the snap target's leading edge at `snapOffset` pixels from the viewport origin.
618
+ *
619
+ * Returns the clamped target offset.
620
+ *
621
+ * Shared by [ReactScrollView] and [ReactHorizontalScrollView].
622
+ *
623
+ * @param focusedStart the start coordinate of the snap target in scroll view coordinates
624
+ * @param snapOffset the per-item pixel offset from the viewport origin
625
+ * @param snapInterval the snap interval (0 if not set)
626
+ * @param maxScrollOffset the maximum scroll offset for clamping
627
+ */
628
+ @JvmStatic
629
+ public fun computeScrollSnapTargetForOffset(
630
+ focusedStart: Int,
631
+ snapOffset: Int,
632
+ snapInterval: Int,
633
+ maxScrollOffset: Int,
634
+ ): Int {
635
+ var targetOffset = focusedStart - snapOffset
636
+ if (snapInterval > 0) {
637
+ targetOffset = (Math.floor(targetOffset.toDouble() / snapInterval) * snapInterval).toInt()
638
+ }
639
+ return Math.max(0, Math.min(targetOffset, maxScrollOffset))
600
640
  }
601
641
 
602
642
  /**
@@ -175,6 +175,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
175
175
  private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null
176
176
  private var needsOffscreenAlphaCompositing = false
177
177
  public var scrollSnapAlign: String? = null
178
+ public var scrollSnapOffset: Int? = null
178
179
  private var backfaceOpacity = 0f
179
180
  private var backfaceVisible = false
180
181
  private var childrenRemovedWhileTransitioning: MutableSet<Int>? = null
@@ -192,6 +192,11 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
192
192
  view.scrollSnapAlign = value
193
193
  }
194
194
 
195
+ @ReactProp(name = "scrollSnapOffset", defaultFloat = Float.NaN)
196
+ public open fun setScrollSnapOffset(view: ReactViewGroup, value: Float) {
197
+ view.scrollSnapOffset = if (value.isNaN()) null else value.dpToPx().toInt()
198
+ }
199
+
195
200
  @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage")
196
201
  public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?) {
197
202
  if (backgroundImage != null && backgroundImage.size() > 0) {
@@ -22,7 +22,7 @@ struct ReactNativeVersionType {
22
22
  int32_t Major = 0;
23
23
  int32_t Minor = 86;
24
24
  int32_t Patch = 0;
25
- std::string_view Prerelease = "0rc2";
25
+ std::string_view Prerelease = "0rc3";
26
26
  };
27
27
 
28
28
  constexpr ReactNativeVersionType ReactNativeVersion;
@@ -455,6 +455,12 @@ BaseViewProps::BaseViewProps(
455
455
  "scrollSnapAlign",
456
456
  sourceProps.scrollSnapAlign,
457
457
  {}))
458
+ ,scrollSnapOffset(ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.scrollSnapOffset : convertRawProp(
459
+ context,
460
+ rawProps,
461
+ "scrollSnapOffset",
462
+ sourceProps.scrollSnapOffset,
463
+ {}))
458
464
  #endif
459
465
  {}
460
466
 
@@ -528,6 +534,7 @@ void BaseViewProps::setProp(
528
534
  RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusLeft);
529
535
  RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusRight);
530
536
  RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
537
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapOffset);
531
538
  #endif
532
539
  // events field
533
540
  VIEW_EVENT_CASE(PointerEnter);
@@ -126,6 +126,7 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
126
126
 
127
127
  #if TARGET_OS_TV
128
128
  std::optional<std::string> scrollSnapAlign;
129
+ std::optional<int> scrollSnapOffset;
129
130
  #endif
130
131
 
131
132
  #pragma mark - Convenience Methods
@@ -109,6 +109,15 @@ HostPlatformViewProps::HostPlatformViewProps(
109
109
  "scrollSnapAlign",
110
110
  sourceProps.scrollSnapAlign,
111
111
  {})),
112
+ scrollSnapOffset(
113
+ ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
114
+ ? sourceProps.scrollSnapOffset
115
+ : convertRawProp(
116
+ context,
117
+ rawProps,
118
+ "scrollSnapOffset",
119
+ sourceProps.scrollSnapOffset,
120
+ {})),
112
121
  needsOffscreenAlphaCompositing(
113
122
  ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
114
123
  ? sourceProps.needsOffscreenAlphaCompositing
@@ -229,6 +238,7 @@ void HostPlatformViewProps::setProp(
229
238
  RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
230
239
  RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus);
231
240
  RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
241
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapOffset);
232
242
  RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing);
233
243
  RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid);
234
244
  RAW_SET_PROP_SWITCH_CASE_BASIC(screenReaderFocusable);
@@ -1113,6 +1123,14 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
1113
1123
  }
1114
1124
  }
1115
1125
 
1126
+ if (scrollSnapOffset != oldProps->scrollSnapOffset) {
1127
+ if (scrollSnapOffset.has_value()) {
1128
+ result["scrollSnapOffset"] = scrollSnapOffset.value();
1129
+ } else {
1130
+ result["scrollSnapOffset"] = folly::dynamic(nullptr);
1131
+ }
1132
+ }
1133
+
1116
1134
  return result;
1117
1135
  }
1118
1136
 
@@ -48,6 +48,7 @@ class HostPlatformViewProps : public BaseViewProps {
48
48
  bool trapFocusLeft{false};
49
49
  bool trapFocusRight{false};
50
50
  std::optional<std::string> scrollSnapAlign{};
51
+ std::optional<int> scrollSnapOffset{};
51
52
 
52
53
  bool needsOffscreenAlphaCompositing{false};
53
54
  bool renderToHardwareTextureAndroid{false};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tvos",
3
- "version": "0.86.0-0rc2",
3
+ "version": "0.86.0-0rc3",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -154,7 +154,7 @@
154
154
  "featureflags-check": "node ./scripts/featureflags/index.js --verify-unchanged"
155
155
  },
156
156
  "peerDependencies": {
157
- "@react-native/jest-preset": "0.86.0-rc.2",
157
+ "@react-native/jest-preset": "0.86.0-rc.3",
158
158
  "@types/react": "^19.1.1",
159
159
  "react": "^19.2.3"
160
160
  },
@@ -167,12 +167,12 @@
167
167
  }
168
168
  },
169
169
  "dependencies": {
170
- "@react-native/assets-registry": "0.86.0-rc.2",
171
- "@react-native/codegen": "0.86.0-rc.2",
172
- "@react-native/community-cli-plugin": "0.86.0-rc.2",
173
- "@react-native/gradle-plugin": "0.86.0-rc.2",
174
- "@react-native/js-polyfills": "0.86.0-rc.2",
175
- "@react-native/normalize-colors": "0.86.0-rc.2",
170
+ "@react-native/assets-registry": "0.86.0-rc.3",
171
+ "@react-native/codegen": "0.86.0-rc.3",
172
+ "@react-native/community-cli-plugin": "0.86.0-rc.3",
173
+ "@react-native/gradle-plugin": "0.86.0-rc.3",
174
+ "@react-native/js-polyfills": "0.86.0-rc.3",
175
+ "@react-native/normalize-colors": "0.86.0-rc.3",
176
176
  "abort-controller": "^3.0.0",
177
177
  "anser": "^1.4.9",
178
178
  "ansi-regex": "^5.0.0",
@@ -180,7 +180,7 @@
180
180
  "base64-js": "^1.5.1",
181
181
  "commander": "^12.0.0",
182
182
  "flow-enums-runtime": "^0.0.6",
183
- "hermes-compiler": "250829098.0.13",
183
+ "hermes-compiler": "250829098.0.14",
184
184
  "invariant": "^2.2.4",
185
185
  "memoize-one": "^5.0.0",
186
186
  "metro-runtime": "^0.84.3",
@@ -198,7 +198,7 @@
198
198
  "whatwg-fetch": "^3.0.0",
199
199
  "ws": "^7.5.10",
200
200
  "yargs": "^17.6.2",
201
- "@react-native-tvos/virtualized-lists": "0.86.0-0rc2"
201
+ "@react-native-tvos/virtualized-lists": "0.86.0-0rc3"
202
202
  },
203
203
  "codegenConfig": {
204
204
  "libraries": [
@@ -1 +1 @@
1
- hermes-v250829098.0.13
1
+ hermes-v250829098.0.14
@@ -1,2 +1,2 @@
1
1
  HERMES_VERSION_NAME=0.17.0
2
- HERMES_V1_VERSION_NAME=250829098.0.13
2
+ HERMES_V1_VERSION_NAME=250829098.0.14
@@ -30,6 +30,15 @@ declare module 'react-native' {
30
30
  */
31
31
  scrollSnapAlign?: 'start' | 'center' | 'end' | undefined;
32
32
 
33
+ /**
34
+ * Per-item scroll snap offset in dp/pt. When set, the focus engine lands
35
+ * this view's top at viewport y = scrollSnapOffset. Standalone alternative
36
+ * to `scrollSnapAlign`; takes precedence if both are set on the same View.
37
+ *
38
+ * @platform tv
39
+ */
40
+ scrollSnapOffset?: number | undefined;
41
+
33
42
  /**
34
43
  * Android TV only prop
35
44
  */