react-native-tvos 0.83.3-0 → 0.83.4-0

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 (38) hide show
  1. package/Libraries/Components/ScrollView/AndroidHorizontalScrollViewNativeComponent.js +1 -0
  2. package/Libraries/Components/ScrollView/ScrollView.d.ts +2 -1
  3. package/Libraries/Components/ScrollView/ScrollView.js +8 -1
  4. package/Libraries/Components/ScrollView/ScrollViewNativeComponent.js +2 -0
  5. package/Libraries/Components/ScrollView/ScrollViewNativeComponentType.js +2 -1
  6. package/Libraries/Components/TV/TVViewPropTypes.js +7 -0
  7. package/Libraries/Components/View/View.js +6 -0
  8. package/Libraries/Core/ReactNativeVersion.js +1 -1
  9. package/Libraries/NativeComponent/TVViewConfig.js +1 -0
  10. package/Libraries/Pressability/Pressability.js +7 -0
  11. package/React/Base/RCTVersion.m +1 -1
  12. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.h +1 -0
  13. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm +3 -0
  14. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +110 -4
  15. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +2 -0
  16. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +17 -0
  17. package/ReactAndroid/gradle.properties +1 -1
  18. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.kt +1 -1
  19. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollView.java +49 -5
  20. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactHorizontalScrollViewManager.kt +7 -0
  21. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollView.java +46 -4
  22. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewHelper.kt +69 -0
  23. package/ReactAndroid/src/main/java/com/facebook/react/views/scroll/ReactScrollViewManager.kt +7 -0
  24. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt +1 -0
  25. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +4 -0
  26. package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
  27. package/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.cpp +10 -0
  28. package/ReactCommon/react/renderer/components/scrollview/BaseScrollViewProps.h +1 -0
  29. package/ReactCommon/react/renderer/components/scrollview/conversions.h +6 -0
  30. package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.cpp +18 -1
  31. package/ReactCommon/react/renderer/components/scrollview/platform/android/react/renderer/components/scrollview/HostPlatformScrollViewProps.h +2 -0
  32. package/ReactCommon/react/renderer/components/scrollview/primitives.h +1 -1
  33. package/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +11 -1
  34. package/ReactCommon/react/renderer/components/view/BaseViewProps.h +4 -0
  35. package/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.cpp +18 -0
  36. package/ReactCommon/react/renderer/components/view/platform/android/react/renderer/components/view/HostPlatformViewProps.h +1 -0
  37. package/package.json +8 -8
  38. package/types/public/ReactNativeTVTypes.d.ts +19 -2
@@ -58,6 +58,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = {
58
58
  borderLeftColor: {
59
59
  process: require('../../StyleSheet/processColor').default,
60
60
  },
61
+ snapToItemPadding: true,
61
62
  pointerEvents: true,
62
63
  },
63
64
  };
@@ -515,8 +515,9 @@ 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
519
  */
519
- snapToAlignment?: 'start' | 'center' | 'end' | undefined;
520
+ snapToAlignment?: 'start' | 'center' | 'end' | 'item' | undefined;
520
521
 
521
522
  /**
522
523
  * Fires when the scroll view scrolls to top after the status bar has been tapped
@@ -612,8 +612,15 @@ type ScrollViewBaseProps = $ReadOnly<{
612
612
  * - `'start'` (the default) will align the snap at the left (horizontal) or top (vertical)
613
613
  * - `'center'` will align the snap in the center
614
614
  * - `'end'` will align the snap at the right (horizontal) or bottom (vertical)
615
+ * - `'item'` will align the snap according to the value of `scrollSnapAlign` for individual items in the scroll view (TV platforms only).
615
616
  */
616
- snapToAlignment?: ?('start' | 'center' | 'end'),
617
+ snapToAlignment?: ?('start' | 'center' | 'end' | 'item'),
618
+ /**
619
+ * Padding applied when snapping to items using `snapToAlignment="item"`.
620
+ * This is set on the parent scroll view, not directly on child items.
621
+ * Only used when `snapToAlignment` is set to `'item'`.
622
+ */
623
+ snapToItemPadding?: ?number,
617
624
  /**
618
625
  * When set, causes the scroll view to stop at multiples of the value of
619
626
  * `snapToInterval`. This can be used for paginating through children
@@ -86,6 +86,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
86
86
  borderLeftColor: {
87
87
  process: require('../../StyleSheet/processColor').default,
88
88
  },
89
+ snapToItemPadding: true,
89
90
  pointerEvents: true,
90
91
  isInvertedVirtualizedList: true,
91
92
  },
@@ -152,6 +153,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
152
153
  showsHorizontalScrollIndicator: true,
153
154
  showsVerticalScrollIndicator: true,
154
155
  showsScrollIndex: true,
156
+ snapToItemPadding: true,
155
157
  snapToAlignment: true,
156
158
  snapToEnd: true,
157
159
  snapToInterval: true,
@@ -69,11 +69,12 @@ export type ScrollViewNativeProps = $ReadOnly<{
69
69
  scrollPerfTag?: ?string,
70
70
  scrollToOverflowEnabled?: ?boolean,
71
71
  scrollsToTop?: ?boolean,
72
+ snapToItemPadding?: ?number,
72
73
  sendMomentumEvents?: ?boolean,
73
74
  showsHorizontalScrollIndicator?: ?boolean,
74
75
  showsScrollIndex?: ?boolean,
75
76
  showsVerticalScrollIndicator?: ?boolean,
76
- snapToAlignment?: ?('start' | 'center' | 'end'),
77
+ snapToAlignment?: ?('start' | 'center' | 'end' | 'item'),
77
78
  snapToEnd?: ?boolean,
78
79
  snapToInterval?: ?number,
79
80
  snapToOffsets?: ?$ReadOnlyArray<number>,
@@ -124,4 +124,11 @@ export type TVViewProps = $ReadOnly<{|
124
124
  *
125
125
  */
126
126
  nextFocusUp?: ?number,
127
+
128
+ /**
129
+ * Scroll snap alignment (used with snapToAlignment="item" in ScrollView).
130
+ *
131
+ */
132
+ scrollSnapAlign?: ?('start' | 'center' | 'end'),
133
+
127
134
  |}>;
@@ -153,6 +153,12 @@ 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) {
159
+ processedProps.collapsable = false;
160
+ }
161
+
156
162
  const actualView =
