diff --git a/RNTester/js/examples/Accessibility/AccessibilityExample.js b/RNTester/js/examples/Accessibility/AccessibilityExample.js index c2fc33a0ae4eb5..38fa2b3cdb2409 100644 --- a/RNTester/js/examples/Accessibility/AccessibilityExample.js +++ b/RNTester/js/examples/Accessibility/AccessibilityExample.js @@ -13,18 +13,30 @@ const React = require('react'); const { AccessibilityInfo, Button, + Image, Text, View, TouchableOpacity, TouchableWithoutFeedback, Alert, - UIManager, - findNodeHandle, - Platform, + StyleSheet, } = require('react-native'); const RNTesterBlock = require('../../components/RNTesterBlock'); +const checkImageSource = require('./check.png'); +const uncheckImageSource = require('./uncheck.png'); +const mixedCheckboxImageSource = require('./mixed.png'); + +const styles = StyleSheet.create({ + image: { + width: 20, + height: 20, + resizeMode: 'contain', + marginRight: 10, + }, +}); + class AccessibilityExample extends React.Component { render() { return ( @@ -161,13 +173,6 @@ class CheckboxExample extends React.Component { this.setState({ checkboxState: checkboxState, }); - - if (Platform.OS === 'android') { - UIManager.sendAccessibilityEvent( - findNodeHandle(this), - UIManager.AccessibilityEventTypes.typeViewClicked, - ); - } }; render() { @@ -195,13 +200,6 @@ class SwitchExample extends React.Component { this.setState({ switchState: switchState, }); - - if (Platform.OS === 'android') { - UIManager.sendAccessibilityEvent( - findNodeHandle(this), - UIManager.AccessibilityEventTypes.typeViewClicked, - ); - } }; render() { @@ -252,13 +250,6 @@ class SelectionExample extends React.Component { isSelected: !this.state.isSelected, }); } - - if (Platform.OS === 'android') { - UIManager.sendAccessibilityEvent( - findNodeHandle(this.selectableElement.current), - UIManager.AccessibilityEventTypes.typeViewClicked, - ); - } }} accessibilityLabel="element 19" accessibilityState={{ @@ -292,13 +283,6 @@ class ExpandableElementExample extends React.Component { this.setState({ expandState: expandState, }); - - if (Platform.OS === 'android') { - UIManager.sendAccessibilityEvent( - findNodeHandle(this), - UIManager.AccessibilityEventTypes.typeViewClicked, - ); - } }; render() { @@ -314,6 +298,114 @@ class ExpandableElementExample extends React.Component { } } +class NestedCheckBox extends React.Component { + state = { + checkbox1: false, + checkbox2: false, + checkbox3: false, + }; + + _onPress1 = () => { + let checkbox1 = false; + if (this.state.checkbox1 === false) { + checkbox1 = true; + } else if (this.state.checkbox1 === 'mixed') { + checkbox1 = false; + } else { + checkbox1 = false; + } + setTimeout(() => { + this.setState({ + checkbox1: checkbox1, + checkbox2: checkbox1, + checkbox3: checkbox1, + }); + }, 2000); + }; + + _onPress2 = () => { + const checkbox2 = !this.state.checkbox2; + + this.setState({ + checkbox2: checkbox2, + checkbox1: + checkbox2 && this.state.checkbox3 + ? true + : checkbox2 || this.state.checkbox3 + ? 'mixed' + : false, + }); + }; + + _onPress3 = () => { + const checkbox3 = !this.state.checkbox3; + + this.setState({ + checkbox3: checkbox3, + checkbox1: + this.state.checkbox2 && checkbox3 + ? true + : this.state.checkbox2 || checkbox3 + ? 'mixed' + : false, + }); + }; + + render() { + return ( + + + + Meat + + + + Beef + + + + Bacon + + + ); + } +} + class AccessibilityRoleAndStateExample extends React.Component<{}> { render() { return ( @@ -412,6 +504,9 @@ class AccessibilityRoleAndStateExample extends React.Component<{}> { + + + ); } diff --git a/RNTester/js/examples/Accessibility/check.png b/RNTester/js/examples/Accessibility/check.png new file mode 100644 index 00000000000000..de4c1492b8fd97 Binary files /dev/null and b/RNTester/js/examples/Accessibility/check.png differ diff --git a/RNTester/js/examples/Accessibility/mixed.png b/RNTester/js/examples/Accessibility/mixed.png new file mode 100644 index 00000000000000..5f5957ce375ecf Binary files /dev/null and b/RNTester/js/examples/Accessibility/mixed.png differ diff --git a/RNTester/js/examples/Accessibility/uncheck.png b/RNTester/js/examples/Accessibility/uncheck.png new file mode 100644 index 00000000000000..8c967281c590f9 Binary files /dev/null and b/RNTester/js/examples/Accessibility/uncheck.png differ diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 771caf6084c7f8..0d1084c3771a42 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -206,9 +206,13 @@ - (RCTShadowView *)shadowView } if (newState.count > 0) { view.reactAccessibilityElement.accessibilityState = newState; + // Post a layout change notification to make sure VoiceOver get notified for the state + // changes that don't happen upon users' click. + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); } else { view.reactAccessibilityElement.accessibilityState = nil; } + } RCT_CUSTOM_VIEW_PROPERTY(nativeID, NSString *, RCTView) 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 9c8b35b8ea5500..65c2fb3e387dd6 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -9,6 +9,7 @@ import android.text.TextUtils; import android.view.View; import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; @@ -170,6 +171,13 @@ public void setViewState(@NonNull T view, @Nullable ReadableMap accessibilitySta && accessibilityState.getType(STATE_CHECKED) == ReadableType.String)) { updateViewContentDescription(view); break; + } else if (view.isAccessibilityFocused()) { + // Internally Talkback ONLY uses TYPE_VIEW_CLICKED for "checked" and + // "selected" announcements. Send a click event to make sure Talkback + // get notified for the state changes that don't happen upon users' click. + // For the state changes that happens immediately, Talkback will skip + // the duplicated click event. + view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); } } }