511 lines · 23.3 KB · Text
RawDownload
diff --git a/ios/MenuViewManager.mm b/ios/MenuViewManager.mmindex 3d149b3e7426b8f7c999603ddf1e15e12331c33a..e80fe2caae86e339fcb03408419a7d64b3e5b04f 100644--- a/ios/MenuViewManager.mm+++ b/ios/MenuViewManager.mm@@ -65,6 +65,10 @@ - (UIView *)view  * onOpenMenu: callback to be called when the menu is opened  */ RCT_EXPORT_VIEW_PROPERTY(onOpenMenu, RCTDirectEventBlock);+/**+ * onMenuInteractionStart: callback to be called when UIKit starts preparing a menu interaction+ */+RCT_EXPORT_VIEW_PROPERTY(onMenuInteractionStart, RCTDirectEventBlock); /**  * shouldOpenOnLongPress: determines whether menu should be opened after long press or normal press  */diff --git a/ios/NewArch/FabricActionSheetView.swift b/ios/NewArch/FabricActionSheetView.swiftindex 484ff8a1d73a6d445273883c5b28b1c8e80901ec..bc92310d00369b853a80f4d2080578f3903191d2 100644--- a/ios/NewArch/FabricActionSheetView.swift+++ b/ios/NewArch/FabricActionSheetView.swift@@ -3,6 +3,7 @@ public class FabricActionSheetView: ActionSheetView, FabricViewImplementationPro     public var onPressAction: ((String) -> Void)?     public var onCloseMenu: (() -> Void)?     public var onOpenMenu: (() -> Void)?+    public var onMenuInteractionStart: (() -> Void)?          @objc override func sendButtonAction(_ action: String) {         if let onPress = onPressAction {diff --git a/ios/NewArch/FabricMenuViewImplementation.swift b/ios/NewArch/FabricMenuViewImplementation.swiftindex 33d27c5a66593ae1a9b143804413578eb79f1a7d..359cd19fa53d10c0374b2b537517ea8629e79d0c 100644--- a/ios/NewArch/FabricMenuViewImplementation.swift+++ b/ios/NewArch/FabricMenuViewImplementation.swift@@ -12,6 +12,7 @@ public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImp     public var onPressAction: ((String) -> Void)?     public var onCloseMenu: (() -> Void)?     public var onOpenMenu: (() -> Void)?+    public var onMenuInteractionStart: (() -> Void)?      @objc override func sendButtonAction(_ action: UIAction) {         if let onPress = onPressAction {@@ -31,4 +32,10 @@ public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImp         }     } +    @objc override func sendMenuInteractionStart() {+        if let onMenuInteractionStart = onMenuInteractionStart {+            onMenuInteractionStart()+        }+    }+ }diff --git a/ios/NewArch/FabricViewImplementationProtocol.swift b/ios/NewArch/FabricViewImplementationProtocol.swiftindex 2cef2fb8c74497da46775a490edc58cf8cbf29cc..54429c756eeecf6e96b55d62cc1e70213096f62a 100644--- a/ios/NewArch/FabricViewImplementationProtocol.swift+++ b/ios/NewArch/FabricViewImplementationProtocol.swift@@ -8,4 +8,5 @@ import Foundation     var onPressAction: ((String) -> Void)? { get set }     var onCloseMenu: (() -> Void)? { get set }     var onOpenMenu: (() -> Void)? { get set }+    var onMenuInteractionStart: (() -> Void)? { get set } }diff --git a/ios/NewArch/MenuView.mm b/ios/NewArch/MenuView.mmindex a54e619eb39e3402fa63f2ee4f734063d4f8fc58..773f6f1f5d4e0663fd3211fe51085a7f3935e445 100644--- a/ios/NewArch/MenuView.mm+++ b/ios/NewArch/MenuView.mm@@ -46,6 +46,10 @@ - (instancetype)initWithFrame:(CGRect)frame             [self onOpenMenu];         }; +        _view.onMenuInteractionStart = ^{+            [self onMenuInteractionStart];+        };+         _view.onCloseMenu = ^{             [self onCloseMenu];         };@@ -92,6 +96,14 @@ - (void)onOpenMenu {     } } +- (void)onMenuInteractionStart {+    // If screen is already unmounted then there will be no event emitter+    const auto eventEmitter = [self getEventEmitter];+    if (eventEmitter != nullptr) {+        eventEmitter->onMenuInteractionStart({});+    }+}+ /**  Responsible for iterating through the C++ vector<struct> and convert each struct element to NSDictionary, then return it all in an NSArray  */@@ -105,6 +117,27 @@ - (void)onOpenMenu {         NSMutableArray<NSDictionary *> *subactionsArray = [NSMutableArray arrayWithCapacity:actions.size()];         if (action.subactions.size() > 0) {             for (const MenuViewActionsSubactionsStruct &subaction : action.subactions) {+                NSMutableArray<NSDictionary *> *subSubactionsArray =+                    [NSMutableArray arrayWithCapacity:subaction.subactions.size()];+                for (const MenuViewActionsSubactionsSubactionsStruct &subSubaction : subaction.subactions) {+                    NSDictionary *subSubactionDict = @{+                        @"id": [NSString stringWithUTF8String:subSubaction.id.c_str()],+                        @"title": [NSString stringWithUTF8String:subSubaction.title.c_str()],+                        @"titleColor": @(subSubaction.titleColor),+                        @"subtitle": [NSString stringWithUTF8String:subSubaction.subtitle.c_str()],+                        @"state": [NSString stringWithUTF8String:subSubaction.state.c_str()],+                        @"image": [NSString stringWithUTF8String:subSubaction.image.c_str()],+                        @"imageColor": @(subSubaction.imageColor),+                        @"displayInline": @(subSubaction.displayInline),+                        @"attributes": @{+                            @"destructive": @(subSubaction.attributes.destructive),+                            @"disabled": @(subSubaction.attributes.disabled),+                            @"hidden": @(subSubaction.attributes.hidden),+                            @"keepsMenuPresented": @(subSubaction.attributes.keepsMenuPresented),+                        },+                    };+                    [subSubactionsArray addObject:subSubactionDict];+                }                 NSDictionary *subactionDict = @{                     @"id": [NSString stringWithUTF8String:subaction.id.c_str()],                     @"title": [NSString stringWithUTF8String:subaction.title.c_str()],@@ -118,7 +151,9 @@ - (void)onOpenMenu {                         @"destructive": @(subaction.attributes.destructive),                         @"disabled": @(subaction.attributes.disabled),                         @"hidden": @(subaction.attributes.hidden),+                        @"keepsMenuPresented": @(subaction.attributes.keepsMenuPresented),                     },+                    @"subactions": subSubactionsArray,                 };                 [subactionsArray addObject:subactionDict];             }@@ -138,6 +173,7 @@ - (void)onOpenMenu {                 @"destructive": @(action.attributes.destructive),                 @"disabled": @(action.attributes.disabled),                 @"hidden": @(action.attributes.hidden),+                @"keepsMenuPresented": @(action.attributes.keepsMenuPresented),             },             @"subactions": subactionsArray,         };diff --git a/ios/OldArch/LegacyActionSheetView.swift b/ios/OldArch/LegacyActionSheetView.swiftindex 3cd1a2517624d99b51331c564fba9145d7c8d799..e458504bea6ef167d9b0db0c889f0f76ea965287 100644--- a/ios/OldArch/LegacyActionSheetView.swift+++ b/ios/OldArch/LegacyActionSheetView.swift@@ -3,6 +3,7 @@ public class LegacyActionSheetView: ActionSheetView {     @objc var onPressAction: RCTDirectEventBlock?     @objc var onCloseMenu: RCTDirectEventBlock?     @objc var onOpenMenu: RCTDirectEventBlock?+    @objc var onMenuInteractionStart: RCTDirectEventBlock?   diff --git a/ios/OldArch/LegacyMenuViewImplementation.swift b/ios/OldArch/LegacyMenuViewImplementation.swiftindex 6a2a205f95c1001f3fdf6fb835815fa7c4c02c16..4f42b64349966fb1438f72439cec8256a76aab9c 100644--- a/ios/OldArch/LegacyMenuViewImplementation.swift+++ b/ios/OldArch/LegacyMenuViewImplementation.swift@@ -5,6 +5,7 @@ public class LegacyMenuViewImplementation: MenuViewImplementation {     @objc var onPressAction: RCTDirectEventBlock?     @objc var onCloseMenu: RCTDirectEventBlock?     @objc var onOpenMenu: RCTDirectEventBlock?+    @objc var onMenuInteractionStart: RCTDirectEventBlock?          @objc override func sendButtonAction(_ action: UIAction) {         if let onPress = onPressAction {@@ -24,4 +25,10 @@ public class LegacyMenuViewImplementation: MenuViewImplementation {         }     } +    @objc override func sendMenuInteractionStart() {+        if let onMenuInteractionStart = onMenuInteractionStart {+            onMenuInteractionStart([:])+        }+    }+ }diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swiftindex 5c4e0da4292b15d3a27b5ea1555f11452a470815..4d8abe3c4fc45bda48c2958bc9ca933c77f2cc47 100644--- a/ios/Shared/MenuViewImplementation.swift+++ b/ios/Shared/MenuViewImplementation.swift@@ -60,17 +60,46 @@ public class MenuViewImplementation: UIButton {     }         public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {-        sendMenuOpen()+        // Flush updates deferred by the presented-guard before the action+        // provider snapshots self.menu (covers a stuck flag from an+        // interaction that never ended cleanly).+        if pendingMenu != nil {+            pendingMenu = nil+            isMenuPresented = false+            self.setup()+        }+        isMenuPresented = true+        sendMenuInteractionStart()         return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in             guard let self = self else { return nil }             return self.menu         }     }-    ++    public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {+        // UIKit can request a configuration for a press that never opens a+        // menu. Notify React only once presentation actually begins, and+        // preserve UIButton's presentation behavior by calling super.+        super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator)+        sendMenuOpen()+    }+     public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {+        super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)         sendMenuClose()+        isMenuPresented = false+        if pendingMenu != nil {+            // Re-run the full assignment now that presentation is over, so+            // any props deferred by the presented-guard (menu contents, press+            // mode) land on the button.+            pendingMenu = nil+            self.setup()+        }     } +    private var isMenuPresented = false+    private var pendingMenu: UIMenu?+     func setup () {         let menu = UIMenu(title: _title,             identifier: nil,@@ -86,8 +115,98 @@ public class MenuViewImplementation: UIButton {             }         } +        if isMenuPresented {+            // An action fired with keepsMenuPresented leaves the menu on+            // screen, and reassigning self.menu while it is presented makes+            // UIKit dismiss it — the update triggered by a selection would+            // defeat the keep-presented attribute. Update the visible copy in+            // place instead and defer the button assignment to dismissal.+            pendingMenu = menu+            self.refreshPresentedMenu(menu)+            return+        }+         self.menu = menu         self.showsMenuAsPrimaryAction = !shouldOpenOnLongPress+        // In long-press mode the button must not intercept touches: as a+        // full-bounds contentView it sits IN FRONT of the React children of+        // the hosting component view, which would swallow their taps (e.g. a+        // row Pressable wrapped by MenuView). The context-menu interaction is+        // hosted by the superview instead (see updateLongPressInteraction) so+        // long-press and pointer secondary-click still present the menu, with+        // the wrapped content as the zoom preview.+        self.isUserInteractionEnabled = !shouldOpenOnLongPress+        self.updateLongPressInteraction()+    }++    private func refreshPresentedMenu(_ menu: UIMenu) {+        // Tap mode presents through the button's built-in interaction,+        // long-press mode through the superview-hosted one; cover both (plus+        // the interaction added in init) and dedupe by identity.+        var candidates = self.interactions.compactMap { $0 as? UIContextMenuInteraction }+        if let builtin = self.contextMenuInteraction {+            candidates.append(builtin)+        }+        if let host = longPressInteraction {+            candidates.append(host)+        }+        var visited: Set<ObjectIdentifier> = []+        for interaction in candidates where visited.insert(ObjectIdentifier(interaction)).inserted {+            // The block receives whichever menu level is currently on screen —+            // the navigated submenu when the user picked inside one, not the+            // root. Swap in the matching node from the rebuilt tree (stable+            // identifiers from the JS action ids) so that level updates in+            // place; returning an unrelated menu instead makes UIKit render it+            // as navigation into a foreign menu, with a stale or blank+            // expanded-submenu header row above the actions. The root carries+            // an auto-generated identifier that never matches, so it falls+            // through to a children-only replacement.+            interaction.updateVisibleMenu { [weak self] visibleMenu in+                guard let self = self else { return visibleMenu }+                if let replacement = self.menuMatching(visibleMenu.identifier, in: menu) {+                    return replacement+                }+                return visibleMenu.replacingChildren(menu.children)+            }+        }+    }++    private func menuMatching(_ identifier: UIMenu.Identifier, in menu: UIMenu) -> UIMenu? {+        if menu.identifier == identifier {+            return menu+        }+        for element in menu.children {+            if let submenu = element as? UIMenu, let match = menuMatching(identifier, in: submenu) {+                return match+            }+        }+        return nil+    }++    private var longPressInteraction: UIContextMenuInteraction?++    public override func didMoveToSuperview() {+        super.didMoveToSuperview()+        self.updateLongPressInteraction()+    }++    private func updateLongPressInteraction() {+        if shouldOpenOnLongPress, let host = self.superview {+            if let existing = longPressInteraction, existing.view === host {+                return+            }+            if let existing = longPressInteraction {+                existing.view?.removeInteraction(existing)+            }+            let interaction = UIContextMenuInteraction(delegate: self)+            host.addInteraction(interaction)+            longPressInteraction = interaction+            return+        }+        if let existing = longPressInteraction {+            existing.view?.removeInteraction(existing)+            longPressInteraction = nil+        }     }      public override func reactSetFrame(_ frame: CGRect) {@@ -124,4 +243,8 @@ public class MenuViewImplementation: UIButton {     @objc func sendMenuOpen() {         // NO-OP (should be overriden by parent)     }++    @objc func sendMenuInteractionStart() {+        // NO-OP (should be overriden by parent)+    } }diff --git a/ios/Shared/RCTMenuItem.swift b/ios/Shared/RCTMenuItem.swiftindex bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f619443d5a 100644--- a/ios/Shared/RCTMenuItem.swift+++ b/ios/Shared/RCTMenuItem.swift@@ -103,10 +103,18 @@ class RCTMenuAction {                 subMenuActions.append(subaction.createUIMenuElement(handler))             }             var menu: UIMenu;+            // Stable identifiers let updateVisibleMenu match submenu nodes in+            // place, so a refresh while the menu is presented keeps the user's+            // current submenu level instead of popping back to the root.+            // Inline sections don't navigate, so they stay stock.+            let menuIdentifier = identifier.map { UIMenu.Identifier(rawValue: $0.rawValue) }             if self.displayInline {                 menu = UIMenu(title: title, image: image, options: .displayInline, children: subMenuActions)             } else {-                menu = UIMenu(title: title, image: image, children: subMenuActions)+                menu = UIMenu(title: title, image: image, identifier: menuIdentifier, children: subMenuActions)+            }+            if #available(iOS 15.0, *) {+                menu.subtitle = subtitle             }              if #available(iOS 16.0, *) {diff --git a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.tsindex e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed99580776353 100644--- a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts+++ b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts@@ -48,6 +48,7 @@ export interface NativeProps extends ViewProps { 	onPressAction?: DirectEventHandler<{ event: string }>; 	onCloseMenu?: DirectEventHandler<{ event: string }>; 	onOpenMenu?: DirectEventHandler<{ event: string }>;+	onMenuInteractionStart?: DirectEventHandler<{ event: string }>; 	actions: Array<MenuAction>; 	actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) 	title?: string;diff --git a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.tsindex e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed99580776353 100644--- a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts+++ b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts@@ -48,6 +48,7 @@ export interface NativeProps extends ViewProps { 	onPressAction?: DirectEventHandler<{ event: string }>; 	onCloseMenu?: DirectEventHandler<{ event: string }>; 	onOpenMenu?: DirectEventHandler<{ event: string }>;+	onMenuInteractionStart?: DirectEventHandler<{ event: string }>; 	actions: Array<MenuAction>; 	actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) 	title?: string;diff --git a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.tsindex 24ee7fdcd9f81cfe2c813cfca9d4e276b846fb7b..5ee7b45bafea8547d68949aaba70584055f9fb53 100644--- a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts+++ b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts@@ -41,6 +41,9 @@ export interface NativeProps extends ViewProps {     onOpenMenu?: DirectEventHandler<{         event: string;     }>;+    onMenuInteractionStart?: DirectEventHandler<{+        event: string;+    }>;     actions: Array<MenuAction>;     actionsHash: string;     title?: string;diff --git a/lib/typescript/src/index.d.ts b/lib/typescript/src/index.d.tsindex 61d210aa9364f0354f77bddaecdc0a127b1cdbb4..84ac2bc8b4edac045ed59896d5eb4e6eaef4725d 100644--- a/lib/typescript/src/index.d.ts+++ b/lib/typescript/src/index.d.ts@@ -4,6 +4,7 @@ declare const MenuView: import("react").ForwardRefExoticComponent<{     onPressAction?: ({ nativeEvent }: NativeActionEvent) => void;     onCloseMenu?: () => void;     onOpenMenu?: () => void;+    onMenuInteractionStart?: () => void;     actions: MenuAction[];     title?: string;     isAnchoredToRight?: boolean;diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.tsindex eac7940ab7eb6a69ac329848c9f3eeffdcbaf43d..271cd27f777be06bc02f05d4b4dac5bb940f5f0f 100644--- a/lib/typescript/src/types.d.ts+++ b/lib/typescript/src/types.d.ts@@ -113,6 +113,12 @@ type MenuComponentPropsBase = {      * Callback function that will be called when the menu opens.      */     onOpenMenu?: () => void;+    /**+     * Callback function that will be called when UIKit starts preparing a menu interaction.+     * Preparation can end without the menu opening.+     * @platform iOS+     */+    onMenuInteractionStart?: () => void;     /**      * Actions to be displayed in the menu.      */@@ -170,6 +176,7 @@ export type NativeMenuComponentProps = {     onPressAction?: ({ nativeEvent }: NativeActionEvent) => void;     onCloseMenu?: () => void;     onOpenMenu?: () => void;+    onMenuInteractionStart?: () => void;     actions: ProcessedMenuAction[];     actionsHash: string;     title?: string;diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.tsindex e6509355275596c451f9d082223cc16bfe504e1a..604dba0d90a2545910f8724a80205565de2391d4 100644--- a/src/NativeModuleSpecs/UIMenuNativeComponent.ts+++ b/src/NativeModuleSpecs/UIMenuNativeComponent.ts@@ -13,6 +13,22 @@ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNati   types here, to avoid issues while `pod install` takes place. */ +type SubSubAction = {+	id?: string;+	title: string;+	titleColor?: Int32;+	subtitle?: string;+	state?: string;+	image?: string;+	imageColor?: Int32;+	displayInline?: boolean;+	attributes?: {+		destructive?: boolean;+		disabled?: boolean;+		hidden?: boolean;+		keepsMenuPresented?: boolean;+	};+}; type SubAction = { 	id?: string; 	title: string;@@ -26,7 +42,11 @@ type SubAction = { 		destructive?: boolean; 		disabled?: boolean; 		hidden?: boolean;+		keepsMenuPresented?: boolean; 	};+	// One extra nesting level (menu → submenu → nested submenu leaves); the+	// codegen structs can't recurse, so depth is capped explicitly.+	subactions?: Array<SubSubAction>; }; type MenuAction = { 	id?: string;@@ -41,6 +61,7 @@ type MenuAction = { 		destructive?: boolean; 		disabled?: boolean; 		hidden?: boolean;+		keepsMenuPresented?: boolean; 	}; 	subactions?: Array<SubAction>; };@@ -48,6 +69,7 @@ export interface NativeProps extends ViewProps { 	onPressAction?: DirectEventHandler<{ event: string }>; 	onCloseMenu?: DirectEventHandler<{ event: string }>; 	onOpenMenu?: DirectEventHandler<{ event: string }>;+	onMenuInteractionStart?: DirectEventHandler<{ event: string }>; 	actions: Array<MenuAction>; 	actionsHash: string; // just a workaround to make sure we don't have to manually compare MenuActions manually in C++ (since it's a struct and that's a pain) 	title?: string;diff --git a/src/types.ts b/src/types.tsindex 9c323d9c510e7359c83d4d6633affde673570632..5d5fdfb8b8235fcaa8295b85630443eaf5991a17 100644--- a/src/types.ts+++ b/src/types.ts@@ -123,6 +123,12 @@ type MenuComponentPropsBase = { 	 * Callback function that will be called when the menu opens. 	 */ 	onOpenMenu?: () => void;+	/**+	 * Callback function that will be called when UIKit starts preparing a menu interaction.+	 * Preparation can end without the menu opening.+	 * @platform iOS+	 */+	onMenuInteractionStart?: () => void;

Lines 1–500 of 511.