157
163
  ref == null ? (
158
164
  <ViewNativeComponent {...processedProps} />
@@ -28,7 +28,7 @@
28
28
  export default class ReactNativeVersion {
29
29
  static major: number = 0;
30
30
  static minor: number = 83;
31
- static patch: number = 3;
31
+ static patch: number = 4;
32
32
  static prerelease: string | null = '0';
33
33
 
34
34
  static getVersionString(): string {
@@ -25,5 +25,6 @@ export const validAttributesForTVProps = {
25
25
  trapFocusRight: true,
26
26
  trapFocusDown: true,
27
27
  trapFocusUp: true,
28
+ scrollSnapAlign: true,
28
29
  };
29
30
 
@@ -398,6 +398,7 @@ export default class Pressability {
398
398
  _touchActivateTime: ?number;
399
399
  _touchState: TouchState = 'NOT_RESPONDER';
400
400
  _longPressSent: boolean = false;
401
+ _tvPressInReceived: boolean = false;
401
402
 
402
403
  constructor(config: PressabilityConfig) {
403
404
  this.configure(config);
@@ -416,6 +417,7 @@ export default class Pressability {
416
417
  this._cancelLongPressDelayTimeout();
417
418
  this._cancelPressDelayTimeout();
418
419
  this._cancelPressOutDelayTimeout();
420
+ this._tvPressInReceived = false;
419
421
 
420
422
  // Ensure that, if any async event handlers are fired after unmount
421
423
  // due to a race, we don't call any configured callbacks.
@@ -443,6 +445,7 @@ export default class Pressability {
443
445
  return;
444
446
  }
445
447
 
448
+ this._tvPressInReceived = true;
446
449
  this._longPressSent = false;
447
450
 
448
451
  const {onPressIn, onLongPress} = this._config;
@@ -463,6 +466,10 @@ export default class Pressability {
463
466
  if (this._config.disabled === true) {
464
467
  return;
465
468
  }
469
+ if (!this._tvPressInReceived) {
470
+ return;
471
+ }
472
+ this._tvPressInReceived = false;
466
473
  this._cancelLongPressDelayTimeout();
467
474
  const {onPress, onLongPress, onPressOut, android_disableSound} =
468
475
  this._config;
@@ -23,7 +23,7 @@ NSDictionary* RCTGetReactNativeVersion(void)
23
23
  __rnVersion = @{
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(83),
26
- RCTVersionPatch: @(3),
26
+ RCTVersionPatch: @(4),
27
27
  RCTVersionPrerelease: @"0",
28
28
  };
29
29
  });
@@ -51,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
51
51
  @property (nonatomic, assign) BOOL snapToStart;
52
52
  @property (nonatomic, assign) BOOL snapToEnd;
53
53
  @property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
54
+ @property (nonatomic, assign) BOOL scrollSnapEnabled;
54
55
 
55
56
  /*
56
57
  * Makes `setContentOffset:` method no-op when given `block` is executed.
@@ -132,6 +132,9 @@
132
132
 
133
133
  - (id<UIScrollViewDelegate>)delegate
134
134
  {
135
+ if (_scrollSnapEnabled) {
136
+ return [super delegate];
137
+ }
135
138
  return _publicDelegate;
136
139
  }
137
140
 
@@ -30,6 +30,7 @@
30
30
  #import <React/RCTTVRemoteHandler.h>
31
31
  #import <React/RCTTVNavigationEventNotification.h>
32
32
  #import "React/RCTI18nUtil.h"
33
+ #import "RCTViewComponentView.h"
33
34
  #endif
34
35
 
35
36
  using namespace facebook::react;
@@ -40,6 +41,7 @@ static NSString *kOnScrollEndEvent = @"onScrollEnded";
40
41
 
41
42
  static const CGFloat kClippingLeeway = 44.0;
42
43
  static const float TV_DEFAULT_SWIPE_DURATION = 0.3;
44
+ static const CGPoint NO_PREFERRED_CONTENT_OFFSET = CGPointMake(CGFLOAT_MIN, CGFLOAT_MIN);
43
45
 
44
46
  static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(const ScrollViewProps &props)
45
47
  {
@@ -97,6 +99,8 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
97
99
  RCTScrollableProtocol,
98
100
  RCTEnhancedScrollViewOverridingDelegate>
99
101
 
102
+ @property (nonatomic, assign) CGPoint preferredContentOffset;
103
+
100
104
  @end
101
105
 
102
106
  @implementation RCTScrollViewComponentView {
@@ -172,6 +176,7 @@ RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInt
172
176
  _endDraggingSensitivityMultiplier = 1;
173
177
 
174
178
  _tvRemoteGestureRecognizers = [NSMutableDictionary new];
179
+ _preferredContentOffset = NO_PREFERRED_CONTENT_OFFSET;
175
180
  }
176
181
 
177
182
  return self;
@@ -465,6 +470,12 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
465
470
  scrollView.keyboardDismissMode = RCTUIKeyboardDismissModeFromProps(newScrollViewProps);
466
471
  }
467
472
 
473
+ #if TARGET_OS_TV
474
+ if (oldScrollViewProps.snapToAlignment != newScrollViewProps.snapToAlignment) {
475
+ scrollView.scrollSnapEnabled = newScrollViewProps.snapToAlignment == ScrollViewSnapToAlignment::Item;
476
+ }
477
+ #endif
478
+
468
479
  [super updateProps:props oldProps:oldProps];
469
480
  }
470
481
 
@@ -722,7 +733,10 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
722
733
  withVelocity:(CGPoint)velocity
723
734
  targetContentOffset:(inout CGPoint *)targetContentOffset
724
735
  {
725
- if (fabs(_endDraggingSensitivityMultiplier - 1) > 0.0001f) {
736
+ if (!CGPointEqualToPoint(self.preferredContentOffset, NO_PREFERRED_CONTENT_OFFSET))
737
+ {
738
+ *targetContentOffset = self.preferredContentOffset;
739
+ } else if (fabs(_endDraggingSensitivityMultiplier - 1) > 0.0001f) {
726
740
  if (targetContentOffset->y > 0) {
727
741
  const CGFloat travel = targetContentOffset->y - scrollView.contentOffset.y;
728
742
  targetContentOffset->y = scrollView.contentOffset.y + travel * _endDraggingSensitivityMultiplier;
@@ -1137,19 +1151,109 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1137
1151
  #pragma mark Apple TV swipe and focus handling
1138
1152
 
1139
1153
  #if TARGET_OS_TV
1154
+ // Focus marker helper: traverses view hierarchy to find scrollSnapAlign prop
1155
+ // Returns the view that has the property via the output parameter
1156
+ - (NSString *)findScrollSnapAlignInView:(UIView *)view foundView:(UIView **)outView
1157
+ {
1158
+ UIView *testView = view;
1159
+ UIView *snapTarget;
1160
+ NSString *marker;
1161
+
1162
+ while (testView && testView != self) {
1163
+ if (![testView isKindOfClass:RCTViewComponentView.class])
1164
+ {
1165
+ testView = [testView superview];
1166
+ continue;
1167
+ }
1168
+ RCTViewComponentView *componentView = (RCTViewComponentView *)testView;
1169
+
1170
+ const auto &viewProps = static_cast<const facebook::react::BaseViewProps &>(*componentView.props);
1171
+ if (viewProps.scrollSnapAlign.has_value() && !viewProps.scrollSnapAlign.value().empty()) {
1172
+ marker = [NSString stringWithUTF8String:viewProps.scrollSnapAlign.value().c_str()];
1173
+ snapTarget = componentView;
1174
+ }
1175
+
1176
+ testView = [testView superview];
1177
+ }
1178
+ *outView = snapTarget;
1179
+ return marker;
1180
+ }
1181
+
1182
+ - (void)_handleScrollSnapForFocusedView:(UIView *)focusedView
1183
+ {
1184
+ const auto &scrollProps = static_cast<const ScrollViewProps &>(*_props);
1185
+ UIView *snapAlignView = nil;
1186
+ NSString *scrollSnapAlign = [self findScrollSnapAlignInView:focusedView foundView:&snapAlignView];
1187
+ if (scrollSnapAlign == nil || snapAlignView == nil) {
1188
+ return;
1189
+ }
1190
+
1191
+ RCTEnhancedScrollView *scrollView = (RCTEnhancedScrollView *)_scrollView;
1192
+ CGRect focusedFrame = [snapAlignView convertRect:snapAlignView.bounds toView:_scrollView];
1193
+ CGFloat targetOffset;
1194
+ CGFloat snapToItemPadding = scrollProps.snapToItemPadding;
1195
+
1196
+ BOOL isHorizontalSnap = _scrollView.contentSize.width > self.frame.size.width;
1197
+ // Determine axis-specific properties
1198
+ CGFloat viewportSize, focusedOrigin, focusedSize, currentOffset, maxContentSize;
1199
+ if (isHorizontalSnap) {
1200
+ viewportSize = scrollView.bounds.size.width;
1201
+ focusedOrigin = focusedFrame.origin.x;
1202
+ focusedSize = focusedFrame.size.width;
1203
+ currentOffset = scrollView.contentOffset.x;
1204
+ maxContentSize = scrollView.contentSize.width;
1205
+ } else {
1206
+ viewportSize = scrollView.bounds.size.height;
1207
+ focusedOrigin = focusedFrame.origin.y;
1208
+ focusedSize = focusedFrame.size.height;
1209
+ currentOffset = scrollView.contentOffset.y;
1210
+ maxContentSize = scrollView.contentSize.height;
1211
+ }
1212
+ // Calculate target offset based on scrollSnapAlign (unified for both axes)
1213
+ if ([scrollSnapAlign isEqualToString:@"start"]) {
1214
+ targetOffset = focusedOrigin - snapToItemPadding;
1215
+ } else if ([scrollSnapAlign isEqualToString:@"center"]) {
1216
+ CGFloat viewportCenter = viewportSize / 2;
1217
+ CGFloat focusedCenter = focusedOrigin + (focusedSize / 2);
1218
+ targetOffset = focusedCenter - viewportCenter + (snapToItemPadding / 2);
1219
+ } else if ([scrollSnapAlign isEqualToString:@"end"]) {
1220
+ targetOffset = (focusedOrigin + focusedSize) - viewportSize + snapToItemPadding;
1221
+ } else {
1222
+ targetOffset = currentOffset;
1223
+ }
1224
+
1225
+ // Apply snap-to-interval if configured
1226
+ if (scrollView.snapToInterval > 0) {
1227
+ CGFloat interval = scrollView.snapToInterval;
1228
+ targetOffset = floor(targetOffset / interval) * interval;
1229
+ }
1230
+
1231
+ // Clamp to valid range
1232
+ CGFloat maxOffset = MAX(maxContentSize - viewportSize, 0);
1233
+ targetOffset = MAX(0, MIN(targetOffset, maxOffset));
1234
+ CGPoint targetContentOffset = isHorizontalSnap
1235
+ ? CGPointMake(targetOffset, scrollView.contentOffset.y)
1236
+ : CGPointMake(scrollView.contentOffset.x, targetOffset);
1237
+ self.preferredContentOffset = targetContentOffset;
1238
+ [_scrollView setContentOffset:targetContentOffset animated:YES];
1239
+ }
1240
+
1140
1241
  - (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context
1141
1242
  withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator
1142
1243
  {
1143
- if (context.previouslyFocusedView == context.nextFocusedView || !_props->isTVSelectable) {
1244
+ self.preferredContentOffset = NO_PREFERRED_CONTENT_OFFSET;
1245
+ const auto &scrollProps = static_cast<const ScrollViewProps &>(*_props);
1246
+ BOOL hasItemSnapAlignment = scrollProps.snapToAlignment == ScrollViewSnapToAlignment::Item;
1247
+ if (context.previouslyFocusedView == context.nextFocusedView || (!_props->isTVSelectable && !hasItemSnapAlignment)) {
1144
1248
  return;
1145
1249
  }
1146
- if (context.nextFocusedView == self) {
1250
+ if (_props->isTVSelectable && context.nextFocusedView == self) {
1147
1251
  [self becomeFirstResponder];
1148
1252
  [self addSwipeGestureRecognizers];
1149
1253
  // if we enter the scroll view from different view then block first touch event since it is the event that triggered the focus
1150
1254
  _blockFirstTouch = (unsigned long)context.focusHeading != 0;
1151
1255
  [self addArrowsListeners];
1152
- } else if (context.previouslyFocusedView == self) {
1256
+ } else if (_props->isTVSelectable && context.previouslyFocusedView == self) {
1153
1257
  [self removeArrowsListeners];
1154
1258
  [self removeSwipeGestureRecognizers];
1155
1259
  [self resignFirstResponder];
@@ -1171,6 +1275,8 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1171
1275
  [self scrollToHorizontalOffset:scrollView.contentSize.width];
1172
1276
  }
1173
1277
  }
1278
+ } else if ([context.nextFocusedView isDescendantOfView:_scrollView]) {
1279
+ [self _handleScrollSnapForFocusedView:context.nextFocusedView];
1174
1280
  }
1175
1281
  }
1176
1282
 
@@ -80,6 +80,8 @@ NS_ASSUME_NONNULL_BEGIN
80
80
  @property(nonatomic, nullable) UIFocusGuide *focusGuideLeft;
81
81
  @property(nonatomic, nullable) UIFocusGuide *focusGuideRight;
82
82
  @property(nonatomic, nullable, strong) RCTTVRemoteSelectHandler *tvRemoteSelectHandler;
83
+
84
+ - (NSString *)scrollSnapAlign;
83
85
  #endif
84
86
 
85
87
  /**
@@ -87,6 +87,7 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
87
87
  BOOL _trapFocusRight;
88
88
  NSArray* _focusDestinations;
89
89
  id<UIFocusItem> _previouslyFocusedItem;
90
+ NSString *_scrollSnapAlign;
90
91
  RCTSwiftUIContainerViewWrapper *_swiftUIWrapper;
91
92
  BOOL _shouldFocusOnMount;
92
93
  }
@@ -441,6 +442,13 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
441
442
  _motionEffectsAdded = NO;
442
443
  }
443
444
 
445
+ - (NSString *)scrollSnapAlign
446
+ {
447
+ #if TARGET_OS_TV
448
+ return _scrollSnapAlign;
449
+ #endif
450
+ return nil;
451
+ }
444
452
 
445
453
  - (BOOL)isTVFocusGuide
446
454
  {
@@ -1170,6 +1178,15 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
1170
1178
  _trapFocusDown = newViewProps.trapFocusDown;
1171
1179
  _trapFocusLeft = newViewProps.trapFocusLeft;
1172
1180
  _trapFocusRight = newViewProps.trapFocusRight;
1181
+
1182
+ // `scrollSnapAlign`
1183
+ if (oldViewProps.scrollSnapAlign != newViewProps.scrollSnapAlign) {
1184
+ if (newViewProps.scrollSnapAlign.has_value()) {
1185
+ _scrollSnapAlign = [[NSString alloc] initWithUTF8String:newViewProps.scrollSnapAlign.value().c_str()];
1186
+ } else {
1187
+ _scrollSnapAlign = nil;
1188
+ }
1189
+ }
1173
1190
  #endif
1174
1191
 
1175
1192
  _needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;
@@ -1,4 +1,4 @@
1
- VERSION_NAME=0.83.3-0
1
+ VERSION_NAME=0.83.4-0
2
2
  react.internal.publishingGroup=io.github.react-native-tvos
3
3
  react.internal.hermesPublishingGroup=com.facebook.hermes
4
4
 
@@ -14,7 +14,7 @@ public object ReactNativeVersion {
14
14
  public val VERSION: Map<String, Any?> = mapOf(
15
15
  "major" to 0,
16
16
  "minor" to 83,
17
- "patch" to 3,
17
+ "patch" to 4,
18
18
  "prerelease" to "0"
19
19
  )
20
20
  }
@@ -10,6 +10,7 @@ package com.facebook.react.views.scroll;
10
10
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_CENTER;
11
11
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED;
12
12
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
13
+ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_ITEM;
13
14
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
14
15
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView;
15
16
 
@@ -134,6 +135,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
134
135
  private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper;
135
136
  private int mFadingEdgeLengthStart = 0;
136
137
  private int mFadingEdgeLengthEnd = 0;
138
+ private int mSnapToItemPadding;
137
139
 
138
140
  public ReactHorizontalScrollView(Context context) {
139
141
  this(context, null);
@@ -382,6 +384,10 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
382
384
  invalidate();
383
385
  }
384
386
 
387
+ public void setSnapToItemPadding(int snapToItemPadding) {
388
+ mSnapToItemPadding = snapToItemPadding;
389
+ }
390
+
385
391
  @Override
386
392
  protected float getLeftFadingEdgeStrength() {
387
393
  float max = Math.max(mFadingEdgeLengthStart, mFadingEdgeLengthEnd);
@@ -538,6 +544,40 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
538
544
  }
539
545
  }
540
546
 
547
+ /**
548
+ * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
549
+ * Returns true if snap scrolling was performed, false otherwise.
550
+ */
551
+ private boolean tryScrollSnapToChild(View focused) {
552
+ if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
553
+ return false;
554
+ }
555
+
556
+ kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
557
+ if (result == null) {
558
+ return false;
559
+ }
560
+
561
+ View snapTarget = result.getFirst();
562
+ String alignment = result.getSecond();
563
+
564
+ Rect rect = new Rect();
565
+ snapTarget.getDrawingRect(rect);
566
+ offsetDescendantRectToMyCoords(snapTarget, rect);
567
+
568
+ int viewportWidth = getWidth() - getPaddingLeft() - getPaddingRight();
569
+ int maxScrollX = Math.max(0, computeHorizontalScrollRange() - getWidth());
570
+
571
+ Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
572
+ rect.left, rect.right, viewportWidth, alignment, mSnapInterval, mSnapToItemPadding, maxScrollX);
573
+ if (targetOffset == null) {
574
+ return false;
575
+ }
576
+
577
+ reactSmoothScrollTo(targetOffset, getScrollY());
578
+ return true;
579
+ }
580
+
541
581
  /**
542
582
  * Since ReactHorizontalScrollView handles layout changes on JS side, it does not call
543
583
  * super.onlayout due to which mIsLayoutDirty flag in HorizontalScrollView remains true and
@@ -547,8 +587,12 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
547
587
  */
548
588
  @Override
549
589
  public void requestChildFocus(View child, View focused) {
550
- if (focused != null && !mPagingEnabled) {
551
- scrollToChild(focused);
590
+ if (focused != null) {
591
+ if (!tryScrollSnapToChild(focused)) {
592
+ if (!mPagingEnabled) {
593
+ scrollToChild(focused);
594
+ }
595
+ }
552
596
  }
553
597
  requestChildFocusWithoutScroll(child, focused);
554
598
  }
@@ -832,7 +876,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
832
876
  && (mPagingEnabled
833
877
  || mSnapInterval != 0
834
878
  || mSnapOffsets != null
835
- || mSnapToAlignment != SNAP_ALIGNMENT_DISABLED)) {
879
+ || (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM))) {
836
880
  // Cancel any pending runnable and reschedule
837
881
  if (mPostTouchRunnable != null) {
838
882
  removeCallbacks(mPostTouchRunnable);
@@ -1280,7 +1324,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
1280
1324
  }
1281
1325
 
1282
1326
  // pagingEnabled only allows snapping one interval at a time
1283
- if (mSnapInterval == 0 && mSnapOffsets == null && mSnapToAlignment == SNAP_ALIGNMENT_DISABLED) {
1327
+ if (mSnapInterval == 0 && mSnapOffsets == null && (mSnapToAlignment == SNAP_ALIGNMENT_DISABLED || mSnapToAlignment == SNAP_ALIGNMENT_ITEM)) {
1284
1328
  smoothScrollAndSnap(velocityX);
1285
1329
  return;
1286
1330
  }
@@ -1324,7 +1368,7 @@ public class ReactHorizontalScrollView extends HorizontalScrollView
1324
1368
  }
1325
1369
  }
1326
1370
  }
1327
- } else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED) {
1371
+ } else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
1328
1372
  if (mSnapInterval > 0) {
1329
1373
  double ratio = (double) targetOffset / mSnapInterval;
1330
1374
  smallerOffset =
@@ -157,6 +157,13 @@ constructor(private val fpsListener: FpsListener? = null) :
157
157
  view.setSnapToEnd(snapToEnd)
158
158
  }
