Skip to content

Commit

Permalink
Initial support for UIA and tab navigation for a child Island (micros…
Browse files Browse the repository at this point in the history
…oft#14305)

* Basic support for stitching the UIA tree for a ContentIslandComponentView's child

* Updated comment

* Change files

* Support shift+tab, and move Automation event handlers to ContentIslandComponentView
  • Loading branch information
JesseCol authored and acoates-ms committed Jan 29, 2025
1 parent f5b2db4 commit 005d94a
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "Basic support for stitching the UIA tree for a ContentIslandComponentView's child",
"packageName": "react-native-windows",
"email": "email not defined",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <Fabric/Composition/SwitchComponentView.h>
#include <Fabric/Composition/TextInput/WindowsTextInputComponentView.h>
#include <Unicode.h>
#include <winrt/Microsoft.UI.Content.h>
#include "RootComponentView.h"
#include "UiaHelpers.h"

Expand All @@ -27,12 +28,29 @@ CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider(
}
}

#ifdef USE_EXPERIMENTAL_WINUI3
CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider(
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
const winrt::Microsoft::UI::Content::ChildSiteLink &childSiteLink) noexcept
: m_view{componentView}, m_childSiteLink{childSiteLink} {}
#endif // USE_EXPERIMENTAL_WINUI3

HRESULT __stdcall CompositionDynamicAutomationProvider::Navigate(
NavigateDirection direction,
IRawElementProviderFragment **pRetVal) {
if (pRetVal == nullptr)
return E_POINTER;

#ifdef USE_EXPERIMENTAL_WINUI3
if (m_childSiteLink) {
if (direction == NavigateDirection_FirstChild || direction == NavigateDirection_LastChild) {
auto fragment = m_childSiteLink.AutomationProvider().try_as<IRawElementProviderFragment>();
*pRetVal = fragment.detach();
return S_OK;
}
}
#endif // USE_EXPERIMENTAL_WINUI3

return UiaNavigateHelper(m_view.view(), direction, *pRetVal);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
CompositionDynamicAutomationProvider(
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept;

#ifdef USE_EXPERIMENTAL_WINUI3
CompositionDynamicAutomationProvider(
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
const winrt::Microsoft::UI::Content::ChildSiteLink &childContentLink) noexcept;
#endif // USE_EXPERIMENTAL_WINUI3

// inherited via IRawElementProviderFragment
virtual HRESULT __stdcall Navigate(NavigateDirection direction, IRawElementProviderFragment **pRetVal) override;
virtual HRESULT __stdcall GetRuntimeId(SAFEARRAY **pRetVal) override;
Expand Down Expand Up @@ -86,6 +92,10 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
private:
::Microsoft::ReactNative::ReactTaggedView m_view;
std::vector<winrt::com_ptr<IRawElementProviderSimple>> m_selectionItems;
#ifdef USE_EXPERIMENTAL_WINUI3
// Non-null when this UIA node is the peer of a ContentIslandComponentView.
winrt::Microsoft::UI::Content::ChildSiteLink m_childSiteLink{nullptr};
#endif
};

} // namespace winrt::Microsoft::ReactNative::implementation
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@
#include <UI.Xaml.Controls.h>
#include <Utils/ValueUtils.h>
#include <winrt/Microsoft.UI.Content.h>
#include <winrt/Microsoft.UI.Input.h>
#include <winrt/Windows.UI.Composition.h>
#include "CompositionContextHelper.h"
#include "RootComponentView.h"

#include "Composition.ContentIslandComponentView.g.cpp"

#include "CompositionDynamicAutomationProvider.h"

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

