Skip to content

Commit 28194ac

Browse files
author
Nitin Chaudhary
committed
Fix SHIFT+F10 keyboard shortcut for context menu in TextInput
1 parent 90a0dc5 commit 28194ac

File tree

9 files changed

+276
-111
lines changed

9 files changed

+276
-111
lines changed

packages/playground/windows/playground-composition/Playground-Composition.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,22 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
539539
winrt::Microsoft::ReactNative::ForwardWindowMessage(rns, hwnd, message, wparam, lparam);
540540
break;
541541
}
542+
case WM_CONTEXTMENU: {
543+
// Forward context menu requests to ReactNativeIsland when using ContentIsland hosting
544+
// This handles SHIFT+F10 and Context Menu key which generate WM_CONTEXTMENU
545+
if (windowData && windowData->m_compRootView) {
546+
// lparam == -1 indicates keyboard-triggered context menu (SHIFT+F10 or VK_APPS)
547+
bool isKeyboardTriggered = (lparam == -1);
548+
winrt::Windows::Foundation::Point screenPosition{0, 0};
549+
if (!isKeyboardTriggered) {
550+
screenPosition = {static_cast<float>(GET_X_LPARAM(lparam)), static_cast<float>(GET_Y_LPARAM(lparam))};
551+
}
552+
if (windowData->m_compRootView.NotifyContextMenu(screenPosition, isKeyboardTriggered)) {
553+
return 0; // Handled
554+
}
555+
}
556+
break;
557+
}
542558
}
543559

544560
return DefWindowProc(hwnd, message, wparam, lparam);

vnext/Microsoft.ReactNative/Fabric/ComponentView.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,13 @@ void ComponentView::OnCharacterReceived(
609609
}
610610
}
611611

612+
void ComponentView::OnContextMenu(
613+
const winrt::Windows::Foundation::Point & /*position*/,
614+
bool /*isKeyboardTriggered*/) noexcept {
615+
// Default implementation does nothing - derived classes can override to handle context menu
616+
// This is called when WM_CONTEXTMENU is received (SHIFT+F10 or Context Menu key)
617+
}
618+
612619
bool ComponentView::focusable() const noexcept {
613620
return false;
614621
}

vnext/Microsoft.ReactNative/Fabric/ComponentView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ struct ComponentView
253253
virtual void OnKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept;
254254
virtual void OnCharacterReceived(
255255
const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs &args) noexcept;
256+
virtual void OnContextMenu(const winrt::Windows::Foundation::Point &position, bool isKeyboardTriggered) noexcept;
256257