159
159
 
160
+ @ReactProp(name = "snapToItemPadding")
161
+ public fun setSnapToItemPadding(view: ReactHorizontalScrollView, value: Float) {
162
+ val density = getDisplayMetricDensity()
163
+ val px = (value * density).toInt()
164
+ view.setSnapToItemPadding(px)
165
+ }
166
+
160
167
  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
161
168
  public fun setRemoveClippedSubviews(
162
169
  view: ReactHorizontalScrollView,
@@ -10,6 +10,7 @@ package com.facebook.react.views.scroll;
10
10
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_CENTER;
11
11
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_DISABLED;
12
12
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_END;
13
+ import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_ITEM;
13
14
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.SNAP_ALIGNMENT_START;
14
15
  import static com.facebook.react.views.scroll.ReactScrollViewHelper.findNextFocusableView;
15
16
 
@@ -132,6 +133,7 @@ public class ReactScrollView extends ScrollView
132
133
  private @Nullable MaintainVisibleScrollPositionHelper mMaintainVisibleContentPositionHelper;
133
134
  private int mFadingEdgeLengthStart;
134
135
  private int mFadingEdgeLengthEnd;
136
+ private int mSnapToItemPadding;
135
137
 
136
138
  public ReactScrollView(Context context) {
137
139
  this(context, null);
@@ -352,6 +354,10 @@ public class ReactScrollView extends ScrollView
352
354
  invalidate();
353
355
  }
354
356
 
357
+ public void setSnapToItemPadding(int snapToItemPadding) {
358
+ mSnapToItemPadding = snapToItemPadding;
359
+ }
360
+
355
361
  @Override
356
362
  protected float getTopFadingEdgeStrength() {
357
363
  float max = Math.max(mFadingEdgeLengthStart, mFadingEdgeLengthEnd);
@@ -499,6 +505,40 @@ public class ReactScrollView extends ScrollView
499
505
  return nextFocus;
500
506
  }
501
507
 
508
+ /**
509
+ * Attempts to scroll-snap to the focused child based on snapToAlignment/scrollSnapAlign.
510
+ * Returns true if snap scrolling was performed, false otherwise.
511
+ */
512
+ private boolean tryScrollSnapToChild(View focused) {
513
+ if (mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
514
+ return false;
515
+ }
516
+
517
+ kotlin.Pair<View, String> result = ReactScrollViewHelper.findScrollSnapAlign(focused, this);
518
+ if (result == null) {
519
+ return false;
520
+ }
521
+
522
+ View snapTarget = result.getFirst();
523
+ String alignment = result.getSecond();
524
+
525
+ Rect rect = new Rect();
526
+ snapTarget.getDrawingRect(rect);
527
+ offsetDescendantRectToMyCoords(snapTarget, rect);
528
+
529
+ int viewportHeight = getHeight() - getPaddingTop() - getPaddingBottom();
530
+ int maxScrollY = getMaxScrollY();
531
+
532
+ Integer targetOffset = ReactScrollViewHelper.computeScrollSnapOffset(
533
+ rect.top, rect.bottom, viewportHeight, alignment, mSnapInterval, mSnapToItemPadding, maxScrollY);
534
+ if (targetOffset == null) {
535
+ return false;
536
+ }
537
+
538
+ reactSmoothScrollTo(getScrollX(), targetOffset);
539
+ return true;
540
+ }
541
+
502
542
  /**
503
543
  * Since ReactScrollView handles layout changes on JS side, it does not call super.onlayout due to
504
544
  * which mIsLayoutDirty flag in ScrollView remains true and prevents scrolling to child when
@@ -509,7 +549,9 @@ public class ReactScrollView extends ScrollView
509
549
  @Override
510
550
  public void requestChildFocus(View child, View focused) {
511
551
  if (focused != null) {
512
- scrollToChild(focused);
552
+ if (!tryScrollSnapToChild(focused)) {
553
+ scrollToChild(focused);
554
+ }
513
555
  }
514
556
  requestChildFocusWithoutScroll(child, focused);
515
557
  }
@@ -686,7 +728,7 @@ public class ReactScrollView extends ScrollView
686
728
  && (mPagingEnabled
687
729
  || mSnapInterval != 0
688
730
  || mSnapOffsets != null
689
- || mSnapToAlignment != SNAP_ALIGNMENT_DISABLED)) {
731
+ || (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM))) {
690
732
  // Cancel any pending post-touch runnable and reschedule
691
733
  if (mPostTouchRunnable != null) {
692
734
  removeCallbacks(mPostTouchRunnable);
@@ -1043,7 +1085,7 @@ public class ReactScrollView extends ScrollView
1043
1085
  }
1044
1086
 
1045
1087
  // pagingEnabled only allows snapping one interval at a time
1046
- if (mSnapInterval == 0 && mSnapOffsets == null && mSnapToAlignment == SNAP_ALIGNMENT_DISABLED) {
1088
+ if (mSnapInterval == 0 && mSnapOffsets == null && (mSnapToAlignment == SNAP_ALIGNMENT_DISABLED || mSnapToAlignment == SNAP_ALIGNMENT_ITEM)) {
1047
1089
  smoothScrollAndSnap(velocityY);
1048
1090
  return;
1049
1091
  }
@@ -1082,7 +1124,7 @@ public class ReactScrollView extends ScrollView
1082
1124
  }
1083
1125
  }
1084
1126
 
1085
- } else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED) {
1127
+ } else if (mSnapToAlignment != SNAP_ALIGNMENT_DISABLED && mSnapToAlignment != SNAP_ALIGNMENT_ITEM) {
1086
1128
  if (mSnapInterval > 0) {
1087
1129
  double ratio = (double) targetOffset / mSnapInterval;
1088
1130
  smallerOffset =
@@ -11,6 +11,7 @@ import android.animation.Animator
11
11
  import android.animation.ValueAnimator
12
12
  import android.content.Context
13
13
  import android.graphics.Point
14
+ import android.graphics.Rect
14
15
  import android.os.Build
15
16
  import android.view.View
16
17
  import android.view.ViewGroup
@@ -32,6 +33,7 @@ import com.facebook.react.uimanager.StateWrapper
32
33
  import com.facebook.react.uimanager.UIManagerHelper
33
34
  import com.facebook.react.uimanager.common.UIManagerType
34
35
  import com.facebook.react.uimanager.common.ViewUtil
36
+ import com.facebook.react.views.view.ReactViewGroup
35
37
  import java.lang.ref.WeakReference
36
38
  import java.util.concurrent.CopyOnWriteArrayList
37
39
  import kotlin.math.abs
@@ -54,6 +56,7 @@ public object ReactScrollViewHelper {
54
56
  public const val SNAP_ALIGNMENT_START: Int = 1
55
57
  public const val SNAP_ALIGNMENT_CENTER: Int = 2
56
58
  public const val SNAP_ALIGNMENT_END: Int = 3
59
+ public const val SNAP_ALIGNMENT_ITEM: Int = 4
57
60
 
58
61
  // Support global native listeners for scroll events
59
62
  private val scrollListeners = CopyOnWriteArrayList<WeakReference<ScrollListener>>()
@@ -209,6 +212,8 @@ public object ReactScrollViewHelper {
209
212
  SNAP_ALIGNMENT_CENTER
210
213
  } else if ("end" == alignment) {
211
214
  SNAP_ALIGNMENT_END
215
+ } else if ("item".equals(alignment, ignoreCase = true)) {
216
+ SNAP_ALIGNMENT_ITEM
212
217
  } else {
213
218
  FLog.w(ReactConstants.TAG, "wrong snap alignment value: $alignment")
214
219
  SNAP_ALIGNMENT_DISABLED
@@ -569,6 +574,70 @@ public object ReactScrollViewHelper {
569
574
  return host.findViewById(nextFocusableViewId)
570
575
  }
571
576
 
577
+ /**
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.
580
+ *
581
+ * Shared by [ReactScrollView] and [ReactHorizontalScrollView].
582
+ */
583
+ @JvmStatic
584
+ public fun findScrollSnapAlign(focused: View, scrollView: ViewGroup): Pair<View, String>? {
585
+ var view: View? = focused
586
+ var snapTarget: View? = null
587
+ var alignment: String? = null
588
+ while (view != null && view !== scrollView) {
589
+ if (view is ReactViewGroup) {
590
+ val snap = view.scrollSnapAlign
591
+ if (snap != null) {
592
+ alignment = snap
593
+ snapTarget = view
594
+ }
595
+ }
596
+ val parent = view.parent
597
+ view = if (parent is View) parent else null
598
+ }
599
+ return if (alignment != null && snapTarget != null) Pair(snapTarget, alignment) else null
600
+ }
601
+
602
+ /**
603
+ * Computes the target scroll offset for scroll-snap based on the focused view's position,
604
+ * alignment, snap interval, scroll padding, and maximum scroll range.
605
+ *
606
+ * Returns the clamped target offset, or null if the alignment is unknown.
607
+ *
608
+ * Shared by [ReactScrollView] and [ReactHorizontalScrollView].
609
+ *
610
+ * @param focusedStart the start coordinate of the snap target in scroll view coordinates
611
+ * @param focusedEnd the end coordinate of the snap target in scroll view coordinates
612
+ * @param viewportSize the visible viewport size on the scroll axis
613
+ * @param alignment the scrollSnapAlign value ("start", "center", or "end")
614
+ * @param snapInterval the snap interval (0 if not set)
615
+ * @param snapToItemPadding the snap-to-item padding value
616
+ * @param maxScrollOffset the maximum scroll offset for clamping
617
+ */
618
+ @JvmStatic
619
+ public fun computeScrollSnapOffset(
620
+ focusedStart: Int,
621
+ focusedEnd: Int,
622
+ viewportSize: Int,
623
+ alignment: String,
624
+ snapInterval: Int,
625
+ snapToItemPadding: Int,
626
+ maxScrollOffset: Int,
627
+ ): Int? {
628
+ val focusedCenter = (focusedStart + focusedEnd) / 2
629
+ var targetOffset = when (alignment) {
630
+ "start" -> focusedStart - snapToItemPadding
631
+ "center" -> focusedCenter - (viewportSize / 2) + (snapToItemPadding / 2)
632
+ "end" -> focusedEnd - viewportSize + snapToItemPadding
633
+ else -> return null
634
+ }
635
+ if (snapInterval > 0) {
636
+ targetOffset = (Math.floor(targetOffset.toDouble() / snapInterval) * snapInterval).toInt()
637
+ }
638
+ return Math.max(0, Math.min(targetOffset, maxScrollOffset))
639
+ }
640
+
572
641
  @JvmStatic
573
642
  public fun resolveAbsoluteDirection(
574
643
  @FocusRealDirection direction: Int,
@@ -141,6 +141,13 @@ constructor(private val fpsListener: FpsListener? = null) :
141
141
  view.setSnapToEnd(snapToEnd)
142
142
  }
143
143
 
144
+ @ReactProp(name = "snapToItemPadding")
145
+ public fun setSnapToItemPadding(view: ReactScrollView, value: Float) {
146
+ val density = getDisplayMetricDensity()
147
+ val px = (value * density).toInt()
148
+ view.setSnapToItemPadding(px)
149
+ }
150
+
144
151
  @ReactProp(name = ReactClippingViewGroupHelper.PROP_REMOVE_CLIPPED_SUBVIEWS)
145
152
  public fun setRemoveClippedSubviews(view: ReactScrollView, removeClippedSubviews: Boolean) {
146
153
  view.removeClippedSubviews = removeClippedSubviews
@@ -173,6 +173,7 @@ public open class ReactViewGroup public constructor(context: Context?) :
173
173
  private var childrenLayoutChangeListener: ChildrenLayoutChangeListener? = null
174
174
  private var onInterceptTouchEventListener: OnInterceptTouchEventListener? = null
175
175
  private var needsOffscreenAlphaCompositing = false
176
+ public var scrollSnapAlign: String? = null
176
177
  private var backfaceOpacity = 0f
177
178
  private var backfaceVisible = false
178
179
  private var childrenRemovedWhileTransitioning: MutableSet<Int>? = null
@@ -190,6 +190,10 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
190
190
  view.setTrapFocusRight(enabled)
191
191
  }
192
192
 
193
+ @ReactProp(name = "scrollSnapAlign")
194
+ public open fun setScrollSnapAlign(view: ReactViewGroup, value: String?) {
195
+ view.scrollSnapAlign = value
196
+ }
193
197
 
194
198
  @ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage")
195
199
  public open fun setBackgroundImage(view: ReactViewGroup, backgroundImage: ReadableArray?) {
@@ -14,14 +14,14 @@
14
14
 
15
15
  #define REACT_NATIVE_VERSION_MAJOR 0
16
16
  #define REACT_NATIVE_VERSION_MINOR 83
17
- #define REACT_NATIVE_VERSION_PATCH 3
17
+ #define REACT_NATIVE_VERSION_PATCH 4
18
18
 
19
19
  namespace facebook::react {
20
20
 
21
21
  constexpr struct {
22
22
  int32_t Major = 0;
23
23
  int32_t Minor = 83;
24
- int32_t Patch = 3;
24
+ int32_t Patch = 4;
25
25
  std::string_view Prerelease = "0";
26
26
  } ReactNativeVersion;
27
27
 
@@ -32,6 +32,15 @@ BaseScrollViewProps::BaseScrollViewProps(
32
32
  "showsScrollIndex",
33
33
  sourceProps.showsScrollIndex,
34
34
  {})),
35
+ snapToItemPadding(
36
+ ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
37
+ ? sourceProps.snapToItemPadding
38
+ : convertRawProp(
39
+ context,
40
+ rawProps,
41
+ "snapToItemPadding",
42
+ sourceProps.snapToItemPadding,
43
+ (Float)0)),
35
44
  #endif
36
45
  alwaysBounceHorizontal(
37
46
  ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
@@ -400,6 +409,7 @@ void BaseScrollViewProps::setProp(
400
409
  switch (hash) {
401
410
  #if TARGET_OS_TV
402
411
  RAW_SET_PROP_SWITCH_CASE_BASIC(showsScrollIndex);
412
+ RAW_SET_PROP_SWITCH_CASE_BASIC(snapToItemPadding);
403
413
  #endif
404
414
  RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceHorizontal);
405
415
  RAW_SET_PROP_SWITCH_CASE_BASIC(alwaysBounceVertical);
@@ -31,6 +31,7 @@ class BaseScrollViewProps : public ViewProps {
31
31
 
32
32
  #if TARGET_OS_TV
33
33
  bool showsScrollIndex{true};
34
+ Float snapToItemPadding{0};
34
35
  #endif
35
36
  bool alwaysBounceHorizontal{};
36
37
  bool alwaysBounceVertical{};
@@ -30,6 +30,10 @@ inline void fromRawValue(const PropsParserContext &context, const RawValue &valu
30
30
  result = ScrollViewSnapToAlignment::End;
31
31
  return;
32
32
  }
33
+ if (string == "item") {
34
+ result = ScrollViewSnapToAlignment::Item;
35
+ return;
36
+ }
33
37
  abort();
34
38
  }
35
39
 
@@ -117,6 +121,8 @@ inline std::string toString(const ScrollViewSnapToAlignment &value)
117
121
  return "center";
118
122
  case ScrollViewSnapToAlignment::End:
119
123
  return "end";
124
+ case ScrollViewSnapToAlignment::Item:
125
+ return "item";
120
126
  }
121
127
  }
122
128
 
@@ -65,7 +65,16 @@ HostPlatformScrollViewProps::HostPlatformScrollViewProps(
65
65
  rawProps,
66
66
  "endFillColor",
67
67
  sourceProps.endFillColor,
68
- clearColor())) {}
68
+ clearColor())),
69
+ snapToItemPadding(
70
+ ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
71
+ ? sourceProps.snapToItemPadding
72
+ : convertRawProp(
73
+ context,
74
+ rawProps,
75
+ "snapToItemPadding",
76
+ sourceProps.snapToItemPadding,
77
+ {})) {}
69
78
 
