react-native-tvos 0.76.1-0 → 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 (84) hide show
  1. package/Libraries/AppDelegate/React-RCTAppDelegate.podspec +1 -1
  2. package/Libraries/Components/Pressable/Pressable.d.ts +9 -1
  3. package/Libraries/Components/Pressable/Pressable.js +4 -16
  4. package/Libraries/Components/TV/TVViewPropTypes.js +2 -1
  5. package/Libraries/Components/TextInput/TextInput.d.ts +1 -1
  6. package/Libraries/Components/Touchable/Touchable.js +0 -43
  7. package/Libraries/Components/Touchable/TouchableBounce.js +0 -33
  8. package/Libraries/Components/Touchable/TouchableHighlight.js +12 -47
  9. package/Libraries/Components/Touchable/TouchableNativeFeedback.js +0 -33
  10. package/Libraries/Components/Touchable/TouchableOpacity.js +12 -44
  11. package/Libraries/Components/Touchable/TouchableWithoutFeedback.js +0 -19
  12. package/Libraries/Components/View/ViewNativeComponent.js +6 -0
  13. package/Libraries/Components/View/ViewPropTypes.d.ts +12 -1
  14. package/Libraries/Components/View/ViewPropTypes.js +7 -0
  15. package/Libraries/Core/ReactNativeVersion.js +1 -1
  16. package/Libraries/Core/setUpErrorHandling.js +1 -7
  17. package/Libraries/LogBox/Data/LogBoxData.js +2 -2
  18. package/Libraries/NativeComponent/BaseViewConfig.android.js +19 -0
  19. package/Libraries/NativeComponent/BaseViewConfig.ios.js +6 -0
  20. package/Libraries/NativeComponent/TVViewConfig.js +4 -0
  21. package/Libraries/Pressability/Pressability.js +45 -28
  22. package/Libraries/Text/TextInput/RCTBackedTextInputViewProtocol.h +1 -0
  23. package/Libraries/Types/CoreEventTypes.d.ts +21 -0
  24. package/Libraries/Types/CoreEventTypes.js +6 -0
  25. package/README.md +9 -7
  26. package/React/Base/RCTTVRemoteHandler.m +0 -19
  27. package/React/Base/RCTTVRemoteSelectHandler.h +27 -0
  28. package/React/Base/RCTTVRemoteSelectHandler.m +120 -0
  29. package/React/Base/RCTVersion.m +1 -1
  30. package/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +12 -8
  31. package/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +47 -3
  32. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.h +8 -0
  33. package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +51 -44
  34. package/React/Views/RCTTVView.h +19 -6
  35. package/React/Views/RCTTVView.m +63 -55
  36. package/React/Views/RCTViewManager.m +4 -0
  37. package/React/Views/ScrollView/RCTScrollView.m +12 -8
  38. package/ReactAndroid/api/ReactAndroid.api +0 -1
  39. package/ReactAndroid/cmake-utils/ReactNative-application.cmake +1 -1
  40. package/ReactAndroid/gradle.properties +1 -1
  41. package/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.kt +2 -0
  42. package/ReactAndroid/src/main/java/com/facebook/react/modules/core/TimingModule.kt +0 -8
  43. package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java +1 -1
  44. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +16 -0
  45. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +20 -0
  46. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/BlurEvent.kt +16 -0
  47. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FocusEvent.kt +16 -0
  48. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PressInEvent.kt +16 -0
  49. package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PressOutEvent.kt +16 -0
  50. package/ReactAndroid/src/main/java/com/facebook/react/views/modal/ReactModalHostView.kt +11 -3
  51. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +212 -4
  52. package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +47 -4
  53. package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
  54. package/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp +3 -2
  55. package/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp +18 -0
  56. package/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h +8 -0
  57. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.h +12 -1
  58. package/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTAttributedTextUtils.mm +165 -2
  59. package/cli.js +1 -1
  60. package/index.js +0 -4
  61. package/package.json +8 -8
  62. package/scripts/codegen/generate-artifacts-executor.js +3 -3
  63. package/sdks/.hermesversion +1 -1
  64. package/sdks/hermesc/osx-bin/hermes +0 -0
  65. package/sdks/hermesc/osx-bin/hermesc +0 -0
  66. package/sdks/hermesc/win64-bin/hermesc.exe +0 -0
  67. package/types/modules/Codegen.d.ts +6 -0
  68. package/types/public/ReactNativeTVTypes.d.ts +2 -2
  69. package/Libraries/Components/TabBarIOS/RCTTabBarItemNativeComponent.js +0 -99
  70. package/Libraries/Components/TabBarIOS/RCTTabBarNativeComponent.js +0 -32
  71. package/Libraries/Components/TabBarIOS/TabBarIOS.ios.js +0 -59
  72. package/Libraries/Components/TabBarIOS/TabBarIOS.js +0 -52
  73. package/Libraries/Components/TabBarIOS/TabBarIOSProps.js +0 -52
  74. package/Libraries/Components/TabBarIOS/TabBarItemIOS.ios.js +0 -177
  75. package/Libraries/Components/TabBarIOS/TabBarItemIOS.js +0 -55
  76. package/Libraries/Components/Touchable/TVTouchable.js +0 -71
  77. package/React/Views/RCTTabBar.h +0 -22
  78. package/React/Views/RCTTabBar.m +0 -237
  79. package/React/Views/RCTTabBarItem.h +0 -35
  80. package/React/Views/RCTTabBarItem.m +0 -139
  81. package/React/Views/RCTTabBarItemManager.h +0 -12
  82. package/React/Views/RCTTabBarItemManager.m +0 -38
  83. package/React/Views/RCTTabBarManager.h +0 -12
  84. package/React/Views/RCTTabBarManager.m +0 -81
