react-native-tvos 0.77.0-0rc5 → 0.77.2-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 (80) hide show
  1. package/Libraries/Components/TV/TVFocusGuideView.js +0 -1
  2. package/Libraries/Components/View/ViewPropTypes.d.ts +4 -2
  3. package/Libraries/Core/ReactNativeVersion.js +2 -2
  4. package/Libraries/Core/setUpDeveloperTools.js +2 -3
  5. package/Libraries/Image/Image.android.js +2 -0
  6. package/Libraries/Image/ImageViewNativeComponent.js +3 -4
  7. package/Libraries/Image/RCTImageLoader.mm +9 -1
  8. package/Libraries/Pressability/Pressability.js +2 -2
  9. package/Libraries/Text/TextInput/RCTBaseTextInputView.mm +1 -1
  10. package/Libraries/Utilities/HMRClient.js +0 -28
  11. package/Libraries/Utilities/HMRClientProdShim.js +0 -1
  12. package/React/Base/RCTConvert.mm +3 -1
  13. package/React/Base/RCTVersion.m +2 -2
  14. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +85 -31
  15. package/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +1 -7
  16. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +5 -2
  17. package/React/Views/RCTTVView.m +5 -2
  18. package/React/Views/ScrollView/RCTScrollView.m +63 -26
  19. package/ReactAndroid/api/ReactAndroid.api +3 -0
  20. package/ReactAndroid/cmake-utils/ReactNative-application.cmake +13 -3
  21. package/ReactAndroid/gradle.properties +3 -3
  22. package/ReactAndroid/src/main/AndroidManifest.xml +1 -1
  23. package/ReactAndroid/src/main/java/com/facebook/react/ReactActivity.java +8 -0
  24. package/ReactAndroid/src/main/java/com/facebook/react/ReactActivityDelegate.java +12 -1
  25. package/ReactAndroid/src/main/java/com/facebook/react/ReactDelegate.java +37 -0
  26. package/ReactAndroid/src/main/java/com/facebook/react/ReactFragment.java +6 -1
  27. package/ReactAndroid/src/main/java/com/facebook/react/bridge/BaseJavaModule.java +1 -1
  28. package/ReactAndroid/src/main/java/com/facebook/react/devsupport/DebugOverlayController.java +7 -2
  29. package/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +8 -2
  30. package/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +15 -8
  31. package/ReactAndroid/src/main/java/com/facebook/react/modules/i18nmanager/I18nManagerModule.kt +6 -1
  32. package/ReactAndroid/src/main/java/com/facebook/react/modules/permissions/PermissionsModule.kt +15 -4
  33. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java +2 -2
  34. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/BackgroundStyleApplicator.kt +21 -41
  35. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BackgroundDrawable.kt +0 -1
  36. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/BorderDrawable.kt +0 -1
  37. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CSSBackgroundDrawable.java +0 -1
  38. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/CompositeBackgroundDrawable.kt +141 -158
  39. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/Drawable.kt +17 -0
  40. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/LayerDrawable.kt +19 -0
  41. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutlineDrawable.kt +0 -1
  42. package/ReactAndroid/src/main/java/com/facebook/react/views/image/ReactImageManager.kt +1 -1
  43. package/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +22 -2
  44. package/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java +10 -2
  45. package/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +36 -27
  46. package/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java +3 -3
  47. package/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +16 -2
  48. package/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +38 -31
  49. package/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java +4 -2
  50. package/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java +13 -2
  51. package/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputShadowNode.java +12 -2
  52. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactDrawableHelper.java +2 -1
  53. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +1 -0
  54. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.kt +15 -0
  55. package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
  56. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTInteropTurboModule.mm +22 -4
  57. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.h +5 -0
  58. package/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm +51 -22
  59. package/ReactCommon/react/renderer/attributedstring/TextAttributes.cpp +6 -0
  60. package/ReactCommon/react/renderer/attributedstring/TextAttributes.h +2 -0
  61. package/ReactCommon/react/renderer/attributedstring/conversions.h +5 -0
  62. package/ReactCommon/react/renderer/components/text/BaseTextProps.cpp +12 -0
  63. package/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +2 -3
  64. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +24 -3
  65. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +6 -46
  66. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +4 -5
  67. package/gradle/libs.versions.toml +1 -1
  68. package/package.json +15 -12
  69. package/react-native.config.js +11 -21
  70. package/scripts/codegen/generate-artifacts-executor.js +8 -4
  71. package/scripts/generate-codegen-artifacts.js +6 -1
  72. package/sdks/hermes-engine/hermes-utils.rb +2 -2
  73. package/sdks/hermesc/linux64-bin/hermesc +0 -0
  74. package/sdks/hermesc/osx-bin/hermes +0 -0
  75. package/sdks/hermesc/osx-bin/hermesc +0 -0
  76. package/sdks/hermesc/win64-bin/hermesc.exe +0 -0
  77. package/sdks/hermesc/win64-bin/msvcp140.dll +0 -0
  78. package/sdks/hermesc/win64-bin/vcruntime140.dll +0 -0
  79. package/sdks/hermesc/win64-bin/vcruntime140_1.dll +0 -0
  80. package/src/private/featureflags/ReactNativeFeatureFlags.js +2 -2