70
79
  void HostPlatformScrollViewProps::setProp(
71
80
  const PropsParserContext& context,
@@ -85,6 +94,7 @@ void HostPlatformScrollViewProps::setProp(
85
94
  RAW_SET_PROP_SWITCH_CASE_BASIC(fadingEdgeLength);
86
95
  RAW_SET_PROP_SWITCH_CASE_BASIC(overScrollMode);
87
96
  RAW_SET_PROP_SWITCH_CASE_BASIC(endFillColor);
97
+ RAW_SET_PROP_SWITCH_CASE_BASIC(snapToItemPadding);
88
98
  }
89
99
  }
90
100
 
@@ -327,6 +337,9 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
327
337
  case ScrollViewSnapToAlignment::End:
328
338
  result["snapToAlignment"] = "end";
329
339
  break;
340
+ case ScrollViewSnapToAlignment::Item:
341
+ result["snapToAlignment"] = "item";
342
+ break;
330
343
  }
331
344
  }
332
345
 
@@ -396,6 +409,10 @@ folly::dynamic HostPlatformScrollViewProps::getDiffProps(
396
409
  result["endFillColor"] = *endFillColor;
397
410
  }
398
411
 
412
+ if (snapToItemPadding != oldProps->snapToItemPadding) {
413
+ result["snapToItemPadding"] = snapToItemPadding;
414
+ }
415
+
399
416
  return result;
