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
122 changes: 122 additions & 0 deletions vnext/Microsoft.ReactNative.IntegrationTests/UiaHelpersTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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));
}
};

} // 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,19 @@ 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) {
winrt::Microsoft::ReactNative::implementation::RaiseSelectionItemAutomationEvent(
m_innerAutomationProvider.get(), newSelected.value_or(false));
Comment on lines +1289 to +1291
}
}
}

const winrt::Microsoft::ReactNative::IComponentProps ViewComponentView::userProps(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#pragma once

#include <UIAutomationClient.h>
#include <cstddef>
#include <functional>
#include <optional>
#include <vector>

namespace winrt::Microsoft::ReactNative::implementation {

inline bool ShouldRaiseSelectionItemStateChanged(
bool isMounted,
const std::optional<bool> &oldSelected,
const std::optional<bool> &newSelected) noexcept {
return isMounted && oldSelected.value_or(false) != newSelected.value_or(false);
}

template <typename Node, typename GetChildren, typename IsMounted, typename IsSelectionContainer, typename IsSelected>
std::vector<Node> GetSelectedItemsInSelectionContainer(
const Node &selectionContainer,
GetChildren &&getChildren,
IsMounted &&isMounted,
IsSelectionContainer &&isSelectionContainer,
IsSelected &&isSelected) {
std::vector<Node> selectedItems;
std::function<void(const Node &)> 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;
}

} // namespace winrt::Microsoft::ReactNative::implementation
Loading
Loading