diff --git a/.changeset/macos-fabric-dev-menu.md b/.changeset/macos-fabric-dev-menu.md new file mode 100644 index 0000000000..1d3c799d8f --- /dev/null +++ b/.changeset/macos-fabric-dev-menu.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/react-native-host": patch +--- + +Wire up the dev menu on macOS Fabric root views so secondary-click shows it. On react-native-macos 0.81+, this sets the new `RCTSurfaceHostingView.devMenu` property so upstream's `menuForEvent:` fires. On older versions, it installs a secondary-click gesture recognizer that pops up the dev menu directly. Without this, consumers of `host.viewWithModuleName:` lose the dev menu on the new architecture because they bypass `RCTRootViewFactory`, which is where upstream does the wiring. diff --git a/packages/react-native-host/cocoa/ReactNativeHost+View.mm b/packages/react-native-host/cocoa/ReactNativeHost+View.mm index 617c549162..826a306707 100644 --- a/packages/react-native-host/cocoa/ReactNativeHost+View.mm +++ b/packages/react-native-host/cocoa/ReactNativeHost+View.mm @@ -15,6 +15,11 @@ #import #endif // USE_FABRIC +#if defined(RCT_DEV_MENU) && RCT_DEV_MENU && __has_include() +#import +#define RNX_WIRE_DEV_MENU 1 +#endif + @implementation ReactNativeHost (View) + (instancetype)hostFromRootView:(RNXView *)rootView @@ -52,15 +57,16 @@ - (RNXView *)viewWithModuleName:(NSString *)moduleName initialProps[kReactConcurrentRoot] = @YES; } + RNXView *rootView; #if __has_include() - return [[RCTFabricSurfaceHostingProxyRootView alloc] initWithBridge:self.bridge - moduleName:moduleName - initialProperties:initialProps]; + rootView = [[RCTFabricSurfaceHostingProxyRootView alloc] initWithBridge:self.bridge + moduleName:moduleName + initialProperties:initialProps]; #elif USE_BRIDGELESS RCTFabricSurface *surface = [self.reactHost createSurfaceWithModuleName:moduleName initialProperties:initialProps]; #if __has_include() // >=0.77 - return [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface]; + rootView = [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface]; #else // `-initWithSurface:` implicitly calls `start` and causes race conditions. // This was fixed in 0.76.7, but for backwards compatibility, we should call @@ -68,16 +74,47 @@ - (RNXView *)viewWithModuleName:(NSString *)moduleName // https://github.com/facebook/react-native/pull/47313. RCTSurfaceSizeMeasureMode sizeMeasureMode = RCTSurfaceSizeMeasureModeWidthExact | RCTSurfaceSizeMeasureModeHeightExact; - return [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface - sizeMeasureMode:sizeMeasureMode]; + rootView = [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface + sizeMeasureMode:sizeMeasureMode]; #endif // __has_include() #else // __has_include() RCTFabricSurface *surface = [[RCTFabricSurface alloc] initWithSurfacePresenter:self.surfacePresenter moduleName:moduleName initialProperties:initialProps]; - return [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface]; + rootView = [[RCTSurfaceHostingProxyRootView alloc] initWithSurface:surface]; #endif // __has_include() + +#if defined(RNX_WIRE_DEV_MENU) && TARGET_OS_OSX + // react-native-macos 0.81+ exposes a `devMenu` property on + // `RCTSurfaceHostingView` whose `menuForEvent:` returns the dev menu on + // secondary click. Wire it up here so it actually fires; upstream's + // `RCTRootViewFactory` does the same, but consumers of this host bypass it. + if ([rootView respondsToSelector:@selector(setDevMenu:)]) { + [self usingModule:[RCTDevMenu class] + block:^(id _Nullable module) { + if (module == nil) { + return; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [rootView performSelector:@selector(setDevMenu:) withObject:module]; +#pragma clang diagnostic pop + }]; + } else { + // Older react-native-macos versions don't override `menuForEvent:` on + // the Fabric root view, so secondary-click does nothing. Install a + // gesture recognizer that pops up the dev menu directly. + NSClickGestureRecognizer *recognizer = [[NSClickGestureRecognizer alloc] + initWithTarget:self + action:@selector(rnx_showFallbackDevMenu:)]; + recognizer.buttonMask = 1 << 1; // Secondary (right) button + recognizer.numberOfClicksRequired = 1; + [rootView addGestureRecognizer:recognizer]; + } +#endif // RNX_WIRE_DEV_MENU && TARGET_OS_OSX + + return rootView; #else return [[RCTRootView alloc] initWithBridge:self.bridge moduleName:moduleName @@ -85,4 +122,31 @@ - (RNXView *)viewWithModuleName:(NSString *)moduleName #endif // USE_FABRIC } +#if defined(RNX_WIRE_DEV_MENU) && TARGET_OS_OSX + +- (void)rnx_showFallbackDevMenu:(NSGestureRecognizer *)recognizer +{ + NSView *view = recognizer.view; + if (view == nil) { + return; + } + [self usingModule:[RCTDevMenu class] + block:^(id _Nullable module) { + if (module == nil || ![module respondsToSelector:@selector(menu)]) { + return; + } + NSMenu *menu = [(RCTDevMenu *)module menu]; + if (menu == nil) { + return; + } + NSEvent *event = NSApp.currentEvent; + if (event == nil) { + return; + } + [NSMenu popUpContextMenu:menu withEvent:event forView:view]; + }]; +} + +#endif // RNX_WIRE_DEV_MENU && TARGET_OS_OSX + @end