react-native-tvos 0.76.1-1 → 0.76.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 (56) hide show
  1. package/Libraries/AppDelegate/React-RCTAppDelegate.podspec +1 -1
  2. package/Libraries/Components/Pressable/Pressable.d.ts +8 -0
  3. package/Libraries/Components/Pressable/Pressable.js +4 -1
  4. package/Libraries/Core/ReactNativeVersion.js +2 -2
  5. package/Libraries/Core/setUpErrorHandling.js +1 -7
  6. package/Libraries/LogBox/Data/LogBoxData.js +2 -2
  7. package/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +1 -0
  8. package/Libraries/Types/CoreEventTypes.d.ts +3 -3
  9. package/README.md +9 -7
  10. package/React/Base/RCTTVRemoteHandler.m +0 -19
  11. package/React/Base/RCTTVRemoteSelectHandler.h +27 -0
  12. package/React/Base/RCTTVRemoteSelectHandler.m +120 -0
  13. package/React/Base/RCTVersion.m +2 -2
  14. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +12 -8
  15. package/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +47 -3
  16. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +8 -0
  17. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +35 -42
  18. package/React/Views/RCTTVView.h +6 -6
  19. package/React/Views/RCTTVView.m +34 -46
  20. package/React/Views/ScrollView/RCTScrollView.m +12 -8
  21. package/ReactAndroid/api/ReactAndroid.api +0 -1
  22. package/ReactAndroid/cmake-utils/ReactNative-application.cmake +1 -1
  23. package/ReactAndroid/gradle.properties +1 -1
  24. package/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt +2 -0
  25. package/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.kt +0 -8
  26. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java +2 -2
  27. package/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +11 -3
  28. package/ReactCommon/cxxreact/ReactNativeVersion.h +2 -2
  29. package/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp +3 -2
  30. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +12 -1
  31. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +165 -2
  32. package/cli.js +1 -1
  33. package/index.js +0 -4
  34. package/package.json +8 -8
  35. package/scripts/codegen/generate-artifacts-executor.js +3 -3
  36. package/sdks/.hermesversion +1 -1
  37. package/sdks/hermesc/osx-bin/hermes +0 -0
  38. package/sdks/hermesc/osx-bin/hermesc +0 -0
  39. package/sdks/hermesc/win64-bin/hermesc.exe +0 -0
  40. package/types/modules/Codegen.d.ts +6 -0
  41. package/types/public/ReactNativeTVTypes.d.ts +1 -1
  42. package/Libraries/Components/TabBarIOS/RCTTabBarItemNativeComponent.js +0 -99
  43. package/Libraries/Components/TabBarIOS/RCTTabBarNativeComponent.js +0 -32
  44. package/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +0 -59
  45. package/Libraries/Components/TabBarIOS/TabBarIOS.js +0 -52
  46. package/Libraries/Components/TabBarIOS/TabBarIOSProps.js +0 -52
  47. package/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +0 -177
  48. package/Libraries/Components/TabBarIOS/TabBarItemIOS.js +0 -55
  49. package/React/Views/RCTTabBar.h +0 -22
  50. package/React/Views/RCTTabBar.m +0 -237
  51. package/React/Views/RCTTabBarItem.h +0 -35
  52. package/React/Views/RCTTabBarItem.m +0 -139
  53. package/React/Views/RCTTabBarItemManager.h +0 -12
  54. package/React/Views/RCTTabBarItemManager.m +0 -38
  55. package/React/Views/RCTTabBarManager.h +0 -12
  56. package/React/Views/RCTTabBarManager.m +0 -81
@@ -63,7 +63,7 @@ Pod::Spec.new do |s|
63
63
  "CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard(),
64
64
  "DEFINES_MODULE" => "YES"
65
65
  }
66
- s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/React-Core\""}
66
+ s.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/Headers/Private/React-Core\" \"$(PODS_ROOT)/Headers/Private/Yoga\""}
67
67
 
68
68
  s.dependency "React-Core"
69
69
  s.dependency "RCT-Folly", folly_version