@@ -110,6 +110,18 @@ const bubblingEventTypes = {
110
110
  bubbled: 'onClick',
111
111
  },
112
112
  },
113
+ topFocus: {
114
+ phasedRegistrationNames: {
115
+ captured: 'onFocusCapture',
116
+ bubbled: 'onFocus',
117
+ },
118
+ },
119
+ topBlur: {
120
+ phasedRegistrationNames: {
121
+ captured: 'onBlurCapture',
122
+ bubbled: 'onBlur',
123
+ },
124
+ },
113
125
  };
114
126
 
115
127
  const directEventTypes = {
@@ -123,6 +135,13 @@ const directEventTypes = {
123
135
  registrationName: 'onGestureHandlerStateChange',
124
136
  }),
125
137
 
138
+ topPressIn: {
139
+ registrationName: 'onPressIn',
140
+ },
141
+ topPressOut: {
142
+ registrationName: 'onPressOut',
143
+ },
144
+
126
145
  // Direct events from UIManagerModuleConstants.java
127
146
  topContentSizeChange: {
128
147
  registrationName: 'onContentSizeChange',
@@ -181,6 +181,12 @@ const directEventTypes = {
181
181
  onGestureHandlerStateChange: DynamicallyInjectedByGestureHandler({
182
182
  registrationName: 'onGestureHandlerStateChange',
183
183
  }),
184
+ topPressIn: {
185
+ registrationName: 'onPressIn',
186
+ },
187
+ topPressOut: {
188
+ registrationName: 'onPressOut',
189
+ },
184
190
  };
185
191
 
186
192
  const validAttributesForNonEventProps = {
@@ -16,6 +16,10 @@ export const validAttributesForTVProps = {
16
16
  nextFocusLeft: true,
17
17
  nextFocusRight: true,
18
18
  nextFocusUp: true,
19
+ onFocus: true,
20
+ onBlur: true,
21
+ onPressIn: true,
22
+ onPressOut: true,
19
23
  trapFocusLeft: true,
20
24
  trapFocusRight: true,
21
25
  trapFocusDown: true,
@@ -157,7 +157,8 @@ export type EventHandlers = $ReadOnly<{|
157
157
  onResponderTerminate: (event: PressEvent) => void,
158
158
  onResponderTerminationRequest: () => boolean,
159
159
  onStartShouldSetResponder: () => boolean,
160
- onTVEvent: (event: any) => void,
160
+ onPressIn: (event: any) => void,
161
+ onPressOut: (event: any) => void,
161
162
  |}>;
162
163
 
163
164
  type TouchState =
@@ -397,6 +398,7 @@ export default class Pressability {
397
398
  |}>;
398
399
  _touchActivateTime: ?number;
399
400
  _touchState: TouchState = 'NOT_RESPONDER';
401
+ _longPressSent: boolean = false;
400
402
 
401
403
  constructor(config: PressabilityConfig) {
402
404
  this.configure(config);
@@ -436,35 +438,50 @@ export default class Pressability {
436
438
  }
437
439
 
438
440
  _createEventHandlers(): EventHandlers {
439
- const tvEventHandlers = {
440
- onTVEvent: (evt: any): void => {
441
- if (this._config.disabled !== false) {
442
- // $FlowFixMe[prop-missing]
443
- if (evt?.eventType === 'focus') {
444
- const {onFocus} = this._config;
445
- onFocus && onFocus(evt);
446
- // $FlowFixMe[prop-missing]
447
- } else if (evt.eventType === 'blur') {
448
- const {onBlur} = this._config;
449
- onBlur && onBlur(evt);
450
- } else if (evt.eventType === 'select') {
451
- const {onPress, onPressIn, onPressOut} = this._config;
452
- // $FlowFixMe[incompatible-exact]
453
- onPressIn && onPressIn(evt);
454
- onPress && onPress(evt);
455
- setTimeout(() => {
456
- onPressOut && onPressOut(evt);
457
- }, this._config.minPressDuration ?? DEFAULT_MIN_PRESS_DURATION);
458
- } else if (evt.eventType === 'longSelect') {
459
- const {onLongPress, onPressIn, onPressOut} = this._config;
460
- onLongPress && onLongPress(evt);
461
- evt?.eventKeyAction === 0
462
- ? onPressIn && onPressIn(evt)
463
- : onPressOut && onPressOut(evt);
441
+ const tvPressEventHandlers = {
442
+ onPressIn: (evt: any): void => {
443
+ if (this._config.disabled === false) {
444
+ return;
445
+ }
446
+
447
+ this._longPressSent = false;
448
+
449
+ const {onPressIn, onLongPress} = this._config;
450
+ onPressIn && onPressIn(evt);
451
+
452
+ const delayPressIn = normalizeDelay(this._config.delayPressIn);
453
+ const delayLongPress = normalizeDelay(
454
+ this._config.delayLongPress,
455
+ 10,
456
+ DEFAULT_LONG_PRESS_DELAY_MS - delayPressIn,
457
+ );
458
+ this._longPressDelayTimeout = setTimeout(() => {
459
+ onLongPress && onLongPress(evt);
460
+ this._longPressSent = true;
461
+ }, delayLongPress + delayPressIn);
462
+ },
463
+ onPressOut: (evt: any): void => {
464
+ if (this._config.disabled === false) {
465
+ return;
466
+ }
467
+ this._cancelLongPressDelayTimeout();
468
+ const {onPress, onLongPress, onPressOut, android_disableSound} =
469
+ this._config;
470
+ onPressOut && onPressOut(evt);
471
+
472
+ if (onPress != null) {
473
+ const isPressCanceledByLongPress =
474
+ onLongPress != null && this._longPressSent;
475
+ if (!isPressCanceledByLongPress) {
476
+ if (Platform.OS === 'android' && android_disableSound !== true) {
477
+ SoundManager.playTouchSound();
478
+ }
479
+ onPress(evt);
464
480
  }
465
481
  }
466
482
  },
467
483
  };
484
+
468
485
  const focusEventHandlers = {
469
486
  onBlur: (event: BlurEvent): void => {
470
487
  const {onBlur} = this._config;
@@ -645,7 +662,7 @@ export default class Pressability {
645
662
  };
646
663
  }
647
664
  return {
648
- ...tvEventHandlers,
665
+ ...tvPressEventHandlers,
649
666
  ...focusEventHandlers,
650
667
  ...responderEventHandlers,
651
668
  ...hoverPointerEvents,
@@ -698,7 +715,7 @@ export default class Pressability {
698
715
  },
699
716
  };
700
717
  return {
701
- ...tvEventHandlers,
718
+ ...tvPressEventHandlers,
702
719
  ...focusEventHandlers,
703
720
  ...responderEventHandlers,
704
721
  ...mouseEventHandlers,
@@ -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,6 +246,15 @@ export interface GestureResponderEvent
246
246
 
247
247
  export interface MouseEvent extends NativeSyntheticEvent<NativeMouseEvent> {}
248
248
 
249
+ export interface NativeFocusEvent extends TargetedEvent {}
250
+ export interface FocusEvent extends NativeSyntheticEvent<NativeFocusEvent> {}
251
+
252
+ export interface NativeBlurEvent extends TargetedEvent {}
253
+ export interface BlurEvent extends NativeSyntheticEvent<NativeBlurEvent> {}
254
+
255
+ export interface NativePressEvent extends TargetedEvent {}
256
+ export interface PressEvent extends NativeSyntheticEvent<NativePressEvent> {}
257
+
249
258
  export interface TargetedEvent {
250
259
  target: number;
251
260
  }
@@ -265,6 +274,18 @@ export interface PointerEvents {
265
274
  onPointerUpCapture?: ((event: PointerEvent) => void) | undefined;
266
275
  }
267
276
 
277
+ export interface PressEvents {
278
+ onPressIn?: ((event: PressEvent) => void) | undefined;
279
+ onPressOut?: ((event: PressEvent) => void) | undefined;
280
+ }
281
+
282
+ export interface FocusEvents {
283
+ onFocus?: ((event: FocusEvent) => void) | undefined;
284
+ onFocusCapture?: ((event: FocusEvent) => void) | undefined;
285
+ onBlur?: ((event: BlurEvent) => void) | undefined;
286
+ onBlurCapture?: ((event: BlurEvent) => void) | undefined;
287
+ }
288
+
268
289
  export interface TVRemoteEvent {
269
290
  tag?: number | undefined;
270
291
  target?: number | undefined;
@@ -282,6 +282,12 @@ export type FocusEvent = SyntheticEvent<
282
282
  |}>,
283
283
  >;
284
284
 
285
+ export type RemotePressEvent = SyntheticEvent<
286
+ $ReadOnly<{|
287
+ target: number,
288
+ |}>,
289
+ >;
290
+
285
291
  export type MouseEvent = SyntheticEvent<
286
292
  $ReadOnly<{|
287
293
  clientX: number,
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,7 +23,7 @@ NSDictionary* RCTGetReactNativeVersion(void)
23
23
  __rnVersion = @{
24
24
  RCTVersionMajor: @(0),
25
25
  RCTVersionMinor: @(76),
26
- RCTVersionPatch: @(1),
26
+ RCTVersionPatch: @(2),
27
27
  RCTVersionPrerelease: @"0",
28
28
  };
29
29
  });
@@ -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
  /**