diff --git a/change/react-native-windows-07a91306-5a2e-4459-b70b-ce9761c3ac91.json b/change/react-native-windows-07a91306-5a2e-4459-b70b-ce9761c3ac91.json new file mode 100644 index 00000000000..c87fc06590c --- /dev/null +++ b/change/react-native-windows-07a91306-5a2e-4459-b70b-ce9761c3ac91.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Added support for Narrator announcing accessibilityState changes", + "packageName": "react-native-windows", + "email": "agnel@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp b/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp index f208240fbe6..86de820ef0d 100644 --- a/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp +++ b/vnext/Microsoft.ReactNative/Views/FrameworkElementViewManager.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include "Utils/PropertyHandlerUtils.h" @@ -416,13 +417,30 @@ bool FrameworkElementViewManager::UpdateProperty( const std::string &innerName = pair.first; const auto &innerValue = pair.second; - if (innerName == "selected") + auto peer = xaml::Automation::Peers::FrameworkElementAutomationPeer::FromElement(element); + + if (innerName == "selected") { states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Selected)] = innerValue.AsBoolean(); - else if (innerName == "disabled") + const auto prevSelectedState = DynamicAutomationProperties::GetAccessibilityStateSelected(element); + if (peer != nullptr && prevSelectedState != innerValue.AsBoolean()) { + peer.RaisePropertyChangedEvent( + winrt::SelectionItemPatternIdentifiers::IsSelectedProperty(), + winrt::box_value(prevSelectedState), + winrt::box_value(innerValue.AsBoolean())); + } + } else if (innerName == "disabled") { states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Disabled)] = innerValue.AsBoolean(); - else if (innerName == "checked") { + const auto prevDisabledState = DynamicAutomationProperties::GetAccessibilityStateDisabled(element); + + if (peer != nullptr && prevDisabledState != innerValue.AsBoolean()) { + peer.RaisePropertyChangedEvent( + winrt::AutomationElementIdentifiers::IsEnabledProperty(), + winrt::box_value(!prevDisabledState), + winrt::box_value(!innerValue.AsBoolean())); + } + } else if (innerName == "checked") { states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Checked)] = innerValue.Type() == winrt::Microsoft::ReactNative::JSValueType::Boolean && innerValue.AsBoolean(); states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Unchecked)] = @@ -430,6 +448,30 @@ bool FrameworkElementViewManager::UpdateProperty( // 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()). + const auto prevCheckedState = DynamicAutomationProperties::GetAccessibilityStateChecked(element); + const auto prevUncheckedState = DynamicAutomationProperties::GetAccessibilityStateUnchecked(element); + + if (peer != nullptr) { + if (prevCheckedState != + states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Checked)] || + prevUncheckedState != + states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Unchecked)]) { + // Checking if either state has changed here to catch changes involving "mixed" state. + const auto oldValue = prevCheckedState ? winrt::ToggleState::On : winrt::ToggleState::Off; + if (innerValue.Type() != winrt::Microsoft::ReactNative::JSValueType::Boolean) { + peer.RaisePropertyChangedEvent( + winrt::TogglePatternIdentifiers::ToggleStateProperty(), + winrt::box_value(oldValue), + winrt::box_value(winrt::ToggleState::Indeterminate)); + } else { + const auto newValue = innerValue.AsBoolean() ? winrt::ToggleState::On : winrt::ToggleState::Off; + peer.RaisePropertyChangedEvent( + winrt::TogglePatternIdentifiers::ToggleStateProperty(), + winrt::box_value(oldValue), + winrt::box_value(newValue)); + } + } + } } else if (innerName == "busy") states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Busy)] = !innerValue.IsNull() && innerValue.AsBoolean(); @@ -438,6 +480,19 @@ bool FrameworkElementViewManager::UpdateProperty( !innerValue.IsNull() && innerValue.AsBoolean(); states[static_cast(winrt::Microsoft::ReactNative::AccessibilityStates::Collapsed)] = innerValue.IsNull() || !innerValue.AsBoolean(); + + const auto prevExpandedState = DynamicAutomationProperties::GetAccessibilityStateExpanded(element); + + if (peer != nullptr && prevExpandedState != innerValue.AsBoolean()) { + const auto newValue = + innerValue.AsBoolean() ? winrt::ExpandCollapseState::Expanded : winrt::ExpandCollapseState::Collapsed; + const auto oldValue = + prevExpandedState ? winrt::ExpandCollapseState::Expanded : winrt::ExpandCollapseState::Collapsed; + peer.RaisePropertyChangedEvent( + winrt::ExpandCollapsePatternIdentifiers::ExpandCollapseStateProperty(), + winrt::box_value(oldValue), + winrt::box_value(newValue)); + } } } }