react-native-tvos 0.85.3-0 → 0.85.3-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) 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 +2 -2
  23. 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
  |}>;
@@ -153,9 +153,13 @@ component View(
153
153
  delete processedProps.isTVSelectable;
154
154
  }
155
155
 
156
- // Views with scrollSnapAlign must not be flattened by Fabric, otherwise
157
- // the prop never reaches the native view and scroll snapping breaks.
158
- if (processedProps.scrollSnapAlign != null) {
156
+ // Views with scrollSnapAlign or scrollSnapOffset must not be flattened by
157
+ // Fabric, otherwise the prop never reaches the native view and scroll
158
+ // snapping breaks.
159
+ if (
160
+ processedProps.scrollSnapAlign != null ||
161
+ processedProps.scrollSnapOffset != null
162
+ ) {
159
163
  processedProps.collapsable = false;
160
164
  }
161
165
 
@@ -29,7 +29,7 @@ export default class ReactNativeVersion {
29
29
  static major: number = 0;
30
30
  static minor: number = 85;
31
31
  static patch: number = 3;
32
- static prerelease: string | null = '0';
32
+ static prerelease: string | null = '1';
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: @(85),
26
26
  RCTVersionPatch: @(3),
27
- RCTVersionPrerelease: @"0",
27
+ RCTVersionPrerelease: @"1",
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.85.3-0
1
+ VERSION_NAME=0.85.3-1
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 85,
17
17
  "patch" to 3,
18
- "prerelease" to "0"
18
+ "prerelease" to "1"
19
19
  )
20
20
  }
@@ -586,29 +586,39 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
586
586
  }
587
587
 
588
588
  /**
589
- * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
590
- * Returns true if snap scrolling was performed, false otherwise.
589
+ * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign
590
+ * or scrollSnapOffset. Returns true if snap scrolling was performed, false otherwise.
591
591
  */
592
592
  private boolean tryScrollSnapToChild(View focused) {
593
593
  if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
594
594
  return false;
595
595
  }
596
596
 
597
- kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
597
+ kotlin.Triple<View, String, Integer> result =
598
+ ReactScrollViewHelper.findScrollSnap(focused, this);
598
599
  if (result == null) {
599
600
  return false;
600
601
  }
601
602
 
602
603
  View snapTarget = result.getFirst();
603
604
  String alignment = result.getSecond();
605
+ Integer snapOffset = result.getThird();
604
606
 
605
607
  Rect rect = new Rect();
606
608
  snapTarget.getDrawingRect(rect);
607
609
  offsetDescendantRectToMyCoords(snapTarget, rect);
608
610
 
609
- int viewportWidth = getWidth() - getPaddingLeft() - getPaddingRight();
610
611
  int maxScrollX = Math.max(0, computeHorizontalScrollRange() - getWidth());
611
612
 
613
+ if (snapOffset != null) {
614
+ int targetOffset = ReactScrollViewHelper.computeScrollSnapTargetForOffset(
615
+ rect.left, snapOffset, mSnapInterval, maxScrollX);
616
+ reactSmoothScrollTo(targetOffset, getScrollY());
617
+ return true;
618
+ }
619
+
620
+ int viewportWidth = getWidth() - getPaddingLeft() - getPaddingRight();
621
+
612
622
  Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
613
623
  rect.left, rect.right, viewportWidth, alignment, mSnapInterval, mSnapToItemPadding, maxScrollX);
614
624
  if (targetOffset == null) {
@@ -519,29 +519,39 @@ public class ReactScrollView extends ScrollView
519
519
  }
520
520
 
521
521
  /**
522
- * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
523
- * Returns true if snap scrolling was performed, false otherwise.
522
+ * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign
523
+ * or scrollSnapOffset. Returns true if snap scrolling was performed, false otherwise.
524
524
  */
525
525
  private boolean tryScrollSnapToChild(View focused) {
526
526
  if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
527
527
  return false;
528
528
  }
529
529
 
530
- kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
530
+ kotlin.Triple<View, String, Integer> result =
531
+ ReactScrollViewHelper.findScrollSnap(focused, this);
531
532
  if (result == null) {
532
533
  return false;
533
534
  }
534
535
 
535
536
  View snapTarget = result.getFirst();
536
537
  String alignment = result.getSecond();
538
+ Integer snapOffset = result.getThird();
537
539
 
538
540
  Rect rect = new Rect();
539
541
  snapTarget.getDrawingRect(rect);
540
542
  offsetDescendantRectToMyCoords(snapTarget, rect);
541
543
 
542
- int viewportHeight = getHeight() - getPaddingTop() - getPaddingBottom();
543
544
  int maxScrollY = getMaxScrollY();
544
545
 
546
+ if (snapOffset != null) {
547
+ int targetOffset = ReactScrollViewHelper.computeScrollSnapTargetForOffset(
548
+ rect.top, snapOffset, mSnapInterval, maxScrollY);
549
+ reactSmoothScrollTo(getScrollX(), targetOffset);
550
+ return true;
551
+ }
552
+
553
+ int viewportHeight = getHeight() - getPaddingTop() - getPaddingBottom();
554
+
545
555
  Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
546
556
  rect.top, rect.bottom, viewportHeight, alignment, mSnapInterval, mSnapToItemPadding, maxScrollY);
