diff --git a/Libraries/Components/Button.js b/Libraries/Components/Button.js index 7a233cfc584edc..90c0c6869a7cdd 100644 --- a/Libraries/Components/Button.js +++ b/Libraries/Components/Button.js @@ -20,7 +20,16 @@ const View = require('View'); const invariant = require('invariant'); -import type {PressEvent} from 'CoreEventTypes'; +import type {SyntheticEvent, PressEvent} from 'CoreEventTypes'; + +type TargetEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + |}>, +>; + +type AccessibilityBlurEvent = TargetEvent; +type AccessibilityFocusEvent = TargetEvent; type ButtonProps = $ReadOnly<{| /** @@ -92,6 +101,17 @@ type ButtonProps = $ReadOnly<{| * Used to locate this view in end-to-end tests. */ testID?: ?string, + + onAccessibilityBlur?: ?(AccessibilityBlurEvent) => void, + + /** + * When `accessible` is true and VoiceOver (iOS) or TalkBack (Android) is + * enabled, this event is fired immediately once the element gains the screen + * reader focus. + * + * See http://facebook.github.io/react-native/docs/view.html#onaccessibilityfocus + */ + onAccessibilityFocus?: ?(AccessibilityFocusEvent) => void, |}>; /** @@ -127,6 +147,8 @@ class Button extends React.Component { const { accessibilityLabel, color, + onAccessibilityFocus, + onAccessibilityBlur, onPress, title, hasTVPreferredFocus, @@ -165,6 +187,8 @@ class Button extends React.Component { void, + onAccessibilityFocus?: ?(AccessibilityFocusEvent) => void, onBlur?: ?(e: BlurEvent) => void, onFocus?: ?(e: FocusEvent) => void, onLayout?: ?(event: LayoutEvent) => mixed, diff --git a/Libraries/Components/View/ViewPropTypes.js b/Libraries/Components/View/ViewPropTypes.js index 998baca13bd97f..72f9a58e3d3793 100644 --- a/Libraries/Components/View/ViewPropTypes.js +++ b/Libraries/Components/View/ViewPropTypes.js @@ -10,7 +10,12 @@ 'use strict'; -import type {PressEvent, Layout, LayoutEvent} from 'CoreEventTypes'; +import type { + SyntheticEvent, + PressEvent, + Layout, + LayoutEvent, +} from 'CoreEventTypes'; import type {EdgeInsetsProp} from 'EdgeInsetsPropType'; import type React from 'React'; import type {ViewStyleProp} from 'StyleSheet'; @@ -20,6 +25,15 @@ import type {AccessibilityRole, AccessibilityStates} from 'ViewAccessibility'; export type ViewLayout = Layout; export type ViewLayoutEvent = LayoutEvent; +type TargetEvent = SyntheticEvent< + $ReadOnly<{| + target: number, + |}>, +>; + +export type AccessibilityBlurEvent = TargetEvent; +export type AccessibilityFocusEvent = TargetEvent; + type DirectEventProps = $ReadOnly<{| /** * When `accessible` is true, the system will try to invoke this function @@ -29,6 +43,25 @@ type DirectEventProps = $ReadOnly<{| */ onAccessibilityAction?: ?(string) => void, + /** + * When `accessible` is true and VoiceOver (iOS) or TalkBack (Android) is + * enabled, this event is fired immediately once the element loses the screen + * reader focus. + * + * See http://facebook.github.io/react-native/docs/view.html#onaccessibilityblur + */ + + onAccessibilityBlur?: ?(AccessibilityBlurEvent) => void, + + /** + * When `accessible` is true and VoiceOver (iOS) or TalkBack (Android) is + * enabled, this event is fired immediately once the element gains the screen + * reader focus. + * + * See http://facebook.github.io/react-native/docs/view.html#onaccessibilityfocus + */ + onAccessibilityFocus?: ?(AccessibilityFocusEvent) => void, + /** * When `accessible` is true, the system will try to invoke this function * when the user performs accessibility tap gesture. diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 2467ca1333f9af..704265ab3b3565 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -27,6 +27,8 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; @property (nonatomic, copy) RCTDirectEventBlock onAccessibilityTap; @property (nonatomic, copy) RCTDirectEventBlock onMagicTap; @property (nonatomic, copy) RCTDirectEventBlock onAccessibilityEscape; +@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityFocus; +@property (nonatomic, copy) RCTDirectEventBlock onAccessibilityBlur; /** * Accessibility properties diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index ea305016e453ba..3dc58d10462fcc 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -357,6 +357,24 @@ - (BOOL)accessibilityPerformEscape } } +- (void)accessibilityElementDidLoseFocus +{ + [super accessibilityElementDidLoseFocus]; + + if (_onAccessibilityBlur) { + _onAccessibilityBlur(nil); + } +} + +- (void)accessibilityElementDidBecomeFocused +{ + [super accessibilityElementDidBecomeFocused]; + + if (_onAccessibilityFocus) { + _onAccessibilityFocus(nil); + } +} + - (NSString *)description { NSString *superDescription = super.description; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 6efc77c07771a8..4dbc3588dd3980 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -133,6 +133,8 @@ - (RCTShadowView *)shadowView RCT_REMAP_VIEW_PROPERTY(onAccessibilityTap, reactAccessibilityElement.onAccessibilityTap, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(onMagicTap, reactAccessibilityElement.onMagicTap, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(onAccessibilityEscape, reactAccessibilityElement.onAccessibilityEscape, RCTDirectEventBlock) +RCT_REMAP_VIEW_PROPERTY(onAccessibilityBlur, reactAccessibilityElement.onAccessibilityBlur, RCTDirectEventBlock) +RCT_REMAP_VIEW_PROPERTY(onAccessibilityFocus, reactAccessibilityElement.onAccessibilityFocus, RCTDirectEventBlock) RCT_REMAP_VIEW_PROPERTY(testID, reactAccessibilityElement.accessibilityIdentifier, NSString) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityDelegateUtil.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityDelegateUtil.java index e5def5725ef020..3e760a6dda3c8c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityDelegateUtil.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/AccessibilityDelegateUtil.java @@ -6,22 +6,26 @@ package com.facebook.react.uimanager; import android.content.Context; -import androidx.core.view.AccessibilityDelegateCompat; -import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; import android.text.SpannableString; import android.text.style.URLSpan; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; import androidx.core.view.AccessibilityDelegateCompat; import androidx.core.view.ViewCompat; -import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.CollectionItemInfoCompat; -import android.view.View; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.R; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; + import java.util.Locale; import javax.annotation.Nullable; +import com.facebook.react.R; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.uimanager.events.RCTEventEmitter; + /** * Utility class that handles the addition of a "role" for accessibility to * either a View or AccessibilityNodeInfo. @@ -29,6 +33,17 @@ public class AccessibilityDelegateUtil { + public static final String BLUR_EVENT_NAME = "onAccessibilityBlur"; + public static final String FOCUS_EVENT_NAME = "onAccessibilityFocus"; + + public static String getBlurEventName() { + return BLUR_EVENT_NAME; + } + + public static String getFocusEventName() { + return FOCUS_EVENT_NAME; + } + /** * These roles are defined by Google's TalkBack screen reader, and this list * should be kept up to date with their implementation. Details can be seen in @@ -108,17 +123,34 @@ public static void setDelegate(final View view) { // if a view already has an accessibility delegate, replacing it could cause // problems, // so leave it alone. - if (!ViewCompat.hasAccessibilityDelegate(view) - && (accessibilityRole != null || view.getTag(R.id.accessibility_states) != null)) { + if (!ViewCompat.hasAccessibilityDelegate(view)) { ViewCompat.setAccessibilityDelegate(view, new AccessibilityDelegateCompat() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); - setRole(info, accessibilityRole, view.getContext()); - // states are changable. - ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states); - if (accessibilityStates != null) { - setState(info, accessibilityStates, view.getContext()); + + if (accessibilityRole != null || view.getTag(R.id.accessibility_states) != null) { + setRole(info, accessibilityRole, view.getContext()); + // states are changable. + ReadableArray accessibilityStates = (ReadableArray) view.getTag(R.id.accessibility_states); + if (accessibilityStates != null) { + setState(info, accessibilityStates, view.getContext()); + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent accessibilityEvent) { + super.onInitializeAccessibilityEvent(host, accessibilityEvent); + ReactContext reactContext = (ReactContext)host.getContext(); + + WritableMap event = Arguments.createMap(); + event.putInt("target", host.getId()); + + if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED) { + reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(host.getId(), BLUR_EVENT_NAME, event); + } else if (accessibilityEvent.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { + reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(host.getId(), FOCUS_EVENT_NAME, event); } } }); diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 3135afa71120f5..c6260abadbfd0a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -13,11 +13,13 @@ import java.util.HashMap; import com.facebook.react.R; +import com.facebook.react.common.MapBuilder; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.uimanager.AccessibilityDelegateUtil.AccessibilityRole; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.util.ReactFindViewUtil; +import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -288,4 +290,12 @@ protected void onAfterUpdateTransaction(@Nonnull T view) { super.onAfterUpdateTransaction(view); updateViewAccessibility(view); } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.builder() + .put(AccessibilityDelegateUtil.getBlurEventName(), MapBuilder.of("registrationName", "onAccessibilityBlur")) + .put(AccessibilityDelegateUtil.getFocusEventName(), MapBuilder.of("registrationName", "onAccessibilityFocus")) + .build(); + } }