Skip to content

Commit a95adab

Browse files
authored
Initial support for UIA and tab navigation for a child Island (#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
1 parent a823be8 commit a95adab

9 files changed

+225
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Basic support for stitching the UIA tree for a ContentIslandComponentView's child",
4+
"packageName": "react-native-windows",
5+
"email": "email not defined",
6+
"dependentChangeType": "patch"
7+
}

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.cpp

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <Fabric/Composition/SwitchComponentView.h>
55
#include <Fabric/Composition/TextInput/WindowsTextInputComponentView.h>
66
#include <Unicode.h>
7+
#include <winrt/Microsoft.UI.Content.h>
78
#include "RootComponentView.h"
89
#include "UiaHelpers.h"
910

@@ -27,12 +28,29 @@ CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider(
2728
}
2829
}
2930

31+
#ifdef USE_EXPERIMENTAL_WINUI3
32+
CompositionDynamicAutomationProvider::CompositionDynamicAutomationProvider(
33+
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
34+
const winrt::Microsoft::UI::Content::ChildSiteLink &childSiteLink) noexcept
35+
: m_view{componentView}, m_childSiteLink{childSiteLink} {}
36+
#endif // USE_EXPERIMENTAL_WINUI3
37+
3038
HRESULT __stdcall CompositionDynamicAutomationProvider::Navigate(
3139
NavigateDirection direction,
3240
IRawElementProviderFragment **pRetVal) {
3341
if (pRetVal == nullptr)
3442
return E_POINTER;
3543

44+
#ifdef USE_EXPERIMENTAL_WINUI3
45+
if (m_childSiteLink) {
46+
if (direction == NavigateDirection_FirstChild || direction == NavigateDirection_LastChild) {
47+
auto fragment = m_childSiteLink.AutomationProvider().try_as<IRawElementProviderFragment>();
48+
*pRetVal = fragment.detach();
49+
return S_OK;
50+
}
51+
}
52+
#endif // USE_EXPERIMENTAL_WINUI3
53+
3654
return UiaNavigateHelper(m_view.view(), direction, *pRetVal);
3755
}
3856

vnext/Microsoft.ReactNative/Fabric/Composition/CompositionDynamicAutomationProvider.h

+10
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
2525
CompositionDynamicAutomationProvider(
2626
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView) noexcept;
2727

28+
#ifdef USE_EXPERIMENTAL_WINUI3
29+
CompositionDynamicAutomationProvider(
30+
const winrt::Microsoft::ReactNative::Composition::ComponentView &componentView,
31+
const winrt::Microsoft::UI::Content::ChildSiteLink &childContentLink) noexcept;
32+
#endif // USE_EXPERIMENTAL_WINUI3
33+
2834
// inherited via IRawElementProviderFragment
2935
virtual HRESULT __stdcall Navigate(NavigateDirection direction, IRawElementProviderFragment **pRetVal) override;
3036
virtual HRESULT __stdcall GetRuntimeId(SAFEARRAY **pRetVal) override;
@@ -86,6 +92,10 @@ class CompositionDynamicAutomationProvider : public winrt::implements<
8692
private:
8793
::Microsoft::ReactNative::ReactTaggedView m_view;
8894
std::vector<winrt::com_ptr<IRawElementProviderSimple>> m_selectionItems;
95+
#ifdef USE_EXPERIMENTAL_WINUI3
96+
// Non-null when this UIA node is the peer of a ContentIslandComponentView.
97+
winrt::Microsoft::UI::Content::ChildSiteLink m_childSiteLink{nullptr};
98+
#endif
8999
};
90100

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

vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp

+152-1
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@
1111
#include <UI.Xaml.Controls.h>
1212
#include <Utils/ValueUtils.h>
1313
#include <winrt/Microsoft.UI.Content.h>
14+
#include <winrt/Microsoft.UI.Input.h>
1415
#include <winrt/Windows.UI.Composition.h>
1516
#include "CompositionContextHelper.h"
1617
#include "RootComponentView.h"
1718

