@@ -100,10 +100,10 @@ - (NSString *)title
100100@end
101101
102102#if !TARGET_OS_OSX // [macOS]
103-
104103typedef void (^RCTDevMenuAlertActionHandler)(UIAlertAction *action);
105-
106- #endif // [macOS]
104+ #else // [macOS
105+ typedef void (^RCTDevMenuAlertActionHandler)(NSModalResponse response);
106+ #endif // macOS]
107107
108108@interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
109109
@@ -112,6 +112,8 @@ @interface RCTDevMenu () <RCTBridgeModule, RCTInvalidating, NativeDevMenuSpec>
112112@implementation RCTDevMenu {
113113#if !TARGET_OS_OSX // [macOS]
114114 UIAlertController *_actionSheet;
115+ #else // [macOS
116+ NSAlert *_alert;
115117#endif // [macOS]
116118 NSMutableArray <RCTDevMenuItem *> *_extraMenuItems;
117119}
@@ -247,12 +249,16 @@ - (void)toggle
247249 [self show ];
248250 }
249251}
252+ #endif // macOS]
250253
251254- (BOOL )isActionSheetShown
252255{
256+ #if !TARGET_OS_OSX // [macOS]
253257 return _actionSheet != nil ;
258+ #else // [macOS
259+ return _alert != nil ;
260+ #endif // macOS]
254261}
255- #endif // [macOS]
256262
257263- (void )addItem : (NSString *)title handler : (void (^)(void ))handler
258264{
@@ -433,43 +439,42 @@ - (void)setDefaultJSBundle
433439 [alert addButtonWithTitle: @" Cancel" ];
434440 [alert setAlertStyle: NSAlertStyleWarning];
435441
436- [alert beginSheetModalForWindow: [NSApp keyWindow ]
437- completionHandler: ^(NSModalResponse response) {
438- if (response == NSAlertFirstButtonReturn ) {
439- // Apply Changes
440- NSString *ipAddress = ipTextField.stringValue ;
441- NSString *port = portTextField.stringValue ;
442- NSString *bundleRoot = entrypointTextField.stringValue ;
443-
444- if (ipAddress.length == 0 && port.length == 0 ) {
445- [weakSelf setDefaultJSBundle ];
446- return ;
447- }
448-
449- NSNumberFormatter *formatter = [NSNumberFormatter new ];
450- formatter.numberStyle = NSNumberFormatterDecimalStyle;
451- NSNumber *portNumber = [formatter numberFromString: port];
452- if (portNumber == nil ) {
453- portNumber = [NSNumber numberWithInt: RCT_METRO_PORT];
454- }
455-
456- [RCTBundleURLProvider sharedSettings ].jsLocation =
457- [NSString stringWithFormat: @" %@ :%d " , ipAddress, portNumber.intValue];
458-
459- if (bundleRoot.length == 0 ) {
460- [bundleManager resetBundleURL ];
461- } else {
462- bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings ]
463- jsBundleURLForBundleRoot: bundleRoot];
464- }
465-
466- RCTTriggerReloadCommandListeners (@" Dev menu - apply changes" );
467- } else if (response == NSAlertSecondButtonReturn ) {
468- // Reset to Default
469- [weakSelf setDefaultJSBundle ];
470- }
471- // Cancel - do nothing
472- }];
442+ NSModalResponse response = [alert runModal ];
443+
444+ if (response == NSAlertFirstButtonReturn ) {
445+ // Apply Changes
446+ NSString *ipAddress = ipTextField.stringValue ;
447+ NSString *port = portTextField.stringValue ;
448+ NSString *bundleRoot = entrypointTextField.stringValue ;
449+
450+ if (ipAddress.length == 0 && port.length == 0 ) {
451+ [weakSelf setDefaultJSBundle ];
452+ return ;
453+ }
454+
455+ NSNumberFormatter *formatter = [NSNumberFormatter new ];
456+ formatter.numberStyle = NSNumberFormatterDecimalStyle;
457+ NSNumber *portNumber = [formatter numberFromString: port];
458+ if (portNumber == nil ) {
459+ portNumber = [NSNumber numberWithInt: RCT_METRO_PORT];
460+ }
461+
462+ [RCTBundleURLProvider sharedSettings ].jsLocation =
463+ [NSString stringWithFormat: @" %@ :%d " , ipAddress, portNumber.intValue];
464+
465+ if (bundleRoot.length == 0 ) {
466+ [bundleManager resetBundleURL ];
467+ } else {
468+ bundleManager.bundleURL = [[RCTBundleURLProvider sharedSettings ]
469+ jsBundleURLForBundleRoot: bundleRoot];
470+ }
471+
472+ RCTTriggerReloadCommandListeners (@" Dev menu - apply changes" );
473+ } else if (response == NSAlertSecondButtonReturn ) {
474+ // Reset to Default
475+ [weakSelf setDefaultJSBundle ];
476+ }
477+ // Cancel - do nothing
473478#endif // macOS]
474479 }]];
475480
@@ -483,19 +488,29 @@ - (void)setDefaultJSBundle
483488 if (_actionSheet || RCTRunningInAppExtension ()) {
484489 return ;
485490 }
491+ #else // [macOS
492+ if (_alert) {
493+ return ;
494+ }
495+ #endif // [macOS]
486496
487497 NSString *bridgeDescription = _bridge.bridgeDescription ;
488498 NSString *description =
489499 bridgeDescription.length > 0 ? [NSString stringWithFormat: @" Running %@ " , bridgeDescription] : nil ;
490500
501+ #if !TARGET_OS_OSX // [macOS]
491502 // On larger devices we don't have an anchor point for the action sheet
492503 UIAlertControllerStyle style = [[UIDevice currentDevice ] userInterfaceIdiom ] == UIUserInterfaceIdiomPhone
493504 ? UIAlertControllerStyleActionSheet
494505 : UIAlertControllerStyleAlert;
506+ #else // [macOS
507+ NSAlertStyle style = NSAlertStyleInformational;
508+ #endif // macOS]
495509
496510 NSString *devMenuType = [self .bridge isKindOfClass: RCTBridge.class ] ? @" Bridge" : @" Bridgeless" ;
497511 NSString *devMenuTitle = [NSString stringWithFormat: @" React Native Dev Menu (%@ )" , devMenuType];
498512
513+ #if !TARGET_OS_OSX // [macOS]
499514 _actionSheet = [UIAlertController alertControllerWithTitle: devMenuTitle message: description preferredStyle: style];
500515
501516 NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
@@ -513,20 +528,38 @@ - (void)setDefaultJSBundle
513528
514529 _presentedItems = items;
515530 [RCTPresentedViewController () presentViewController: _actionSheet animated: YES completion: nil ];
516-
517531#else // [macOS
518- NSMenu *menu = [self menu ];
519- NSWindow *window = [NSApp keyWindow ];
520- NSEvent *event = [NSEvent mouseEventWithType: NSEventTypeLeftMouseUp
521- location: CGPointMake (0 , 0 )
522- modifierFlags: 0
523- timestamp: NSTimeIntervalSince1970
524- windowNumber: [window windowNumber ]
525- context: nil
526- eventNumber: 0
527- clickCount: 0
528- pressure: 0.1 ];
529- [NSMenu popUpContextMenu: menu withEvent: event forView: [window contentView ]];
532+ _alert = [NSAlert new ];
533+ [_alert setMessageText: devMenuTitle];
534+ [_alert setInformativeText: description];
535+ [_alert setAlertStyle: NSAlertStyleInformational];
536+
537+ NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
538+ for (RCTDevMenuItem *item in items) {
539+ [_alert addButtonWithTitle: item.title];
540+ }
541+
542+ [_alert addButtonWithTitle: @" Cancel" ];
543+
544+ _presentedItems = items;
545+
546+ // If Invoked from Metro, both the key window and main window may be nil, so we fallback to the first window in that case
547+ NSWindow *window = RCTKeyWindow () ?: [NSApp mainWindow ] ?: [[NSApp windows ] firstObject ];
548+
549+
550+ [_alert beginSheetModalForWindow: window completionHandler: ^(NSModalResponse response) {
551+ // Button responses are NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, etc.
552+ // The last button (Cancel) will have response = NSAlertFirstButtonReturn + menuItems.count
553+ NSInteger buttonIndex = response - NSAlertFirstButtonReturn ;
554+
555+ RCTDevMenuItem *selectedItem = nil ;
556+ if (buttonIndex >= 0 && buttonIndex < self->_presentedItems .count ) {
557+ // Execute the corresponding menu item
558+ selectedItem = self->_presentedItems [buttonIndex];
559+ }
560+ RCTDevMenuAlertActionHandler handler = [self alertActionHandlerForDevItem: selectedItem];
561+ handler (response);
562+ }];
530563#endif // macOS]
531564
532565 [_callableJSModules invokeModule: @" RCTNativeAppEventEmitter" method: @" emit" withArgs: @[ @" RCTDevMenuShown" ]];
@@ -544,37 +577,44 @@ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem:(RCTDevMenuItem *__
544577 };
545578}
546579#else // [macOS
580+ - (RCTDevMenuAlertActionHandler)alertActionHandlerForDevItem : (RCTDevMenuItem *__nullable)item
581+ {
582+ return ^(NSModalResponse response) {
583+ if (item) {
584+ [item callHandler ];
585+ }
586+
587+ self->_alert = nil ;
588+ };
589+ }
590+ #endif // [macOS]
591+
592+ #if TARGET_OS_OSX // [macOS
547593- (NSMenu *)menu
548594{
549- if ([_bridge.devSettings isSecondaryClickToShowDevMenuEnabled ]) {
550- NSMenu *menu = nil ;
551- if (_bridge) {
552- NSString *desc = _bridge.bridgeDescription ;
553- if (desc.length == 0 ) {
554- desc = NSStringFromClass ([_bridge class ]);
555- }
556- NSString *title = [NSString stringWithFormat: @" React Native: Development\n (%@ )" , desc];
557-
558- menu = [NSMenu new ];
559-
560- NSMutableAttributedString *attributedTitle = [[NSMutableAttributedString alloc ] initWithString: title];
561- [attributedTitle setAttributes: @{NSFontAttributeName : [NSFont menuFontOfSize: 0 ]}
562- range: NSMakeRange (0 , [attributedTitle length ])];
563- NSMenuItem *titleItem = [NSMenuItem new ];
564- [titleItem setAttributedTitle: attributedTitle];
565- [menu addItem: titleItem];
566-
567- [menu addItem: [NSMenuItem separatorItem ]];
568-
569- NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
570- for (RCTDevMenuItem *item in items) {
571- NSMenuItem *menuItem = [[NSMenuItem alloc ] initWithTitle: [item title ]
572- action: @selector (menuItemSelected: )
573- keyEquivalent: @" " ];
574- [menuItem setTarget: self ];
575- [menuItem setRepresentedObject: item];
576- [menu addItem: menuItem];
577- }
595+ if ([((RCTDevSettings *)[_moduleRegistry moduleForName: " DevSettings" ]) isSecondaryClickToShowDevMenuEnabled ]) {
596+ NSMenu *menu = [NSMenu new ];
597+
598+ NSString *devMenuType = [self .bridge isKindOfClass: RCTBridge.class ] ? @" Bridge" : @" Bridgeless" ;
599+ NSString *devMenuTitle = [NSString stringWithFormat: @" React Native Dev Menu (%@ )" , devMenuType];
600+
601+ NSMenuItem *titleItem = [NSMenuItem sectionHeaderWithTitle: devMenuTitle];
602+ if (@available (macOS 14.4 , *)) {
603+ NSString *bridgeDescription = _bridge.bridgeDescription ;
604+ NSString *description =
605+ bridgeDescription.length > 0 ? [NSString stringWithFormat: @" Running %@ " , bridgeDescription] : nil ;
606+ [titleItem setSubtitle: description];
607+ }
608+ [menu addItem: titleItem];
609+
610+ NSArray <RCTDevMenuItem *> *items = [self _menuItemsToPresent ];
611+ for (RCTDevMenuItem *item in items) {
612+ NSMenuItem *menuItem = [[NSMenuItem alloc ] initWithTitle: [item title ]
613+ action: @selector (menuItemSelected: )
614+ keyEquivalent: @" " ];
615+ [menuItem setTarget: self ];
616+ [menuItem setRepresentedObject: item];
617+ [menu addItem: menuItem];
578618 }
579619 return menu;
580620 }
0 commit comments