Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
<ClCompile Include="TestEventService.cpp" />
<ClCompile Include="TestReactNativeHostHolder.cpp" />
<ClCompile Include="TurboModuleTests.cpp" />
<ClCompile Include="UiaHelpersTests.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<ClCompile Include="ReactPropertyBagTests.cpp" />
<ClCompile Include="ReactNativeHostTests.cpp" />
<ClCompile Include="TurboModuleTests.cpp" />
<ClCompile Include="UiaHelpersTests.cpp" />
<ClCompile Include="main.cpp">
<Filter>Utilities</Filter>
</ClCompile>
Expand Down
141 changes: 141 additions & 0 deletions vnext/Microsoft.ReactNative.IntegrationTests/UiaHelpersTests.cpp
Original file line number Diff line number Diff line change
@@ -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<bool> selected;
std::vector<SelectionNode *> children;
};

std::vector<SelectionNode *> 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
Original file line number Diff line number Diff line change
Expand Up @@ -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<const facebook::react::ViewProps>(
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(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,
Expand Down Expand Up @@ -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<int> selectedItems;
for (size_t i = 0; i < m_selectionItems.size(); i++) {
auto selectionItem = m_selectionItems.at(i);

winrt::com_ptr<IUnknown> unkSelectionItemProvider;
auto hr = selectionItem->GetPatternProvider(UIA_SelectionItemPatternId, unkSelectionItemProvider.put());
if (FAILED(hr))
return hr;

auto selectionItemProvider = unkSelectionItemProvider.try_as<ISelectionItemProvider>();
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<winrt::Microsoft::ReactNative::Composition::implementation::ComponentView>();
if (!selectionItem) {
SafeArrayDestroy(*pRetVal);
*pRetVal = nullptr;
return E_FAIL;
}

auto selectionItemProvider = selectionItem->EnsureUiaProvider().try_as<IRawElementProviderSimple>();
if (!selectionItemProvider) {
SafeArrayDestroy(*pRetVal);
*pRetVal = nullptr;
return E_FAIL;
}

auto pos = static_cast<long>(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<IRawElementProviderSimple> &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<IRawElementProviderSimple> &item) {
std::erase(m_selectionItems, item);
}

HRESULT __stdcall CompositionDynamicAutomationProvider::AddToSelection() {
auto strongView = m_view.view();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,6 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
virtual HRESULT __stdcall RemoveFromSelection() override;
virtual HRESULT __stdcall Select() override;

void AddToSelectionItems(winrt::com_ptr<IRawElementProviderSimple> &item);
void RemoveFromSelectionItems(winrt::com_ptr<IRawElementProviderSimple> &item);
winrt::Microsoft::ReactNative::ComponentView GetSelectionContainer() noexcept;

void SetChildSiteLink(winrt::Microsoft::UI::Content::ChildSiteLink childSiteLink) {
Expand All @@ -111,7 +109,6 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
::Microsoft::ReactNative::ReactTaggedView m_view;
winrt::com_ptr<ITextProvider2> m_textProvider;
winrt::com_ptr<IAnnotationProvider> m_annotationProvider;
std::vector<winrt::com_ptr<IRawElementProviderSimple>> m_selectionItems;
// Non-null when this UIA node is the peer of a ContentIslandComponentView.
winrt::Microsoft::UI::Content::ChildSiteLink m_childSiteLink{nullptr};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -963,19 +964,6 @@ void ComponentView::updateAccessibilityProps(
static_cast<int>(winrt::Microsoft::ReactNative::implementation::GetExpandCollapseState(oldExpanded)),
static_cast<int>(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<std::string> ComponentView::getAccessiblityValue() noexcept {
Expand Down Expand Up @@ -1271,6 +1259,10 @@ void ViewComponentView::updateProps(
facebook::react::Props::Shared const &oldProps) noexcept {
const auto &oldViewProps = *std::static_pointer_cast<const facebook::react::ViewProps>(oldProps ? oldProps : m_props);
const auto &newViewProps = *std::static_pointer_cast<const facebook::react::ViewProps>(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) {
Expand All @@ -1286,6 +1278,20 @@ void ViewComponentView::updateProps(
base_type::updateProps(props, oldProps);

m_props = std::static_pointer_cast<facebook::react::ViewProps const>(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(
Expand Down
Loading
Loading