From fbae0848422bac0f3a774f8c0ab87235aeff041d Mon Sep 17 00:00:00 2001 From: Nitin Chaudhary Date: Tue, 3 Feb 2026 17:31:50 +0530 Subject: [PATCH] Fix SHIFT+F10 keyboard shortcut for context menu in TextInput --- ...-98ecf15d-5d4a-4fd9-8197-192eaf831e5f.json | 7 ++ vnext/Microsoft.ReactNative/ComponentView.idl | 2 + .../Composition.Input.idl | 7 ++ .../Fabric/ComponentView.cpp | 18 +++++ .../Fabric/ComponentView.h | 9 +++ .../Fabric/Composition/Composition.Input.cpp | 12 +++ .../Fabric/Composition/Composition.Input.h | 15 ++++ .../Composition/CompositionEventHandler.cpp | 75 +++++++++++++++++++ .../Composition/CompositionEventHandler.h | 1 + .../WindowsTextInputComponentView.cpp | 58 +++++++++----- .../TextInput/WindowsTextInputComponentView.h | 3 + 11 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 change/react-native-windows-98ecf15d-5d4a-4fd9-8197-192eaf831e5f.json diff --git a/change/react-native-windows-98ecf15d-5d4a-4fd9-8197-192eaf831e5f.json b/change/react-native-windows-98ecf15d-5d4a-4fd9-8197-192eaf831e5f.json new file mode 100644 index 00000000000..bb9f9495f87 --- /dev/null +++ b/change/react-native-windows-98ecf15d-5d4a-4fd9-8197-192eaf831e5f.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Fix SHIFT+F10 keyboard shortcut for context menu in TextInput", + "packageName": "react-native-windows", + "email": "nitchaudhary@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/vnext/Microsoft.ReactNative/ComponentView.idl b/vnext/Microsoft.ReactNative/ComponentView.idl index 2481b3344db..cd724297724 100644 --- a/vnext/Microsoft.ReactNative/ComponentView.idl +++ b/vnext/Microsoft.ReactNative/ComponentView.idl @@ -106,6 +106,8 @@ namespace Microsoft.ReactNative DOC_STRING("Used to handle key up events when this component is focused, or if a child component did not handle the key up") event Windows.Foundation.EventHandler KeyUp; event Windows.Foundation.EventHandler CharacterReceived; + DOC_STRING("Used to handle context menu key events (SHIFT+F10 or Context Menu key) when this component is focused") + event Windows.Foundation.EventHandler ContextMenuKey; event Windows.Foundation.EventHandler PointerPressed; event Windows.Foundation.EventHandler PointerReleased; event Windows.Foundation.EventHandler PointerMoved; diff --git a/vnext/Microsoft.ReactNative/Composition.Input.idl b/vnext/Microsoft.ReactNative/Composition.Input.idl index 68d9e4f5da6..f5348e0566d 100644 --- a/vnext/Microsoft.ReactNative/Composition.Input.idl +++ b/vnext/Microsoft.ReactNative/Composition.Input.idl @@ -34,6 +34,13 @@ namespace Microsoft.ReactNative.Composition.Input KeyboardSource KeyboardSource { get; }; }; + DOC_STRING("Event arguments for context menu key events (SHIFT+F10 or Context Menu key)") + interface ContextMenuKeyEventArgs requires RoutedEventArgs + { + DOC_STRING("Gets or sets whether the event was handled. Set to true to prevent default behavior.") + Boolean Handled { get; set; }; + }; + interface IPointerPointTransform { IPointerPointTransform Inverse { get; }; diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp index 6c2aee7ba13..64d8a427941 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp @@ -469,6 +469,16 @@ void ComponentView::CharacterReceived(winrt::event_token const &token) noexcept m_characterReceivedEvent.remove(token); } +winrt::event_token ComponentView::ContextMenuKey( + winrt::Windows::Foundation::EventHandler< + winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs> const &handler) noexcept { + return m_contextMenuKeyEvent.add(handler); +} + +void ComponentView::ContextMenuKey(winrt::event_token const &token) noexcept { + m_contextMenuKeyEvent.remove(token); +} + winrt::event_token ComponentView::PointerPressed( winrt::Windows::Foundation::EventHandler< winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs> const &handler) noexcept { @@ -609,6 +619,14 @@ void ComponentView::OnCharacterReceived( } } +void ComponentView::OnContextMenuKey( + const winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs &args) noexcept { + m_contextMenuKeyEvent(*this, args); + if (m_parent && !args.Handled()) { + winrt::get_self(m_parent)->OnContextMenuKey(args); + } +} + bool ComponentView::focusable() const noexcept { return false; } diff --git a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h index 7700ac9e3fa..9f73308d436 100644 --- a/vnext/Microsoft.ReactNative/Fabric/ComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/ComponentView.h @@ -167,6 +167,10 @@ struct ComponentView winrt::Windows::Foundation::EventHandler< winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs> const &handler) noexcept; void CharacterReceived(winrt::event_token const &token) noexcept; + winrt::event_token ContextMenuKey( + winrt::Windows::Foundation::EventHandler< + winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs> const &handler) noexcept; + void ContextMenuKey(winrt::event_token const &token) noexcept; winrt::event_token PointerPressed( winrt::Windows::Foundation::EventHandler< winrt::Microsoft::ReactNative::Composition::Input::PointerRoutedEventArgs> const &handler) noexcept; @@ -253,6 +257,8 @@ struct ComponentView virtual void OnKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept; virtual void OnCharacterReceived( const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs &args) noexcept; + virtual void OnContextMenuKey( + const winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs &args) noexcept; protected: winrt::com_ptr m_builder; @@ -277,6 +283,9 @@ struct ComponentView winrt::event> m_characterReceivedEvent; + winrt::event> + m_contextMenuKeyEvent; winrt::event> m_pointerPressedEvent; diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.cpp index 47ecc141b25..6e925c40584 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.cpp @@ -136,6 +136,18 @@ winrt::Microsoft::ReactNative::Composition::Input::KeyboardSource CharacterRecei return m_source; } +ContextMenuKeyEventArgs::ContextMenuKeyEventArgs(facebook::react::Tag tag) : m_tag(tag) {} + +int32_t ContextMenuKeyEventArgs::OriginalSource() noexcept { + return m_tag; +} +bool ContextMenuKeyEventArgs::Handled() noexcept { + return m_handled; +} +void ContextMenuKeyEventArgs::Handled(bool value) noexcept { + m_handled = value; +} + Pointer::Pointer(winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType type, uint32_t id) : m_type(type), m_id(id) {} diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.h b/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.h index 3a8bb6f042d..5d87eb928fd 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/Composition.Input.h @@ -78,6 +78,21 @@ struct CharacterReceivedRoutedEventArgs const winrt::Microsoft::ReactNative::Composition::Input::KeyboardSource m_source; }; +struct ContextMenuKeyEventArgs : winrt::implements< + ContextMenuKeyEventArgs, + winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs, + winrt::Microsoft::ReactNative::Composition::Input::RoutedEventArgs> { + ContextMenuKeyEventArgs(facebook::react::Tag tag); + + int32_t OriginalSource() noexcept; + bool Handled() noexcept; + void Handled(bool value) noexcept; + + private: + facebook::react::Tag m_tag{-1}; + bool m_handled{false}; +}; + struct Pointer : PointerT { Pointer(winrt::Microsoft::ReactNative::Composition::Input::PointerDeviceType type, uint32_t id); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp index 556409a0fee..0ff00758bb7 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.cpp @@ -320,6 +320,32 @@ void CompositionEventHandler::Initialize() noexcept { } } }); + + m_contextMenuKeyToken = + keyboardSource.ContextMenuKey([wkThis = weak_from_this()]( + winrt::Microsoft::UI::Input::InputKeyboardSource const & /*source*/, + winrt::Microsoft::UI::Input::ContextMenuKeyEventArgs const &args) { + if (auto strongThis = wkThis.lock()) { + if (auto strongRootView = strongThis->m_wkRootView.get()) { + if (strongThis->SurfaceId() == -1) + return; + + auto focusedComponent = strongThis->RootComponentView().GetFocusedComponent(); + if (focusedComponent) { + auto tag = + winrt::get_self(focusedComponent) + ->Tag(); + auto contextMenuArgs = winrt::make< + winrt::Microsoft::ReactNative::Composition::Input::implementation::ContextMenuKeyEventArgs>(tag); + winrt::get_self(focusedComponent) + ->OnContextMenuKey(contextMenuArgs); + if (contextMenuArgs.Handled()) { + args.Handled(true); + } + } + } + } + }); } } @@ -336,6 +362,7 @@ CompositionEventHandler::~CompositionEventHandler() { keyboardSource.KeyDown(m_keyDownToken); keyboardSource.KeyUp(m_keyUpToken); keyboardSource.CharacterReceived(m_characterReceivedToken); + keyboardSource.ContextMenuKey(m_contextMenuKeyToken); } } @@ -443,6 +470,54 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w } return 0; } + case WM_RBUTTONDOWN: { + if (auto strongRootView = m_wkRootView.get()) { + auto pp = winrt::make( + hwnd, msg, wParam, lParam, strongRootView.ScaleFactor()); + onPointerPressed(pp, GetKeyModifiers(wParam)); + } + return 0; + } + case WM_RBUTTONUP: { + if (auto strongRootView = m_wkRootView.get()) { + auto pp = winrt::make( + hwnd, msg, wParam, lParam, strongRootView.ScaleFactor()); + onPointerReleased(pp, GetKeyModifiers(wParam)); + } + return 0; + } + case WM_MBUTTONDOWN: { + if (auto strongRootView = m_wkRootView.get()) { + auto pp = winrt::make( + hwnd, msg, wParam, lParam, strongRootView.ScaleFactor()); + onPointerPressed(pp, GetKeyModifiers(wParam)); + } + return 0; + } + case WM_MBUTTONUP: { + if (auto strongRootView = m_wkRootView.get()) { + auto pp = winrt::make( + hwnd, msg, wParam, lParam, strongRootView.ScaleFactor()); + onPointerReleased(pp, GetKeyModifiers(wParam)); + } + return 0; + } + case WM_XBUTTONDOWN: { + if (auto strongRootView = m_wkRootView.get()) { + auto pp = winrt::make( + hwnd, msg, wParam, lParam, strongRootView.ScaleFactor()); + onPointerPressed(pp, GetKeyModifiers(wParam)); + } + return 0; + } + case WM_XBUTTONUP: { + if (auto strongRootView = m_wkRootView.get()) { + auto pp = winrt::make( + hwnd, msg, wParam, lParam, strongRootView.ScaleFactor()); + onPointerReleased(pp, GetKeyModifiers(wParam)); + } + return 0; + } case WM_POINTERUP: { if (auto strongRootView = m_wkRootView.get()) { auto pp = winrt::make( diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h index 810b2872fb9..82e80b3dde8 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/CompositionEventHandler.h @@ -175,6 +175,7 @@ class CompositionEventHandler : public std::enable_shared_from_this { auto pt = m_outer->getClientOffset(); m_outer->m_caretVisual.Position({x - pt.x, y - pt.y}); + m_outer->m_caretPosition = {x, y}; return true; } @@ -696,17 +697,10 @@ void WindowsTextInputComponentView::OnPointerPressed( } if (m_textServices && msg) { - if (msg == WM_RBUTTONUP && !windowsTextInputProps().contextMenuHidden) { - ShowContextMenu(position); - args.Handled(true); - } else if (msg == WM_RBUTTONUP && windowsTextInputProps().contextMenuHidden) { - args.Handled(true); - } else { - LRESULT lresult; - DrawBlock db(*this); - auto hr = m_textServices->TxSendMessage(msg, static_cast(wParam), static_cast(lParam), &lresult); - args.Handled(hr != S_FALSE); - } + LRESULT lresult; + DrawBlock db(*this); + auto hr = m_textServices->TxSendMessage(msg, static_cast(wParam), static_cast(lParam), &lresult); + args.Handled(hr != S_FALSE); } // Emits the OnPressIn event @@ -768,10 +762,18 @@ void WindowsTextInputComponentView::OnPointerReleased( } if (m_textServices && msg) { - LRESULT lresult; - DrawBlock db(*this); - auto hr = m_textServices->TxSendMessage(msg, static_cast(wParam), static_cast(lParam), &lresult); - args.Handled(hr != S_FALSE); + // Show context menu on right button release (standard Windows behavior) + if (msg == WM_RBUTTONUP && !windowsTextInputProps().contextMenuHidden) { + ShowContextMenu(LocalToScreen(position)); + args.Handled(true); + } else if (msg == WM_RBUTTONUP) { + // Context menu is hidden - don't mark as handled, let app add custom behavior + } else { + LRESULT lresult; + DrawBlock db(*this); + auto hr = m_textServices->TxSendMessage(msg, static_cast(wParam), static_cast(lParam), &lresult); + args.Handled(hr != S_FALSE); + } } // Emits the OnPressOut event @@ -1879,6 +1881,21 @@ void WindowsTextInputComponentView::updateSpellCheck(bool enable) noexcept { m_textServices->TxSendMessage(EM_SETLANGOPTIONS, IMF_SPELLCHECKING, enable ? newLangOptions : 0, &lresult)); } +void WindowsTextInputComponentView::OnContextMenuKey( + const winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs &args) noexcept { + // Handle context menu key event (SHIFT+F10 or Context Menu key) + if (!windowsTextInputProps().contextMenuHidden) { + // m_caretPosition is stored from TxSetCaretPos in RichEdit client rect space (physical pixels). + // LocalToScreen expects logical (DIP) coordinates, so divide by pointScaleFactor. + auto screenPt = LocalToScreen(winrt::Windows::Foundation::Point{ + static_cast(m_caretPosition.x) / m_layoutMetrics.pointScaleFactor, + static_cast(m_caretPosition.y) / m_layoutMetrics.pointScaleFactor}); + ShowContextMenu(screenPt); + args.Handled(true); + } + // If contextMenuHidden, don't mark as handled - let app handle it +} + void WindowsTextInputComponentView::ShowContextMenu(const winrt::Windows::Foundation::Point &position) noexcept { HMENU menu = CreatePopupMenu(); if (!menu) @@ -1898,13 +1915,16 @@ void WindowsTextInputComponentView::ShowContextMenu(const winrt::Windows::Founda AppendMenuW(menu, MF_STRING | (canPaste ? 0 : MF_GRAYED), 3, L"Paste"); AppendMenuW(menu, MF_STRING | (!isEmpty && !isReadOnly ? 0 : MF_GRAYED), 4, L"Select All"); - POINT cursorPos; - GetCursorPos(&cursorPos); - HWND hwnd = GetActiveWindow(); int cmd = TrackPopupMenu( - menu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, cursorPos.x, cursorPos.y, 0, hwnd, NULL); + menu, + TPM_LEFTALIGN | TPM_TOPALIGN | TPM_RETURNCMD | TPM_NONOTIFY, + static_cast(position.X), + static_cast(position.Y), + 0, + hwnd, + NULL); if (cmd == 1) { // Cut m_textServices->TxSendMessage(WM_CUT, 0, 0, &res); diff --git a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h index 26dc207961c..634b5884ab9 100644 --- a/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h +++ b/vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h @@ -67,6 +67,8 @@ struct WindowsTextInputComponentView void OnKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept override; void OnCharacterReceived(const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs &args) noexcept override; + void OnContextMenuKey( + const winrt::Microsoft::ReactNative::Composition::Input::ContextMenuKeyEventArgs &args) noexcept override; void onMounted() noexcept override; std::optional getAccessiblityValue() noexcept override; @@ -146,6 +148,7 @@ struct WindowsTextInputComponentView DWORD m_propBitsMask{0}; DWORD m_propBits{0}; HCURSOR m_hcursor{nullptr}; + POINT m_caretPosition{0, 0}; std::chrono::steady_clock::time_point m_lastClickTime{}; std::vector m_submitKeyEvents; };