400
417
  }
401
418
 
@@ -33,6 +33,8 @@ class HostPlatformScrollViewProps : public BaseScrollViewProps {
33
33
  std::string overScrollMode{"auto"};
34
34
  SharedColor endFillColor{clearColor()};
35
35
 
36
+ Float snapToItemPadding{0};
37
+
36
38
  #pragma mark - DebugStringConvertible
37
39
 
38
40
  #if RN_DEBUG_STRING_CONVERTIBLE
@@ -12,7 +12,7 @@
12
12
 
13
13
  namespace facebook::react {
14
14
 
15
- enum class ScrollViewSnapToAlignment { Start, Center, End };
15
+ enum class ScrollViewSnapToAlignment { Start, Center, End, Item };
16
16
 
17
17
  enum class ScrollViewIndicatorStyle { Default, Black, White };
18
18
 
@@ -447,7 +447,16 @@ BaseViewProps::BaseViewProps(
447
447
  rawProps,
448
448
  "removeClippedSubviews",
449
449
  sourceProps.removeClippedSubviews,
450
- false)) {}
450
+ false))
451
+ #if TARGET_OS_TV
452
+ ,scrollSnapAlign(ReactNativeFeatureFlags::enableCppPropsIteratorSetter() ? sourceProps.scrollSnapAlign : convertRawProp(
453
+ context,
454
+ rawProps,
455
+ "scrollSnapAlign",
456
+ sourceProps.scrollSnapAlign,
457
+ {}))
458
+ #endif
459
+ {}
451
460
 