257258
protected:
258259
winrt::com_ptr<winrt::Microsoft::ReactNative::Composition::ReactCompositionViewComponentBuilder> m_builder;

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,54 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w
443443
}
444444
return 0;
445445
}
446+
case WM_RBUTTONDOWN: {
447+
if (auto strongRootView = m_wkRootView.get()) {
448+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
449+
hwnd, msg, wParam, lParam, strongRootView.ScaleFactor());
450+
onPointerPressed(pp, GetKeyModifiers(wParam));
451+
}
452+
return 0;
453+
}
454+
case WM_RBUTTONUP: {
455+
if (auto strongRootView = m_wkRootView.get()) {
456+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
457+
hwnd, msg, wParam, lParam, strongRootView.ScaleFactor());
458+
onPointerReleased(pp, GetKeyModifiers(wParam));
459+
}
460+
return 0;
461+
}
462+
case WM_MBUTTONDOWN: {
463+
if (auto strongRootView = m_wkRootView.get()) {
464+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
465+
hwnd, msg, wParam, lParam, strongRootView.ScaleFactor());
466+
onPointerPressed(pp, GetKeyModifiers(wParam));
467+
}
468+
return 0;
469+
}
470+
case WM_MBUTTONUP: {
471+
if (auto strongRootView = m_wkRootView.get()) {
472+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
473+
hwnd, msg, wParam, lParam, strongRootView.ScaleFactor());
474+
onPointerReleased(pp, GetKeyModifiers(wParam));
475+
}
476+
return 0;
477+
}
478+
case WM_XBUTTONDOWN: {
479+
if (auto strongRootView = m_wkRootView.get()) {
480+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
481+
hwnd, msg, wParam, lParam, strongRootView.ScaleFactor());
482+
onPointerPressed(pp, GetKeyModifiers(wParam));
483+
}
484+
return 0;
485+
}
486+
case WM_XBUTTONUP: {
487+
if (auto strongRootView = m_wkRootView.get()) {
488+
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
489+
hwnd, msg, wParam, lParam, strongRootView.ScaleFactor());
490+
onPointerReleased(pp, GetKeyModifiers(wParam));
491+
}
492+
return 0;
493+
}
446494
case WM_POINTERUP: {
447495
if (auto strongRootView = m_wkRootView.get()) {
448496
auto pp = winrt::make<winrt::Microsoft::ReactNative::Composition::Input::implementation::PointerPoint>(
@@ -526,6 +574,29 @@ int64_t CompositionEventHandler::SendMessage(HWND hwnd, uint32_t msg, uint64_t w
526574
UpdateCursor();
527575
return 1;
528576
}
577+
case WM_CONTEXTMENU: {
578+
// WM_CONTEXTMENU is generated by Windows when SHIFT+F10 or the Context Menu key is pressed
579+
// lParam contains the screen coordinates, or -1 if triggered via keyboard
580+
if (auto strongRootView = m_wkRootView.get()) {
581+
auto focusedComponent = RootComponentView().GetFocusedComponent();
582+
if (focusedComponent) {
583+
// lParam == -1 indicates keyboard-triggered context menu (SHIFT+F10 or VK_APPS)
584+
bool isKeyboardTriggered = (lParam == -1);
585+
winrt::Windows::Foundation::Point position{0, 0};
586+
if (!isKeyboardTriggered) {
587+
// Convert screen coordinates to client coordinates
588+
POINT pt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
589+
ScreenToClient(hwnd, &pt);
590+
position = {static_cast<float>(pt.x), static_cast<float>(pt.y)};
591+
}
592+
// Dispatch to the focused component
593+
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)
594+
->OnContextMenu(position, isKeyboardTriggered);
595+
return 1; // Indicate that we handled the message
596+
}
597+
}
598+
return 0;
599+
}
529600
}
530601

531602
return 0;

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,26 @@ int64_t ReactNativeIsland::SendMessage(uint32_t msg, uint64_t wParam, int64_t lP
448448
return 0;
449449
}
450450

451+
bool ReactNativeIsland::NotifyContextMenu(
452+
winrt::Windows::Foundation::Point screenPosition,
453+
bool isKeyboardTriggered) noexcept {
454+
if (m_rootTag == -1 || !m_CompositionEventHandler)
455+
return false;
456+
457+
// Convert screen coordinates to local coordinates
458+
auto localPosition = ConvertScreenToLocal(screenPosition);
459+
460+
// Get the focused component and dispatch the context menu event
461+
auto focusedComponent = GetComponentView()->GetFocusedComponent();
462+
if (focusedComponent) {
463+
winrt::get_self<winrt::Microsoft::ReactNative::implementation::ComponentView>(focusedComponent)
464+
->OnContextMenu(localPosition, isKeyboardTriggered);
465+
return true;
466+
}
467+
468+
return false;
469+
}
470+
451471
bool ReactNativeIsland::CapturePointer(
452472
const winrt::Microsoft::ReactNative::Composition::Input::Pointer &pointer,
453473
facebook::react::Tag tag) noexcept {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ struct ReactNativeIsland
124124
void SetWindow(uint64_t hwnd) noexcept;
125125
int64_t SendMessage(uint32_t msg, uint64_t wParam, int64_t lParam) noexcept;
126126

127+
// Notify the ReactNativeIsland of a context menu request (e.g., from WM_CONTEXTMENU)
128+
// Used when hosting with ContentIsland - host app should call this from its WM_CONTEXTMENU handler
129+
bool NotifyContextMenu(winrt::Windows::Foundation::Point screenPosition, bool isKeyboardTriggered) noexcept;
130+
127131
winrt::Windows::Foundation::Point ConvertScreenToLocal(winrt::Windows::Foundation::Point pt) noexcept;
128132
winrt::Windows::Foundation::Point ConvertLocalToScreen(winrt::Windows::Foundation::Point pt) noexcept;
129133

vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.cpp

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -696,17 +696,10 @@ void WindowsTextInputComponentView::OnPointerPressed(
696696
}
697697

698698
if (m_textServices && msg) {
699-
if (msg == WM_RBUTTONUP && !windowsTextInputProps().contextMenuHidden) {
700-
ShowContextMenu(position);
701-
args.Handled(true);
702-
} else if (msg == WM_RBUTTONUP && windowsTextInputProps().contextMenuHidden) {
703-
args.Handled(true);
704-
} else {
705-
LRESULT lresult;
706-
DrawBlock db(*this);
707-
auto hr = m_textServices->TxSendMessage(msg, static_cast<WPARAM>(wParam), static_cast<LPARAM>(lParam), &lresult);
708-
args.Handled(hr != S_FALSE);
709-
}
699+
LRESULT lresult;
700+
DrawBlock db(*this);
701+
auto hr = m_textServices->TxSendMessage(msg, static_cast<WPARAM>(wParam), static_cast<LPARAM>(lParam), &lresult);
702+
args.Handled(hr != S_FALSE);
710703
}
711704

712705
// Emits the OnPressIn event
@@ -768,10 +761,19 @@ void WindowsTextInputComponentView::OnPointerReleased(
768761
}
769762

770763
if (m_textServices && msg) {
771-
LRESULT lresult;
772-
DrawBlock db(*this);
773-
auto hr = m_textServices->TxSendMessage(msg, static_cast<WPARAM>(wParam), static_cast<LPARAM>(lParam), &lresult);
774-
args.Handled(hr != S_FALSE);
764+
// Show context menu on right button release (standard Windows behavior)
765+
if (msg == WM_RBUTTONUP && !windowsTextInputProps().contextMenuHidden) {
766+
ShowContextMenu(position);
767+
args.Handled(true);
768+
} else if (msg == WM_RBUTTONUP && windowsTextInputProps().contextMenuHidden) {
769+
// Context menu is hidden, just mark as handled
770+
args.Handled(true);
771+
} else {
772+
LRESULT lresult;
773+
DrawBlock db(*this);
774+
auto hr = m_textServices->TxSendMessage(msg, static_cast<WPARAM>(wParam), static_cast<LPARAM>(lParam), &lresult);
775+
args.Handled(hr != S_FALSE);
776+
}
775777
}
776778

777779
// Emits the OnPressOut event
@@ -850,6 +852,24 @@ void WindowsTextInputComponentView::OnPointerWheelChanged(
850852
}
851853
void WindowsTextInputComponentView::OnKeyDown(
852854
const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept {
855+
// Handle context menu keyboard shortcuts
856+
// VK_APPS (Application/Context Menu key) is handled here
857+
// SHIFT+F10 is handled via WM_CONTEXTMENU for HWND hosting (CompositionHwndHost)
858+
// For Island hosting (ReactNativeAppBuilder), SHIFT+F10 requires InputPreTranslateKeyboardSource - TODO
859+
bool isShiftDown =
860+
(args.KeyboardSource().GetKeyState(winrt::Windows::System::VirtualKey::Shift) &
861+
winrt::Microsoft::UI::Input::VirtualKeyStates::Down) == winrt::Microsoft::UI::Input::VirtualKeyStates::Down;
862+
bool isShiftF10 = args.Key() == winrt::Windows::System::VirtualKey::F10 && isShiftDown;
863+
bool isContextMenuKey = args.Key() == winrt::Windows::System::VirtualKey::Application;
864+
865+
if ((isShiftF10 || isContextMenuKey) && !windowsTextInputProps().contextMenuHidden) {
866+
// Use caret position for context menu when triggered via keyboard
867+
winrt::Windows::Foundation::Point caretPosition{0, 0};
868+
ShowContextMenu(caretPosition);
869+
args.Handled(true);
870+
return;
871+
}
872+
853873
// Do not forward tab keys into the TextInput, since we want that to do the tab loop instead. This aligns with
854874
// WinUI behavior We do forward Ctrl+Tab to the textinput.
855875
if (args.Key() != winrt::Windows::System::VirtualKey::Tab ||
@@ -1879,6 +1899,15 @@ void WindowsTextInputComponentView::updateSpellCheck(bool enable) noexcept {
18791899
m_textServices->TxSendMessage(EM_SETLANGOPTIONS, IMF_SPELLCHECKING, enable ? newLangOptions : 0, &lresult));
18801900
}
18811901

1902+
void WindowsTextInputComponentView::OnContextMenu(
1903+
const winrt::Windows::Foundation::Point &position,
1904+
bool isKeyboardTriggered) noexcept {
1905+
// Handle WM_CONTEXTMENU message (generated by Windows for SHIFT+F10 or Context Menu key)
1906+
if (!windowsTextInputProps().contextMenuHidden) {
1907+
ShowContextMenu(position);
1908+
}
1909+
}
1910+
18821911
void WindowsTextInputComponentView::ShowContextMenu(const winrt::Windows::Foundation::Point &position) noexcept {
18831912
HMENU menu = CreatePopupMenu();
18841913
if (!menu)

vnext/Microsoft.ReactNative/Fabric/Composition/TextInput/WindowsTextInputComponentView.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ struct WindowsTextInputComponentView
6767
void OnKeyUp(const winrt::Microsoft::ReactNative::Composition::Input::KeyRoutedEventArgs &args) noexcept override;
6868
void OnCharacterReceived(const winrt::Microsoft::ReactNative::Composition::Input::CharacterReceivedRoutedEventArgs
6969
&args) noexcept override;
70+
void OnContextMenu(const winrt::Windows::Foundation::Point &position, bool isKeyboardTriggered) noexcept override;
7071
void onMounted() noexcept override;
7172

7273
std::optional<std::string> getAccessiblityValue() noexcept override;

0 commit comments

Comments
 (0)