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;