diff --git a/change/react-native-windows-2020-04-15-15-25-43-AccessibilityState.json b/change/react-native-windows-2020-04-15-15-25-43-AccessibilityState.json new file mode 100644 index 00000000000..6099cacb2eb --- /dev/null +++ b/change/react-native-windows-2020-04-15-15-25-43-AccessibilityState.json @@ -0,0 +1,8 @@ +{ + "type": "prerelease", + "comment": "implement accessibilityState", + "packageName": "react-native-windows", + "email": "kmelmon@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-04-15T22:25:43.418Z" +} \ No newline at end of file diff --git a/vnext/ReactUWP/Views/FrameworkElementViewManager.cpp b/vnext/ReactUWP/Views/FrameworkElementViewManager.cpp index f1c223c7632..4beb3492efc 100644 --- a/vnext/ReactUWP/Views/FrameworkElementViewManager.cpp +++ b/vnext/ReactUWP/Views/FrameworkElementViewManager.cpp @@ -141,12 +141,21 @@ void FrameworkElementViewManager::TransferProperties(XamlView oldView, XamlView } } +static folly::dynamic GetAccessibilityStateProps() { + folly::dynamic props = folly::dynamic::object(); + + props.update(folly::dynamic::object("selected", "boolean")("disabled", "boolean")("checked", "string")( + "busy", "boolean")("expanded", "boolean")); + return props; +} + folly::dynamic FrameworkElementViewManager::GetNativeProps() const { folly::dynamic props = Super::GetNativeProps(); props.update(folly::dynamic::object("accessible", "boolean")("accessibilityRole", "string")( - "accessibilityStates", "array")("accessibilityHint", "string")("accessibilityLabel", "string")( - "accessibilityPosInSet", "number")("accessibilitySetSize", "number")("testID", "string")("tooltip", "string")( - "accessibilityActions", "array")("accessibilityLiveRegion", "string")); + "accessibilityStates", "array")("accessibilityState", GetAccessibilityStateProps())( + "accessibilityHint", "string")("accessibilityLabel", "string")("accessibilityPosInSet", "number")( + "accessibilitySetSize", "number")("testID", "string")("tooltip", "string")("accessibilityActions", "array")( + "accessibilityLiveRegion", "string")); return props; } @@ -410,6 +419,49 @@ bool FrameworkElementViewManager::UpdateProperty( } } + DynamicAutomationProperties::SetAccessibilityStateSelected( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Selected)]); + DynamicAutomationProperties::SetAccessibilityStateDisabled( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Disabled)]); + DynamicAutomationProperties::SetAccessibilityStateChecked( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Checked)]); + DynamicAutomationProperties::SetAccessibilityStateUnchecked( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Unchecked)]); + DynamicAutomationProperties::SetAccessibilityStateBusy( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Busy)]); + DynamicAutomationProperties::SetAccessibilityStateExpanded( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Expanded)]); + DynamicAutomationProperties::SetAccessibilityStateCollapsed( + element, states[static_cast(winrt::react::uwp::AccessibilityStates::Collapsed)]); + } else if (propertyName == "accessibilityState") { + bool states[static_cast(winrt::react::uwp::AccessibilityStates::CountStates)] = {}; + + if (propertyValue.isObject()) { + for (const auto &pair : propertyValue.items()) { + const std::string &innerName = pair.first.getString(); + const folly::dynamic &innerValue = pair.second; + + if (innerName == "selected") + states[static_cast(winrt::react::uwp::AccessibilityStates::Selected)] = innerValue.getBool(); + else if (innerName == "disabled") + states[static_cast(winrt::react::uwp::AccessibilityStates::Disabled)] = innerValue.getBool(); + else if (innerName == "checked") { + states[static_cast(winrt::react::uwp::AccessibilityStates::Checked)] = + innerValue.isBool() && innerValue.getBool(); + states[static_cast(winrt::react::uwp::AccessibilityStates::Unchecked)] = + innerValue.isBool() && !innerValue.getBool(); + // If the state is "mixed" we'll just set both Checked and Unchecked to false, + // then later in the IToggleProvider implementation it will return the Intermediate state + // due to both being set to false (see DynamicAutomationPeer::ToggleState()). + } else if (innerName == "busy") + states[static_cast(winrt::react::uwp::AccessibilityStates::Busy)] = innerValue.getBool(); + else if (innerName == "expanded") { + states[static_cast(winrt::react::uwp::AccessibilityStates::Expanded)] = innerValue.getBool(); + states[static_cast(winrt::react::uwp::AccessibilityStates::Collapsed)] = !innerValue.getBool(); + } + } + } + DynamicAutomationProperties::SetAccessibilityStateSelected( element, states[static_cast(winrt::react::uwp::AccessibilityStates::Selected)]); DynamicAutomationProperties::SetAccessibilityStateDisabled( diff --git a/vnext/src/RNTester/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx b/vnext/src/RNTester/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx index 69d4a64fa4b..d9c5ac0f559 100644 --- a/vnext/src/RNTester/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx +++ b/vnext/src/RNTester/js/examples-win/Accessibility/AccessibilityExampleWindows.tsx @@ -270,7 +270,7 @@ class AccessibilityStateExamples extends React.Component { public state = { viewDisabled: false, itemsSelected: [false, false, false], - viewChecked: false, + viewChecked: 0, viewBusy: false, viewCollapsed: false, }; @@ -294,7 +294,7 @@ class AccessibilityStateExamples extends React.Component { backgroundColor: this.state.viewDisabled ? 'gray' : 'lightskyblue', }} accessibilityRole="none" - accessibilityStates={this.state.viewDisabled ? ['disabled'] : []}> + accessibilityState={{disabled: this.state.viewDisabled}}> This View should be{' '} {this.state.viewDisabled ? 'disabled' : 'enabled'} according to UIA @@ -318,9 +318,9 @@ class AccessibilityStateExamples extends React.Component { }} accessibilityRole="button" accessibilityLabel={'Selectable item ' + (item.index + 1)} - accessibilityStates={ - this.state.itemsSelected[item.index] ? ['selected'] : [] - } + accessibilityState={{ + selected: this.state.itemsSelected[item.index], + }} onPress={() => this.selectPress(item.index)}> {this.state.itemsSelected[item.index] @@ -332,8 +332,8 @@ class AccessibilityStateExamples extends React.Component { keyExtractor={(item, index) => index.toString()} /> - The following TouchableHighlight toggles accessibilityState.checked - and accessibilityState.unchecked for the View under it: + The following TouchableHighlight cycles accessibilityState.checked + through unchecked/checked/mixed for the View under it: + accessibilityState={{ + checked: + this.state.viewChecked === 0 + ? false + : this.state.viewChecked === 1 + ? true + : 'mixed', + }}> This View should be{' '} - {this.state.viewChecked ? 'Checked' : 'Unchecked'} according to UIA + {this.state.viewChecked === 0 + ? 'Unchecked' + : this.state.viewChecked === 1 + ? 'Checked' + : 'Mixed'}{' '} + according to UIA - The following TouchableHighlight toggles accessibilityState.busy for - the View under it: + The following TouchableHighlight toggles the acessibilityState.busy + for the View under it: + accessibilityState={{busy: this.state.viewBusy}}> This View should be {this.state.viewBusy ? 'Busy' : 'Not Busy'}{' '} according to UIA @@ -395,9 +410,9 @@ class AccessibilityStateExamples extends React.Component { }} accessibilityRole="none" //@ts-ignore - accessibilityStates={ - this.state.viewCollapsed ? ['collapsed'] : ['expanded'] - }> + accessibilityState={{ + expanded: !this.state.viewCollapsed, + }}> This View should be{' '} {this.state.viewCollapsed ? 'Collapsed' : 'Expanded'} according to @@ -419,7 +434,11 @@ class AccessibilityStateExamples extends React.Component { }; private checkedPress = () => { - this.setState({viewChecked: !this.state.viewChecked}); + let newChecked = this.state.viewChecked + 1; + if (newChecked === 3) { + newChecked = 0; + } + this.setState({viewChecked: newChecked}); }; private busyPress = () => {