@@ -8,7 +8,6 @@
8
8
  * @format
9
9
  */
10
10
 
11
- import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
12
11
  import type {ViewProps} from '../View/ViewPropTypes';
13
12
  import type {ComponentOrHandleType} from './tagForComponentOrHandle';
14
13
 
@@ -17,6 +17,8 @@ import {
17
17
  PointerEvents,
18
18
  FocusEvents,
19
19
  PressEvents,
20
+ NativeFocusEvent,
21
+ NativeBlurEvent,
20
22
  } from '../../Types/CoreEventTypes';
21
23
  import {Touchable} from '../Touchable/Touchable';
22
24
  import {AccessibilityProps} from './ViewAccessibility';
@@ -227,6 +229,6 @@ export interface ViewProps
227
229
  */
228
230
  experimental_layoutConformance?: 'strict' | 'classic' | undefined;
229
231
 
230
- readonly onFocus?: BubblingEventHandler<Event> | undefined;
231
- readonly onBlur?: BubblingEventHandler<Event> | undefined;
232
+ readonly onFocus?: BubblingEventHandler<NativeFocusEvent> | undefined;
233
+ readonly onBlur?: BubblingEventHandler<NativeBlurEvent> | undefined;
232
234
  }
@@ -16,8 +16,8 @@ const version: $ReadOnly<{
16
16
  }> = {
17
17
  major: 0,
18
18
  minor: 77,
19
- patch: 0,
20
- prerelease: '0rc5',
19
+ patch: 2,
20
+ prerelease: '0',
21
21
  };
22
22
 
23
23
  module.exports = {version};
