diff --git a/change/react-native-windows-8549cb15-fcb5-48e9-b8bf-8d6e2ae1d896.json b/change/react-native-windows-8549cb15-fcb5-48e9-b8bf-8d6e2ae1d896.json new file mode 100644 index 00000000000..21972245d99 --- /dev/null +++ b/change/react-native-windows-8549cb15-fcb5-48e9-b8bf-8d6e2ae1d896.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "[Fabric] Add various native focus APIs", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Desktop/module.g.cpp b/vnext/Desktop/module.g.cpp index b5eef16b644..f311ff8e5b2 100644 --- a/vnext/Desktop/module.g.cpp +++ b/vnext/Desktop/module.g.cpp @@ -13,6 +13,7 @@ void *winrt_make_Microsoft_ReactNative_Composition_Experimental_MicrosoftComposi void *winrt_make_Microsoft_ReactNative_Composition_Experimental_SystemCompositionContextHelper(); void *winrt_make_Microsoft_ReactNative_Composition_CompositionUIService(); void* winrt_make_Microsoft_ReactNative_Composition_ViewComponentView(); +void *winrt_make_Microsoft_ReactNative_Composition_FocusManager(); void* winrt_make_Microsoft_ReactNative_JsiRuntime(); void* winrt_make_Microsoft_ReactNative_ReactCoreInjection(); void* winrt_make_Microsoft_ReactNative_ReactDispatcherHelper(); @@ -30,6 +31,9 @@ void* winrt_make_facebook_react_NativeTraceEventSource(); void* winrt_make_Microsoft_ReactNative_Composition_ViewComponentView() { winrt::throw_hresult(E_NOTIMPL); } +void *winrt_make_Microsoft_ReactNative_Composition_FocusManager() { + winrt::throw_hresult(E_NOTIMPL); +} #endif bool __stdcall winrt_can_unload_now() noexcept @@ -68,6 +72,9 @@ void* __stdcall winrt_get_activation_factory([[maybe_unused]] std::wstring_view if (requal(name, L"Microsoft.ReactNative.Composition.CompositionUIService")) { return winrt_make_Microsoft_ReactNative_Composition_CompositionUIService(); } + if (requal(name, L"Microsoft.ReactNative.Composition.FocusManager")) { + return winrt_make_Microsoft_ReactNative_Composition_FocusManager(); + } if (requal(name, L"Microsoft.ReactNative.Composition.ViewComponentView")) { return winrt_make_Microsoft_ReactNative_Composition_ViewComponentView(); } diff --git a/vnext/Microsoft.ReactNative/ComponentView.idl b/vnext/Microsoft.ReactNative/ComponentView.idl index 527d08c033b..a65ecc1ab98 100644 --- a/vnext/Microsoft.ReactNative/ComponentView.idl +++ b/vnext/Microsoft.ReactNative/ComponentView.idl @@ -50,6 +50,26 @@ namespace Microsoft.ReactNative IReactContext ReactContext { get;}; }; + [experimental] + [webhosthidden] + runtimeclass LosingFocusEventArgs : Microsoft.ReactNative.Composition.Input.RoutedEventArgs { + Microsoft.ReactNative.ComponentView NewFocusedComponent { get; }; + Microsoft.ReactNative.ComponentView OldFocusedComponent { get; }; + + void TryCancel(); + void TrySetNewFocusedComponent(Microsoft.ReactNative.ComponentView component); + }; + + [experimental] + [webhosthidden] + runtimeclass GettingFocusEventArgs : Microsoft.ReactNative.Composition.Input.RoutedEventArgs { + Microsoft.ReactNative.ComponentView NewFocusedComponent { get; }; + Microsoft.ReactNative.ComponentView OldFocusedComponent { get; }; + + void TryCancel(); + void TrySetNewFocusedComponent(Microsoft.ReactNative.ComponentView component); + }; + [experimental] [webhosthidden] unsealed runtimeclass ComponentView { @@ -84,6 +104,12 @@ namespace Microsoft.ReactNative overridable void OnPointerExited(Microsoft.ReactNative.Composition.Input.PointerRoutedEventArgs args); overridable void OnPointerCaptureLost(); + Boolean TryFocus(); + + event Windows.Foundation.EventHandler LosingFocus; + event Windows.Foundation.EventHandler GettingFocus; + event Windows.Foundation.EventHandler LostFocus; + event Windows.Foundation.EventHandler GotFocus; }; } // namespace Microsoft.ReactNative diff --git a/vnext/Microsoft.ReactNative/CompositionComponentView.idl b/vnext/Microsoft.ReactNative/CompositionComponentView.idl index 2cd29187d2c..b2627f4ddf8 100644 --- a/vnext/Microsoft.ReactNative/CompositionComponentView.idl +++ b/vnext/Microsoft.ReactNative/CompositionComponentView.idl @@ -48,6 +48,7 @@ namespace Microsoft.ReactNative.Composition ComponentView(CreateCompositionComponentViewArgs args); Microsoft.UI.Composition.Compositor Compositor { get; }; + RootComponentView Root { get; }; Theme Theme; overridable void OnThemeChanged(); Boolean CapturePointer(Microsoft.ReactNative.Composition.Input.Pointer pointer); @@ -88,6 +89,7 @@ namespace Microsoft.ReactNative.Composition [webhosthidden] [default_interface] unsealed runtimeclass RootComponentView : ViewComponentView { + Microsoft.ReactNative.ComponentView GetFocusedComponent(); }; [experimental] diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp index 65cbd2dd0d4..6876376d759 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp @@ -143,19 +143,16 @@ ComponentView::rootComponentView() noexcept { } void ComponentView::parent(const winrt::Microsoft::ReactNative::ComponentView &parent) noexcept { - if (!parent) { - auto root = rootComponentView(); - winrt::Microsoft::ReactNative::ComponentView view{nullptr}; - winrt::check_hresult( - QueryInterface(winrt::guid_of(), winrt::put_abi(view))); - if (root && root->GetFocusedComponent() == view) { - root->SetFocusedComponent(nullptr); // TODO need move focus logic - where should focus go? - } - } - if (m_parent != parent) { + auto oldRootView = rootComponentView(); m_rootView = nullptr; + auto oldParent = m_parent; m_parent = parent; + if (!parent) { + if (oldRootView && oldRootView->GetFocusedComponent() == *this) { + oldRootView->TrySetFocusedComponent(oldParent); + } + } if (parent) { theme(winrt::get_self(parent)->theme()); } @@ -220,9 +217,83 @@ facebook::react::Point ComponentView::getClientOffset() const noexcept { return {}; } -void ComponentView::onFocusLost() noexcept {} +void ComponentView::onLostFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + m_lostFocusEvent(*this, args); + if (m_parent) { + winrt::get_self(m_parent)->onLostFocus(args); + } +} + +void ComponentView::onGotFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + m_gotFocusEvent(*this, args); + if (m_parent) { + winrt::get_self(m_parent)->onGotFocus(args); + } +} + +void ComponentView::onLosingFocus(const winrt::Microsoft::ReactNative::LosingFocusEventArgs &args) noexcept { + m_losingFocusEvent(*this, args); + if (m_parent) { + winrt::get_self(m_parent)->onLosingFocus(args); + } +} + +void ComponentView::onGettingFocus(const winrt::Microsoft::ReactNative::GettingFocusEventArgs &args) noexcept { + m_gettingFocusEvent(*this, args); + if (m_parent) { + winrt::get_self(m_parent)->onGettingFocus(args); + } +} -void ComponentView::onFocusGained() noexcept {} +winrt::event_token ComponentView::LosingFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept { + return m_losingFocusEvent.add(handler); +} + +void ComponentView::LosingFocus(winrt::event_token const &token) noexcept { + m_losingFocusEvent.remove(token); +} + +winrt::event_token ComponentView::GettingFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept { + return m_gettingFocusEvent.add(handler); +} + +void ComponentView::GettingFocus(winrt::event_token const &token) noexcept { + m_gettingFocusEvent.remove(token); +} + +winrt::event_token ComponentView::LostFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept { + return m_lostFocusEvent.add(handler); +} + +void ComponentView::LostFocus(winrt::event_token const &token) noexcept { + m_lostFocusEvent.remove(token); +} + +winrt::event_token ComponentView::GotFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept { + return m_gotFocusEvent.add(handler); +} + +void ComponentView::GotFocus(winrt::event_token const &token) noexcept { + m_gotFocusEvent.remove(token); +} + +bool ComponentView::TryFocus() noexcept { + if (auto root = rootComponentView()) { + return root->TrySetFocusedComponent(*get_strong()); + } + + return false; +} void ComponentView::OnPointerEntered( const winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs &args) noexcept {} diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index f75124dcb1b..5683d19a5f3 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -87,8 +87,29 @@ struct ComponentView : public ComponentViewT { virtual RECT getClientRect() const noexcept; // The offset from this elements parent to its children (accounts for things like scroll position) virtual facebook::react::Point getClientOffset() const noexcept; - virtual void onFocusLost() noexcept; - virtual void onFocusGained() noexcept; + virtual void onLosingFocus(const winrt::Microsoft::ReactNative::LosingFocusEventArgs &args) noexcept; + virtual void onGettingFocus(const winrt::Microsoft::ReactNative::GettingFocusEventArgs &args) noexcept; + virtual void onLostFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept; + virtual void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept; + + winrt::event_token LosingFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept; + void LosingFocus(winrt::event_token const &token) noexcept; + winrt::event_token GettingFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept; + void GettingFocus(winrt::event_token const &token) noexcept; + winrt::event_token LostFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept; + void LostFocus(winrt::event_token const &token) noexcept; + winrt::event_token GotFocus( + winrt::Windows::Foundation::EventHandler const + &handler) noexcept; + void GotFocus(winrt::event_token const &token) noexcept; + + bool TryFocus() noexcept; virtual bool focusable() const noexcept; virtual facebook::react::SharedViewEventEmitter eventEmitterAtPoint(facebook::react::Point pt) noexcept; @@ -158,6 +179,16 @@ struct ComponentView : public ComponentViewT { winrt::Microsoft::ReactNative::ComponentView m_parent{nullptr}; winrt::Windows::Foundation::Collections::IVector m_children{ winrt::single_threaded_vector()}; + winrt::event> + m_losingFocusEvent; + winrt::event> + m_gettingFocusEvent; + winrt::event< + winrt::Windows::Foundation::EventHandler> + m_lostFocusEvent; + winrt::event< + winrt::Windows::Foundation::EventHandler> + m_gotFocusEvent; }; // Run fn on all nodes of the component view tree starting from this one until fn returns true diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.cpp index 2cdb551184e..557bff6a61b 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.cpp @@ -132,6 +132,12 @@ CompositionRootView::CompositionRootView(const winrt::Microsoft::UI::Composition #endif CompositionRootView::~CompositionRootView() noexcept { +#ifdef USE_WINUI3 + if (m_island && m_island.IsConnected()) { + m_island.AutomationProviderRequested(m_islandAutomationProviderRequestedToken); + } +#endif + if (m_uiDispatcher) { assert(m_uiDispatcher.HasThreadAccess()); UninitRootView(); @@ -193,7 +199,7 @@ void CompositionRootView::RemoveRenderedVisual( bool CompositionRootView::TrySetFocus() noexcept { #ifdef USE_WINUI3 - if (m_island) { + if (m_island && m_island.IsConnected()) { auto focusController = winrt::Microsoft::UI::Input::InputFocusController::GetForIsland(m_island); return focusController.TrySetFocus(); } @@ -664,20 +670,29 @@ winrt::Microsoft::UI::Content::ContentIsland CompositionRootView::Island() noexc rootVisual)); m_island = winrt::Microsoft::UI::Content::ContentIsland::Create(rootVisual); - m_island.AutomationProviderRequested( - [this]( + // ContentIsland does not support weak_ref, so we cannot use auto_revoke for these events + m_islandAutomationProviderRequestedToken = m_island.AutomationProviderRequested( + [weakThis = get_weak()]( winrt::Microsoft::UI::Content::ContentIsland const &, winrt::Microsoft::UI::Content::ContentIslandAutomationProviderRequestedEventArgs const &args) { - auto provider = GetUiaProvider(); - auto pRootProvider = - static_cast( - provider.as().get()); - if (pRootProvider != nullptr) { - pRootProvider->SetIsland(m_island); + if (auto pThis = weakThis.get()) { + auto provider = pThis->GetUiaProvider(); + auto pRootProvider = + static_cast( + provider.as().get()); + if (pRootProvider != nullptr) { + pRootProvider->SetIsland(pThis->m_island); + } + args.AutomationProvider(std::move(provider)); + args.Handled(true); } - args.AutomationProvider(std::move(provider)); - args.Handled(true); }); + + m_islandFrameworkClosedToken = m_island.FrameworkClosed([weakThis = get_weak()]() { + if (auto pThis = weakThis.get()) { + pThis->m_island = nullptr; + } + }); } return m_island; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.h index eaf25680aab..c477d43adef 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionRootView.h @@ -121,6 +121,8 @@ struct CompositionRootView #ifdef USE_WINUI3 winrt::Microsoft::UI::Composition::Compositor m_compositor{nullptr}; winrt::Microsoft::UI::Content::ContentIsland m_island{nullptr}; + winrt::event_token m_islandFrameworkClosedToken; + winrt::event_token m_islandAutomationProviderRequestedToken; #endif HWND m_hwnd{0}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 1ca3cf654e3..3af66f71275 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -121,6 +121,10 @@ winrt::Microsoft::ReactNative::Composition::Theme ComponentView::Theme() const n return theme()->get_strong().as(); } +winrt::Microsoft::ReactNative::Composition::RootComponentView ComponentView::Root() noexcept { + return *rootComponentView(); +} + winrt::Microsoft::UI::Composition::Compositor ComponentView::Compositor() const noexcept { return winrt::Microsoft::ReactNative::Composition::Experimental::MicrosoftCompositionContextHelper::InnerCompositor( m_compContext); @@ -179,32 +183,38 @@ void ComponentView::FinalizeUpdates(winrt::Microsoft::ReactNative::ComponentView base_type::FinalizeUpdates(updateMask); } -void ComponentView::onFocusLost() noexcept { - m_eventEmitter->onBlur(); - showFocusVisual(false); - if (m_uiaProvider) { - winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( - m_uiaProvider, UIA_HasKeyboardFocusPropertyId, true, false); +void ComponentView::onLostFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + if (args.OriginalSource() == Tag()) { + m_eventEmitter->onBlur(); + showFocusVisual(false); + if (m_uiaProvider) { + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + m_uiaProvider, UIA_HasKeyboardFocusPropertyId, true, false); + } } - base_type::onFocusLost(); + base_type::onLostFocus(args); } -void ComponentView::onFocusGained() noexcept { - m_eventEmitter->onFocus(); - if (m_enableFocusVisual) { - showFocusVisual(true); - } - if (m_uiaProvider) { - auto spProviderSimple = m_uiaProvider.try_as(); - if (spProviderSimple != nullptr) { - winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( - m_uiaProvider, UIA_HasKeyboardFocusPropertyId, false, true); - UiaRaiseAutomationEvent(spProviderSimple.get(), UIA_AutomationFocusChangedEventId); +void ComponentView::onGotFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + if (args.OriginalSource() == Tag()) { + m_eventEmitter->onFocus(); + if (m_enableFocusVisual) { + showFocusVisual(true); + } + if (m_uiaProvider) { + auto spProviderSimple = m_uiaProvider.try_as(); + if (spProviderSimple != nullptr) { + winrt::Microsoft::ReactNative::implementation::UpdateUiaProperty( + m_uiaProvider, UIA_HasKeyboardFocusPropertyId, false, true); + UiaRaiseAutomationEvent(spProviderSimple.get(), UIA_AutomationFocusChangedEventId); + } } - } - StartBringIntoView({}); - base_type::onFocusGained(); + StartBringIntoView({}); + } + base_type::onGotFocus(args); } void ComponentView::StartBringIntoView( @@ -234,13 +244,13 @@ void ComponentView::HandleCommand( const winrt::Microsoft::ReactNative::IJSValueReader &args) noexcept { if (commandName == L"focus") { if (auto root = rootComponentView()) { - root->SetFocusedComponent(*get_strong()); + root->TrySetFocusedComponent(*get_strong()); } return; } if (commandName == L"blur") { if (auto root = rootComponentView()) { - root->SetFocusedComponent(nullptr); // Todo store this component as previously focused element + root->TrySetFocusedComponent(nullptr); // Todo store this component as previously focused element } return; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h index f6ed39ca49b..1837b301417 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.h @@ -69,11 +69,14 @@ struct ComponentView assert(false); return emptyProps; }; + + winrt::Microsoft::ReactNative::Composition::RootComponentView Root() noexcept; + void Theme(const winrt::Microsoft::ReactNative::Composition::Theme &theme) noexcept; winrt::Microsoft::ReactNative::Composition::Theme Theme() const noexcept; void onThemeChanged() noexcept override; - void onFocusLost() noexcept override; - void onFocusGained() noexcept override; + void onLostFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override; + void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override; bool CapturePointer(const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer) noexcept; void ReleasePointerCapture(const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer) noexcept; @@ -189,6 +192,7 @@ struct ViewComponentView : public ViewComponentViewT + +namespace winrt::Microsoft::ReactNative::implementation { + +LostFocusEventArgs::LostFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource) + : m_originalSource(originalSource ? originalSource.Tag() : -1) {} +int32_t LostFocusEventArgs::OriginalSource() noexcept { + return m_originalSource; +} + +GotFocusEventArgs::GotFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource) + : m_originalSource(originalSource ? originalSource.Tag() : -1) {} +int32_t GotFocusEventArgs::OriginalSource() noexcept { + return m_originalSource; +} + +LosingFocusEventArgs::LosingFocusEventArgs( + const winrt::Microsoft::ReactNative::ComponentView &originalSource, + const winrt::Microsoft::ReactNative::ComponentView &oldFocusedComponent, + const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent) + : m_originalSource(originalSource ? originalSource.Tag() : -1), + m_old(oldFocusedComponent), + m_new(newFocusedComponent) {} + +int32_t LosingFocusEventArgs::OriginalSource() noexcept { + return m_originalSource; +} +winrt::Microsoft::ReactNative::ComponentView LosingFocusEventArgs::NewFocusedComponent() noexcept { + return m_new; +} +winrt::Microsoft::ReactNative::ComponentView LosingFocusEventArgs::OldFocusedComponent() noexcept { + return m_old; +} + +void LosingFocusEventArgs::TryCancel() noexcept { + m_new = m_old; +} + +void LosingFocusEventArgs::TrySetNewFocusedComponent( + const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent) noexcept { + auto selfView = winrt::get_self(newFocusedComponent); + if (selfView->focusable()) { + m_new = newFocusedComponent; + } else { + auto target = + winrt::Microsoft::ReactNative::Composition::FocusManager::FindFirstFocusableElement(newFocusedComponent); + if (!target) + return; + m_new = target; + } +} + +GettingFocusEventArgs::GettingFocusEventArgs( + const winrt::Microsoft::ReactNative::ComponentView &originalSource, + const winrt::Microsoft::ReactNative::ComponentView &oldFocusedComponent, + const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent) + : m_originalSource(originalSource ? originalSource.Tag() : -1), + m_old(oldFocusedComponent), + m_new(newFocusedComponent) {} + +int32_t GettingFocusEventArgs::OriginalSource() noexcept { + return m_originalSource; +} +winrt::Microsoft::ReactNative::ComponentView GettingFocusEventArgs::NewFocusedComponent() noexcept { + return m_new; +} +winrt::Microsoft::ReactNative::ComponentView GettingFocusEventArgs::OldFocusedComponent() noexcept { + return m_old; +} + +void GettingFocusEventArgs::TryCancel() noexcept { + m_new = m_old; +} + +void GettingFocusEventArgs::TrySetNewFocusedComponent( + const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent) noexcept { + auto selfView = winrt::get_self(newFocusedComponent); + if (selfView->focusable()) { + m_new = newFocusedComponent; + } else { + auto target = + winrt::Microsoft::ReactNative::Composition::FocusManager::FindFirstFocusableElement(newFocusedComponent); + if (!target) + return; + m_new = target; + } +} + +} // namespace winrt::Microsoft::ReactNative::implementation + +namespace winrt::Microsoft::ReactNative::Composition::implementation { + +winrt::Microsoft::ReactNative::implementation::ComponentView *NavigateFocusHelper( + winrt::Microsoft::ReactNative::implementation::ComponentView &view, + winrt::Microsoft::ReactNative::FocusNavigationReason reason) { + if (reason == winrt::Microsoft::ReactNative::FocusNavigationReason::First) { + if (view.focusable()) { + return &view; + } + } + winrt::Microsoft::ReactNative::implementation::ComponentView *toFocus = nullptr; + + Mso::Functor fn = + [reason, &toFocus](::winrt::Microsoft::ReactNative::implementation::ComponentView &v) noexcept + -> bool { return (toFocus = NavigateFocusHelper(v, reason)); }; + + if (view.runOnChildren(reason == winrt::Microsoft::ReactNative::FocusNavigationReason::First, fn)) { + return toFocus; + } + + if (reason == winrt::Microsoft::ReactNative::FocusNavigationReason::Last) { + if (view.focusable()) { + return &view; + } + } + + return nullptr; +} + +winrt::Microsoft::ReactNative::ComponentView FocusManager::FindFirstFocusableElement( + const winrt::Microsoft::ReactNative::ComponentView &searchScope) noexcept { + auto selfSearchScope = winrt::get_self(searchScope); + auto view = NavigateFocusHelper(*selfSearchScope, winrt::Microsoft::ReactNative::FocusNavigationReason::First); + if (view) { + winrt::Microsoft::ReactNative::ComponentView component{nullptr}; + winrt::check_hresult(view->QueryInterface( + winrt::guid_of(), winrt::put_abi(component))); + return *view; + } + return nullptr; +} + +winrt::Microsoft::ReactNative::ComponentView FocusManager::FindLastFocusableElement( + const winrt::Microsoft::ReactNative::ComponentView &searchScope) noexcept { + auto selfSearchScope = winrt::get_self(searchScope); + auto view = NavigateFocusHelper(*selfSearchScope, winrt::Microsoft::ReactNative::FocusNavigationReason::Last); + if (view) { + winrt::Microsoft::ReactNative::ComponentView component{nullptr}; + winrt::check_hresult(view->QueryInterface( + winrt::guid_of(), winrt::put_abi(component))); + return *view; + } + return nullptr; +} + +} // namespace winrt::Microsoft::ReactNative::Composition::implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.h b/vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.h new file mode 100644 index 00000000000..3789c1f5370 --- /dev/null +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/FocusManager.h @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include "Composition.FocusManager.g.h" +#include "GettingFocusEventArgs.g.h" +#include "LosingFocusEventArgs.g.h" +#include +#include + +namespace winrt::Microsoft::ReactNative::implementation { + +struct LostFocusEventArgs + : winrt::implements { + LostFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource); + int32_t OriginalSource() noexcept; + + private: + const int32_t m_originalSource; +}; + +struct GotFocusEventArgs + : winrt::implements { + GotFocusEventArgs(const winrt::Microsoft::ReactNative::ComponentView &originalSource); + int32_t OriginalSource() noexcept; + + private: + const int32_t m_originalSource; +}; + +struct LosingFocusEventArgs + : winrt::Microsoft::ReactNative::implementation::LosingFocusEventArgsT { + LosingFocusEventArgs( + const winrt::Microsoft::ReactNative::ComponentView &originalSource, + const winrt::Microsoft::ReactNative::ComponentView &oldFocusedComponent, + const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent); + int32_t OriginalSource() noexcept; + winrt::Microsoft::ReactNative::ComponentView NewFocusedComponent() noexcept; + winrt::Microsoft::ReactNative::ComponentView OldFocusedComponent() noexcept; + + void TryCancel() noexcept; + void TrySetNewFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent) noexcept; + + private: + const int32_t m_originalSource; + winrt::Microsoft::ReactNative::ComponentView m_old{nullptr}; + winrt::Microsoft::ReactNative::ComponentView m_new{nullptr}; +}; + +struct GettingFocusEventArgs + : winrt::Microsoft::ReactNative::implementation::GettingFocusEventArgsT { + GettingFocusEventArgs( + const winrt::Microsoft::ReactNative::ComponentView &originalSource, + const winrt::Microsoft::ReactNative::ComponentView &oldFocusedComponent, + const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent); + int32_t OriginalSource() noexcept; + winrt::Microsoft::ReactNative::ComponentView NewFocusedComponent() noexcept; + winrt::Microsoft::ReactNative::ComponentView OldFocusedComponent() noexcept; + + void TryCancel() noexcept; + void TrySetNewFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &newFocusedComponent) noexcept; + + private: + const int32_t m_originalSource; + winrt::Microsoft::ReactNative::ComponentView m_old{nullptr}; + winrt::Microsoft::ReactNative::ComponentView m_new{nullptr}; +}; +} // namespace winrt::Microsoft::ReactNative::implementation + +namespace winrt::Microsoft::ReactNative::Composition::implementation { + +struct FocusManager : FocusManagerT { + FocusManager() = default; + + static winrt::Microsoft::ReactNative::ComponentView FindFirstFocusableElement( + const winrt::Microsoft::ReactNative::ComponentView &searchScope) noexcept; + static winrt::Microsoft::ReactNative::ComponentView FindLastFocusableElement( + const winrt::Microsoft::ReactNative::ComponentView &searchScope) noexcept; +}; + +} // namespace winrt::Microsoft::ReactNative::Composition::implementation + +namespace winrt::Microsoft::ReactNative::Composition::factory_implementation { +struct FocusManager : FocusManagerT {}; +} // namespace winrt::Microsoft::ReactNative::Composition::factory_implementation diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index d84b8645c80..dd205ceed20 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -45,7 +45,7 @@ RootComponentView *RootComponentView::rootComponentView() noexcept { return this; } -winrt::Microsoft::ReactNative::ComponentView &RootComponentView::GetFocusedComponent() noexcept { +winrt::Microsoft::ReactNative::ComponentView RootComponentView::GetFocusedComponent() noexcept { return m_focusedComponent; } void RootComponentView::SetFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &value) noexcept { @@ -53,69 +53,69 @@ void RootComponentView::SetFocusedComponent(const winrt::Microsoft::ReactNative: return; if (m_focusedComponent) { - winrt::get_self(m_focusedComponent)->onFocusLost(); + auto args = winrt::make(m_focusedComponent); + winrt::get_self(m_focusedComponent) + ->onLostFocus(args); } if (value) { if (auto rootView = m_wkRootView.get()) { winrt::get_self(rootView)->TrySetFocus(); } - winrt::get_self(value)->onFocusGained(); + auto args = winrt::make(value); + winrt::get_self(value)->onGotFocus(args); } m_focusedComponent = value; } -winrt::Microsoft::ReactNative::implementation::ComponentView *NavigateFocusHelper( - winrt::Microsoft::ReactNative::implementation::ComponentView &view, - winrt::Microsoft::ReactNative::FocusNavigationReason reason) { - if (reason == winrt::Microsoft::ReactNative::FocusNavigationReason::First) { - if (view.focusable()) { - return &view; - } - } - winrt::Microsoft::ReactNative::implementation::ComponentView *toFocus = nullptr; - - Mso::Functor fn = - [reason, &toFocus](::winrt::Microsoft::ReactNative::implementation::ComponentView &v) noexcept - -> bool { return (toFocus = NavigateFocusHelper(v, reason)); }; - - if (view.runOnChildren(reason == winrt::Microsoft::ReactNative::FocusNavigationReason::First, fn)) { - return toFocus; - } - - if (reason == winrt::Microsoft::ReactNative::FocusNavigationReason::Last) { - if (view.focusable()) { - return &view; - } - } - - return nullptr; -} - bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept { if (request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::Restore) { // No-op for now return m_focusedComponent != nullptr; } - auto view = NavigateFocusHelper(*this, request.Reason()); + auto view = (request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::First) + ? FocusManager::FindFirstFocusableElement(*this) + : FocusManager::FindLastFocusableElement(*this); if (view) { - winrt::Microsoft::ReactNative::ComponentView component{nullptr}; - winrt::check_hresult(view->QueryInterface( - winrt::guid_of(), winrt::put_abi(component))); - SetFocusedComponent(component); + TrySetFocusedComponent(view); } return view != nullptr; } bool RootComponentView::TrySetFocusedComponent(const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { - auto selfView = winrt::get_self(view); - if (selfView->focusable()) { - selfView->rootComponentView()->SetFocusedComponent(view); - return true; + auto target = view; + auto selfView = winrt::get_self(target); + if (selfView && !selfView->focusable()) { + target = FocusManager::FindFirstFocusableElement(target); + if (!target) + return false; + selfView = winrt::get_self(target); + } + + if (selfView && selfView->rootComponentView() != this) + return false; + + auto losingFocusArgs = winrt::make( + target, m_focusedComponent, target); + if (m_focusedComponent) { + winrt::get_self(m_focusedComponent) + ->onLosingFocus(losingFocusArgs); } - return false; + + if (losingFocusArgs.NewFocusedComponent()) { + auto gettingFocusArgs = winrt::make( + target, m_focusedComponent, losingFocusArgs.NewFocusedComponent()); + winrt::get_self(losingFocusArgs.NewFocusedComponent()) + ->onGettingFocus(gettingFocusArgs); + + winrt::get_self(losingFocusArgs.NewFocusedComponent()) + ->rootComponentView() + ->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent()); + } + + return true; } bool RootComponentView::TryMoveFocus(bool next) noexcept { diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h index cd1cf962e6a..76e5201d96d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h @@ -8,6 +8,7 @@ #include #include "CompositionViewComponentView.h" +#include "FocusManager.h" #include "Theme.h" #include "Composition.RootComponentView.g.h" @@ -25,7 +26,7 @@ struct RootComponentView : RootComponentViewT { winrt::Microsoft::ReactNative::ComponentView view{nullptr}; winrt::check_hresult( m_outer->QueryInterface(winrt::guid_of(), winrt::put_abi(view))); - m_outer->rootComponentView()->SetFocusedComponent(view); + m_outer->rootComponentView()->TrySetFocusedComponent(view); // assert(false); // TODO focus } @@ -929,8 +929,9 @@ void WindowsTextInputComponentView::UnmountChildComponentView( base_type::UnmountChildComponentView(childComponentView, index); } -void WindowsTextInputComponentView::onFocusLost() noexcept { - Super::onFocusLost(); +void WindowsTextInputComponentView::onLostFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + Super::onLostFocus(args); if (m_textServices) { LRESULT lresult; DrawBlock db(*this); @@ -939,8 +940,9 @@ void WindowsTextInputComponentView::onFocusLost() noexcept { m_caretVisual.IsVisible(false); } -void WindowsTextInputComponentView::onFocusGained() noexcept { - Super::onFocusGained(); +void WindowsTextInputComponentView::onGotFocus( + const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { + Super::onGotFocus(args); if (m_textServices) { LRESULT lresult; DrawBlock db(*this); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index 4b8bd88ee9c..ee0aef6e434 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -49,8 +49,8 @@ struct WindowsTextInputComponentView void HandleCommand(winrt::hstring commandName, const winrt::Microsoft::ReactNative::IJSValueReader &args) noexcept override; void OnRenderingDeviceLost() noexcept override; - void onFocusLost() noexcept override; - void onFocusGained() noexcept override; + void onLostFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override; + void onGotFocus(const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept override; std::string DefaultControlType() const noexcept override; std::string DefaultAccessibleName() const noexcept override; std::string DefaultHelpText() const noexcept override; diff --git a/vnext/Microsoft.ReactNative/FocusManager.idl b/vnext/Microsoft.ReactNative/FocusManager.idl new file mode 100644 index 00000000000..bbe7dc056b1 --- /dev/null +++ b/vnext/Microsoft.ReactNative/FocusManager.idl @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import "ComponentView.idl"; +#include "DocString.h" + +namespace Microsoft.ReactNative.Composition +{ + + [default_interface] + [webhosthidden] + [experimental] + runtimeclass FocusManager + { + DOC_STRING("Retrieves the first component that can receive focus based on the specified scope.") + static Microsoft.ReactNative.ComponentView FindFirstFocusableElement(Microsoft.ReactNative.ComponentView searchScope); + + DOC_STRING("Retrieves the last component that can receive focus based on the specified scope.") + static Microsoft.ReactNative.ComponentView FindLastFocusableElement(Microsoft.ReactNative.ComponentView searchScope); + } + +} // namespace Microsoft.ReactNative.Composition diff --git a/vnext/Shared/Shared.vcxitems b/vnext/Shared/Shared.vcxitems index 2ebf5740954..66f487124f0 100644 --- a/vnext/Shared/Shared.vcxitems +++ b/vnext/Shared/Shared.vcxitems @@ -84,6 +84,11 @@ $(ReactNativeWindowsDir)Microsoft.ReactNative\CompositionUIService.idl Code + + true + $(ReactNativeWindowsDir)Microsoft.ReactNative\FocusManager.idl + Code + true $(ReactNativeWindowsDir)Microsoft.ReactNative\UriImageManager.idl @@ -642,6 +647,7 @@ +