452
461
  #define VIEW_EVENT_CASE(eventType) \
453
462
  case CONSTEXPR_RAW_PROPS_KEY_HASH("on" #eventType): { \
@@ -518,6 +527,7 @@ void BaseViewProps::setProp(
518
527
  RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusDown);
519
528
  RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusLeft);
520
529
  RAW_SET_PROP_SWITCH_CASE_BASIC(trapFocusRight);
530
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
521
531
  #endif
522
532
  // events field
523
533
  VIEW_EVENT_CASE(PointerEnter);
@@ -124,6 +124,10 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
124
124
 
125
125
  bool removeClippedSubviews{false};
126
126
 
127
+ #if TARGET_OS_TV
128
+ std::optional<std::string> scrollSnapAlign;
129
+ #endif
130
+
127
131
  #pragma mark - Convenience Methods
128
132
 
129
133
  CascadedBorderWidths getBorderWidths() const;
@@ -99,6 +99,15 @@ HostPlatformViewProps::HostPlatformViewProps(
99
99
  "trapFocusRight",
100
100
  sourceProps.trapFocusRight,
101
101
  false)),
102
+ scrollSnapAlign(
103
+ ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
104
+ ? sourceProps.scrollSnapAlign
105
+ : convertRawProp(
106
+ context,
107
+ rawProps,
108
+ "scrollSnapAlign",
109
+ sourceProps.scrollSnapAlign,
110
+ {})),
102
111
  needsOffscreenAlphaCompositing(
103
112
  ReactNativeFeatureFlags::enableCppPropsIteratorSetter()
104
113
  ? sourceProps.needsOffscreenAlphaCompositing
@@ -201,6 +210,7 @@ void HostPlatformViewProps::setProp(
201
210
  RAW_SET_PROP_SWITCH_CASE(nativeForeground, "nativeForegroundAndroid");
202
211
  RAW_SET_PROP_SWITCH_CASE_BASIC(focusable);
203
212
  RAW_SET_PROP_SWITCH_CASE_BASIC(hasTVPreferredFocus);
213
+ RAW_SET_PROP_SWITCH_CASE_BASIC(scrollSnapAlign);
204
214
  RAW_SET_PROP_SWITCH_CASE_BASIC(needsOffscreenAlphaCompositing);
205
215
  RAW_SET_PROP_SWITCH_CASE_BASIC(renderToHardwareTextureAndroid);
206
216
  RAW_SET_PROP_SWITCH_CASE_BASIC(screenReaderFocusable);
@@ -1105,6 +1115,14 @@ folly::dynamic HostPlatformViewProps::getDiffProps(
1105
1115
  }
1106
1116
  }
1107
1117
 
1118
+ if (scrollSnapAlign != oldProps->scrollSnapAlign) {
1119
+ if (scrollSnapAlign.has_value()) {
1120
+ result["scrollSnapAlign"] = scrollSnapAlign.value();
1121
+ } else {
1122
+ result["scrollSnapAlign"] = folly::dynamic(nullptr);
1123
+ }
1124
+ }
1125
+
1108
1126
  return result;
1109
1127
  }