@@ -42,9 +42,8 @@ if (__DEV__) {
42
42
  if (!Platform.isTesting) {
43
43
  const HMRClient = require('../Utilities/HMRClient');
44
44
 
45
- if (global.__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__) {
46
- HMRClient.unstable_notifyFuseboxConsoleEnabled();
47
- } else if (console._isPolyfilled) {
45
+ // TODO(T214991636): Remove legacy Metro log forwarding
46
+ if (console._isPolyfilled) {
48
47
  // We assume full control over the console and send JavaScript logs to Metro.
49
48
  [
50
49
  'trace',
@@ -133,6 +133,7 @@ let BaseImage: AbstractImageAndroid = React.forwardRef(
133
133
  width: undefined,
134
134
  height: undefined,
135
135
  };
136
+ const defaultSource = resolveAssetSource(props.defaultSource);
136
137
  const loadingIndicatorSource = resolveAssetSource(
137
138
  props.loadingIndicatorSource,
138
139
  );
@@ -178,6 +179,7 @@ let BaseImage: AbstractImageAndroid = React.forwardRef(
178
179
  /* $FlowFixMe(>=0.78.0 site=react_native_android_fb) This issue was found
179
180
  * when making Flow check .android.js files. */
180
181
  headers: (source?.[0]?.headers || source?.headers: ?{[string]: string}),
182
+ defaultSource: defaultSource ? defaultSource.uri : null,
181
183
  loadingIndicatorSrc: loadingIndicatorSource
182
184
  ? loadingIndicatorSource.uri
183
185
  : null,
@@ -21,6 +21,7 @@ import type {
21
21
  } from '../StyleSheet/StyleSheet';
22
22
  import type {ResolvedAssetSource} from './AssetSourceResolver';
23
23
  import type {ImageProps} from './ImageProps';
24
+ import type {ImageSource} from './ImageSource';
24
25
 
25
26
  import * as NativeComponentRegistry from '../NativeComponent/NativeComponentRegistry';
26
27
  import {ConditionallyIgnoredEventHandlers} from '../NativeComponent/ViewConfigIgnore';
@@ -42,7 +43,7 @@ type Props = $ReadOnly<{
42
43
  | ?ResolvedAssetSource
43
44
  | ?$ReadOnlyArray<?$ReadOnly<{uri?: ?string, ...}>>,
44
45
  headers?: ?{[string]: string},
45
- defaultSrc?: ?string,
46
+ defaultSource?: ?ImageSource | ?string,
46
47
  loadingIndicatorSrc?: ?string,
47
48
  }>;
48
49
 
@@ -82,9 +83,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
82
83
  },
83
84
  validAttributes: {
84
85
  blurRadius: true,
85
- defaultSource: {
86
- process: require('./resolveAssetSource'),
87
- },
86
+ defaultSource: true,
88
87
  internal_analyticTag: true,
89
88
  resizeMethod: true,
90
89
  resizeMode: true,
@@ -477,7 +477,15 @@ static UIImage *RCTResizeImageIfNeeded(UIImage *image, CGSize size, CGFloat scal
477
477
 
478
478
  // Add missing png extension
479
479
  if (request.URL.fileURL && request.URL.pathExtension.length == 0) {
480
- mutableRequest.URL = [request.URL URLByAppendingPathExtension:@"png"];
480
+ // Check if there exists a file with that url on disk already
481
+ // This should fix issue https://github.com/facebook/react-native/issues/46870
482
+ if ([[NSFileManager defaultManager] fileExistsAtPath:request.URL.path]) {
483
+ mutableRequest.URL = request.URL;
484
+ } else {
485
+ // This is the default behavior in case there is no file on disk with no extension.
486
+ // We assume that the extension is `png`.
487
+ mutableRequest.URL = [request.URL URLByAppendingPathExtension:@"png"];
488
+ }
481
489
  }
482
490
  if (_redirectDelegate != nil) {
483
491
  mutableRequest.URL = [_redirectDelegate redirectAssetsURL:mutableRequest.URL];
@@ -439,7 +439,7 @@ export default class Pressability {
439
439
  _createEventHandlers(): EventHandlers {
440
440
  const tvPressEventHandlers = {
441
441
  onPressIn: (evt: any): void => {
442
- if (this._config.disabled === false) {
442
+ if (this._config.disabled === true) {
443
443
  return;
444
444
  }
445
445
 
@@ -460,7 +460,7 @@ export default class Pressability {
460
460
  }, delayLongPress + delayPressIn);
461
461
  },
462
462
  onPressOut: (evt: any): void => {
463
- if (this._config.disabled === false) {
463
+ if (this._config.disabled === true) {
464
464
  return;
465
465
  }
466
466
  this._cancelLongPressDelayTimeout();
@@ -480,7 +480,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithFrame : (CGRect)frame)
480
480
  _maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length,
481
481
  0);
482
482
 
483
- if (text.length > _maxLength.integerValue) {
483
+ if (text.length > allowedLength) {
484
484
  // If we typed/pasted more than one character, limit the text inputted.
485
485
  if (text.length > 1) {
486
486
  if (allowedLength > 0) {
@@ -26,7 +26,6 @@ let hmrUnavailableReason: string | null = null;
26
26
  let currentCompileErrorMessage: string | null = null;
27
27
  let didConnect: boolean = false;
28
28
  let pendingLogs: Array<[LogLevel, $ReadOnlyArray<mixed>]> = [];
29
- let pendingFuseboxConsoleNotification = false;
30
29
 
31
30
  type LogLevel =
32
31
  | 'trace'
@@ -52,7 +51,6 @@ export type HMRClientNativeInterface = {|
52
51
  isEnabled: boolean,
53
52
  scheme?: string,
54
53
  ): void,
55
- unstable_notifyFuseboxConsoleEnabled(): void,
56
54
  |};
57
55
 
58
56
  /**
@@ -142,29 +140,6 @@ const HMRClient: HMRClientNativeInterface = {
142
140
  }
143
141
  },
144
142
 
145
- unstable_notifyFuseboxConsoleEnabled() {
146
- if (!hmrClient) {
147
- pendingFuseboxConsoleNotification = true;
148
- return;
149
- }
150
- hmrClient.send(
151
- JSON.stringify({
152
- type: 'log',
153
- level: 'info',
154
- data: [
155
- '\n' +
156
- '\u001B[7m' +
157
- ' \u001B[1m💡 JavaScript logs have moved!\u001B[22m They can now be ' +
158
- 'viewed in React Native DevTools. Tip: Type \u001B[1mj\u001B[22m in ' +
159
- 'the terminal to open (requires Google Chrome or Microsoft Edge).' +
160
- '\u001B[27m' +
161
- '\n',
162
- ],
163
- }),
164
- );
165
- pendingFuseboxConsoleNotification = false;
166
- },
167
-
168
143
  // Called once by the bridge on startup, even if Fast Refresh is off.
169
144
  // It creates the HMR client but doesn't actually set up the socket yet.
170
145
  setup(
@@ -341,9 +316,6 @@ function flushEarlyLogs(client: MetroHMRClient) {
341
316
  pendingLogs.forEach(([level, data]) => {
342
317
  HMRClient.log(level, data);
343
318
  });
344
- if (pendingFuseboxConsoleNotification) {
345
- HMRClient.unstable_notifyFuseboxConsoleEnabled();
346
- }
347
319
  } finally {
348
320
  pendingLogs.length = 0;
349
321
  }
@@ -25,7 +25,6 @@ const HMRClientProdShim: HMRClientNativeInterface = {
25
25
  disable() {},
26
26
  registerBundle() {},
27
27
  log() {},
28
- unstable_notifyFuseboxConsoleEnabled() {},
29
28
  };
30
29
 
31
30
  module.exports = HMRClientProdShim;
@@ -529,17 +529,19 @@ RCT_ENUM_CONVERTER(
529
529
  }),
530
530
  NSNotFound,
531
531
  unsignedIntegerValue)
532
+ #endif
532
533
  RCT_ENUM_CONVERTER(
533
534
  UIModalPresentationStyle,
534
535
  (@{
535
536
  @"fullScreen" : @(UIModalPresentationFullScreen),
537
+ #if !TARGET_OS_TV
536
538
  @"pageSheet" : @(UIModalPresentationPageSheet),
537
539
  @"formSheet" : @(UIModalPresentationFormSheet),
540
+ #endif
538
541
  @"overFullScreen" : @(UIModalPresentationOverFullScreen),
539
542
  }),
540
543
  UIModalPresentationFullScreen,
541
544
  integerValue)
542
- #endif
543
545
 
544
546
  RCT_ENUM_CONVERTER(
545
547
  UIViewContentMode,
@@ -23,8 +23,8 @@ NSDictionary* RCTGetReactNativeVersion(void)
23
23
  __rnVersion = @{
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(77),
26
- RCTVersionPatch: @(0),
27
- RCTVersionPrerelease: @"0rc5",
26
+ RCTVersionPatch: @(2),
27
+ RCTVersionPrerelease: @"0",
28
28
  };
29
29
  });
30
30
  return __rnVersion;
@@ -1023,18 +1023,23 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1023
1023
  [self sendBlurNotification];
1024
1024
  [self removeSwipeGestureRecognizers];
1025
1025
  [self resignFirstResponder];
1026
- // if we leave the scroll view and go up, then scroll to top; if going down,
1027
- // scroll to bottom
1028
- // Similarly for left and right
1026
+ // If scrolling is enabled:
1027
+ // - Scroll to the top when moving up and to the bottom when moving down.
1028
+ // - Similarly, scroll towards leading edge when moving towards leading edge and to the trailing edge when moving towards the trailing edge.
1029
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1030
+ BOOL isMovingTowardsLeadingEdge = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1031
+ BOOL isMovingTowardsTrailingEdge = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1029
1032
  RCTEnhancedScrollView *scrollView = (RCTEnhancedScrollView *)_scrollView;
1030
- if (context.focusHeading == UIFocusHeadingUp && scrollView.snapToStart) {
1031
- [self swipeVerticalScrollToOffset:0.0];
1032
- } else if(context.focusHeading == UIFocusHeadingDown && scrollView.snapToEnd) {
1033
- [self swipeVerticalScrollToOffset:scrollView.contentSize.height];
1034
- } else if(context.focusHeading == UIFocusHeadingLeft && scrollView.snapToStart) {
1035
- [self swipeHorizontalScrollToOffset:0.0];
1036
- } else if(context.focusHeading == UIFocusHeadingRight && scrollView.snapToEnd) {
1037
- [self swipeHorizontalScrollToOffset:scrollView.contentSize.width];
1033
+ if (scrollView.isScrollEnabled) {
1034
+ if (context.focusHeading == UIFocusHeadingUp && scrollView.snapToStart) {
1035
+ [self scrollToVerticalOffset:0.0];
1036
+ } else if(context.focusHeading == UIFocusHeadingDown && scrollView.snapToEnd) {
1037
+ [self scrollToVerticalOffset:scrollView.contentSize.height];
1038
+ } else if(isMovingTowardsLeadingEdge && scrollView.snapToStart) {
1039
+ [self scrollToHorizontalOffset:0.0];
1040
+ } else if(isMovingTowardsTrailingEdge && scrollView.snapToEnd) {
1041
+ [self scrollToHorizontalOffset:scrollView.contentSize.width];
1042
+ }
1038
1043
  }
1039
1044
  }
1040
1045
  }
@@ -1093,23 +1098,32 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1093
1098
 
1094
1099
  - (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
1095
1100
  {
1101
+ // If the previously focused item is this view and scrolling is disabled, defer to the superclass
1102
+ if (context.previouslyFocusedItem == self && !self.scrollView.isScrollEnabled) {
1103
+ return [super shouldUpdateFocusInContext:context];
1104
+ }
1105
+
1096
1106
  // Determine if the layout is Right-to-Left
1097
1107
  BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1098
1108
  BOOL isHorizontal = _scrollView.contentSize.width > self.frame.size.width;
1099
1109
  // Adjust for horizontal scrolling with RTL support
1100
1110
  if (isHorizontal) {
1101
- BOOL isNavigatingToEnd = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1102
- BOOL isNavigatingToStart = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1111
+ BOOL isMovingTowardsLeadingEdge = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1112
+ BOOL isMovingTowardsTrailingEdge = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1103
1113
 
1104
- if ((isNavigatingToEnd && self.scrollView.contentOffset.x < self.scrollView.contentSize.width - self.scrollView.visibleSize.width) ||
1105
- (isNavigatingToStart && self.scrollView.contentOffset.x > 0)) {
1106
- return [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem];
1114
+ BOOL isScrollingToLeading = (isMovingTowardsLeadingEdge && self.scrollView.contentOffset.x > 0);
1115
+ BOOL isScrollingToTrailing = (isMovingTowardsTrailingEdge && self.scrollView.contentOffset.x < self.scrollView.contentSize.width - MAX(self.scrollView.visibleSize.width, 1));
1116
+
1117
+ if (isScrollingToLeading || isScrollingToTrailing) {
1118
+ return (context.nextFocusedItem && [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem]);
1107
1119
  }
1108
1120
  } else {
1109
1121
  // Handle vertical scrolling as before
1110
- if ((context.focusHeading == UIFocusHeadingUp && self.scrollView.contentOffset.y > 0) ||
1111
- (context.focusHeading == UIFocusHeadingDown && self.scrollView.contentOffset.y < self.scrollView.contentSize.height - self.scrollView.visibleSize.height)) {
1112
- return [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem];
1122
+ BOOL isMovingUp = (context.focusHeading == UIFocusHeadingUp && self.scrollView.contentOffset.y > 0);
1123
+ BOOL isMovingDown = (context.focusHeading == UIFocusHeadingDown && self.scrollView.contentOffset.y < self.scrollView.contentSize.height - MAX(self.scrollView.visibleSize.height, 1));
1124
+
1125
+ if (isMovingUp || isMovingDown) {
1126
+ return (context.nextFocusedItem && [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem]);
1113
1127
  }
1114
1128
  }
1115
1129
  return [super shouldUpdateFocusInContext:context];
@@ -1153,60 +1167,100 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1153
1167
  return duration;
1154
1168
  }
1155
1169
 
1156
- - (void)swipeVerticalScrollToOffset:(CGFloat)yOffset
1170
+ - (void)scrollToVerticalOffset:(CGFloat)yOffset
1157
1171
  {
1158
1172
  _blockFirstTouch = NO;
1159
1173
  dispatch_async(dispatch_get_main_queue(), ^{
1160
1174
  CGFloat limitedOffset = yOffset;
1175
+
1176
+ // Ensure content size and visible size are non-negative
1177
+ CGFloat contentHeight = MAX(self.scrollView.contentSize.height, 0.0);
1178
+ CGFloat visibleHeight = MAX(self.scrollView.visibleSize.height, 0.0);
1179
+
1180
+ // Compute the maximum offset, ensuring it's non-negative
1181
+ CGFloat maxOffset = MAX(contentHeight - visibleHeight, 0.0);
1182
+
1183
+ // Clamp the offset within valid bounds
1161
1184
  limitedOffset = MAX(limitedOffset, 0.0);
1162
- limitedOffset = MIN(limitedOffset, self.scrollView.contentSize.height - self.scrollView.visibleSize.height);
1185
+ limitedOffset = MIN(limitedOffset, maxOffset);
1186
+
1163
1187
  [UIView animateWithDuration:[self swipeDuration] animations:^{
1164
1188
  self.scrollView.contentOffset =
1165
- CGPointMake(self.scrollView.contentOffset.x, limitedOffset);
1189
+ CGPointMake(self.scrollView.contentOffset.x, limitedOffset);
1166
1190
  }];
1167
1191
  });
1168
1192
  }
1169
1193
 
1170
- - (void)swipeHorizontalScrollToOffset:(CGFloat)xOffset
1194
+ - (void)scrollToHorizontalOffset:(CGFloat)xOffset
1171
1195
  {
1172
1196
  _blockFirstTouch = NO;
1173
1197
  dispatch_async(dispatch_get_main_queue(), ^{
1174
1198
  CGFloat limitedOffset = xOffset;
1199
+
1200
+ // Ensure content size and visible size are non-negative
1201
+ CGFloat contentWidth = MAX(self.scrollView.contentSize.width, 0.0);
1202
+ CGFloat visibleWidth = MAX(self.scrollView.visibleSize.width, 0.0);
1203
+
1204
+ // Compute the maximum offset, ensuring it's non-negative
1205
+ CGFloat maxOffset = MAX(contentWidth - visibleWidth, 0.0);
1206
+
1207
+ // Clamp the offset within valid bounds
1175
1208
  limitedOffset = MAX(limitedOffset, 0.0);
1176
- limitedOffset = MIN(limitedOffset, self.scrollView.contentSize.width - self.scrollView.visibleSize.width);
1209
+ limitedOffset = MIN(limitedOffset, maxOffset);
1210
+
1177
1211
  [UIView animateWithDuration:[self swipeDuration] animations:^{
1178
1212
  self.scrollView.contentOffset =
1179
- CGPointMake(limitedOffset, self.scrollView.contentOffset.y);
1213
+ CGPointMake(limitedOffset, self.scrollView.contentOffset.y);
1180
1214
  }];
1181
1215
  });
1182
1216
  }
1183
1217
 
1184
1218
  - (void)swipedUp
1185
1219
  {
1220
+ if (!self.scrollView.scrollEnabled) {
1221
+ return;
1222
+ }
1223
+
1186
1224
  CGFloat newOffset = self.scrollView.contentOffset.y - [self swipeVerticalInterval];
1187
1225
  // NSLog(@"Swiped up to %f", newOffset);
1188
- [self swipeVerticalScrollToOffset:newOffset];
1226
+ [self scrollToVerticalOffset:newOffset];
1189
1227
  }
1190
1228
 
1191
1229
  - (void)swipedDown
1192
1230
  {
1231
+ if (!self.scrollView.scrollEnabled) {
1232
+ return;
1233
+ }
1234
+
1193
1235
  CGFloat newOffset = self.scrollView.contentOffset.y + [self swipeVerticalInterval];
1194
1236
  // NSLog(@"Swiped down to %f", newOffset);
1195
- [self swipeVerticalScrollToOffset:newOffset];
1237
+ [self scrollToVerticalOffset:newOffset];
1196
1238
  }
1197
1239
 
1198
1240
  - (void)swipedLeft
1199
1241
  {
1200
- CGFloat newOffset = self.scrollView.contentOffset.x - [self swipeHorizontalInterval];
1242
+ if (!self.scrollView.scrollEnabled) {
1243
+ return;
1244
+ }
1245
+
1246
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1247
+ NSInteger horizontalInterval = [self swipeHorizontalInterval];
1248
+ CGFloat newOffset = self.scrollView.contentOffset.x + (isRTL ? horizontalInterval : -horizontalInterval);
1201
1249
  // NSLog(@"Swiped left to %f", newOffset);
1202
- [self swipeHorizontalScrollToOffset:newOffset];
1250
+ [self scrollToHorizontalOffset:newOffset];
1203
1251
  }
1204
1252
 
1205
1253
  - (void)swipedRight
1206
1254
  {
1207
- CGFloat newOffset = self.scrollView.contentOffset.x + [self swipeHorizontalInterval];
1255
+ if (!self.scrollView.scrollEnabled) {
1256
+ return;
1257
+ }
1258
+
1259
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1260
+ NSInteger horizontalInterval = [self swipeHorizontalInterval];
1261
+ CGFloat newOffset = self.scrollView.contentOffset.x + (isRTL ? -horizontalInterval : horizontalInterval);
1208
1262
  // NSLog(@"Swiped right to %f", newOffset);
1209
- [self swipeHorizontalScrollToOffset:newOffset];
1263
+ [self scrollToHorizontalOffset:newOffset];
1210
1264
  }
1211
1265
 
1212
1266
  - (void)addSwipeGestureRecognizers
@@ -101,11 +101,7 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
101
101
  NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
102
102
  [_backedTextInputView.defaultTextAttributes mutableCopy];
103
103
 
104
- #if !TARGET_OS_MACCATALYST
105
- RCTWeakEventEmitterWrapper *eventEmitterWrapper = [RCTWeakEventEmitterWrapper new];
106
- eventEmitterWrapper.eventEmitter = _eventEmitter;
107
- defaultAttributes[RCTAttributedStringEventEmitterKey] = eventEmitterWrapper;
108
- #endif
104
+ defaultAttributes[RCTAttributedStringEventEmitterKey] = RCTWrapEventEmitter(_eventEmitter);
109
105
 
110
106
  _backedTextInputView.defaultTextAttributes = defaultAttributes;
111
107
  }
@@ -266,10 +262,8 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
266
262
  if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
267
263
  NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
268
264
  RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
269
- #if !TARGET_OS_MACCATALYST
270
265
  defaultAttributes[RCTAttributedStringEventEmitterKey] =
271
266
  _backedTextInputView.defaultTextAttributes[RCTAttributedStringEventEmitterKey];
272
- #endif
273
267
  _backedTextInputView.defaultTextAttributes = defaultAttributes;
274
268
  }
275
269
 
@@ -585,7 +585,7 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
585
585
  [self handleFocusGuide];
586
586
  }
587
587
 
588
- if (context.nextFocusedView == self && self.isUserInteractionEnabled && ![self isTVFocusGuide]) {
588
+ if (context.nextFocusedView == self) {
589
589
  if(_eventEmitter) _eventEmitter->onFocus();
590
590
 
591
591
  [self becomeFirstResponder];
@@ -594,7 +594,10 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
594
594
  [self addParallaxMotionEffects];
595
595
  [self sendFocusNotification:context];
596
596
  } completion:^(void){}];
597
- } else {
597
+ // Without this check, onBlur would also trigger when `TVFocusGuideView` transfers focus to its children.
598
+ // [self isTVFocusGuide] is false when autofocus and destinations are not used, so we cannot use that.
599
+ // Generally speaking, it would happen for any non-collapsable `View`.
600
+ } else if (context.previouslyFocusedView == self) {
598
601
  if (_eventEmitter) _eventEmitter->onBlur();
599
602
 
600
603
  [self disableDirectionalFocusGuides];
@@ -303,7 +303,7 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : unused)
303
303
  [self handleFocusGuide];
304
304
  }
305
305
 
306
- if (context.nextFocusedView == self && ![self isTVFocusGuide] && self.isTVSelectable ) {
306
+ if (context.nextFocusedView == self) {
307
307
  if (self.onFocus) self.onFocus(nil);
308
308
  [self becomeFirstResponder];
309
309
  [self enableDirectionalFocusGuides];
@@ -311,7 +311,10 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : unused)
311
311
  [self addParallaxMotionEffects];
312
312
  [self sendFocusNotification:context];
313
313
  } completion:^(void){}];
314
- } else {
314
+ // Without this check, onBlur would also trigger when `TVFocusGuideView` transfers focus to its children.
315
+ // [self isTVFocusGuide] is false when autofocus and destinations are not used, so we cannot use that.
316
+ // Generally speaking, it would happen for any non-collapsable `View`.
317
+ } else if (context.previouslyFocusedView == self ) {
315
318
  if (self.onBlur) self.onBlur(nil);
316
319
  [self disableDirectionalFocusGuides];
317
320
  [coordinator addCoordinatedAnimations:^(void){
@@ -1025,19 +1025,23 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
1025
1025
  [self sendBlurNotification];
1026
1026
  [self removeSwipeGestureRecognizers];
1027
1027
  [self resignFirstResponder];
1028
- // if we leave the scroll view and go up, then scroll to top; if going down,
1029
- // scroll to bottom
1030
- // Similarly for left and right
1031
- if (context.focusHeading == UIFocusHeadingUp && self.snapToStart) {
1032
- [self scrollToVerticalOffset:0.0];
1033
- } else if(context.focusHeading == UIFocusHeadingDown && self.snapToEnd) {
1034
- [self scrollToVerticalOffset:self.scrollView.contentSize.height];
1035
- } else if(context.focusHeading == UIFocusHeadingLeft && self.snapToStart) {
1036
- [self scrollToHorizontalOffset:0.0];
1037
- } else if(context.focusHeading == UIFocusHeadingRight && self.snapToEnd) {
1038
- [self scrollToHorizontalOffset:self.scrollView.contentSize.width];
1028
+ // If scrolling is enabled:
1029
+ // - Scroll to the top when moving up and to the bottom when moving down.
1030
+ // - Similarly, scroll towards leading edge when moving towards leading edge and to the trailing edge when moving towards the trailing edge.
1031
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1032
+ BOOL isMovingTowardsLeadingEdge = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1033
+ BOOL isMovingTowardsTrailingEdge = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1034
+ if (self.scrollView.isScrollEnabled) {
1035
+ if (context.focusHeading == UIFocusHeadingUp && self.snapToStart) {
1036
+ [self scrollToVerticalOffset:0.0];
1037
+ } else if(context.focusHeading == UIFocusHeadingDown && self.snapToEnd) {
1038
+ [self scrollToVerticalOffset:self.scrollView.contentSize.height];
1039
+ } else if(isMovingTowardsLeadingEdge && self.snapToStart) {
1040
+ [self scrollToHorizontalOffset:0.0];
1041
+ } else if(isMovingTowardsLeadingEdge && self.snapToEnd) {
1042
+ [self scrollToHorizontalOffset:self.scrollView.contentSize.width];
1043
+ }
1039
1044
  }
1040
-
1041
1045
  }
1042
1046
  }
1043
1047
 
@@ -1093,22 +1097,31 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
1093
1097
 
1094
1098
  - (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
1095
1099
  {
1100
+ // If the previously focused item is this view and scrolling is disabled, defer to the superclass
1101
+ if (context.previouslyFocusedItem == self && !self.scrollView.isScrollEnabled) {
1102
+ return [super shouldUpdateFocusInContext:context];
1103
+ }
1104
+
1096
1105
  // Determine if the layout is Right-to-Left
1097
1106
  BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1098
1107
  // Adjust for horizontal scrolling with RTL support
1099
1108
  if ([self isHorizontal:self.scrollView]) {
1100
- BOOL isNavigatingToEnd = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1101
- BOOL isNavigatingToStart = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1109
+ BOOL isMovingTowardsLeadingEdge = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1110
+ BOOL isMovingTowardsTrailingEdge = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1111
+
1112
+ BOOL isScrollingToLeading = (isMovingTowardsLeadingEdge && self.scrollView.contentOffset.x > 0);
1113
+ BOOL isScrollingToTrailing = (isMovingTowardsTrailingEdge && self.scrollView.contentOffset.x < self.scrollView.contentSize.width - MAX(self.scrollView.visibleSize.width, 1));
1102
1114
 
1103
- if ((isNavigatingToEnd && self.scrollView.contentOffset.x < self.scrollView.contentSize.width - self.scrollView.visibleSize.width) ||
1104
- (isNavigatingToStart && self.scrollView.contentOffset.x > 0)) {
1105
- return [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem];
1115
+ if (isScrollingToLeading || isScrollingToTrailing) {
1116
+ return (context.nextFocusedItem && [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem]);
1106
1117
  }
1107
1118
  } else {
1108
1119
  // Handle vertical scrolling as before
1109
- if ((context.focusHeading == UIFocusHeadingUp && self.scrollView.contentOffset.y > 0) ||
1110
- (context.focusHeading == UIFocusHeadingDown && self.scrollView.contentOffset.y < self.scrollView.contentSize.height - self.scrollView.visibleSize.height)) {
1111
- return [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem];
1120
+ BOOL isMovingUp = (context.focusHeading == UIFocusHeadingUp && self.scrollView.contentOffset.y > 0);
1121
+ BOOL isMovingDown = (context.focusHeading == UIFocusHeadingDown && self.scrollView.contentOffset.y < self.scrollView.contentSize.height - MAX(self.scrollView.visibleSize.height, 1));
1122
+
1123
+ if (isMovingUp || isMovingDown) {
1124
+ return (context.nextFocusedItem && [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem]);
1112
1125
  }
1113
1126
  }
1114
1127
  return [super shouldUpdateFocusInContext:context];
@@ -1154,11 +1167,21 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
1154
1167
  _blockFirstTouch = NO;
1155
1168
  dispatch_async(dispatch_get_main_queue(), ^{
1156
1169
  CGFloat limitedOffset = yOffset;
1170
+
1171
+ // Ensure content size and visible size are non-negative
1172
+ CGFloat contentHeight = MAX(self.scrollView.contentSize.height, 0.0);
1173
+ CGFloat visibleHeight = MAX(self.scrollView.visibleSize.height, 0.0);
1174
+
1175
+ // Compute the maximum offset, ensuring it's non-negative
1176
+ CGFloat maxOffset = MAX(contentHeight - visibleHeight, 0.0);
1177
+
1178
+ // Clamp the offset within valid bounds
1157
1179
  limitedOffset = MAX(limitedOffset, 0.0);
1158
- limitedOffset = MIN(limitedOffset, self.scrollView.contentSize.height - self.scrollView.visibleSize.height);
1180
+ limitedOffset = MIN(limitedOffset, maxOffset);
1181
+
1159
1182
  [UIView animateWithDuration:[self swipeDuration] animations:^{
1160
1183
  self.scrollView.contentOffset =
1161
- CGPointMake(self.scrollView.contentOffset.x, limitedOffset);
1184
+ CGPointMake(self.scrollView.contentOffset.x, limitedOffset);
1162
1185
  }];
1163
1186
  });
1164
1187
  }
@@ -1168,11 +1191,21 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
1168
1191
  _blockFirstTouch = NO;
1169
1192
  dispatch_async(dispatch_get_main_queue(), ^{
1170
1193
  CGFloat limitedOffset = xOffset;
1194
+
1195
+ // Ensure content size and visible size are non-negative
1196
+ CGFloat contentWidth = MAX(self.scrollView.contentSize.width, 0.0);
1197
+ CGFloat visibleWidth = MAX(self.scrollView.visibleSize.width, 0.0);
1198
+
1199
+ // Compute the maximum offset, ensuring it's non-negative
1200
+ CGFloat maxOffset = MAX(contentWidth - visibleWidth, 0.0);
1201
+
1202
+ // Clamp the offset within valid bounds
1171
1203
  limitedOffset = MAX(limitedOffset, 0.0);
1172
- limitedOffset = MIN(limitedOffset, self.scrollView.contentSize.width - self.scrollView.visibleSize.width);
1204
+ limitedOffset = MIN(limitedOffset, maxOffset);
1205
+
1173
1206
  [UIView animateWithDuration:[self swipeDuration] animations:^{
1174
1207
  self.scrollView.contentOffset =
1175
- CGPointMake(limitedOffset, self.scrollView.contentOffset.y);
1208
+ CGPointMake(limitedOffset, self.scrollView.contentOffset.y);
1176
1209
  }];
1177
1210
  });
1178
1211
  }
@@ -1205,7 +1238,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
1205
1238
  return;
1206
1239
  }
1207
1240
 
1208
- CGFloat newOffset = self.scrollView.contentOffset.x - [self swipeHorizontalInterval];
1241
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1242
+ NSInteger horizontalInterval = [self swipeHorizontalInterval];
1243
+ CGFloat newOffset = self.scrollView.contentOffset.x + (isRTL ? horizontalInterval : -horizontalInterval);
1209
1244
  // NSLog(@"Swiped left to %f", newOffset);
1210
1245
  [self scrollToHorizontalOffset:newOffset];
1211
1246
  }
@@ -1216,7 +1251,9 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
1216
1251
  return;
1217
1252
  }
1218
1253
 
1219
- CGFloat newOffset = self.scrollView.contentOffset.x + [self swipeHorizontalInterval];
1254
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1255
+ NSInteger horizontalInterval = [self swipeHorizontalInterval];
1256
+ CGFloat newOffset = self.scrollView.contentOffset.x + (isRTL ? -horizontalInterval : horizontalInterval);
1220
1257
  // NSLog(@"Swiped right to %f", newOffset);
1221
1258
  [self scrollToHorizontalOffset:newOffset];
1222
1259
  }