547
557
  if (targetOffset == null) {
@@ -569,27 +569,67 @@ public object ReactScrollViewHelper {
569
569
 
570
570
  /**
571
571
  * Walks up the view hierarchy from the focused view to find a ReactViewGroup with
572
- * scrollSnapAlign set. Returns a Pair of (snapTarget, alignment) or null if not found.
572
+ * either scrollSnapAlign or scrollSnapOffset set. Returns a Triple of (snapTarget,
573
+ * alignment, offsetPx) or null if neither marker was found. Exactly one of
574
+ * alignment/offsetPx is non-null on return.
575
+ *
576
+ * Walk is inner→outer, latest marker wins. On a single view declaring both,
577
+ * scrollSnapOffset wins as the more specific config.
573
578
  *
574
579
  * Shared by [ReactScrollView] and [ReactHorizontalScrollView].
575
580
  */
576
581
  @JvmStatic
577
- public fun findScrollSnapAlign(focused: View, scrollView: ViewGroup): Pair<View, String>? {
582
+ public fun findScrollSnap(focused: View, scrollView: ViewGroup): Triple<View, String?, Int?>? {
578
583
  var view: View? = focused
579
584
  var snapTarget: View? = null
580
585
  var alignment: String? = null
586
+ var offset: Int? = null
581
587
  while (view != null && view !== scrollView) {
582
588
  if (view is ReactViewGroup) {
583
- val snap = view.scrollSnapAlign
584
- if (snap != null) {
585
- alignment = snap
589
+ val o = view.scrollSnapOffset
590
+ val a = view.scrollSnapAlign
591
+ if (o != null) {
592
+ offset = o
593
+ alignment = null
594
+ snapTarget = view
595
+ } else if (a != null) {
596
+ alignment = a
597
+ offset = null
586
598
  snapTarget = view
587
599
  }
588
600
  }
589
601
  val parent = view.parent
590
602
  view = if (parent is View) parent else null
591
603
  }
592
- return if (alignment != null && snapTarget != null) Pair(snapTarget, alignment) else null
604
+ return if (snapTarget != null && (alignment != null || offset != null))
605
+ Triple(snapTarget, alignment, offset) else null
606
+ }
607
+
608
+ /**
609
+ * Computes the target scroll offset for the per-item scrollSnapOffset path:
610
+ * land the snap target's leading edge at `snapOffset` pixels from the viewport origin.
611
+ *
612
+ * Returns the clamped target offset.
613
+ *
614
+ * Shared by [ReactScrollView] and [ReactHorizontalScrollView].
615
+ *
616
+ * @param focusedStart the start coordinate of the snap target in scroll view coordinates
617
+ * @param snapOffset the per-item pixel offset from the viewport origin
618
+ * @param snapInterval the snap interval (0 if not set)
619
+ * @param maxScrollOffset the maximum scroll offset for clamping
620
+ */
621
+ @JvmStatic
622
+ public fun computeScrollSnapTargetForOffset(
623
+ focusedStart: Int,
624
+ snapOffset: Int,
625
+ snapInterval: Int,
626
+ maxScrollOffset: Int,
627
+ ): Int {
628
+ var targetOffset = focusedStart - snapOffset
629
+ if (snapInterval > 0) {
630
+ targetOffset = (Math.floor(targetOffset.toDouble() / snapInterval) * snapInterval).toInt()
631
+ }
632
+ return Math.max(0, Math.min(targetOffset, maxScrollOffset))
593
633
  }
594
634
 
595
635
  /**
@@ -174,6 +174,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
174
174
  private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null
175
175
  private var needsOffscreenAlphaCompositing = false
176
176
  public var scrollSnapAlign: String? = null
177
+ public var scrollSnapOffset: Int? = null
177
178
  private var backfaceOpacity = 0f
178
179
  private var backfaceVisible = false
179
180
  private var childrenRemovedWhileTransitioning: MutableSet<Int>? = null
@@ -195,6 +195,11 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
195
195
  view.scrollSnapAlign = value
196
196
  }
197
197
 
198
+ @ReactProp(name = "scrollSnapOffset", defaultFloat = Float.NaN)
199
+ public open fun setScrollSnapOffset(view: ReactViewGroup, value: Float) {
200
+ view.scrollSnapOffset = if (value.isNaN()) null else value.dpToPx().toInt()
201
+ }
202
+
198
203
  @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage")
199
204
  public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?) {
200
205
  if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) {
@@ -22,7 +22,7 @@ struct ReactNativeVersionType {
22
22
  int32_t Major = 0;
23
23
  int32_t Minor = 85;
24
24
  int32_t Patch = 3;
25
- std::string_view Prerelease = "0";
25
+ std::string_view Prerelease = "1";
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
@@ -212,6 +221,7 @@ void HostPlatformViewProps::setProp(
212
221
  RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
213
222
  RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus);
214
223
  RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
224
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapOffset);
215
225
  RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing);
216
226
  RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid);
217
227
  RAW_SET_PROP_SWITCH_CASE_BASIC(screenReaderFocusable);
@@ -1065,6 +1075,14 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
1065
1075
  }
1066
1076
  }
1067
1077
 
1078
+ if (scrollSnapOffset != oldProps->scrollSnapOffset) {
1079
+ if (scrollSnapOffset.has_value()) {
1080
+ result["scrollSnapOffset"] = scrollSnapOffset.value();
1081
+ } else {
1082
+ result["scrollSnapOffset"] = folly::dynamic(nullptr);
1083
+ }
1084
+ }
1085
+
1068
1086
  return result;
1069
1087
  }
1070
1088
 
@@ -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.85.3-0",
3
+ "version": "0.85.3-1",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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.85.3-0"
201
+ "@react-native-tvos/virtualized-lists": "0.85.3-1"
202
202
  },
203
203
  "codegenConfig": {
204
204
  "libraries": [
@@ -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
  */