1110
1128
 
@@ -47,6 +47,7 @@ class HostPlatformViewProps : public BaseViewProps {
47
47
  bool trapFocusDown{false};
48
48
  bool trapFocusLeft{false};
49
49
  bool trapFocusRight{false};
50
+ std::optional<std::string> scrollSnapAlign{};
50
51
 
51
52
  bool needsOffscreenAlphaCompositing{false};
52
53
  bool renderToHardwareTextureAndroid{false};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-tvos",
3
- "version": "0.83.3-0",
3
+ "version": "0.83.4-0",
4
4
  "description": "A framework for building native apps using React",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -165,12 +165,12 @@
165
165
  },
166
166
  "dependencies": {
167
167
  "@jest/create-cache-key-function": "^29.7.0",
168
- "@react-native/assets-registry": "0.83.3",
169
- "@react-native/codegen": "0.83.3",
170
- "@react-native/community-cli-plugin": "0.83.3",
171
- "@react-native/gradle-plugin": "0.83.3",
172
- "@react-native/js-polyfills": "0.83.3",
173
- "@react-native/normalize-colors": "0.83.3",
168
+ "@react-native/assets-registry": "0.83.4",
169
+ "@react-native/codegen": "0.83.4",
170
+ "@react-native/community-cli-plugin": "0.83.4",
171
+ "@react-native/gradle-plugin": "0.83.4",
172
+ "@react-native/js-polyfills": "0.83.4",
173
+ "@react-native/normalize-colors": "0.83.4",
174
174
  "abort-controller": "^3.0.0",
175
175
  "anser": "^1.4.9",
176
176
  "ansi-regex": "^5.0.0",
@@ -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.83.3-0"
201
+ "@react-native-tvos/virtualized-lists": "0.83.4-0"
202
202
  },
