diff --git a/change/react-native-windows-022762be-ce5e-4e62-81f6-0c234d69992e.json b/change/react-native-windows-022762be-ce5e-4e62-81f6-0c234d69992e.json new file mode 100644 index 00000000000..2406e2e360f --- /dev/null +++ b/change/react-native-windows-022762be-ce5e-4e62-81f6-0c234d69992e.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix view.focus function", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/change/react-native-windows-edabe4dc-3296-42b9-952c-b3e3f7dc08d0.json b/change/react-native-windows-edabe4dc-3296-42b9-952c-b3e3f7dc08d0.json new file mode 100644 index 00000000000..f21602e18a1 --- /dev/null +++ b/change/react-native-windows-edabe4dc-3296-42b9-952c-b3e3f7dc08d0.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Only show focus visuals when using keyboard to move focus", + "packageName": "react-native-windows", + "email": "30809111+acoates-ms@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap index 5e8c7116993..be21282fc33 100644 --- a/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap +++ b/packages/e2e-test-app-fabric/test/__snapshots__/PressableComponentTest.test.ts.snap @@ -846,6 +846,13 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] "Name": "pressOut", "TextRangePattern.GetText": "pressOut", }, + { + "AutomationId": "", + "ControlType": 50020, + "LocalizedControlType": "text", + "Name": "focus", + "TextRangePattern.GetText": "focus", + }, { "AutomationId": "", "ControlType": 50020, @@ -891,6 +898,10 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", "_Props": {}, }, + { + "Type": "Microsoft.ReactNative.Composition.ParagraphComponentView", + "_Props": {}, + }, ], }, "Visual Tree": { @@ -991,6 +1002,18 @@ exports[`Pressable Tests Pressables can have event handlers, hover and click 2`] }, ], }, + { + "Offset": "11, 85, 0", + "Size": "874, 20", + "Visual Type": "SpriteVisual", + "__Children": [ + { + "Offset": "0, 0, 0", + "Size": "874, 20", + "Visual Type": "SpriteVisual", + }, + ], + }, ], }, } diff --git a/vnext/Microsoft.ReactNative/ComponentView.idl b/vnext/Microsoft.ReactNative/ComponentView.idl index cd724297724..c8fc3913eba 100644 --- a/vnext/Microsoft.ReactNative/ComponentView.idl +++ b/vnext/Microsoft.ReactNative/ComponentView.idl @@ -43,6 +43,14 @@ namespace Microsoft.ReactNative Last, }; + enum FocusState + { + Unfocused = 0, + Pointer, + Keyboard, + Programmatic, + }; + [webhosthidden] [experimental] interface IComponentState @@ -99,7 +107,7 @@ namespace Microsoft.ReactNative LayoutMetrics LayoutMetrics { get; }; IInspectable UserData; - Boolean TryFocus(); + Boolean TryFocus(FocusState focusState); DOC_STRING("Used to handle key down events when this component is focused, or if a child component did not handle the key down") event Windows.Foundation.EventHandler KeyDown; diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp index 4ac8beda5d5..f47bc05e67e 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp @@ -283,6 +283,7 @@ void ComponentView::parent(const winrt::Microsoft::ReactNative::ComponentView &p oldRootView->TrySetFocusedComponent( oldParent, winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic, true /*forceNoSelectionIfCannotMove*/); } } @@ -431,9 +432,10 @@ void ComponentView::GotFocus(winrt::event_token const &token) noexcept { m_gotFocusEvent.remove(token); } -bool ComponentView::TryFocus() noexcept { +bool ComponentView::TryFocus(winrt::Microsoft::ReactNative::FocusState focusState) noexcept { if (auto root = rootComponentView()) { - return root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + return root->TrySetFocusedComponent( + *get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None, focusState); } return false; diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index 13eb1f8f322..25c62c81e8c 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -201,7 +201,7 @@ struct ComponentView LayoutMetrics LayoutMetrics() const noexcept; - bool TryFocus() noexcept; + bool TryFocus(winrt::Microsoft::ReactNative::FocusState focusState) noexcept; virtual bool focusable() const noexcept; virtual facebook::react::SharedViewEventEmitter eventEmitterAtPoint(facebook::react::Point pt) noexcept; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 01eaf359155..2317266c9e7 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -608,6 +608,8 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w void CompositionEventHandler::onKeyDown( const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept { + RootComponentView().UseKeyboardForProgrammaticFocus(true); + if (auto focusedComponent = RootComponentView().GetFocusedComponent()) { winrt::get_self(focusedComponent)->OnKeyDown(args); @@ -631,7 +633,7 @@ void CompositionEventHandler::onKeyDown( } if (!fCtrl && args.Key() == winrt::Windows::System::VirtualKey::Tab) { - if (RootComponentView().TryMoveFocus(!fShift)) { + if (RootComponentView().TryMoveFocus(!fShift, winrt::Microsoft::ReactNative::FocusState::Keyboard)) { args.Handled(true); } @@ -641,6 +643,8 @@ void CompositionEventHandler::onKeyDown( void CompositionEventHandler::onKeyUp( const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept { + RootComponentView().UseKeyboardForProgrammaticFocus(true); + if (auto focusedComponent = RootComponentView().GetFocusedComponent()) { winrt::get_self(focusedComponent)->OnKeyUp(args); @@ -1173,6 +1177,8 @@ void CompositionEventHandler::onPointerPressed( winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept { namespace Composition = winrt::Microsoft::ReactNative::Composition; + RootComponentView().UseKeyboardForProgrammaticFocus(false); + // Clears any active text selection when left pointer is pressed if (pointerPoint.Properties().PointerUpdateKind() != Composition::Input::PointerUpdateKind::RightButtonPressed) { RootComponentView().ClearCurrentTextSelection(); @@ -1269,6 +1275,8 @@ void CompositionEventHandler::onPointerReleased( winrt::Windows::System::VirtualKeyModifiers keyModifiers) noexcept { int pointerId = pointerPoint.PointerId(); + RootComponentView().UseKeyboardForProgrammaticFocus(false); + auto activeTouch = std::find_if(m_activeTouches.begin(), m_activeTouches.end(), [pointerId](const auto &pair) { return pair.second.touch.identifier == pointerId; }); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp index 8948a5bb4e1..3f08e388edc 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionViewComponentView.cpp @@ -373,7 +373,8 @@ void ComponentView::onGotFocus( const winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs &args) noexcept { if (args.OriginalSource() == Tag()) { m_eventEmitter->onFocus(); - if (viewProps()->enableFocusRing) { + if (viewProps()->enableFocusRing && + rootComponentView()->focusState() == winrt::Microsoft::ReactNative::FocusState::Keyboard) { facebook::react::Rect focusRect = m_layoutMetrics.frame; focusRect.origin.x -= (FOCUS_VISUAL_WIDTH * 2); focusRect.origin.y -= (FOCUS_VISUAL_WIDTH * 2); @@ -426,15 +427,20 @@ void ComponentView::HandleCommand(const winrt::Microsoft::ReactNative::HandleCom auto commandName = args.CommandName(); if (commandName == L"focus") { if (auto root = rootComponentView()) { - root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + root->TrySetFocusedComponent( + *get_strong(), + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic); } return; } if (commandName == L"blur") { if (auto root = rootComponentView()) { root->TrySetFocusedComponent( - nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None); // Todo store this component as - // previously focused element + nullptr, + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic); // Todo store this component as + // previously focused element } return; } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp index 690b89d235e..46f404be61f 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ContentIslandComponentView.cpp @@ -82,7 +82,7 @@ void ContentIslandComponentView::ConnectInternal() noexcept { 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); + strongThis->rootComponentView()->TryMoveFocus(next, winrt::Microsoft::ReactNative::FocusState::Programmatic); args.Result(winrt::Microsoft::UI::Input::FocusNavigationResult::Moved); } }); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp index 7974583346e..8e66ee724bd 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/ParagraphComponentView.cpp @@ -622,7 +622,10 @@ void ParagraphComponentView::OnPointerPressed( // Focuses so we receive onLostFocus when clicking elsewhere if (auto root = rootComponentView()) { - root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + root->TrySetFocusedComponent( + *get_strong(), + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Pointer); } args.Handled(true); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp index d666530456b..b2a6b887ee3 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.cpp @@ -82,7 +82,8 @@ winrt::Microsoft::ReactNative::ComponentView RootComponentView::GetFocusedCompon void RootComponentView::SetFocusedComponent( const winrt::Microsoft::ReactNative::ComponentView &value, - winrt::Microsoft::ReactNative::FocusNavigationDirection direction) noexcept { + winrt::Microsoft::ReactNative::FocusNavigationDirection direction, + winrt::Microsoft::ReactNative::FocusState focusState) noexcept { if (m_focusedComponent == value) return; @@ -97,11 +98,26 @@ void RootComponentView::SetFocusedComponent( winrt::get_self(rootView)->TrySetFocus(); } m_focusedComponent = value; + if (focusState == winrt::Microsoft::ReactNative::FocusState::Programmatic) { + focusState = + (!m_useKeyboardForProgrammaticFocus || m_focusState == winrt::Microsoft::ReactNative::FocusState::Pointer) + ? winrt::Microsoft::ReactNative::FocusState::Pointer + : winrt::Microsoft::ReactNative::FocusState::Keyboard; + } + m_focusState = focusState; auto args = winrt::make(value, direction); winrt::get_self(value)->onGotFocus(args); } } +winrt::Microsoft::ReactNative::FocusState RootComponentView::focusState() const noexcept { + return m_focusState; +} + +void RootComponentView::UseKeyboardForProgrammaticFocus(bool value) noexcept { + m_useKeyboardForProgrammaticFocus = value; +} + bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::FocusNavigationRequest &request) noexcept { if (request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::Restore) { if (m_focusedComponent) @@ -116,7 +132,8 @@ bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::Focus view, request.Reason() == winrt::Microsoft::ReactNative::FocusNavigationReason::First ? winrt::Microsoft::ReactNative::FocusNavigationDirection::First - : winrt::Microsoft::ReactNative::FocusNavigationDirection::Last); + : winrt::Microsoft::ReactNative::FocusNavigationDirection::Last, + winrt::Microsoft::ReactNative::FocusState::Programmatic); } return view != nullptr; } @@ -124,6 +141,7 @@ bool RootComponentView::NavigateFocus(const winrt::Microsoft::ReactNative::Focus bool RootComponentView::TrySetFocusedComponent( const winrt::Microsoft::ReactNative::ComponentView &view, winrt::Microsoft::ReactNative::FocusNavigationDirection direction, + winrt::Microsoft::ReactNative::FocusState focusState, bool forceNoSelectionIfCannotMove /*= false*/) noexcept { auto target = view; auto selfView = winrt::get_self(target); @@ -157,15 +175,15 @@ bool RootComponentView::TrySetFocusedComponent( winrt::get_self(losingFocusArgs.NewFocusedComponent()) ->rootComponentView() - ->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction); + ->SetFocusedComponent(gettingFocusArgs.NewFocusedComponent(), direction, focusState); } else { - SetFocusedComponent(nullptr, direction); + SetFocusedComponent(nullptr, direction, focusState); } return true; } -bool RootComponentView::TryMoveFocus(bool next) noexcept { +bool RootComponentView::TryMoveFocus(bool next, winrt::Microsoft::ReactNative::FocusState focusState) noexcept { if (!m_focusedComponent) { return NavigateFocus(winrt::Microsoft::ReactNative::FocusNavigationRequest( next ? winrt::Microsoft::ReactNative::FocusNavigationReason::First @@ -173,7 +191,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept { } Mso::Functor fn = - [currentlyFocused = m_focusedComponent, next](const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { + [currentlyFocused = m_focusedComponent, next, focusState]( + const winrt::Microsoft::ReactNative::ComponentView &view) noexcept { if (view == currentlyFocused) return false; auto selfView = winrt::get_self(view); @@ -185,7 +204,8 @@ bool RootComponentView::TryMoveFocus(bool next) noexcept { ->TrySetFocusedComponent( view, next ? winrt::Microsoft::ReactNative::FocusNavigationDirection::Next - : winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous); + : winrt::Microsoft::ReactNative::FocusNavigationDirection::Previous, + focusState); }; if (winrt::Microsoft::ReactNative::implementation::walkTree(m_focusedComponent, next, fn)) { @@ -249,7 +269,10 @@ void RootComponentView::start(const winrt::Microsoft::ReactNative::ReactNativeIs } void RootComponentView::stop() noexcept { - SetFocusedComponent(nullptr, winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + SetFocusedComponent( + nullptr, + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic); if (m_visualAddedToIsland) { if (auto rootView = m_wkRootView.get()) { winrt::get_self(rootView)->RemoveRenderedVisual( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h index 58b6f1ae2d4..977eb8394b1 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/RootComponentView.h @@ -30,15 +30,17 @@ struct RootComponentView : RootComponentViewT m_wkRootView{nullptr}; winrt::weak_ref m_wkPortal{nullptr}; bool m_visualAddedToIsland{false}; + bool m_useKeyboardForProgrammaticFocus{true}; ::Microsoft::ReactNative::ReactTaggedView m_viewWithTextSelection{ winrt::Microsoft::ReactNative::ComponentView{nullptr}}; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp index 6adffd46183..78412a4cb2d 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/SwitchComponentView.cpp @@ -271,7 +271,10 @@ void SwitchComponentView::OnPointerPressed( m_supressAnimationForNextFrame = true; if (auto root = rootComponentView()) { - root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + root->TrySetFocusedComponent( + *get_strong(), + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Pointer); } updateVisuals(); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp index 843c42e8d2f..59cbe03aa61 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp @@ -233,7 +233,9 @@ struct CompTextHost : public winrt::implements { winrt::check_hresult( m_outer->QueryInterface(winrt::guid_of(), winrt::put_abi(view))); m_outer->rootComponentView()->TrySetFocusedComponent( - view, winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + view, + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic); // assert(false); // TODO focus } @@ -1469,7 +1471,10 @@ void WindowsTextInputComponentView::onMounted() noexcept { // Handle autoFocus property - focus the component when mounted if autoFocus is true if (windowsTextInputProps().autoFocus) { if (auto root = rootComponentView()) { - root->TrySetFocusedComponent(*get_strong(), winrt::Microsoft::ReactNative::FocusNavigationDirection::None); + root->TrySetFocusedComponent( + *get_strong(), + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic); } } } diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp index 6cbb13c4443..cd2084a18ed 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/UiaHelpers.cpp @@ -141,7 +141,10 @@ HRESULT UiaSetFocusHelper(::Microsoft::ReactNative::ReactTaggedView &view) noexc if (rootCV == nullptr) return UIA_E_ELEMENTNOTAVAILABLE; - return rootCV->TrySetFocusedComponent(strongView, winrt::Microsoft::ReactNative::FocusNavigationDirection::None) + return rootCV->TrySetFocusedComponent( + strongView, + winrt::Microsoft::ReactNative::FocusNavigationDirection::None, + winrt::Microsoft::ReactNative::FocusState::Programmatic) ? S_OK : E_FAIL; } diff --git a/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp b/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp index de2c2e6c759..ea96a7c4496 100644 --- a/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp +++ b/vnext/Microsoft.ReactNative/ReactHost/ReactHost.cpp @@ -266,6 +266,10 @@ class ReactNativeWindowsFeatureFlags : public facebook::react::ReactNativeFeatur return true; } + bool enableImperativeFocus() override { + return true; + } + bool fuseboxEnabledRelease() override { return true; // Enable Fusebox (modern CDP backend) by default for React Native Windows }