1819
#include "Composition.ContentIslandComponentView.g.cpp"
1920

21+
#include "CompositionDynamicAutomationProvider.h"
22+
2023
namespace winrt::Microsoft::ReactNative::Composition::implementation {
2124

2225
ContentIslandComponentView::ContentIslandComponentView(
@@ -47,6 +50,22 @@ void ContentIslandComponentView::OnMounted() noexcept {
4750
winrt::Microsoft::ReactNative::Composition::Experimental::CompositionContextHelper::InnerVisual(Visual())
4851
.as<winrt::Microsoft::UI::Composition::ContainerVisual>());
4952
m_childSiteLink.ActualSize({m_layoutMetrics.frame.size.width, m_layoutMetrics.frame.size.height});
53+
54+
m_navigationHost = winrt::Microsoft::UI::Input::InputFocusNavigationHost::GetForSiteLink(m_childSiteLink);
55+
56+
m_navigationHostDepartFocusRequestedToken =
57+
m_navigationHost.DepartFocusRequested([wkThis = get_weak()](const auto &, const auto &args) {
58+
if (auto strongThis = wkThis.get()) {
59+
const bool next = (args.Request().Reason() != winrt::Microsoft::UI::Input::FocusNavigationReason::Last);
60+
strongThis->rootComponentView()->TryMoveFocus(next);
61+
args.Result(winrt::Microsoft::UI::Input::FocusNavigationResult::Moved);
62+
}
63+
});
64+
65+
// We configure automation even if there's no UIA client at this point, because it's possible the first UIA
66+
// request we'll get will be for a child of this island calling upward in the UIA tree.
67+
ConfigureChildSiteLinkAutomation();
68+
5069
if (m_islandToConnect) {
5170
m_childSiteLink.Connect(m_islandToConnect);
5271
m_islandToConnect = nullptr;
@@ -70,6 +89,12 @@ void ContentIslandComponentView::OnMounted() noexcept {
7089

7190
void ContentIslandComponentView::OnUnmounted() noexcept {
7291
m_layoutMetricChangedRevokers.clear();
92+
#ifdef USE_EXPERIMENTAL_WINUI3
93+
if (m_navigationHostDepartFocusRequestedToken && m_navigationHost) {
94+
m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken);
95+
m_navigationHostDepartFocusRequestedToken = {};
96+
}
97+
#endif
7398
}
7499

75100
void ContentIslandComponentView::ParentLayoutChanged() noexcept {
@@ -92,7 +117,79 @@ void ContentIslandComponentView::ParentLayoutChanged() noexcept {
92117
#endif
93118
}
94119

120+
winrt::IInspectable ContentIslandComponentView::EnsureUiaProvider() noexcept {
121+
#ifdef USE_EXPERIMENTAL_WINUI3
122+
if (m_uiaProvider == nullptr) {
123+
m_uiaProvider = winrt::make<winrt::Microsoft::ReactNative::implementation::CompositionDynamicAutomationProvider>(
124+
*get_strong(), m_childSiteLink);
125+
}
126+
return m_uiaProvider;
127+
#else
128+
return Super::EnsureUiaProvider();
129+
#endif
130+
}
131+
132+
bool ContentIslandComponentView::focusable() const noexcept {
133+
#ifdef USE_EXPERIMENTAL_WINUI3
134+
// We don't have a way to check to see if the ContentIsland has focusable content,
135+
// so we'll always return true. We'll have to handle the case where the content doesn't have
136+
// focusable content in the OnGotFocus handler.
137+
return true;
138+
#else
139+
return Super::focusable();
140+
#endif
141+
}
142+
143+
// Helper to convert a FocusNavigationDirection to a FocusNavigationReason.
144+
winrt::Microsoft::UI::Input::FocusNavigationReason GetFocusNavigationReason(
145+
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
146+
switch (direction) {
147+
case winrt::Microsoft::ReactNative::FocusNavigationDirection::First:
148+
case winrt::Microsoft::ReactNative::FocusNavigationDirection::Next:
149+
return winrt::Microsoft::UI::Input::FocusNavigationReason::First;
150+
case winrt::Microsoft::ReactNative::FocusNavigationDirection::Last:
151+
case winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous:
152+
return winrt::Microsoft::UI::Input::FocusNavigationReason::Last;
153+
}
154+
return winrt::Microsoft::UI::Input::FocusNavigationReason::Restore;
155+
}
156+
157+
void ContentIslandComponentView::onGotFocus(
158+
const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept {
159+
#ifdef USE_EXPERIMENTAL_WINUI3
160+
auto gotFocusEventArgs = args.as<winrt::Microsoft::ReactNative::implementation::GotFocusEventArgs>();
161+
const auto navigationReason = GetFocusNavigationReason(gotFocusEventArgs->Direction());
162+
m_navigationHost.NavigateFocus(winrt::Microsoft::UI::Input::FocusNavigationRequest::Create(navigationReason));
163+
#else
164+
return Super::onGotFocus(args);
165+
#endif // USE_EXPERIMENTAL_WINUI3
166+
}
167+
95168
ContentIslandComponentView::~ContentIslandComponentView() noexcept {
169+
#ifdef USE_EXPERIMENTAL_WINUI3
170+
if (m_navigationHostDepartFocusRequestedToken && m_navigationHost) {
171+
m_navigationHost.DepartFocusRequested(m_navigationHostDepartFocusRequestedToken);
172+
m_navigationHostDepartFocusRequestedToken = {};
173+
}
174+
if (m_childSiteLink) {
175+
if (m_fragmentRootAutomationProviderRequestedToken) {
176+
m_childSiteLink.FragmentRootAutomationProviderRequested(m_fragmentRootAutomationProviderRequestedToken);
177+
m_fragmentRootAutomationProviderRequestedToken = {};
178+
}
179+
if (m_parentAutomationProviderRequestedToken) {
180+
m_childSiteLink.ParentAutomationProviderRequested(m_parentAutomationProviderRequestedToken);
181+
m_parentAutomationProviderRequestedToken = {};
182+
}
183+
if (m_nextSiblingAutomationProviderRequestedToken) {
184+
m_childSiteLink.NextSiblingAutomationProviderRequested(m_nextSiblingAutomationProviderRequestedToken);
185+
m_nextSiblingAutomationProviderRequestedToken = {};
186+
}
187+
if (m_previousSiblingAutomationProviderRequestedToken) {
188+
m_childSiteLink.PreviousSiblingAutomationProviderRequested(m_previousSiblingAutomationProviderRequestedToken);
189+
m_previousSiblingAutomationProviderRequestedToken = {};
190+
}
191+
}
192+
#endif // USE_EXPERIMENTAL_WINUI3
96193
if (m_islandToConnect) {
97194
m_islandToConnect.Close();
98195
}
@@ -132,11 +229,65 @@ void ContentIslandComponentView::Connect(const winrt::Microsoft::UI::Content::Co
132229
} else {
133230
m_islandToConnect = contentIsland;
134231
}
135-
#endif
232+
#endif // USE_EXPERIMENTAL_WINUI3
136233
}
137234

138235
void ContentIslandComponentView::prepareForRecycle() noexcept {
139236
Super::prepareForRecycle();
140237
}
141238

239+
#ifdef USE_EXPERIMENTAL_WINUI3
240+
void ContentIslandComponentView::ConfigureChildSiteLinkAutomation() noexcept {
241+
// This automation mode must be set before connecting the child ContentIsland.
242+
// It puts the child content into a mode where it won't own its own framework root. Instead, the child island's
243+
// automation peers will use the same framework root as the automation peer of this ContentIslandComponentView.
244+
m_childSiteLink.AutomationTreeOption(winrt::Microsoft::UI::Content::AutomationTreeOptions::FragmentBased);
245+
246+
// These events are raised in response to the child ContentIsland asking for providers.
247+
// For example, the ContentIsland.FragmentRootAutomationProvider property will return
248+
// the provider we provide here in FragmentRootAutomationProviderRequested.
249+
250+
// We capture "this" as a raw pointer because ContentIslandComponentView doesn't currently support weak ptrs.
251+
// It's safe because we disconnect these events in the destructor.
252+
253+
m_fragmentRootAutomationProviderRequestedToken = m_childSiteLink.FragmentRootAutomationProviderRequested(
254+
[this](
255+
const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
256+
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
257+
// The child island's fragment tree doesn't have its own fragment root.
258+
// Here's how we can provide the correct fragment root to the child's UIA logic.
259+
winrt::com_ptr<IRawElementProviderFragmentRoot> fragmentRoot{nullptr};
260+
auto uiaProvider = this->EnsureUiaProvider();
261+
uiaProvider.as<IRawElementProviderFragment>()->get_FragmentRoot(fragmentRoot.put());
262+
args.AutomationProvider(fragmentRoot.as<IInspectable>());
263+
args.Handled(true);
264+
});
265+
266+
m_parentAutomationProviderRequestedToken = m_childSiteLink.ParentAutomationProviderRequested(
267+
[this](
268+
const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
269+
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
270+
auto uiaProvider = this->EnsureUiaProvider();
271+
args.AutomationProvider(uiaProvider);
272+
args.Handled(true);
273+
});
274+
275+
m_nextSiblingAutomationProviderRequestedToken = m_childSiteLink.NextSiblingAutomationProviderRequested(
276+
[](const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
277+
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
278+
// The ContentIsland will always be the one and only child of this node, so it won't have siblings.
279+
args.AutomationProvider(nullptr);
280+
args.Handled(true);
281+
});
282+
283+
m_previousSiblingAutomationProviderRequestedToken = m_childSiteLink.PreviousSiblingAutomationProviderRequested(
284+
[](const winrt::Microsoft::UI::Content::IContentSiteAutomation &,
285+
const winrt::Microsoft::UI::Content::ContentSiteAutomationProviderRequestedEventArgs &args) {
286+
// The ContentIsland will always be the one and only child of this node, so it won't have siblings.
287+
args.AutomationProvider(nullptr);
288+
args.Handled(true);
289+
});
290+
}
291+
#endif // USE_EXPERIMENTAL_WINUI3
292+
142293
} // namespace winrt::Microsoft::ReactNative::Composition::implementation

vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.h

+16
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include <Microsoft.ReactNative.Cxx/ReactContext.h>
1010
#include <winrt/Microsoft.UI.Content.h>
11+
#include <winrt/Microsoft.UI.Input.h>
1112
#include <winrt/Windows.UI.Composition.h>
1213
#include "CompositionHelpers.h"
1314
#include "CompositionViewComponentView.h"
@@ -37,6 +38,12 @@ struct ContentIslandComponentView : ContentIslandComponentViewT<ContentIslandCom
3738

3839
void prepareForRecycle() noexcept override;
3940

41+
bool focusable() const noexcept override;
42+
43+
winrt::IInspectable EnsureUiaProvider() noexcept override;
44+
45+
void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override;
46+
4047
ContentIslandComponentView(
4148
const winrt::Microsoft::ReactNative::Composition::Experimental::ICompositionContext &compContext,
4249
facebook::react::Tag tag,
@@ -56,6 +63,15 @@ struct ContentIslandComponentView : ContentIslandComponentViewT<ContentIslandCom
5663
std::vector<winrt::Microsoft::ReactNative::ComponentView::LayoutMetricsChanged_revoker> m_layoutMetricChangedRevokers;
5764
#ifdef USE_EXPERIMENTAL_WINUI3
5865
winrt::Microsoft::UI::Content::ChildSiteLink m_childSiteLink{nullptr};
66+
winrt::Microsoft::UI::Input::InputFocusNavigationHost m_navigationHost{nullptr};
67+
winrt::event_token m_navigationHostDepartFocusRequestedToken{};
68+
69+
// Automation
70+
void ConfigureChildSiteLinkAutomation() noexcept;
71+
winrt::event_token m_fragmentRootAutomationProviderRequestedToken{};
72+
winrt::event_token m_parentAutomationProviderRequestedToken{};
73+
winrt::event_token m_nextSiblingAutomationProviderRequestedToken{};
74+
winrt::event_token m_previousSiblingAutomationProviderRequestedToken{};
5975
#endif
6076
};
6177

vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.cpp

+4-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ int32_t LostFocusEventArgs::OriginalSource() noexcept {
1414
return m_originalSource;
1515
}
1616

17-
GotFocusEventArgs::GotFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource)
18-
: m_originalSource(originalSource ? originalSource.Tag() : -1) {}
17+
GotFocusEventArgs::GotFocusEventArgs(
18+
const winrt::Microsoft::ReactNative::ComponentView &originalSource,
19+
winrt::Microsoft::ReactNative::FocusNavigationDirection direction)
20+
: m_originalSource(originalSource ? originalSource.Tag() : -1), m_direction(direction) {}
1921
int32_t GotFocusEventArgs::OriginalSource() noexcept {
2022
return m_originalSource;
2123
}

vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.h

+9-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@ struct LostFocusEventArgs
2121

2222
struct GotFocusEventArgs
2323
: winrt::implements<GotFocusEventArgs, winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs> {
24-
GotFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource);
24+
GotFocusEventArgs(
25+
const winrt::Microsoft::ReactNative::ComponentView &originalSource,
26+
winrt::Microsoft::ReactNative::FocusNavigationDirection direction);
2527
int32_t OriginalSource() noexcept;
2628

29+
winrt::Microsoft::ReactNative::FocusNavigationDirection Direction() const noexcept {
30+
return m_direction;
31+
}
32+
2733
private:
2834
const int32_t m_originalSource;
35+
winrt::Microsoft::ReactNative::FocusNavigationDirection m_direction{
36+
winrt::Microsoft::ReactNative::FocusNavigationDirection::None};
2937
};
3038

3139
struct LosingFocusEventArgs

vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp

+6-4
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ void RootComponentView::updateLayoutMetrics(
7676
winrt::Microsoft::ReactNative::ComponentView RootComponentView::GetFocusedComponent() noexcept {
7777
return m_focusedComponent;
7878
}
79-
void RootComponentView::SetFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &value) noexcept {
79+
void RootComponentView::SetFocusedComponent(
80+
const winrt::Microsoft::ReactNative::ComponentView &value,
81+
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept {
8082
if (m_focusedComponent == value)
8183
return;
8284

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

@@ -151,7 +153,7 @@ bool RootComponentView::TrySetFocusedComponent(
151153

152154
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(losingFocusArgs.NewFocusedComponent())
153155
->rootComponentView()
154-
->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent());
156+
->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction);
155157
}
156158

157159
return true;
@@ -241,7 +243,7 @@ void RootComponentView::start(const winrt::Microsoft::ReactNative::ReactNativeIs
241243
}
242244

243245
void RootComponentView::stop() noexcept {
244-
SetFocusedComponent(nullptr);
246+
SetFocusedComponent(nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None);
245247
if (m_visualAddedToIsland) {
246248
if (auto rootView = m_wkRootView.get()) {
247249
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ReactNativeIsland>(rootView)->RemoveRenderedVisual(

vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ struct RootComponentView : RootComponentViewT<RootComponentView, ViewComponentVi
2727
winrt::Microsoft::ReactNative::ReactContext const &reactContext) noexcept;
2828

2929
winrt::Microsoft::ReactNative::ComponentView GetFocusedComponent() noexcept;
30-
void SetFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &value) noexcept;
30+
void SetFocusedComponent(
31+
const winrt::Microsoft::ReactNative::ComponentView &value,
32+
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept;
3133
bool TrySetFocusedComponent(
3234
const winrt::Microsoft::ReactNative::ComponentView &view,
3335
winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept;

0 commit comments

Comments
 (0)