203
203
  "codegenConfig": {
204
204
  "libraries": [
@@ -4,7 +4,24 @@ import type { View, ScrollViewProps, HostComponent, EventSubscription, TVParalla
4
4
  declare module 'react-native' {
5
5
  export type FocusDestination = null | number | React.Component<any, any> | React.ComponentClass<any>;
6
6
 
7
+ interface ScrollViewProps {
8
+ /**
9
+ * Padding applied when snapping to items using `snapToAlignment="item"`.
10
+ * Set on the parent scroll view, not directly on child items.
11
+ * Only used when `snapToAlignment` is set to `'item'`.
12
+ */
13
+ snapToItemPadding?: number | undefined;
14
+ }
15
+
7
16
  interface ViewProps {
17
+ /**
18
+ * Controls the scroll snap alignment when this view receives focus inside a ScrollView.
19
+ * Used with snapToAlignment="item" on the parent ScrollView.
20
+ *
21
+ * @platform tv
22
+ */
23
+ scrollSnapAlign?: 'start' | 'center' | 'end' | undefined;
24
+
8
25
  /**
9
26
  * Android TV only prop
10
27
  */
@@ -54,8 +71,8 @@ declare module 'react-native' {
54
71
 
55
72
  /**
56
73
  * Hardware event received from TVEventHandler
57
- *
58
- * Note: The 'blur' and 'focus' event types are deprecated and will no longer be
74
+ *
75
+ * Note: The 'blur' and 'focus' event types are deprecated and will no longer be
59
76
  * emitted on new architecture (Fabric). Use onFocus/onBlur component props instead.
60
77
  * See: https://github.com/react-native-tvos/react-native-tvos/issues/1037
61
78
  */