@@ -20,6 +20,7 @@ import {
20
20
  import {View} from '../View/View';
21
21
  import {AccessibilityProps} from '../View/ViewAccessibility';
22
22
  import {ViewProps} from '../View/ViewPropTypes';
23
+ import {TVParallaxProperties} from '../../../types/public/ReactNativeTVTypes';
23
24
 
24
25
  export interface PressableStateCallbackType {
25
26
  readonly pressed: boolean;
@@ -159,6 +160,13 @@ export interface PressableProps
159
160
  * Duration (in milliseconds) to wait after press down before calling onPressIn.
160
161
  */
161
162
  unstable_pressDelay?: number | undefined;
163
+
164
+ /**
165
+ * *(Apple TV only)* Object with properties to control Apple TV parallax effects.
166
+ *
167
+ * @platform ios
168
+ */
169
+ tvParallaxProperties?: TVParallaxProperties;
162
170
  }
163
171
 
164
172
  // TODO use React.AbstractComponent when available
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type {
12
12
  FocusEvent,
13
+ BlurEvent,
13
14
  LayoutEvent,
14
15
  MouseEvent,
15
16
  PressEvent,
@@ -52,7 +53,9 @@ type TVProps = $ReadOnly<{|
52
53
  nextFocusRight?: ?number,
53
54
  nextFocusUp?: ?number,
54
55
  onFocus?: ?(event: FocusEvent) => mixed,
55
- onBlur?: ?(event: FocusEvent) => mixed,
56
+ onBlur?: ?(event: BlurEvent) => mixed,
57
+ onFocusCapture?: ?(event: FocusEvent) => void,
58
+ onBlurCapture?: ?(event: BlurEvent) => void,
56
59
  |}>;
57
60
 
58
61
  type Props = $ReadOnly<{|
@@ -16,8 +16,8 @@ const version: $ReadOnly<{
16
16
  }> = {
17
17
  major: 0,
18
18
  minor: 76,
19
- patch: 1,
20
- prerelease: '1',
19
+ patch: 2,
20
+ prerelease: '0',
21
21
  };
22
22
 
23
23
  module.exports = {version};
@@ -21,13 +21,7 @@ ExceptionsManager.installConsoleErrorReporter();
21
21
  if (!global.__fbDisableExceptionsManager) {
22
22
  const handleError = (e: mixed, isFatal: boolean) => {
23
23
  try {
24
- // TODO(T196834299): We should really use a c++ turbomodule for this
25
- if (
26
- !global.RN$handleException ||
27
- !global.RN$handleException(e, isFatal)
28
- ) {
29
- ExceptionsManager.handleException(e, isFatal);
30
- }
24
+ ExceptionsManager.handleException(e, isFatal);
31
25
  } catch (ee) {
32
26
  console.log('Failed to print error: ', ee.message);
33
27
  throw e;
@@ -82,9 +82,9 @@ let warningFilter: WarningFilter = function (format) {
82
82
  return {
83
83
  finalFormat: format,
84
84
  forceDialogImmediately: false,
85
- suppressDialog_LEGACY: true,
85
+ suppressDialog_LEGACY: false,
86
86
  suppressCompletely: false,
87
- monitorEvent: 'unknown',
87
+ monitorEvent: 'warning_unhandled',
88
88
  monitorListVersion: 0,
89
89
  monitorSampleRate: 1,
90
90
  };
@@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN
35
35
  @property (nonatomic, assign, readonly) CGFloat zoomScale;
36
36
  @property (nonatomic, assign, readonly) CGPoint contentOffset;
37
37
  @property (nonatomic, assign, readonly) UIEdgeInsets contentInset;
38
+ @property (nullable, nonatomic, copy) NSDictionary<NSAttributedStringKey, id> *typingAttributes;
38
39
 
39
40
  // This protocol disallows direct access to `selectedTextRange` property because
40
41
  // unwise usage of it can break the `delegate` behavior. So, we always have to
@@ -246,13 +246,13 @@ export interface GestureResponderEvent
246
246
 
247
247
  export interface MouseEvent extends NativeSyntheticEvent<NativeMouseEvent> {}
248
248
 
249
- export interface NativeFocusEvent {}
249
+ export interface NativeFocusEvent extends TargetedEvent {}
250
250
  export interface FocusEvent extends NativeSyntheticEvent<NativeFocusEvent> {}
251
251
 
252
- export interface NativeBlurEvent {}
252
+ export interface NativeBlurEvent extends TargetedEvent {}
253
253
  export interface BlurEvent extends NativeSyntheticEvent<NativeBlurEvent> {}
254
254
 
255
- export interface NativePressEvent {}
255
+ export interface NativePressEvent extends TargetedEvent {}
256
256
  export interface PressEvent extends NativeSyntheticEvent<NativePressEvent> {}
257
257
 
258
258
  export interface TargetedEvent {
package/README.md CHANGED
@@ -176,19 +176,21 @@ var running_on_apple_tv = Platform.isTVOS;
176
176
 
177
177
  - _Common codebase for Android phone and Android TV_: Apps built for Android using this repo will run on both Android phone and Android TV. Most of the changes for TV are specific to handling focus-based navigation on a TV using the D-Pad on the remote control.
178
178
 
179
- - _Access to touchable controls_: The `Touchable` mixin has code added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `TouchableWithoutFeedback`, `TouchableHighlight` and `TouchableOpacity` will "just work" on both Apple TV and Android TV. In particular:
179
+ - _Pressable and Touchable controls_: In RNTV 0.76.1-1 and later, TV controls are supported with fully native events.
180
+ Code has been added to detect focus changes and use existing methods to style the components properly and initiate the proper actions when the view is selected using the TV remote, so `Pressable`, `TouchableHighlight` and `TouchableOpacity` will "just work" on both Apple TV and Android TV. In particular:
180
181
 
181
182
  - `onFocus()` will be executed when the touchable view goes into focus
182
183
  - `onBlur()` will be executed when the touchable view goes out of focus
183
184
  - `onPress()` will be executed when the touchable view is actually selected by pressing the "select" button on the TV remote (center button on Apple TV remote, or center button on Android TV DPad).
184
- - `onLongPress()` will be executed twice if the "select" button is held down for a length of time. The two events passed into `onLongPress()` will have different values for their `eventKeyAction` property, 0 for key down (start) and 1 for key up (end).
185
+ - `onPressIn()` will be executed when the TV remote "select" button is pressed down (center button on Apple TV remote, or center button on Android TV DPad)
186
+ - `onPressOut()` will be executed when the TV remote "select" button is released
187
+ - `onLongPress()` will be executed if the "select" button is held down for a length of time (this event is generated in the `Pressability` module, the same as for touchscreen long press events).
185
188
 
186
- - _Pressable controls_: The `Pressable` API works with TV. Additional `onFocus` and `onBlur` props are provided to allow you to customize behavior when a Pressable enters or leaves focus. Similar to the `pressed` state that is true while a user is pressing the component on a touchscreen, the `focused` state will be true when it is focused on TV. `PressableExample` in RNTester has been modified appropriately. The `onPress()` and `onLongPress()` methods work the same way as with `Touchable` components.
189
+ `TouchableNativeFeedback` and `TouchableWithoutFeedback` respond to press events, but do not respond to focus and blur events, and are not recommended for TV.
187
190
 
188
- - _Tailwind styles for Pressable controls_: For the 0.76 release, the `Pressable` component also generates the `onPressIn()` and `onPressOut()` events needed to support the [`active:` pseudo class for Tailwind styles](https://www.nativewind.dev/v4/core-concepts/states#hover-focus-and-active-).
189
- - For `onPress()` events (the "select" button on the remote is pressed once), `onPressIn()` is generated, then `onPressOut()` is generated a short time later.
190
- - For `onLongPress()` events (the "select" button on the remote is held down for a length of time), `onPressIn()` is generated once the press down is detected, and `onPressOut()` is generated when the button is released.
191
- - The `focus:` pseudo class is also supported via the `onFocus()` and `onBlur()` events.
191
+ Because focus and blur events are now fully native core events, they will respond correctly to capturing and bubbling event handlers in `View` components. A demo of this has been added to the TVEventHandlerExample in RNTester.
192
+
193
+ - _Tailwind styles for Pressable and Touchable controls_: The above events allow RNTV to support the [`focus:` and `active:` pseudo classes for Tailwind styles](https://www.nativewind.dev/v4/core-concepts/states#hover-focus-and-active-).
192
194
 
193
195
  - _TV remote/keyboard input_: Application code that needs to implement custom handling of TV remote events can create an instance of `TVEventHandler` and listen for these events. For a more convenient API, we provide `useTVEventHandler`.
194
196
 
@@ -119,11 +119,6 @@ static __volatile BOOL __gestureHandlersCancelTouches = YES;
119
119
  pressType:UIPressTypePlayPause
120
120
  name:RCTTVRemoteEventPlayPause];
121
121
 
122
- // Select
123
- [self addTapGestureRecognizerWithSelector:@selector(selectPressed:)
124
- pressType:UIPressTypeSelect
125
- name:RCTTVRemoteEventSelect];
126
-
127
122
  // Page Up/Down
128
123
  if (@available(tvOS 14.3, *)) {
129
124
  [self addTapGestureRecognizerWithSelector:@selector(tappedPageUp:)
@@ -162,10 +157,6 @@ static __volatile BOOL __gestureHandlersCancelTouches = YES;
162
157
  pressType:UIPressTypePlayPause
163
158
  name:RCTTVRemoteEventLongPlayPause];
164
159
 
165
- [self addLongPressGestureRecognizerWithSelector:@selector(longSelectPressed:)
166
- pressType:UIPressTypeSelect
167
- name:RCTTVRemoteEventLongSelect];
168
-
169
160
  [self addLongPressGestureRecognizerWithSelector:@selector(longUpPressed:)
170
161
  pressType:UIPressTypeUpArrow
171
162
  name:RCTTVRemoteEventLongUp];
@@ -355,11 +346,6 @@ static __volatile BOOL __gestureHandlersCancelTouches = YES;
355
346
  [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventMenu keyAction:r.eventKeyAction tag:nil target:nil];
356
347
  }
357
348
 
358
- - (void)selectPressed:(UIGestureRecognizer *)r
359
- {
360
- [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventSelect keyAction:r.eventKeyAction tag:nil target:nil];
361
- }
362
-
363
349
  - (void)longPlayPausePressed:(UIGestureRecognizer *)r
364
350
  {
365
351
  [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventLongPlayPause keyAction:r.eventKeyAction tag:nil target:nil];
@@ -370,11 +356,6 @@ static __volatile BOOL __gestureHandlersCancelTouches = YES;
370
356
  #endif
371
357
  }
372
358
 
373
- - (void)longSelectPressed:(UIGestureRecognizer *)r
374
- {
375
- [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventLongSelect keyAction:r.eventKeyAction tag:nil target:nil];
376
- }
377
-
378
359
  - (void)longUpPressed:(UIGestureRecognizer *)r
379
360
  {
380
361
  [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventLongUp keyAction:r.eventKeyAction tag:nil target:nil];
@@ -0,0 +1,27 @@
1
+ #import <UIKit/UIKit.h>
2
+ #import <Foundation/Foundation.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ @protocol RCTTVRemoteSelectHandlerDelegate <NSObject>
7
+
8
+ - (void)animatePressIn;
9
+ - (void)animatePressOut;
10
+
11
+ - (void)emitPressInEvent;
12
+ - (void)emitPressOutEvent;
13
+
14
+ - (void)sendSelectNotification;
15
+ - (void)sendLongSelectBeganNotification;
16
+ - (void)sendLongSelectEndedNotification;
17
+
18
+ @end
19
+
20
+ @interface RCTTVRemoteSelectHandler : NSObject <UIGestureRecognizerDelegate>
21
+
22
+ - (instancetype _Nonnull )initWithView:(UIView<RCTTVRemoteSelectHandlerDelegate> * _Nonnull)view;
23
+ - (instancetype _Nonnull )init __attribute__((unavailable("init not available, use initWithView:")));
24
+
25
+ @end
26
+
27
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,120 @@
1
+ #import "RCTTVRemoteSelectHandler.h"
2
+
3
+ @interface RCTTVRemoteSelectHandler()
4
+
5
+ @property (nonatomic, strong) UILongPressGestureRecognizer * pressRecognizer;
6
+ @property (nonatomic, strong) UILongPressGestureRecognizer * longPressRecognizer;
7
+
8
+ @property (nonatomic, weak) UIView<RCTTVRemoteSelectHandlerDelegate> *view;
9
+
10
+ @end
11
+
12
+ @implementation RCTTVRemoteSelectHandler {
13
+ NSMutableDictionary<NSString *, UIGestureRecognizer *> *_tvRemoteGestureRecognizers;
14
+ }
15
+
16
+ #pragma mark -
17
+ #pragma mark Public methods
18
+
19
+ - (instancetype)initWithView:(UIView <RCTTVRemoteSelectHandlerDelegate> *)view
20
+ {
21
+ if ((self = [super init])) {
22
+ _view = view;
23
+ [self attachToView];
24
+ }
25
+ return self;
26
+ }
27
+
28
+ - (void)dealloc
29
+ {
30
+ [self detachFromView];
31
+ _view = nil;
32
+ }
33
+
34
+ #pragma mark -
35
+ #pragma UIGestureRecognizerDelegate method
36
+
37
+ // Press recognizer should allow long press recognizer to work (but not the reverse)
38
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
39
+ return gestureRecognizer == _pressRecognizer;
40
+ }
41
+
42
+ #pragma mark -
43
+ #pragma mark Private methods
44
+
45
+ - (void)attachToView {
46
+ UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePress:)];
47
+ pressRecognizer.allowedPressTypes = @[ @(UIPressTypeSelect) ];
48
+ pressRecognizer.minimumPressDuration = 0.0;
49
+ pressRecognizer.delegate = self; // Press recognizer allows other recognizers to run
50
+
51
+ [self.view addGestureRecognizer:pressRecognizer];
52
+ self.pressRecognizer = pressRecognizer;
53
+
54
+ UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
55
+ longPressRecognizer.allowedPressTypes = @[ @(UIPressTypeSelect) ];
56
+ longPressRecognizer.minimumPressDuration = 0.5;
57
+
58
+ [self.view addGestureRecognizer:longPressRecognizer];
59
+ self.longPressRecognizer = longPressRecognizer;
60
+ }
61
+
62
+ - (void)detachFromView {
63
+ if (_pressRecognizer) {
64
+ [self.view removeGestureRecognizer:_pressRecognizer];
65
+ self.pressRecognizer = nil;
66
+ }
67
+ if (_longPressRecognizer) {
68
+ [self.view removeGestureRecognizer:_longPressRecognizer];
69
+ self.longPressRecognizer = nil;
70
+ }
71
+ }
72
+
73
+ - (void)handlePress:(UIGestureRecognizer *)r
74
+ {
75
+ switch (r.state) {
76
+ case UIGestureRecognizerStateBegan:
77
+ [self.view emitPressInEvent];
78
+ [self.view animatePressIn];
79
+ break;
80
+ case UIGestureRecognizerStateCancelled:
81
+ case UIGestureRecognizerStateEnded:
82
+ if (r.enabled) {
83
+ [self.view animatePressOut];
84
+ [self.view emitPressOutEvent];
85
+ [self.view sendSelectNotification];
86
+ }
87
+ break;
88
+ default:
89
+ break;
90
+ }
91
+ }
92
+
93
+ /*
94
+ When a long press starts, the press recognizer has already started
95
+ and called selectGestureBegan(). We disable the press recognizer
96
+ when the long press starts. At the end of the gesture, we execute the
97
+ code for pressOut (since the press recognizer cannot), and then reenable
98
+ the press recgonizer. This guarantees
99
+ only one pressIn and one pressOut, and only longSelect notifications.
100
+ */
101
+ - (void)handleLongPress:(UIGestureRecognizer *)r
102
+ {
103
+ switch (r.state) {
104
+ case UIGestureRecognizerStateBegan:
105
+ self.pressRecognizer.enabled = NO;
106
+ [self.view sendLongSelectBeganNotification];
107
+ break;
108
+ case UIGestureRecognizerStateEnded:
109
+ case UIGestureRecognizerStateCancelled:
110
+ [self.view animatePressOut];
111
+ [self.view emitPressOutEvent];
112
+ [self.view sendLongSelectEndedNotification];
113
+ self.pressRecognizer.enabled = YES;
114
+ break;
115
+ default:
116
+ break;
117
+ }
118
+ }
119
+
120
+ @end
@@ -23,8 +23,8 @@ NSDictionary* RCTGetReactNativeVersion(void)
23
23
  __rnVersion = @{
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(76),
26
- RCTVersionPatch: @(1),
27
- RCTVersionPrerelease: @"1",
26
+ RCTVersionPatch: @(2),
27
+ RCTVersionPrerelease: @"0",
28
28
  };
29
29
  });
30
30
  return __rnVersion;
@@ -27,6 +27,7 @@
27
27
  #if TARGET_OS_TV
28
28
  #import <React/RCTTVRemoteHandler.h>
29
29
  #import <React/RCTTVNavigationEventNotification.h>
30
+ #import "React/RCTI18nUtil.h"
30
31
  #endif
31
32
 
32
33
  using namespace facebook::react;
@@ -1100,22 +1101,25 @@ static inline UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCu
1100
1101
 
1101
1102
  - (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context
1102
1103
  {
1104
+ // Determine if the layout is Right-to-Left
1105
+ BOOL isRTL = [[RCTI18nUtil sharedInstance] isRTL];
1103
1106
  BOOL isHorizontal = _scrollView.contentSize.width > self.frame.size.width;
1104
- // Keep focus inside the scroll view till the end of the content
1107
+ // Adjust for horizontal scrolling with RTL support
1105
1108
  if (isHorizontal) {
1106
- if ((context.focusHeading == UIFocusHeadingLeft && self.scrollView.contentOffset.x > 0)
1107
- || (context.focusHeading == UIFocusHeadingRight && self.scrollView.contentOffset.x < self.scrollView.contentSize.width - self.scrollView.visibleSize.width)
1108
- ) {
1109
+ BOOL isNavigatingToEnd = (isRTL ? context.focusHeading == UIFocusHeadingLeft : context.focusHeading == UIFocusHeadingRight);
1110
+ BOOL isNavigatingToStart = (isRTL ? context.focusHeading == UIFocusHeadingRight : context.focusHeading == UIFocusHeadingLeft);
1111
+
1112
+ if ((isNavigatingToEnd && self.scrollView.contentOffset.x < self.scrollView.contentSize.width - self.scrollView.visibleSize.width) ||
1113
+ (isNavigatingToStart && self.scrollView.contentOffset.x > 0)) {
1109
1114
  return [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem];
1110
1115
  }
1111
1116
  } else {
1112
- if ((context.focusHeading == UIFocusHeadingUp && self.scrollView.contentOffset.y > 0)
1113
- || (context.focusHeading == UIFocusHeadingDown && self.scrollView.contentOffset.y < self.scrollView.contentSize.height - self.scrollView.visibleSize.height)
1114
- ) {
1117
+ // Handle vertical scrolling as before
1118
+ if ((context.focusHeading == UIFocusHeadingUp && self.scrollView.contentOffset.y > 0) ||
1119
+ (context.focusHeading == UIFocusHeadingDown && self.scrollView.contentOffset.y < self.scrollView.contentSize.height - self.scrollView.visibleSize.height)) {
1115
1120
  return [UIFocusSystem environment:self containsEnvironment:context.nextFocusedItem];
1116
1121
  }
1117
1122
  }
1118
-
1119
1123
  return [super shouldUpdateFocusInContext:context];
1120
1124
  }
1121
1125
 
@@ -61,6 +61,13 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
61
61
  */
62
62
  BOOL _comingFromJS;
63
63
  BOOL _didMoveToWindow;
64
+
65
+ /*
66
+ * Newly initialized default typing attributes contain a no-op NSParagraphStyle and NSShadow. These cause inequality
67
+ * between the AttributedString backing the input and those generated from state. We store these attributes to make
68
+ * later comparison insensitive to them.
69
+ */
70
+ NSDictionary<NSAttributedStringKey, id> *_originalTypingAttributes;
64
71
  }
65
72
 
66
73
  #pragma mark - UIView overrides
@@ -76,6 +83,7 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
76
83
  _ignoreNextTextInputCall = NO;
77
84
  _comingFromJS = NO;
78
85
  _didMoveToWindow = NO;
86
+ _originalTypingAttributes = [_backedTextInputView.typingAttributes copy];
79
87
 
80
88
  [self addSubview:_backedTextInputView];
81
89
  [self initializeReturnKeyType];
@@ -84,6 +92,20 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
84
92
  return self;
85
93
  }
86
94
 
95
+ - (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
96
+ {
97
+ [super updateEventEmitter:eventEmitter];
98
+
99
+ NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
100
+ [_backedTextInputView.defaultTextAttributes mutableCopy];
101
+
102
+ RCTWeakEventEmitterWrapper *eventEmitterWrapper = [RCTWeakEventEmitterWrapper new];
103
+ eventEmitterWrapper.eventEmitter = _eventEmitter;
104
+ defaultAttributes[RCTAttributedStringEventEmitterKey] = eventEmitterWrapper;
105
+
106
+ _backedTextInputView.defaultTextAttributes = defaultAttributes;
107
+ }
108
+
87
109
  - (void)didMoveToWindow
88
110
  {
89
111
  [super didMoveToWindow];
@@ -236,8 +258,11 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
236
258
  }
237
259
 
238
260
  if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
239
- _backedTextInputView.defaultTextAttributes =
261
+ NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
240
262
  RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
263
+ defaultAttributes[RCTAttributedStringEventEmitterKey] =
264
+ _backedTextInputView.defaultTextAttributes[RCTAttributedStringEventEmitterKey];
265
+ _backedTextInputView.defaultTextAttributes = defaultAttributes;
241
266
  }
242
267
 
243
268
  if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
@@ -418,6 +443,7 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
418
443
 
419
444
  - (void)textInputDidChangeSelection
420
445
  {
446
+ [self _updateTypingAttributes];
421
447
  if (_comingFromJS) {
422
448
  return;
423
449
  }
@@ -680,9 +706,26 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
680
706
  [_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetStart, 0)];
681
707
  }
682
708
  [self _restoreTextSelection];
709
+ [self _updateTypingAttributes];
683
710
  _lastStringStateWasUpdatedWith = attributedString;
684
711
  }
685
712
 
713
+ // Ensure that newly typed text will inherit any custom attributes. We follow the logic of RN Android, where attributes
714
+ // to the left of the cursor are copied into new text, unless we are at the start of the field, in which case we will
715
+ // copy the attributes from text to the right. This allows consistency between backed input and new AttributedText
716
+ // https://github.com/facebook/react-native/blob/3102a58df38d96f3dacef0530e4dbb399037fcd2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt#L30
717
+ - (void)_updateTypingAttributes
718
+ {
719
+ if (_backedTextInputView.attributedText.length > 0) {
720
+ NSUInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
721
+ toPosition:_backedTextInputView.selectedTextRange.start];
722
+
723
+ NSUInteger samplePoint = offsetStart == 0 ? 0 : offsetStart - 1;
724
+ _backedTextInputView.typingAttributes = [_backedTextInputView.attributedText attributesAtIndex:samplePoint
725
+ effectiveRange:NULL];
726
+ }
727
+ }
728
+
686
729
  - (void)_setMultiline:(BOOL)multiline
687
730
  {
688
731
  [_backedTextInputView removeFromSuperview];
@@ -738,9 +781,10 @@ static NSSet<NSNumber *> *returnKeyTypesSet;
738
781
  _backedTextInputView.markedTextRange || _backedTextInputView.isSecureTextEntry || fontHasBeenUpdatedBySystem;
739
782
 
740
783
  if (shouldFallbackToBareTextComparison) {
741
- return ([newText.string isEqualToString:oldText.string]);
784
+ return [newText.string isEqualToString:oldText.string];
742
785
  } else {
743
- return ([newText isEqualToAttributedString:oldText]);
786
+ return RCTIsAttributedStringEffectivelySame(
787
+ newText, oldText, _originalTypingAttributes, static_cast<const TextInputProps &>(*_props).textAttributes);
744
788
  }
745
789
  }
746
790
 
@@ -11,6 +11,9 @@
11
11
  #import <React/RCTConstants.h>
12
12
  #import <React/RCTTouchableComponentViewProtocol.h>
13
13
  #import <React/UIView+ComponentViewProtocol.h>
14
+ #if TARGET_OS_TV
15
+ #import <React/RCTTVRemoteSelectHandler.h>
16
+ #endif
14
17
  #import <react/renderer/components/view/ViewEventEmitter.h>
15
18
  #import <react/renderer/components/view/ViewProps.h>
16
19
  #import <react/renderer/core/EventEmitter.h>
@@ -25,7 +28,11 @@ NS_ASSUME_NONNULL_BEGIN
25
28
  /**
26
29
  * UIView class for <View> component.
27
30
  */
31
+ #if TARGET_OS_TV
32
+ @interface RCTViewComponentView : UIView <RCTComponentViewProtocol, RCTTouchableComponentViewProtocol, RCTTVRemoteSelectHandlerDelegate> {
33
+ #else
28
34
  @interface RCTViewComponentView : UIView <RCTComponentViewProtocol, RCTTouchableComponentViewProtocol> {
35
+ #endif
29
36
  @protected
30
37
  facebook::react::LayoutMetrics _layoutMetrics;
31
38
  facebook::react::SharedViewProps _props;
@@ -72,6 +79,7 @@ NS_ASSUME_NONNULL_BEGIN
72
79
  @property(nonatomic, nullable) UIFocusGuide *focusGuideDown;
73
80
  @property(nonatomic, nullable) UIFocusGuide *focusGuideLeft;
74
81
  @property(nonatomic, nullable) UIFocusGuide *focusGuideRight;
82
+ @property(nonatomic, nullable, strong) RCTTVRemoteSelectHandler *tvRemoteSelectHandler;
75
83
  #endif
76
84
 
77
85
  /**
@@ -63,7 +63,6 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
63
63
  BOOL _removeClippedSubviews;
64
64
  NSMutableArray<UIView *> *_reactSubviews;
65
65
  BOOL _motionEffectsAdded;
66
- UILongPressGestureRecognizer * _pressRecognizer;
67
66
  NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
68
67
  UIView *_containerView;
69
68
  BOOL _useCustomContainerView;
@@ -267,62 +266,63 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
267
266
  [[NSNotificationCenter defaultCenter] postNavigationBlurEventWithTag:@(self.tag) target:@(self.tag)];
268
267
  }
269
268
 
270
- - (void)sendSelectNotification:(UIGestureRecognizer *)recognizer
269
+ - (void)sendSelectNotification
271
270
  {
272
- [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventSelect keyAction:RCTTVRemoteEventKeyActionUp tag:@(self.tag) target:@(self.tag)];
271
+ [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventSelect keyAction:RCTTVRemoteEventKeyActionUp tag:@(self.tag) target:@(self.tag)];
273
272
  }
274
273
 
275
- - (void)sendLongSelectNotification:(UIGestureRecognizer *)recognizer
274
+ - (void)sendLongSelectBeganNotification
276
275
  {
277
- [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventLongSelect keyAction:recognizer.eventKeyAction tag:@(self.tag) target:@(self.tag)];
276
+ [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventLongSelect keyAction:RCTTVRemoteEventKeyActionDown tag:@(self.tag) target:@(self.tag)];
278
277
  }
279
278
 
280
- - (void)animatePress
279
+ - (void)sendLongSelectEndedNotification
280
+ {
281
+ [[NSNotificationCenter defaultCenter] postNavigationPressEventWithType:RCTTVRemoteEventLongSelect keyAction:RCTTVRemoteEventKeyActionUp tag:@(self.tag) target:@(self.tag)];
282
+ }
283
+
284
+ - (void)animatePressIn
281
285
  {
282
286
  if (_tvParallaxProperties.enabled == YES) {
283
- float magnification = _tvParallaxProperties.magnification;
284
287
  float pressMagnification = _tvParallaxProperties.pressMagnification;
285
288
 
286
289
  // Duration of press animation
287
290
  float pressDuration = _tvParallaxProperties.pressDuration;
288
291
 
289
- // Delay of press animation
290
- float pressDelay = _tvParallaxProperties.pressDelay;
291
-
292
- [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:pressDelay]];
293
-
294
292
  [UIView animateWithDuration:(pressDuration/2)
295
293
  animations:^{
296
294
  self.transform = CGAffineTransformMakeScale(pressMagnification, pressMagnification);
297
295
  }
298
- completion:^(__unused BOOL finished1){
299
- [UIView animateWithDuration:(pressDuration/2)
300
- animations:^{
301
- self.transform = CGAffineTransformMakeScale(magnification, magnification);
302
- }
303
- completion:^(__unused BOOL finished2) {
304
- }];
305
- }];
306
-
296
+ completion:^(__unused BOOL finished){}];
307
297
  }
308
298
  }
309
299
 
310
- - (void)handlePress:(UIGestureRecognizer *)r
300
+ - (void) animatePressOut
311
301
  {
312
- switch (r.state) {
313
- case UIGestureRecognizerStateBegan:
314
- _eventEmitter->onPressIn();
315
- break;
316
- case UIGestureRecognizerStateEnded:
317
- case UIGestureRecognizerStateCancelled:
318
- [self animatePress];
319
- _eventEmitter->onPressOut();
320
- break;
321
- default:
322
- break;
302
+ if (_tvParallaxProperties.enabled == YES) {
303
+ float magnification = _tvParallaxProperties.magnification;
304
+
305
+ // Duration of press animation
306
+ float pressDuration = _tvParallaxProperties.pressDuration;
307
+
308
+ [UIView animateWithDuration:(pressDuration/2)
309
+ animations:^{
310
+ self.transform = CGAffineTransformMakeScale(magnification, magnification);
311
+ }
312
+ completion:^(__unused BOOL finished){}];
323
313
  }
324
314
  }
325
315
 
316
+ - (void)emitPressInEvent
317
+ {
318
+ _eventEmitter->onPressIn();
319
+ }
320
+
321
+ - (void)emitPressOutEvent
322
+ {
323
+ _eventEmitter->onPressOut();
324
+ }
325
+
326
326
  - (void)addParallaxMotionEffects
327
327
  {
328
328
  if(!_tvParallaxProperties.enabled) {
@@ -1040,16 +1040,9 @@ const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
1040
1040
  // `isTVSelectable`
1041
1041
  if (oldViewProps.isTVSelectable != newViewProps.isTVSelectable) {
1042
1042
  if (newViewProps.isTVSelectable && ![self isTVFocusGuide]) {
1043
- UILongPressGestureRecognizer *pressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handlePress:)];
1044
- pressRecognizer.allowedPressTypes = @[ @(UIPressTypeSelect) ];
1045
- pressRecognizer.minimumPressDuration = 0;
1046
-
1047
- [self addGestureRecognizer:pressRecognizer];
1048
- _pressRecognizer = pressRecognizer;
1043
+ self.tvRemoteSelectHandler = [[RCTTVRemoteSelectHandler alloc]initWithView:self];
1049
1044
  } else {
1050
- if (_pressRecognizer) {
1051
- [self removeGestureRecognizer:_pressRecognizer];
1052
- }
1045
+ self.tvRemoteSelectHandler = nil;
1053
1046
  }
1054
1047
  }
1055
1048
  // `tvParallaxProperties