diff --git a/change/react-native-windows-22b1e2a3-5ecc-4c5c-b127-e625595c8026.json b/change/react-native-windows-22b1e2a3-5ecc-4c5c-b127-e625595c8026.json new file mode 100644 index 00000000000..60ac025bc54 --- /dev/null +++ b/change/react-native-windows-22b1e2a3-5ecc-4c5c-b127-e625595c8026.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Raise UI Automation selection events when accessibilityState.selected changes.", + "packageName": "react-native-windows", + "email": "anuagra@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj b/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj index 405d35af50f..3f0bef8e2c4 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj +++ b/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj @@ -144,6 +144,7 @@ + Create diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj.filters b/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj.filters index ad4030aaada..d5f69857d70 100644 --- a/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj.filters +++ b/vnext/Microsoft.ReactNative.IntegrationTests/Microsoft.ReactNative.IntegrationTests.vcxproj.filters @@ -9,6 +9,7 @@ + Utilities diff --git a/vnext/Microsoft.ReactNative.IntegrationTests/UiaHelpersTests.cpp b/vnext/Microsoft.ReactNative.IntegrationTests/UiaHelpersTests.cpp new file mode 100644 index 00000000000..fe69d2ba390 --- /dev/null +++ b/vnext/Microsoft.ReactNative.IntegrationTests/UiaHelpersTests.cpp @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +#include "../Microsoft.ReactNative/Fabric/Composition/SelectionItemAutomationEvent.h" + +namespace ReactNativeIntegrationTests { + +namespace { + +struct SelectionNode { + bool mounted{true}; + bool selectionContainer{false}; + std::optional selected; + std::vector children; +}; + +std::vector GetSelectedItems(SelectionNode &selectionContainer) { + return winrt::Microsoft::ReactNative::implementation::GetSelectedItemsInSelectionContainer( + &selectionContainer, + [](const auto node) -> const auto & { return node->children; }, + [](const auto node) { return node->mounted; }, + [](const auto node) { return node->selectionContainer; }, + [](const auto node) { return node->selected.value_or(false); }); +} + +} // namespace + +TEST_CLASS (UiaHelpersTests) { + TEST_METHOD(UnmountedSelectionItemStateChangesAreSuppressed) { + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged( + false, std::nullopt, true)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(false, false, true)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(false, true, false)); + } + + TEST_METHOD(MountedSelectionItemStateChangesUseMissingAsFalse) { + TestCheck(winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(true, false, true)); + TestCheck(winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(true, true, false)); + TestCheck( + winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(true, std::nullopt, true)); + TestCheck( + winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(true, true, std::nullopt)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged(true, false, false)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged( + true, std::nullopt, false)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged( + true, false, std::nullopt)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged( + true, std::nullopt, std::nullopt)); + } + + TEST_METHOD(SelectedItemsComeFromMountedHierarchyAndStopAtNestedContainers) { + SelectionNode directSelected; + directSelected.selected = true; + SelectionNode directUnselected; + directUnselected.selected = false; + SelectionNode unspecifiedSelection; + SelectionNode wrappedSelected; + wrappedSelected.selected = true; + SelectionNode unmountedSelected; + unmountedSelected.mounted = false; + unmountedSelected.selected = true; + SelectionNode nestedSelected; + nestedSelected.selected = true; + SelectionNode nestedContainer; + nestedContainer.selectionContainer = true; + nestedContainer.selected = true; + nestedContainer.children = {&nestedSelected}; + SelectionNode wrapper; + wrapper.children = {&wrappedSelected, &unmountedSelected, &nestedContainer}; + SelectionNode root; + root.selectionContainer = true; + root.children = {&directSelected, &directUnselected, &unspecifiedSelection, &wrapper}; + + auto selectedItems = GetSelectedItems(root); + + TestCheckEqual(size_t{3}, selectedItems.size()); + TestCheckEqual(&directSelected, selectedItems[0]); + TestCheckEqual(&wrappedSelected, selectedItems[1]); + TestCheckEqual(&nestedContainer, selectedItems[2]); + } + + TEST_METHOD(SelectedItemInSingleSelectionContainerRaisesElementSelected) { + TestCheckEqual( + UIA_SelectionItem_ElementSelectedEventId, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemAutomationEventId(true, false, 1)); + } + + TEST_METHOD(FirstSelectedItemInMultiSelectionContainerRaisesElementSelected) { + TestCheckEqual( + UIA_SelectionItem_ElementSelectedEventId, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemAutomationEventId(true, true, 1)); + } + + TEST_METHOD(AdditionalSelectedItemInMultiSelectionContainerRaisesElementAdded) { + TestCheckEqual( + UIA_SelectionItem_ElementAddedToSelectionEventId, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemAutomationEventId(true, true, 2)); + } + + TEST_METHOD(RemovedItemRaisesElementRemoved) { + TestCheckEqual( + UIA_SelectionItem_ElementRemovedFromSelectionEventId, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemAutomationEventId(false, true, 2)); + } + + TEST_METHOD(RemovedItemInSingleSelectionContainerRaisesElementRemoved) { + TestCheckEqual( + UIA_SelectionItem_ElementRemovedFromSelectionEventId, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemAutomationEventId(false, false, 1)); + } + + TEST_METHOD(RemovalLeavingOneSelectedItemRaisesElementSelected) { + TestCheckEqual( + UIA_SelectionItem_ElementSelectedEventId, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemAutomationEventId(false, true, 1)); + } + + TEST_METHOD(FocusedSelectedItemInSingleSelectionContainerRaisesNotification) { + TestCheck(winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemNotification(true, false, true)); + } + + TEST_METHOD(SelectionItemNotificationIsLimitedToFocusedSingleSelection) { + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemNotification(false, false, true)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemNotification(true, true, true)); + TestCheck(!winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemNotification(true, false, false)); + } + + TEST_METHOD(SelectionItemNotificationIncludesItemNameAndLocalizedState) { + TestCheckEqual( + std::wstring{L"Item 2, selected"}, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemNotificationText(L"Item 2", L"selected")); + TestCheckEqual( + std::wstring{L"selected"}, + winrt::Microsoft::ReactNative::implementation::GetSelectionItemNotificationText(L"", L"selected")); + } +}; + +} // namespace ReactNativeIntegrationTests diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp index a1d7ba41d2f..6d266d02ad6 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp @@ -32,21 +32,7 @@ bool IsHiddenByParent(const winrt::Microsoft::ReactNative::ComponentView &view) CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider( const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept - : m_view{componentView} { - auto strongView = m_view.view(); - - if (!strongView) - return; - - auto props = std::static_pointer_cast( - winrt::get_self(strongView)->props()); - if (!props) - return; - - if (props->accessibilityState.has_value() && props->accessibilityState->selected.has_value()) { - AddSelectionItemsToContainer(this); - } -} + : m_view{componentView} {} CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider( const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView, @@ -951,53 +937,49 @@ HRESULT __stdcall CompositionDynamicAutomationProvider::get_IsSelectionRequired( } HRESULT __stdcall CompositionDynamicAutomationProvider::GetSelection(SAFEARRAY **pRetVal) { + if (pRetVal == nullptr) + return E_POINTER; + + *pRetVal = nullptr; + auto strongView = m_view.view(); if (!strongView) return UIA_E_ELEMENTNOTAVAILABLE; - std::vector selectedItems; - for (size_t i = 0; i < m_selectionItems.size(); i++) { - auto selectionItem = m_selectionItems.at(i); - - winrt::com_ptr unkSelectionItemProvider; - auto hr = selectionItem->GetPatternProvider(UIA_SelectionItemPatternId, unkSelectionItemProvider.put()); - if (FAILED(hr)) - return hr; - - auto selectionItemProvider = unkSelectionItemProvider.try_as(); - if (!selectionItemProvider) - return E_FAIL; - - BOOL selected; - hr = selectionItemProvider->get_IsSelected(&selected); - if (hr == S_OK && selected) { - selectedItems.push_back(int(i)); - } - } + auto selectedItems = GetSelectedItemsInSelectionContainer(strongView); *pRetVal = SafeArrayCreateVector(VT_UNKNOWN, 0, ULONG(selectedItems.size())); if (*pRetVal == nullptr) return E_OUTOFMEMORY; for (size_t i = 0; i < selectedItems.size(); i++) { + auto selectionItem = + selectedItems[i].try_as(); + if (!selectionItem) { + SafeArrayDestroy(*pRetVal); + *pRetVal = nullptr; + return E_FAIL; + } + + auto selectionItemProvider = selectionItem->EnsureUiaProvider().try_as(); + if (!selectionItemProvider) { + SafeArrayDestroy(*pRetVal); + *pRetVal = nullptr; + return E_FAIL; + } + auto pos = static_cast(i); - SafeArrayPutElement(*pRetVal, &pos, m_selectionItems.at(selectedItems.at(i)).get()); + auto hr = SafeArrayPutElement(*pRetVal, &pos, selectionItemProvider.get()); + if (FAILED(hr)) { + SafeArrayDestroy(*pRetVal); + *pRetVal = nullptr; + return hr; + } } return S_OK; } -void CompositionDynamicAutomationProvider::AddToSelectionItems(winrt::com_ptr &item) { - if (std::find(m_selectionItems.begin(), m_selectionItems.end(), item) != m_selectionItems.end()) { - return; - } - m_selectionItems.push_back(item); -} - -void CompositionDynamicAutomationProvider::RemoveFromSelectionItems(winrt::com_ptr &item) { - std::erase(m_selectionItems, item); -} - HRESULT __stdcall CompositionDynamicAutomationProvider::AddToSelection() { auto strongView = m_view.view(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h index 7a5b8d686ac..37bcd0fe8dd 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h @@ -95,8 +95,6 @@ class CompositionDynamicAutomationProvider : public winrt::implements< virtual HRESULT __stdcall RemoveFromSelection() override; virtual HRESULT __stdcall Select() override; - void AddToSelectionItems(winrt::com_ptr &item); - void RemoveFromSelectionItems(winrt::com_ptr &item); winrt::Microsoft::ReactNative::ComponentView GetSelectionContainer() noexcept; void SetChildSiteLink(winrt::Microsoft::UI::Content::ChildSiteLink childSiteLink) { @@ -111,7 +109,6 @@ class CompositionDynamicAutomationProvider : public winrt::implements< ::Microsoft::ReactNative::ReactTaggedView m_view; winrt::com_ptr m_textProvider; winrt::com_ptr m_annotationProvider; - std::vector> m_selectionItems; // Non-null when this UIA node is the peer of a ContentIslandComponentView. winrt::Microsoft::UI::Content::ChildSiteLink m_childSiteLink{nullptr}; }; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 727a3cb4fa6..4b0ce41a8eb 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -23,6 +23,7 @@ #include "CompositionDynamicAutomationProvider.h" #include "CompositionHelpers.h" #include "RootComponentView.h" +#include "SelectionItemAutomationEvent.h" #include "Theme.h" #include "TooltipService.h" #include "UiaHelpers.h" @@ -963,19 +964,6 @@ void ComponentView::updateAccessibilityProps( static_cast(winrt::Microsoft::ReactNative::implementation::GetExpandCollapseState(oldExpanded)), static_cast(winrt::Microsoft::ReactNative::implementation::GetExpandCollapseState(newExpanded))); } - - if ((oldViewProps.accessibilityState.has_value() && oldViewProps.accessibilityState->selected.has_value()) != - ((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value()))) { - EnsureUiaProvider(); - if (m_innerAutomationProvider) { - if ((newViewProps.accessibilityState.has_value() && newViewProps.accessibilityState->selected.has_value())) { - winrt::Microsoft::ReactNative::implementation::AddSelectionItemsToContainer(m_innerAutomationProvider.get()); - } else { - winrt::Microsoft::ReactNative::implementation::RemoveSelectionItemsFromContainer( - m_innerAutomationProvider.get()); - } - } - } } std::optional ComponentView::getAccessiblityValue() noexcept { @@ -1271,6 +1259,10 @@ void ViewComponentView::updateProps( facebook::react::Props::Shared const &oldProps) noexcept { const auto &oldViewProps = *std::static_pointer_cast(oldProps ? oldProps : m_props); const auto &newViewProps = *std::static_pointer_cast(props); + const auto oldSelected = + oldViewProps.accessibilityState.has_value() ? oldViewProps.accessibilityState->selected : std::nullopt; + const auto newSelected = + newViewProps.accessibilityState.has_value() ? newViewProps.accessibilityState->selected : std::nullopt; ensureVisual(); if (oldViewProps.opacity != newViewProps.opacity) { @@ -1286,6 +1278,20 @@ void ViewComponentView::updateProps( base_type::updateProps(props, oldProps); m_props = std::static_pointer_cast(props); + + if (UiaClientsAreListening() && + winrt::Microsoft::ReactNative::implementation::ShouldRaiseSelectionItemStateChanged( + isMounted(), oldSelected, newSelected)) { + auto provider = EnsureUiaProvider(); + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + provider, UIA_SelectionItemIsSelectedPropertyId, oldSelected.value_or(false), newSelected.value_or(false)); + + if (m_innerAutomationProvider) { + auto root = rootComponentView(); + winrt::Microsoft::ReactNative::implementation::RaiseSelectionItemAutomationEvent( + m_innerAutomationProvider.get(), newSelected.value_or(false), root && root->GetFocusedComponent() == *this); + } + } } const winrt::Microsoft::ReactNative::IComponentProps ViewComponentView::userProps( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/SelectionItemAutomationEvent.h b/vnext/Microsoft.ReactNative/Fabric/Composition/SelectionItemAutomationEvent.h new file mode 100644 index 00000000000..6d3db3390f1 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/SelectionItemAutomationEvent.h @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace winrt::Microsoft::ReactNative::implementation { + +inline bool ShouldRaiseSelectionItemStateChanged( + bool isMounted, + const std::optional &oldSelected, + const std::optional &newSelected) noexcept { + return isMounted && oldSelected.value_or(false) != newSelected.value_or(false); +} + +template +std::vector GetSelectedItemsInSelectionContainer( + const Node &selectionContainer, + GetChildren &&getChildren, + IsMounted &&isMounted, + IsSelectionContainer &&isSelectionContainer, + IsSelected &&isSelected) { + std::vector selectedItems; + std::function visitChildren = [&](const Node &parent) { + for (const auto &child : getChildren(parent)) { + if (!isMounted(child)) { + continue; + } + + if (isSelected(child)) { + selectedItems.push_back(child); + } + + if (!isSelectionContainer(child)) { + visitChildren(child); + } + } + }; + + visitChildren(selectionContainer); + return selectedItems; +} + +inline EVENTID +GetSelectionItemAutomationEventId(bool isSelected, bool canSelectMultiple, size_t selectedItemCount) noexcept { + if (isSelected && (!canSelectMultiple || selectedItemCount == 1)) { + return UIA_SelectionItem_ElementSelectedEventId; + } + + if (isSelected) { + return UIA_SelectionItem_ElementAddedToSelectionEventId; + } + + return canSelectMultiple && selectedItemCount == 1 ? UIA_SelectionItem_ElementSelectedEventId + : UIA_SelectionItem_ElementRemovedFromSelectionEventId; +} + +inline bool +ShouldRaiseSelectionItemNotification(bool isSelected, bool canSelectMultiple, bool hasKeyboardFocus) noexcept { + return isSelected && !canSelectMultiple && hasKeyboardFocus; +} + +inline std::wstring GetSelectionItemNotificationText(const std::wstring &name, const std::wstring &selectedText) { + return name.empty() ? selectedText : name + L", " + selectedText; +} + +} // namespace winrt::Microsoft::ReactNative::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index cd2084a18ed..f0ac4d7d2e3 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -3,11 +3,27 @@ #include #include #include +#include #include "CompositionRootAutomationProvider.h" #include "RootComponentView.h" +#include "SelectionItemAutomationEvent.h" namespace winrt::Microsoft::ReactNative::implementation { +namespace { + +using GetStateTextWFn = UINT(WINAPI *)(DWORD, LPWSTR, UINT); + +GetStateTextWFn GetStateTextFunction() noexcept { + static const auto getStateText = []() noexcept { + auto oleacc = LoadLibraryExW(L"oleacc.dll", nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32); + return oleacc ? reinterpret_cast(GetProcAddress(oleacc, "GetStateTextW")) : nullptr; + }(); + return getStateText; +} + +} // namespace + HRESULT UiaNavigateHelper( const winrt::Microsoft::ReactNative::ComponentView &view, NavigateDirection direction, @@ -519,46 +535,103 @@ ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept { } } -void AddSelectionItemsToContainer(CompositionDynamicAutomationProvider *provider) noexcept { - auto selectionContainerView = provider->GetSelectionContainer(); - if (!selectionContainerView) - return; - - auto selectionContainerCompView = - selectionContainerView.try_as(); - if (!selectionContainerCompView) - return; +std::vector GetSelectedItemsInSelectionContainer( + const winrt::Microsoft::ReactNative::ComponentView &selectionContainer) noexcept { + return GetSelectedItemsInSelectionContainer( + selectionContainer, + [](const auto &view) { return view.Children(); }, + [](const auto &view) { + return winrt::get_self(view)->isMounted(); + }, + [](const auto &view) { + auto props = std::static_pointer_cast( + winrt::get_self(view)->props()); + return props && props->accessibilityState.has_value() && + props->accessibilityState->multiselectable.has_value() && props->accessibilityState->required.has_value(); + }, + [](const auto &view) { + auto props = std::static_pointer_cast( + winrt::get_self(view)->props()); + return props && props->accessibilityState.has_value() && props->accessibilityState->selected.value_or(false); + }); +} - selectionContainerCompView->EnsureUiaProvider(); +void RaiseSelectionItemAutomationEvent( + CompositionDynamicAutomationProvider *provider, + bool isSelected, + bool hasKeyboardFocus) noexcept { + BOOL canSelectMultiple = false; + size_t selectedItemCount = isSelected ? 1 : 0; + winrt::com_ptr eventProvider; + eventProvider.copy_from(static_cast(provider)); - if (!selectionContainerCompView->InnerAutomationProvider()) - return; + auto selectionContainerView = provider->GetSelectionContainer(); + if (selectionContainerView) { + auto selectionContainerCompView = + selectionContainerView.try_as(); + if (selectionContainerCompView) { + auto props = selectionContainerCompView->viewProps(); + canSelectMultiple = + props->accessibilityState.has_value() && props->accessibilityState->multiselectable.value_or(false); + + auto selectedItems = GetSelectedItemsInSelectionContainer(selectionContainerView); + selectedItemCount = selectedItems.size(); + if (!isSelected && canSelectMultiple && selectedItemCount == 1) { + auto remainingSelectedItem = + selectedItems[0].try_as(); + if (!remainingSelectedItem) { + return; + } - auto simpleProvider = static_cast(provider); - winrt::com_ptr simpleProviderPtr; - simpleProviderPtr.copy_from(simpleProvider); - selectionContainerCompView->InnerAutomationProvider()->AddToSelectionItems(simpleProviderPtr); -} + auto remainingSelectedItemProvider = + remainingSelectedItem->EnsureUiaProvider().try_as(); + if (!remainingSelectedItemProvider) { + return; + } + eventProvider = std::move(remainingSelectedItemProvider); + } + } + } -void RemoveSelectionItemsFromContainer(CompositionDynamicAutomationProvider *provider) noexcept { - auto selectionContainerView = provider->GetSelectionContainer(); - if (!selectionContainerView) - return; + UiaRaiseAutomationEvent( + eventProvider.get(), GetSelectionItemAutomationEventId(isSelected, canSelectMultiple, selectedItemCount)); - auto selectionContainerCompView = - selectionContainerView.try_as(); - if (!selectionContainerCompView) - return; + if (ShouldRaiseSelectionItemNotification(isSelected, canSelectMultiple, hasKeyboardFocus)) { + const auto getStateText = GetStateTextFunction(); + if (!getStateText) { + return; + } - selectionContainerCompView->EnsureUiaProvider(); + const auto selectedTextLength = getStateText(STATE_SYSTEM_SELECTED, nullptr, 0); + if (selectedTextLength == 0) { + return; + } - if (!selectionContainerCompView->InnerAutomationProvider()) - return; + std::vector selectedText(selectedTextLength + 1); + if (getStateText(STATE_SYSTEM_SELECTED, selectedText.data(), static_cast(selectedText.size())) == 0) { + return; + } - auto simpleProvider = static_cast(provider); - winrt::com_ptr simpleProviderPtr; - simpleProviderPtr.copy_from(simpleProvider); - selectionContainerCompView->InnerAutomationProvider()->RemoveFromSelectionItems(simpleProviderPtr); + VARIANT name{}; + VariantInit(&name); + eventProvider->GetPropertyValue(UIA_NamePropertyId, &name); + auto announcement = + GetSelectionItemNotificationText(name.vt == VT_BSTR && name.bstrVal ? name.bstrVal : L"", selectedText.data()); + VariantClear(&name); + + auto announcementBstr = SysAllocString(announcement.c_str()); + auto activityId = SysAllocString(L"ReactNative.SelectionItem.Selected"); + if (announcementBstr && activityId) { + UiaRaiseNotificationEvent( + eventProvider.get(), + NotificationKind_ActionCompleted, + NotificationProcessing_ImportantMostRecent, + announcementBstr, + activityId); + } + SysFreeString(activityId); + SysFreeString(announcementBstr); + } } ToggleState GetToggleState(const std::optional &state) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h index 3efd94da399..50a4d4e9b33 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace Microsoft::ReactNative { struct winrt::Microsoft::ReactNative::implementation::ComponentView; @@ -75,9 +76,13 @@ void DispatchAccessibilityAction(::Microsoft::ReactNative::ReactTaggedView &view ExpandCollapseState GetExpandCollapseState(const bool &expanded) noexcept; -void AddSelectionItemsToContainer(CompositionDynamicAutomationProvider *provider) noexcept; +std::vector GetSelectedItemsInSelectionContainer( + const winrt::Microsoft::ReactNative::ComponentView &selectionContainer) noexcept; -void RemoveSelectionItemsFromContainer(CompositionDynamicAutomationProvider *provider) noexcept; +void RaiseSelectionItemAutomationEvent( + CompositionDynamicAutomationProvider *provider, + bool isSelected, + bool hasKeyboardFocus) noexcept; ToggleState GetToggleState(const std::optional &state) noexcept;