From 01bcde3ed814332c2c48ec0122579b594db64d3b Mon Sep 17 00:00:00 2001 From: Sam Mathias Weggersen Date: Wed, 17 Apr 2019 09:51:53 -0700 Subject: [PATCH] Keyboard accessibility improvements (#24359) Summary: In order to meet our accessibility requirements we need to have full support for keyboard navigation. The Touchable components works with press/tap with a finger, but doesn't respond to 'enter' when using a keyboard. Navigation works fine. This PR adds an onClick listener to touchable views that have the onPress prop defined. [Android] [Added] - Add View.OnClickListener to Touchable components when onPress is defined Pull Request resolved: https://github.com/facebook/react-native/pull/24359 Differential Revision: D14971230 Pulled By: cpojer fbshipit-source-id: ca5559ca1308ee6c338532a00dcea4d00fa57f42 --- .../Components/Touchable/TouchableBounce.js | 6 ++++ .../Touchable/TouchableHighlight.js | 4 +++ .../TouchableNativeFeedback.android.js | 5 +++ .../Components/Touchable/TouchableOpacity.js | 4 +++ .../Touchable/TouchableWithoutFeedback.js | 3 ++ .../TouchableHighlight-test.js.snap | 2 ++ Libraries/Components/View/ViewPropTypes.js | 14 ++++++++ .../react/uimanager/BaseViewManager.java | 3 +- .../uimanager/UIManagerModuleConstants.java | 1 + .../react/views/view/ReactViewManager.java | 25 +++++++++++++++ .../react/views/view/ViewGroupClickEvent.java | 32 +++++++++++++++++++ 11 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/views/view/ViewGroupClickEvent.java diff --git a/Libraries/Components/Touchable/TouchableBounce.js b/Libraries/Components/Touchable/TouchableBounce.js index 04817aef02bacf..5dd3a46afc8a74 100644 --- a/Libraries/Components/Touchable/TouchableBounce.js +++ b/Libraries/Components/Touchable/TouchableBounce.js @@ -184,6 +184,12 @@ const TouchableBounce = ((createReactClass({ nativeID={this.props.nativeID} testID={this.props.testID} hitSlop={this.props.hitSlop} + clickable={ + this.props.clickable !== false && + this.props.onPress !== undefined && + !this.props.disabled + } + onClick={this.touchableHandlePress} onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onResponderTerminationRequest={ this.touchableHandleResponderTerminationRequest diff --git a/Libraries/Components/Touchable/TouchableHighlight.js b/Libraries/Components/Touchable/TouchableHighlight.js index fa0e083fa8a985..1b5afa6ef70b87 100644 --- a/Libraries/Components/Touchable/TouchableHighlight.js +++ b/Libraries/Components/Touchable/TouchableHighlight.js @@ -422,6 +422,10 @@ const TouchableHighlight = ((createReactClass({ nextFocusLeft={this.props.nextFocusLeft} nextFocusRight={this.props.nextFocusRight} nextFocusUp={this.props.nextFocusUp} + clickable={ + this.props.clickable !== false && this.props.onPress !== undefined + } + onClick={this.touchableHandlePress} onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onResponderTerminationRequest={ this.touchableHandleResponderTerminationRequest diff --git a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js index 77b6e84ea7c60f..f23223a4ac2ec9 100644 --- a/Libraries/Components/Touchable/TouchableNativeFeedback.android.js +++ b/Libraries/Components/Touchable/TouchableNativeFeedback.android.js @@ -325,6 +325,11 @@ const TouchableNativeFeedback = createReactClass({ nextFocusRight: this.props.nextFocusRight, nextFocusUp: this.props.nextFocusUp, hasTVPreferredFocus: this.props.hasTVPreferredFocus, + clickable: + this.props.clickable !== false && + this.props.onPress !== undefined && + !this.props.disabled, + onClick: this.touchableHandlePress, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this .touchableHandleResponderTerminationRequest, diff --git a/Libraries/Components/Touchable/TouchableOpacity.js b/Libraries/Components/Touchable/TouchableOpacity.js index 98a2c5d0afd099..b26b34b999deb8 100644 --- a/Libraries/Components/Touchable/TouchableOpacity.js +++ b/Libraries/Components/Touchable/TouchableOpacity.js @@ -324,6 +324,10 @@ const TouchableOpacity = ((createReactClass({ hasTVPreferredFocus={this.props.hasTVPreferredFocus} tvParallaxProperties={this.props.tvParallaxProperties} hitSlop={this.props.hitSlop} + clickable={ + this.props.clickable !== false && this.props.onPress !== undefined + } + onClick={this.touchableHandlePress} onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder} onResponderTerminationRequest={ this.touchableHandleResponderTerminationRequest diff --git a/Libraries/Components/Touchable/TouchableWithoutFeedback.js b/Libraries/Components/Touchable/TouchableWithoutFeedback.js index 33c64215679ad6..67c3c31eb6cbb8 100755 --- a/Libraries/Components/Touchable/TouchableWithoutFeedback.js +++ b/Libraries/Components/Touchable/TouchableWithoutFeedback.js @@ -249,6 +249,9 @@ const TouchableWithoutFeedback = ((createReactClass({ return (React: any).cloneElement(child, { ...overrides, accessible: this.props.accessible !== false, + clickable: + this.props.clickable !== false && this.props.onPress !== undefined, + onClick: this.touchableHandlePress, onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder, onResponderTerminationRequest: this .touchableHandleResponderTerminationRequest, diff --git a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap index 34d71cc12a523e..00b32eed85ee5c 100644 --- a/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap +++ b/Libraries/Components/Touchable/__tests__/__snapshots__/TouchableHighlight-test.js.snap @@ -3,7 +3,9 @@ exports[`TouchableHighlight renders correctly 1`] = ` void, |}>; type IOSViewProps = $ReadOnly<{| 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 2c2867378988ee..d309317a3d671a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -6,10 +6,9 @@ package com.facebook.react.uimanager; import android.graphics.Color; -import android.os.Build; -import androidx.core.view.ViewCompat; import android.view.View; import android.view.ViewParent; +import androidx.core.view.ViewCompat; import com.facebook.react.R; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.uimanager.AccessibilityDelegateUtil.AccessibilityRole; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java index 36c3162cb77b52..6ca9380ba6422b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java @@ -83,6 +83,7 @@ .put("topLoadingStart", MapBuilder.of(rn, "onLoadingStart")) .put("topSelectionChange", MapBuilder.of(rn, "onSelectionChange")) .put("topMessage", MapBuilder.of(rn, "onMessage")) + .put("topClick", MapBuilder.of(rn, "onClick")) // Scroll events are added as per task T22348735. // Subject for further improvement. .put("topScrollBeginDrag", MapBuilder.of(rn, "onScrollBeginDrag")) diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java index 31178517860232..69dce016617fde 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewManager.java @@ -12,6 +12,7 @@ import android.os.Build; import android.view.View; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; @@ -21,10 +22,12 @@ import com.facebook.react.uimanager.PointerEvents; import com.facebook.react.uimanager.Spacing; import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.ViewProps; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.annotations.ReactPropGroup; +import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.yoga.YogaConstants; import java.util.Locale; import java.util.Map; @@ -223,6 +226,28 @@ public void setCollapsable(ReactViewGroup view, boolean collapsable) { // handled in NativeViewHierarchyOptimizer } + @ReactProp(name = "clickable") + public void setClickable(final ReactViewGroup view, boolean clickable) { + if (clickable) { + view.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + final EventDispatcher mEventDispatcher = ((ReactContext)view.getContext()).getNativeModule(UIManagerModule.class) + .getEventDispatcher(); + mEventDispatcher.dispatchEvent(new ViewGroupClickEvent(view.getId())); + }}); + + // Clickable elements are focusable. On API 26, this is taken care by setClickable. + // Explicitly calling setFocusable here for backward compatibility. + view.setFocusable(true /*isFocusable*/); + } + else { + view.setOnClickListener(null); + view.setClickable(false); + } + } + @ReactProp(name = ViewProps.OVERFLOW) public void setOverflow(ReactViewGroup view, String overflow) { view.setOverflow(overflow); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ViewGroupClickEvent.java b/ReactAndroid/src/main/java/com/facebook/react/views/view/ViewGroupClickEvent.java new file mode 100644 index 00000000000000..16fa8a2cce6005 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ViewGroupClickEvent.java @@ -0,0 +1,32 @@ +package com.facebook.react.views.view; + + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.uimanager.events.Event; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +/** + * Represents a Click on the ReactViewGroup + */ +public class ViewGroupClickEvent extends Event { + private static final String EVENT_NAME = "topClick"; + + public ViewGroupClickEvent(int viewId) { + super(viewId); + } + + @Override + public String getEventName() { + return EVENT_NAME; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), Arguments.createMap()); + } +}