react-native-tvos 0.76.1-0 → 0.76.1-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Libraries/Components/Pressable/Pressable.d.ts +1 -1
- package/Libraries/Components/Pressable/Pressable.js +0 -15
- package/Libraries/Components/TV/TVViewPropTypes.js +2 -1
- package/Libraries/Components/TextInput/TextInput.d.ts +1 -1
- package/Libraries/Components/Touchable/Touchable.js +0 -43
- package/Libraries/Components/Touchable/TouchableBounce.js +0 -33
- package/Libraries/Components/Touchable/TouchableHighlight.js +12 -47
- package/Libraries/Components/Touchable/TouchableNativeFeedback.js +0 -33
- package/Libraries/Components/Touchable/TouchableOpacity.js +12 -44
- package/Libraries/Components/Touchable/TouchableWithoutFeedback.js +0 -19
- package/Libraries/Components/View/ViewNativeComponent.js +6 -0
- package/Libraries/Components/View/ViewPropTypes.d.ts +12 -1
- package/Libraries/Components/View/ViewPropTypes.js +7 -0
- package/Libraries/Core/ReactNativeVersion.js +1 -1
- package/Libraries/NativeComponent/BaseViewConfig.android.js +19 -0
- package/Libraries/NativeComponent/BaseViewConfig.ios.js +6 -0
- package/Libraries/NativeComponent/TVViewConfig.js +4 -0
- package/Libraries/Pressability/Pressability.js +45 -28
- package/Libraries/Types/CoreEventTypes.d.ts +21 -0
- package/Libraries/Types/CoreEventTypes.js +6 -0
- package/React/Base/RCTVersion.m +1 -1
- package/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +39 -25
- package/React/Views/RCTTVView.h +13 -0
- package/React/Views/RCTTVView.m +54 -34
- package/React/Views/RCTViewManager.m +4 -0
- package/ReactAndroid/gradle.properties +1 -1
- package/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java +1 -1
- package/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +16 -0
- package/ReactAndroid/src/main/java/com/facebook/react/uimanager/ReactAccessibilityDelegate.java +20 -0
- package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/BlurEvent.kt +16 -0
- package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/FocusEvent.kt +16 -0
- package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PressInEvent.kt +16 -0
- package/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PressOutEvent.kt +16 -0
- package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.java +212 -4
- package/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +47 -4
- package/ReactCommon/cxxreact/ReactNativeVersion.h +1 -1
- package/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.cpp +18 -0
- package/ReactCommon/react/renderer/components/view/BaseViewEventEmitter.h +8 -0
- package/package.json +2 -2
- package/types/public/ReactNativeTVTypes.d.ts +1 -1
- package/Libraries/Components/Touchable/TVTouchable.js +0 -71
|
@@ -22,16 +22,21 @@ import android.graphics.RectF;
|
|
|
22
22
|
import android.graphics.drawable.Drawable;
|
|
23
23
|
import android.graphics.drawable.LayerDrawable;
|
|
24
24
|
import android.os.Build;
|
|
25
|
+
import android.os.Bundle;
|
|
25
26
|
import android.view.FocusFinder;
|
|
27
|
+
import android.view.KeyEvent;
|
|
26
28
|
import android.view.MotionEvent;
|
|
27
29
|
import android.view.View;
|
|
28
30
|
import android.view.ViewGroup;
|
|
29
31
|
import android.view.ViewParent;
|
|
30
32
|
import android.view.ViewStructure;
|
|
33
|
+
import android.view.accessibility.AccessibilityNodeInfo;
|
|
31
34
|
import android.view.animation.Animation;
|
|
32
35
|
|
|
33
36
|
import androidx.annotation.NonNull;
|
|
34
37
|
import androidx.annotation.Nullable;
|
|
38
|
+
import androidx.core.view.ViewCompat;
|
|
39
|
+
|
|
35
40
|
import com.facebook.common.logging.FLog;
|
|
36
41
|
import com.facebook.infer.annotation.Assertions;
|
|
37
42
|
import com.facebook.react.R;
|
|
@@ -53,6 +58,7 @@ import com.facebook.react.uimanager.LengthPercentageType;
|
|
|
53
58
|
import com.facebook.react.uimanager.MeasureSpecAssertions;
|
|
54
59
|
import com.facebook.react.uimanager.PixelUtil;
|
|
55
60
|
import com.facebook.react.uimanager.PointerEvents;
|
|
61
|
+
import com.facebook.react.uimanager.ReactAccessibilityDelegate;
|
|
56
62
|
import com.facebook.react.uimanager.ReactClippingProhibitedView;
|
|
57
63
|
import com.facebook.react.uimanager.ReactClippingViewGroup;
|
|
58
64
|
import com.facebook.react.uimanager.ReactClippingViewGroupHelper;
|
|
@@ -61,10 +67,16 @@ import com.facebook.react.uimanager.ReactPointerEventsView;
|
|
|
61
67
|
import com.facebook.react.uimanager.ReactZIndexedViewGroup;
|
|
62
68
|
import com.facebook.react.uimanager.RootView;
|
|
63
69
|
import com.facebook.react.uimanager.RootViewUtil;
|
|
70
|
+
import com.facebook.react.uimanager.UIManagerHelper;
|
|
64
71
|
import com.facebook.react.uimanager.ViewGroupDrawingOrderHelper;
|
|
65
72
|
import com.facebook.react.uimanager.common.UIManagerType;
|
|
66
73
|
import com.facebook.react.uimanager.common.ViewUtil;
|
|
67
74
|
import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable;
|
|
75
|
+
import com.facebook.react.uimanager.events.BlurEvent;
|
|
76
|
+
import com.facebook.react.uimanager.events.EventDispatcher;
|
|
77
|
+
import com.facebook.react.uimanager.events.FocusEvent;
|
|
78
|
+
import com.facebook.react.uimanager.events.PressInEvent;
|
|
79
|
+
import com.facebook.react.uimanager.events.PressOutEvent;
|
|
68
80
|
import com.facebook.react.uimanager.style.BackgroundImageLayer;
|
|
69
81
|
import com.facebook.react.uimanager.style.BorderRadiusProp;
|
|
70
82
|
import com.facebook.react.uimanager.style.BorderStyle;
|
|
@@ -98,8 +110,10 @@ public class ReactViewGroup extends ViewGroup
|
|
|
98
110
|
|
|
99
111
|
private @NonNull int[] focusDestinations = new int[0];
|
|
100
112
|
private boolean autoFocus = false;
|
|
113
|
+
private boolean isFocusGuideTalkbackAccessibilityDelegateSet = false;
|
|
101
114
|
private WeakReference<View> lastFocusedElement;
|
|
102
115
|
private boolean mRecoverFocus = false;
|
|
116
|
+
private boolean originalIsFocusable = false;
|
|
103
117
|
private boolean trapFocusUp = false;
|
|
104
118
|
private boolean trapFocusDown = false;
|
|
105
119
|
private boolean trapFocusLeft = false;
|
|
@@ -213,6 +227,8 @@ public class ReactViewGroup extends ViewGroup
|
|
|
213
227
|
updateBackgroundDrawable(null);
|
|
214
228
|
|
|
215
229
|
resetPointerEvents();
|
|
230
|
+
|
|
231
|
+
cleanupFocusGuideTalkbackAccessibilityDelegate();
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
private ViewGroupDrawingOrderHelper getDrawingOrderHelper() {
|
|
@@ -559,7 +575,7 @@ public class ReactViewGroup extends ViewGroup
|
|
|
559
575
|
return super.getChildVisibleRect(child, r, offset);
|
|
560
576
|
}
|
|
561
577
|
|
|
562
|
-
|
|
578
|
+
private View getFirstFocusableView(ReactViewGroup viewGroup) {
|
|
563
579
|
ArrayList<View> focusables = new ArrayList<View>(0);
|
|
564
580
|
/**
|
|
565
581
|
* `addFocusables` is the method used by `FocusFinder` to determine
|
|
@@ -578,10 +594,10 @@ public class ReactViewGroup extends ViewGroup
|
|
|
578
594
|
* The other ones on the list can be non-focusable as well.
|
|
579
595
|
* So, we run a loop till finding the first real focusable element.
|
|
580
596
|
*/
|
|
581
|
-
if (focusables.size() <= 0) return
|
|
597
|
+
if (focusables.size() <= 0) return null;
|
|
582
598
|
|
|
583
599
|
View firstFocusableElement = null;
|
|
584
|
-
|
|
600
|
+
int index = 0;
|
|
585
601
|
while (firstFocusableElement == null && index < focusables.size()) {
|
|
586
602
|
View elem = focusables.get(index);
|
|
587
603
|
if (elem != viewGroup) {
|
|
@@ -591,6 +607,12 @@ public class ReactViewGroup extends ViewGroup
|
|
|
591
607
|
index++;
|
|
592
608
|
}
|
|
593
609
|
|
|
610
|
+
return firstFocusableElement;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
boolean moveFocusToFirstFocusable(ReactViewGroup viewGroup) {
|
|
614
|
+
View firstFocusableElement = this.getFirstFocusableView(viewGroup);
|
|
615
|
+
|
|
594
616
|
if (firstFocusableElement != null) return firstFocusableElement.requestFocus();
|
|
595
617
|
|
|
596
618
|
return false;
|
|
@@ -1244,6 +1266,139 @@ public class ReactViewGroup extends ViewGroup
|
|
|
1244
1266
|
setAlpha(0);
|
|
1245
1267
|
}
|
|
1246
1268
|
|
|
1269
|
+
public void initializeFocusGuideTalkbackAccessibilityDelegate() {
|
|
1270
|
+
if (!this.isTVFocusGuide()) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
this.originalIsFocusable = this.isFocusable();
|
|
1275
|
+
|
|
1276
|
+
ReactAccessibilityDelegate viewAccessibilityDelegate = new ReactAccessibilityDelegate(
|
|
1277
|
+
this, originalIsFocusable, this.getImportantForAccessibility()
|
|
1278
|
+
) {
|
|
1279
|
+
@Override
|
|
1280
|
+
public boolean performAccessibilityAction(View host, int action, Bundle args) {
|
|
1281
|
+
if (!(host instanceof ReactViewGroup self)) {
|
|
1282
|
+
return super.performAccessibilityAction(host, action, args);
|
|
1283
|
+
}
|
|
1284
|
+
if (action == AccessibilityNodeInfo.ACTION_FOCUS) {
|
|
1285
|
+
if (self.interceptAccessibilityEvents(action, args)) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
// Handle case when focus guide cannot find any focusable child
|
|
1289
|
+
if (self.isTVFocusGuide() && self.getFirstFocusableView(self) == null) {
|
|
1290
|
+
if (self.getChildCount() > 0) {
|
|
1291
|
+
View child = self.getChildAt(0);
|
|
1292
|
+
ArrayList<View> childFocusables = new ArrayList<>(0);
|
|
1293
|
+
child.addFocusables(childFocusables, FOCUS_DOWN, 0);
|
|
1294
|
+
if (!childFocusables.isEmpty()) {
|
|
1295
|
+
childFocusables.get(0).performAccessibilityAction(action, args);
|
|
1296
|
+
return true;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return super.performAccessibilityAction(host, action, args);
|
|
1301
|
+
}
|
|
1302
|
+
if (action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS) {
|
|
1303
|
+
if (self.interceptAccessibilityEvents(action, args)) {
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
// Handle case when focus guide cannot find any focusable child
|
|
1307
|
+
if (self.isTVFocusGuide() && self.getFirstFocusableView(self) == null) {
|
|
1308
|
+
if (self.getChildCount() > 0) {
|
|
1309
|
+
View child = self.getChildAt(0);
|
|
1310
|
+
ArrayList<View> childFocusables = new ArrayList<>(0);
|
|
1311
|
+
child.addFocusables(childFocusables, FOCUS_DOWN, 0);
|
|
1312
|
+
if (!childFocusables.isEmpty()) {
|
|
1313
|
+
/*
|
|
1314
|
+
* Instead of forwarding AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS,
|
|
1315
|
+
* let's invoke AccessibilityNodeInfo.ACTION_FOCUS
|
|
1316
|
+
* to trigger focus events on JS side - the AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS
|
|
1317
|
+
* will be automatically invoked later
|
|
1318
|
+
*/
|
|
1319
|
+
childFocusables.get(0).performAccessibilityAction(
|
|
1320
|
+
AccessibilityNodeInfo.ACTION_FOCUS,
|
|
1321
|
+
args
|
|
1322
|
+
);
|
|
1323
|
+
return true;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
/*
|
|
1327
|
+
* Let's consume event here, otherwise there might be an issue with
|
|
1328
|
+
* FocusGuide receiving focus instead of one of its child views
|
|
1329
|
+
*/
|
|
1330
|
+
return true;
|
|
1331
|
+
}
|
|
1332
|
+
return super.performAccessibilityAction(host, action, args);
|
|
1333
|
+
}
|
|
1334
|
+
return super.performAccessibilityAction(host, action, args);
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
ViewCompat.setAccessibilityDelegate(this, viewAccessibilityDelegate);
|
|
1338
|
+
this.setFocusable(true);
|
|
1339
|
+
this.setFocusableInTouchMode(true);
|
|
1340
|
+
// To force Talkback to give the a11y event to the FocusGuide
|
|
1341
|
+
// we need to make it look like it has some a11y label to be announced.
|
|
1342
|
+
// Because FocusGuide should always have at least one focusable child view,
|
|
1343
|
+
// which will receive forwarded a11y event from this FocusGuide,
|
|
1344
|
+
// the following fake label will never be announced
|
|
1345
|
+
this.setContentDescription("FocusGuide");
|
|
1346
|
+
this.isFocusGuideTalkbackAccessibilityDelegateSet = true;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
public void cleanupFocusGuideTalkbackAccessibilityDelegate() {
|
|
1350
|
+
ViewCompat.setAccessibilityDelegate(this, null);
|
|
1351
|
+
this.setFocusable(this.originalIsFocusable);
|
|
1352
|
+
this.setFocusableInTouchMode(this.originalIsFocusable);
|
|
1353
|
+
this.originalIsFocusable = false;
|
|
1354
|
+
this.setContentDescription(null);
|
|
1355
|
+
this.isFocusGuideTalkbackAccessibilityDelegateSet = false;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
public boolean hasFocusGuideTalkbackAccessibilityDelegate() {
|
|
1359
|
+
return this.isFocusGuideTalkbackAccessibilityDelegateSet;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
private boolean interceptAccessibilityEvents(int action, Bundle args) {
|
|
1363
|
+
if (!this.isTVFocusGuide()) {
|
|
1364
|
+
return false;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// If it's "FocusGuide", we want to intercept
|
|
1368
|
+
// the event and redirect it to either:
|
|
1369
|
+
// 1) first available destination view from "destinations" prop
|
|
1370
|
+
// 2) last focused element saved when "autoFocus" prop is `true`
|
|
1371
|
+
// 3) first focusable child view
|
|
1372
|
+
View destinationView = this.findDestinationView();
|
|
1373
|
+
if (destinationView != null) {
|
|
1374
|
+
try {
|
|
1375
|
+
destinationView.performAccessibilityAction(action, args);
|
|
1376
|
+
return true;
|
|
1377
|
+
} catch (Exception e) {
|
|
1378
|
+
FLog.e(TAG, "Exception when performing accessibility action on destination view - falling back to next case (last focused view): " + e);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
View lastFocusedView = this.lastFocusedElement.get();
|
|
1382
|
+
if (lastFocusedView != null) {
|
|
1383
|
+
try {
|
|
1384
|
+
lastFocusedView.performAccessibilityAction(action, args);
|
|
1385
|
+
return true;
|
|
1386
|
+
} catch (Exception e) {
|
|
1387
|
+
FLog.e(TAG, "Exception when performing accessibility action on last focused view - falling back to next case (first focusable view): " + e);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
View firstFocusableView = this.getFirstFocusableView(this);
|
|
1391
|
+
if (firstFocusableView != null) {
|
|
1392
|
+
try {
|
|
1393
|
+
firstFocusableView.performAccessibilityAction(action, args);
|
|
1394
|
+
return true;
|
|
1395
|
+
} catch (Exception e) {
|
|
1396
|
+
FLog.e(TAG, "Exception when performing accessibility action on first focusable view - focus guide will not handle focus: " + e);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1247
1402
|
private View findDestinationView() {
|
|
1248
1403
|
for (int focusDestination : focusDestinations) {
|
|
1249
1404
|
View childViewWithTag = findViewById(focusDestination);
|
|
@@ -1274,7 +1429,7 @@ public class ReactViewGroup extends ViewGroup
|
|
|
1274
1429
|
return focusDestinations.length > 0;
|
|
1275
1430
|
}
|
|
1276
1431
|
|
|
1277
|
-
|
|
1432
|
+
boolean isTVFocusGuide() {
|
|
1278
1433
|
/**
|
|
1279
1434
|
* We don't count a view as `TVFocusGuide` if it has `trapFocus*` props enabled.
|
|
1280
1435
|
* The reason is, it's a seperate functionality that has nothing to do with other
|
|
@@ -1362,6 +1517,59 @@ public class ReactViewGroup extends ViewGroup
|
|
|
1362
1517
|
@Override
|
|
1363
1518
|
protected void onFocusChanged(boolean gainFocus, int direction, @Nullable Rect previouslyFocusedRect) {
|
|
1364
1519
|
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
|
|
1520
|
+
|
|
1521
|
+
final EventDispatcher mEventDispatcher =
|
|
1522
|
+
UIManagerHelper.getEventDispatcherForReactTag(
|
|
1523
|
+
(ReactContext) this.getContext(), this.getId());
|
|
1524
|
+
|
|
1525
|
+
if (mEventDispatcher == null) {
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
if (gainFocus) {
|
|
1530
|
+
mEventDispatcher.dispatchEvent(
|
|
1531
|
+
new FocusEvent(
|
|
1532
|
+
UIManagerHelper.getSurfaceId(this.getContext()), this.getId()));
|
|
1533
|
+
} else {
|
|
1534
|
+
mEventDispatcher.dispatchEvent(
|
|
1535
|
+
new BlurEvent(
|
|
1536
|
+
UIManagerHelper.getSurfaceId(this.getContext()), this.getId()));
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
@Override
|
|
1541
|
+
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
1542
|
+
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) && event.getRepeatCount() == 0 && !this.isTVFocusGuide()) {
|
|
1543
|
+
final EventDispatcher mEventDispatcher =
|
|
1544
|
+
UIManagerHelper.getEventDispatcherForReactTag(
|
|
1545
|
+
(ReactContext) this.getContext(), this.getId());
|
|
1546
|
+
|
|
1547
|
+
if (mEventDispatcher == null) {
|
|
1548
|
+
return super.onKeyDown(keyCode, event);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
mEventDispatcher.dispatchEvent(new PressInEvent(UIManagerHelper.getSurfaceId(this.getContext()), this.getId()));
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
|
|
1555
|
+
return super.onKeyDown(keyCode, event);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
@Override
|
|
1559
|
+
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
1560
|
+
if ((keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) && !this.isTVFocusGuide()) {
|
|
1561
|
+
final EventDispatcher mEventDispatcher =
|
|
1562
|
+
UIManagerHelper.getEventDispatcherForReactTag(
|
|
1563
|
+
(ReactContext) this.getContext(), this.getId());
|
|
1564
|
+
|
|
1565
|
+
if (mEventDispatcher == null) {
|
|
1566
|
+
return super.onKeyUp(keyCode, event);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
mEventDispatcher.dispatchEvent(new PressOutEvent(UIManagerHelper.getSurfaceId(this.getContext()), this.getId()));
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
return super.onKeyUp(keyCode, event);
|
|
1365
1573
|
}
|
|
1366
1574
|
|
|
1367
1575
|
@Override
|
|
@@ -7,15 +7,18 @@
|
|
|
7
7
|
|
|
8
8
|
package com.facebook.react.views.view;
|
|
9
9
|
|
|
10
|
+
import android.accessibilityservice.AccessibilityServiceInfo;
|
|
10
11
|
import android.content.Context;
|
|
11
12
|
import android.content.pm.PackageManager;
|
|
12
13
|
import android.graphics.Rect;
|
|
13
|
-
import android.util.Log;
|
|
14
14
|
import android.view.View;
|
|
15
15
|
import androidx.annotation.ColorInt;
|
|
16
16
|
import android.view.ViewGroup;
|
|
17
|
+
import android.view.accessibility.AccessibilityManager;
|
|
18
|
+
|
|
17
19
|
import androidx.annotation.NonNull;
|
|
18
20
|
import androidx.annotation.Nullable;
|
|
21
|
+
|
|
19
22
|
import com.facebook.common.logging.FLog;
|
|
20
23
|
import com.facebook.infer.annotation.Nullsafe;
|
|
21
24
|
import com.facebook.react.bridge.Dynamic;
|
|
@@ -424,6 +427,13 @@ public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
|
|
|
424
427
|
return new ReactViewGroup(context);
|
|
425
428
|
}
|
|
426
429
|
|
|
430
|
+
@Override
|
|
431
|
+
protected void onAfterUpdateTransaction(@NonNull ReactViewGroup view) {
|
|
432
|
+
super.onAfterUpdateTransaction(view);
|
|
433
|
+
|
|
434
|
+
manageFocusGuideAccessibilityDelegate(view);
|
|
435
|
+
}
|
|
436
|
+
|
|
427
437
|
@Override
|
|
428
438
|
public Map<String, Integer> getCommandsMap() {
|
|
429
439
|
return MapBuilder.of(HOTSPOT_UPDATE_KEY, CMD_HOTSPOT_UPDATE, "setPressed", CMD_SET_PRESSED, "setDestinations", CMD_SET_DESTINATIONS);
|
|
@@ -507,6 +517,39 @@ public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
|
|
|
507
517
|
fd[i] = destinations.getInt(i);
|
|
508
518
|
}
|
|
509
519
|
root.setFocusDestinations(fd);
|
|
520
|
+
this.manageFocusGuideAccessibilityDelegate(root);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private void manageFocusGuideAccessibilityDelegate(ReactViewGroup view) {
|
|
524
|
+
AccessibilityManager accessibilityManager = ((AccessibilityManager) view.getContext()
|
|
525
|
+
.getSystemService(Context.ACCESSIBILITY_SERVICE));
|
|
526
|
+
List<AccessibilityServiceInfo> a11yServiceList = accessibilityManager
|
|
527
|
+
.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_SPOKEN);
|
|
528
|
+
boolean isTalkbackInstalledAndEnabled = false;
|
|
529
|
+
for (int i = 0; i < a11yServiceList.size(); i++) {
|
|
530
|
+
String a11yServiceId = a11yServiceList.get(i).getId();
|
|
531
|
+
if (a11yServiceId != null && a11yServiceId.contains("talkback")) {
|
|
532
|
+
isTalkbackInstalledAndEnabled = true;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
boolean isTVFocusable = view.getDescendantFocusability() != ViewGroup.FOCUS_BLOCK_DESCENDANTS;
|
|
536
|
+
if (!view.hasFocusGuideTalkbackAccessibilityDelegate()) {
|
|
537
|
+
if (view.isTVFocusGuide() && isTVFocusable && isTalkbackInstalledAndEnabled) {
|
|
538
|
+
// Custom accessibility delegate is needed only for Talkback,
|
|
539
|
+
// as it's not handling TV focus guide scenarios as well as e.g. Amazon's VoiceView
|
|
540
|
+
//
|
|
541
|
+
// Delegate should only be set if TVFocusGuideView is focusable
|
|
542
|
+
view.initializeFocusGuideTalkbackAccessibilityDelegate();
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
if (!view.isTVFocusGuide() || !isTVFocusable || !isTalkbackInstalledAndEnabled) {
|
|
546
|
+
// If this view had delegate set, but is no longer a "focus guide"
|
|
547
|
+
// or is no longer focusable
|
|
548
|
+
// or talkback is no longer enabled
|
|
549
|
+
// then the delegate should be cleared
|
|
550
|
+
view.cleanupFocusGuideTalkbackAccessibilityDelegate();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
510
553
|
}
|
|
511
554
|
|
|
512
555
|
/**
|
|
@@ -528,17 +571,17 @@ public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
|
|
|
528
571
|
}
|
|
529
572
|
|
|
530
573
|
@ReactProp(name = "trapFocusDown")
|
|
531
|
-
|
|
574
|
+
public void trapFocusDown(ReactViewGroup view, boolean enabled) {
|
|
532
575
|
view.setTrapFocusDown(enabled);
|
|
533
576
|
}
|
|
534
577
|
|
|
535
578
|
@ReactProp(name = "trapFocusLeft")
|
|
536
|
-
|
|
579
|
+
public void trapFocusLeft(ReactViewGroup view, boolean enabled) {
|
|
537
580
|
view.setTrapFocusLeft(enabled);
|
|
538
581
|
}
|
|
539
582
|
|
|
540
583
|
@ReactProp(name = "trapFocusRight")
|
|
541
|
-
|
|
584
|
+
public void trapFocusRight(ReactViewGroup view, boolean enabled) {
|
|
542
585
|
view.setTrapFocusRight(enabled);
|
|
543
586
|
}
|
|
544
587
|
|
|
@@ -32,6 +32,24 @@ void BaseViewEventEmitter::onAccessibilityEscape() const {
|
|
|
32
32
|
dispatchEvent("accessibilityEscape");
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
#pragma mark - Focus
|
|
36
|
+
void BaseViewEventEmitter::onFocus() const {
|
|
37
|
+
dispatchEvent("focus");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
void BaseViewEventEmitter::onBlur() const {
|
|
41
|
+
dispatchEvent("blur");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#pragma mark - Press
|
|
45
|
+
void BaseViewEventEmitter::onPressIn() const {
|
|
46
|
+
dispatchEvent("pressIn");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void BaseViewEventEmitter::onPressOut() const {
|
|
50
|
+
dispatchEvent("pressOut");
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
#pragma mark - Layout
|
|
36
54
|
|
|
37
55
|
void BaseViewEventEmitter::onLayout(const LayoutMetrics& layoutMetrics) const {
|
|
@@ -28,6 +28,14 @@ class BaseViewEventEmitter : public TouchEventEmitter {
|
|
|
28
28
|
void onAccessibilityMagicTap() const;
|
|
29
29
|
void onAccessibilityEscape() const;
|
|
30
30
|
|
|
31
|
+
#pragma mark - Focus
|
|
32
|
+
void onFocus() const;
|
|
33
|
+
void onBlur() const;
|
|
34
|
+
|
|
35
|
+
#pragma mark - Press
|
|
36
|
+
void onPressIn() const;
|
|
37
|
+
void onPressOut() const;
|
|
38
|
+
|
|
31
39
|
#pragma mark - Layout
|
|
32
40
|
|
|
33
41
|
void onLayout(const LayoutMetrics& layoutMetrics) const;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-tvos",
|
|
3
|
-
"version": "0.76.1-
|
|
3
|
+
"version": "0.76.1-1",
|
|
4
4
|
"description": "A framework for building native apps using React",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"@react-native/gradle-plugin": "0.76.1",
|
|
117
117
|
"@react-native/js-polyfills": "0.76.1",
|
|
118
118
|
"@react-native/normalize-colors": "0.76.1",
|
|
119
|
-
"@react-native-tvos/virtualized-lists": "0.76.1-
|
|
119
|
+
"@react-native-tvos/virtualized-lists": "0.76.1-1",
|
|
120
120
|
"abort-controller": "^3.0.0",
|
|
121
121
|
"anser": "^1.4.9",
|
|
122
122
|
"ansi-regex": "^5.0.0",
|
|
@@ -166,7 +166,7 @@ declare module 'react-native' {
|
|
|
166
166
|
* https://github.com/react-native-tvos/react-native-tvos/blob/tvos-v0.69.8/packages/rn-tester/js/examples/TVFocusGuide/TVFocusGuideAutoFocusExample.js
|
|
167
167
|
*/
|
|
168
168
|
export const TVFocusGuideView: React.ForwardRefExoticComponent<FocusGuideProps & React.RefAttributes<View & FocusGuideMethods>>;
|
|
169
|
-
export interface TVTextScrollViewProps extends ScrollViewProps {
|
|
169
|
+
export interface TVTextScrollViewProps extends Omit<ScrollViewProps, 'onFocus' | 'onBlur'> {
|
|
170
170
|
/**
|
|
171
171
|
* The duration of the scroll animation when a swipe is detected.
|
|
172
172
|
* Default value is 0.3 s
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
3
|
-
*
|
|
4
|
-
* This source code is licensed under the MIT license found in the
|
|
5
|
-
* LICENSE file in the root directory of this source tree.
|
|
6
|
-
*
|
|
7
|
-
* @flow
|
|
8
|
-
* @format
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
import type {
|
|
14
|
-
BlurEvent,
|
|
15
|
-
FocusEvent,
|
|
16
|
-
PressEvent,
|
|
17
|
-
} from '../../Types/CoreEventTypes';
|
|
18
|
-
|
|
19
|
-
import Platform from '../../Utilities/Platform';
|
|
20
|
-
import tagForComponentOrHandle from '../TV/tagForComponentOrHandle';
|
|
21
|
-
import {tvFocusEventHandler} from '../TV/TVFocusEventHandler';
|
|
22
|
-
import invariant from 'invariant';
|
|
23
|
-
|
|
24
|
-
type TVTouchableConfig = $ReadOnly<{|
|
|
25
|
-
getDisabled: () => boolean,
|
|
26
|
-
onBlur: (event: BlurEvent) => mixed,
|
|
27
|
-
onFocus: (event: FocusEvent) => mixed,
|
|
28
|
-
onPress: (event: PressEvent) => mixed,
|
|
29
|
-
onLongPress: (event: PressEvent) => mixed,
|
|
30
|
-
|}>;
|
|
31
|
-
|
|
32
|
-
export default class TVTouchable {
|
|
33
|
-
_enabled: boolean = false;
|
|
34
|
-
_focusEventHandler: ?any = null;
|
|
35
|
-
_viewTag: ?number = null;
|
|
36
|
-
|
|
37
|
-
constructor(component: any, config: TVTouchableConfig) {
|
|
38
|
-
invariant(Platform.isTV, 'TVTouchable: Requires `Platform.isTV`.');
|
|
39
|
-
if (!Platform.isTV) {
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const _tvtouchable = this; // eslint-disable-line consistent-this
|
|
43
|
-
this._viewTag = tagForComponentOrHandle(component);
|
|
44
|
-
tvFocusEventHandler?.register(this._viewTag, tvData => {
|
|
45
|
-
if (!_tvtouchable._enabled) {
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
if (tagForComponentOrHandle(component) === tvData.tag) {
|
|
49
|
-
if (tvData.eventType === 'focus') {
|
|
50
|
-
config.onFocus(tvData);
|
|
51
|
-
} else if (tvData.eventType === 'blur') {
|
|
52
|
-
config.onBlur(tvData);
|
|
53
|
-
} else if (tvData.eventType === 'select') {
|
|
54
|
-
if (!config.getDisabled()) {
|
|
55
|
-
config.onPress(tvData);
|
|
56
|
-
}
|
|
57
|
-
} else if (tvData.eventType === 'longSelect') {
|
|
58
|
-
if (!config.getDisabled()) {
|
|
59
|
-
config.onLongPress(tvData);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
this._enabled = true;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
destroy(): void {
|
|
68
|
-
this._enabled = false;
|
|
69
|
-
tvFocusEventHandler?.unregister(this._viewTag);
|
|
70
|
-
}
|
|
71
|
-
}
|