ContentIslandComponentView::ContentIslandComponentView(
Expand Down Expand Up @@ -47,6 +50,22 @@ void ContentIslandComponentView::OnMounted() noexcept {
winrt::Microsoft::ReactNative::Composition::Experimental::CompositionContextHelper::InnerVisual(Visual())
.as<winrt::Microsoft::UI::Composition::ContainerVisual>());
m_childSiteLink.ActualSize({m_layoutMetrics.frame.size.width, m_layoutMetrics.frame.size.height});

m_navigationHost = winrt::Microsoft::UI::Input::InputFocusNavigationHost::GetForSiteLink(m_childSiteLink);

m_navigationHostDepartFocusRequestedToken =
m_navigationHost.DepartFocusRequested([wkThis = get_weak()](const auto &, const auto &args) {
if (auto strongThis = wkThis.get()) {
const bool next = (args.Request().Reason() != winrt::Microsoft::UI::Input::FocusNavigationReason::Last);
strongThis->rootComponentView()->TryMoveFocus(next);
args.Result(winrt::Microsoft::UI::Input::FocusNavigationResult::Moved);
}
});

// We configure automation even if there's no UIA client at this point, because it's possible the first UIA
// request we'll get will be for a child of this island calling upward in the UIA tree.
ConfigureChildSiteLinkAutomation();

if (m_islandToConnect) {
m_childSiteLink.Connect(m_islandToConnect);
m_islandToConnect = nullptr;
Expand All @@ -70,6 +89,12 @@ void ContentIslandComponentView::OnMounted() noexcept {

void ContentIslandComponentView::OnUnmounted() noexcept {
m_layoutMetricChangedRevokers.clear();
#ifdef USE_EXPERIMENTAL_WINUI3
if (m_navigationHostDepartFocusRequestedToken && m_navigationHost) {
m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken);
m_navigationHostDepartFocusRequestedToken = {};
}
#endif
}

void ContentIslandComponentView::ParentLayoutChanged() noexcept {
Expand All @@ -92,7 +117,79 @@ void ContentIslandComponentView::ParentLayoutChanged() noexcept {
#endif
}

winrt::IInspectable ContentIslandComponentView::EnsureUiaProvider() noexcept {
#ifdef USE_EXPERIMENTAL_WINUI3
if (m_uiaProvider == nullptr) {
m_uiaProvider = winrt::make<winrt::Microsoft::ReactNative::implementation::CompositionDynamicAutomationProvider>(
*get_strong(), m_childSiteLink);
}
return m_uiaProvider;
#else
return Super::EnsureUiaProvider();
#endif
}

bool ContentIslandComponentView::focusable() const noexcept {
#ifdef USE_EXPERIMENTAL_WINUI3
// We don't have a way to check to see if the ContentIsland has focusable content,
// so we'll always return true. We'll have to handle the case where the content doesn't have
// focusable content in the OnGotFocus handler.
return true;
#else
return Super::focusable();
#endif
}

// Helper to convert a FocusNavigationDirection to a FocusNavigationReason.
winrt::Microsoft::UI::Input::FocusNavigationReason GetFocusNavigationReason(
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
switch (direction) {
case winrt::Microsoft::ReactNative::FocusNavigationDirection::First:
case winrt::Microsoft::ReactNative::FocusNavigationDirection::Next:
return winrt::Microsoft::UI::Input::FocusNavigationReason::First;
case winrt::Microsoft::ReactNative::FocusNavigationDirection::Last:
case winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous:
return winrt::Microsoft::UI::Input::FocusNavigationReason::Last;
}
return winrt::Microsoft::UI::Input::FocusNavigationReason::Restore;
}

void ContentIslandComponentView::onGotFocus(
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept {
#ifdef USE_EXPERIMENTAL_WINUI3
auto gotFocusEventArgs = args.as<winrt::Microsoft::ReactNative::implementation::GotFocusEventArgs>();
const auto navigationReason = GetFocusNavigationReason(gotFocusEventArgs->Direction());
m_navigationHost.NavigateFocus(winrt::Microsoft::UI::Input::FocusNavigationRequest::Create(navigationReason));
#else
return Super::onGotFocus(args);
#endif // USE_EXPERIMENTAL_WINUI3
}

ContentIslandComponentView::~ContentIslandComponentView() noexcept {
#ifdef USE_EXPERIMENTAL_WINUI3
if (m_navigationHostDepartFocusRequestedToken && m_navigationHost) {
m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken);
m_navigationHostDepartFocusRequestedToken = {};
}
if (m_childSiteLink) {
if (m_fragmentRootAutomationProviderRequestedToken) {
m_childSiteLink.FragmentRootAutomationProviderRequested(m_fragmentRootAutomationProviderRequestedToken);
m_fragmentRootAutomationProviderRequestedToken = {};
}
if (m_parentAutomationProviderRequestedToken) {
m_childSiteLink.ParentAutomationProviderRequested(m_parentAutomationProviderRequestedToken);
m_parentAutomationProviderRequestedToken = {};
}
if (m_nextSiblingAutomationProviderRequestedToken) {
m_childSiteLink.NextSiblingAutomationProviderRequested(m_nextSiblingAutomationProviderRequestedToken);
m_nextSiblingAutomationProviderRequestedToken = {};
}
if (m_previousSiblingAutomationProviderRequestedToken) {
m_childSiteLink.PreviousSiblingAutomationProviderRequested(m_previousSiblingAutomationProviderRequestedToken);
m_previousSiblingAutomationProviderRequestedToken = {};
}
}
#endif // USE_EXPERIMENTAL_WINUI3
if (m_islandToConnect) {
m_islandToConnect.Close();
}
Expand Down Expand Up @@ -132,11 +229,65 @@ void ContentIslandComponentView::Connect(const winrt::Microsoft::UI::Content::Co
} else {
m_islandToConnect = contentIsland;
}
#endif
#endif // USE_EXPERIMENTAL_WINUI3
}

void ContentIslandComponentView::prepareForRecycle() noexcept {
Super::prepareForRecycle();
}

#ifdef USE_EXPERIMENTAL_WINUI3
void ContentIslandComponentView::ConfigureChildSiteLinkAutomation() noexcept {
// This automation mode must be set before connecting the child ContentIsland.
// It puts the child content into a mode where it won't own its own framework root. Instead, the child island's
// automation peers will use the same framework root as the automation peer of this ContentIslandComponentView.
m_childSiteLink.AutomationTreeOption(winrt::Microsoft::UI::Content::AutomationTreeOptions::FragmentBased);

// These events are raised in response to the child ContentIsland asking for providers.
// For example, the ContentIsland.FragmentRootAutomationProvider property will return
// the provider we provide here in FragmentRootAutomationProviderRequested.

// We capture "this" as a raw pointer because ContentIslandComponentView doesn't currently support weak ptrs.
// It's safe because we disconnect these events in the destructor.

m_fragmentRootAutomationProviderRequestedToken = m_childSiteLink.FragmentRootAutomationProviderRequested(
[this](
const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
// The child island's fragment tree doesn't have its own fragment root.
// Here's how we can provide the correct fragment root to the child's UIA logic.
winrt::com_ptr<IRawElementProviderFragmentRoot> fragmentRoot{nullptr};
auto uiaProvider = this->EnsureUiaProvider();
uiaProvider.as<IRawElementProviderFragment>()->get_FragmentRoot(fragmentRoot.put());
args.AutomationProvider(fragmentRoot.as<IInspectable>());
args.Handled(true);
});

m_parentAutomationProviderRequestedToken = m_childSiteLink.ParentAutomationProviderRequested(
[this](
const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
auto uiaProvider = this->EnsureUiaProvider();
args.AutomationProvider(uiaProvider);
args.Handled(true);
});

m_nextSiblingAutomationProviderRequestedToken = m_childSiteLink.NextSiblingAutomationProviderRequested(
[](const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
// The ContentIsland will always be the one and only child of this node, so it won't have siblings.
args.AutomationProvider(nullptr);
args.Handled(true);
});

m_previousSiblingAutomationProviderRequestedToken = m_childSiteLink.PreviousSiblingAutomationProviderRequested(
[](const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
// The ContentIsland will always be the one and only child of this node, so it won't have siblings.
args.AutomationProvider(nullptr);
args.Handled(true);
});
}
#endif // USE_EXPERIMENTAL_WINUI3

} // namespace winrt::Microsoft::ReactNative::Composition::implementation
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include <Microsoft.ReactNative.Cxx/ReactContext.h>
#include <winrt/Microsoft.UI.Content.h>
#include <winrt/Microsoft.UI.Input.h>
#include <winrt/Windows.UI.Composition.h>
#include "CompositionHelpers.h"
#include "CompositionViewComponentView.h"
Expand Down Expand Up @@ -37,6 +38,12 @@ struct ContentIslandComponentView : ContentIslandComponentViewT<ContentIslandCom

void prepareForRecycle() noexcept override;

bool focusable() const noexcept override;

winrt::IInspectable EnsureUiaProvider() noexcept override;

void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override;

ContentIslandComponentView(
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
facebook::react::Tag tag,
Expand All @@ -56,6 +63,15 @@ struct ContentIslandComponentView : ContentIslandComponentViewT<ContentIslandCom
std::vector<winrt::Microsoft::ReactNative::ComponentView::LayoutMetricsChanged_revoker> m_layoutMetricChangedRevokers;
#ifdef USE_EXPERIMENTAL_WINUI3
winrt::Microsoft::UI::Content::ChildSiteLink m_childSiteLink{nullptr};
winrt::Microsoft::UI::Input::InputFocusNavigationHost m_navigationHost{nullptr};
winrt::event_token m_navigationHostDepartFocusRequestedToken{};

// Automation
void ConfigureChildSiteLinkAutomation() noexcept;
winrt::event_token m_fragmentRootAutomationProviderRequestedToken{};
winrt::event_token m_parentAutomationProviderRequestedToken{};
winrt::event_token m_nextSiblingAutomationProviderRequestedToken{};
winrt::event_token m_previousSiblingAutomationProviderRequestedToken{};
#endif
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ int32_t LostFocusEventArgs::OriginalSource() noexcept {
return m_originalSource;
}

GotFocusEventArgs::GotFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource)
: m_originalSource(originalSource ? originalSource.Tag() : -1) {}
GotFocusEventArgs::GotFocusEventArgs(
const winrt::Microsoft::ReactNative::ComponentView &originalSource,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction)
: m_originalSource(originalSource ? originalSource.Tag() : -1), m_direction(direction) {}
int32_t GotFocusEventArgs::OriginalSource() noexcept {
return m_originalSource;
}
Expand Down
10 changes: 9 additions & 1 deletion vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,19 @@ struct LostFocusEventArgs

struct GotFocusEventArgs
: winrt::implements<GotFocusEventArgs, winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs> {
GotFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource);
GotFocusEventArgs(
const winrt::Microsoft::ReactNative::ComponentView &originalSource,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction);
int32_t OriginalSource() noexcept;

winrt::Microsoft::ReactNative::FocusNavigationDirection Direction() const noexcept {
return m_direction;
}

private:
const int32_t m_originalSource;
winrt::Microsoft::ReactNative::FocusNavigationDirection m_direction{
winrt::Microsoft::ReactNative::FocusNavigationDirection::None};
};

struct LosingFocusEventArgs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ void RootComponentView::updateLayoutMetrics(
winrt::Microsoft::ReactNative::ComponentView RootComponentView::GetFocusedComponent() noexcept {
return m_focusedComponent;
}
void RootComponentView::SetFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &value) noexcept {
void RootComponentView::SetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &value,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
if (m_focusedComponent == value)
return;

Expand All @@ -90,7 +92,7 @@ void RootComponentView::SetFocusedComponent(const winrt::Microsoft::ReactNative:
if (auto rootView = m_wkRootView.get()) {
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(rootView)->TrySetFocus();
}
auto args = winrt::make<winrt::Microsoft::ReactNative::implementation::GotFocusEventArgs>(value);
auto args = winrt::make<winrt::Microsoft::ReactNative::implementation::GotFocusEventArgs>(value, direction);
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(value)->onGotFocus(args);
}

Expand Down Expand Up @@ -151,7 +153,7 @@ bool RootComponentView::TrySetFocusedComponent(

winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(losingFocusArgs.NewFocusedComponent())
->rootComponentView()
->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent());
->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction);
}

return true;
Expand Down Expand Up @@ -241,7 +243,7 @@ void RootComponentView::start(const winrt::Microsoft::ReactNative::ReactNativeIs
}

void RootComponentView::stop() noexcept {
SetFocusedComponent(nullptr);
SetFocusedComponent(nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
if (m_visualAddedToIsland) {
if (auto rootView = m_wkRootView.get()) {
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(rootView)->RemoveRenderedVisual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ struct RootComponentView : RootComponentViewT<RootComponentView, ViewComponentVi
winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept;

winrt::Microsoft::ReactNative::ComponentView GetFocusedComponent() noexcept;
void SetFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &value) noexcept;
void SetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &value,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept;
bool TrySetFocusedComponent(
const winrt::Microsoft::ReactNative::ComponentView &view,
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept;
Expand Down

0 comments on commit 005d94a

Please sign in to comment.