1diff --git a/ios/MenuViewManager.mm b/ios/MenuViewManager.mm
2index 3d149b3e7426b8f7c999603ddf1e15e12331c33a..e80fe2caae86e339fcb03408419a7d64b3e5b04f 100644
3--- a/ios/MenuViewManager.mm
4+++ b/ios/MenuViewManager.mm
5@@ -1,6 +1,6 @@
6 #import <React/RCTViewManager.h>
7 #import <React/RCTUIManager.h>
8-#import "RCTBridge.h"
9+#import <React/RCTBridge.h>
10
11 #ifdef RCT_NEW_ARCH_ENABLED
12 // NEW ARCH
13@@ -65,6 +65,10 @@ - (UIView *)view
14 * onOpenMenu: callback to be called when the menu is opened
15 */
16 RCT_EXPORT_VIEW_PROPERTY(onOpenMenu, RCTDirectEventBlock);
17+/**
18+ * onMenuInteractionStart: callback to be called when UIKit starts preparing a menu interaction
19+ */
20+RCT_EXPORT_VIEW_PROPERTY(onMenuInteractionStart, RCTDirectEventBlock);
21 /**
22 * shouldOpenOnLongPress: determines whether menu should be opened after long press or normal press
23 */
24diff --git a/ios/NewArch/FabricActionSheetView.swift b/ios/NewArch/FabricActionSheetView.swift
25index 484ff8a1d73a6d445273883c5b28b1c8e80901ec..bc92310d00369b853a80f4d2080578f3903191d2 100644
26--- a/ios/NewArch/FabricActionSheetView.swift
27+++ b/ios/NewArch/FabricActionSheetView.swift
28@@ -3,6 +3,7 @@ public class FabricActionSheetView: ActionSheetView, FabricViewImplementationPro
29 public var onPressAction: ((String) -> Void)?
30 public var onCloseMenu: (() -> Void)?
31 public var onOpenMenu: (() -> Void)?
32+ public var onMenuInteractionStart: (() -> Void)?
33
34 @objc override func sendButtonAction(_ action: String) {
35 if let onPress = onPressAction {
36diff --git a/ios/NewArch/FabricMenuViewImplementation.swift b/ios/NewArch/FabricMenuViewImplementation.swift
37index 33d27c5a66593ae1a9b143804413578eb79f1a7d..359cd19fa53d10c0374b2b537517ea8629e79d0c 100644
38--- a/ios/NewArch/FabricMenuViewImplementation.swift
39+++ b/ios/NewArch/FabricMenuViewImplementation.swift
40@@ -12,6 +12,7 @@ public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImp
41 public var onPressAction: ((String) -> Void)?
42 public var onCloseMenu: (() -> Void)?
43 public var onOpenMenu: (() -> Void)?
44+ public var onMenuInteractionStart: (() -> Void)?
45
46 @objc override func sendButtonAction(_ action: UIAction) {
47 if let onPress = onPressAction {
48@@ -31,4 +32,10 @@ public class FabricMenuViewImplementation: MenuViewImplementation, FabricViewImp
49 }
50 }
51
52+ @objc override func sendMenuInteractionStart() {
53+ if let onMenuInteractionStart = onMenuInteractionStart {
54+ onMenuInteractionStart()
55+ }
56+ }
57+
58 }
59diff --git a/ios/NewArch/FabricViewImplementationProtocol.swift b/ios/NewArch/FabricViewImplementationProtocol.swift
60index 2cef2fb8c74497da46775a490edc58cf8cbf29cc..54429c756eeecf6e96b55d62cc1e70213096f62a 100644
61--- a/ios/NewArch/FabricViewImplementationProtocol.swift
62+++ b/ios/NewArch/FabricViewImplementationProtocol.swift
63@@ -8,4 +8,5 @@ import Foundation
64 var onPressAction: ((String) -> Void)? { get set }
65 var onCloseMenu: (() -> Void)? { get set }
66 var onOpenMenu: (() -> Void)? { get set }
67+ var onMenuInteractionStart: (() -> Void)? { get set }
68 }
69diff --git a/ios/NewArch/MenuView.mm b/ios/NewArch/MenuView.mm
70index a54e619eb39e3402fa63f2ee4f734063d4f8fc58..773f6f1f5d4e0663fd3211fe51085a7f3935e445 100644
71--- a/ios/NewArch/MenuView.mm
72+++ b/ios/NewArch/MenuView.mm
73@@ -46,6 +46,10 @@ - (instancetype)initWithFrame:(CGRect)frame
74 [self onOpenMenu];
75 };
76
77+ _view.onMenuInteractionStart = ^{
78+ [self onMenuInteractionStart];
79+ };
80+
81 _view.onCloseMenu = ^{
82 [self onCloseMenu];
83 };
84@@ -92,6 +96,14 @@ - (void)onOpenMenu {
85 }
86 }
87
88+- (void)onMenuInteractionStart {
89+ // If screen is already unmounted then there will be no event emitter
90+ const auto eventEmitter = [self getEventEmitter];
91+ if (eventEmitter != nullptr) {
92+ eventEmitter->onMenuInteractionStart({});
93+ }
94+}
95+
96 /**
97 Responsible for iterating through the C++ vector<struct> and convert each struct element to NSDictionary, then return it all in an NSArray
98 */
99@@ -105,6 +117,27 @@ - (void)onOpenMenu {
100 NSMutableArray<NSDictionary *> *subactionsArray = [NSMutableArray arrayWithCapacity:actions.size()];
101 if (action.subactions.size() > 0) {
102 for (const MenuViewActionsSubactionsStruct &subaction : action.subactions) {
103+ NSMutableArray<NSDictionary *> *subSubactionsArray =
104+ [NSMutableArray arrayWithCapacity:subaction.subactions.size()];
105+ for (const MenuViewActionsSubactionsSubactionsStruct &subSubaction : subaction.subactions) {
106+ NSDictionary *subSubactionDict = @{
107+ @"id": [NSString stringWithUTF8String:subSubaction.id.c_str()],
108+ @"title": [NSString stringWithUTF8String:subSubaction.title.c_str()],
109+ @"titleColor": @(subSubaction.titleColor),
110+ @"subtitle": [NSString stringWithUTF8String:subSubaction.subtitle.c_str()],
111+ @"state": [NSString stringWithUTF8String:subSubaction.state.c_str()],
112+ @"image": [NSString stringWithUTF8String:subSubaction.image.c_str()],
113+ @"imageColor": @(subSubaction.imageColor),
114+ @"displayInline": @(subSubaction.displayInline),
115+ @"attributes": @{
116+ @"destructive": @(subSubaction.attributes.destructive),
117+ @"disabled": @(subSubaction.attributes.disabled),
118+ @"hidden": @(subSubaction.attributes.hidden),
119+ @"keepsMenuPresented": @(subSubaction.attributes.keepsMenuPresented),
120+ },
121+ };
122+ [subSubactionsArray addObject:subSubactionDict];
123+ }
124 NSDictionary *subactionDict = @{
125 @"id": [NSString stringWithUTF8String:subaction.id.c_str()],
126 @"title": [NSString stringWithUTF8String:subaction.title.c_str()],
127@@ -118,7 +151,9 @@ - (void)onOpenMenu {
128 @"destructive": @(subaction.attributes.destructive),
129 @"disabled": @(subaction.attributes.disabled),
130 @"hidden": @(subaction.attributes.hidden),
131+ @"keepsMenuPresented": @(subaction.attributes.keepsMenuPresented),
132 },
133+ @"subactions": subSubactionsArray,
134 };
135 [subactionsArray addObject:subactionDict];
136 }
137@@ -138,6 +173,7 @@ - (void)onOpenMenu {
138 @"destructive": @(action.attributes.destructive),
139 @"disabled": @(action.attributes.disabled),
140 @"hidden": @(action.attributes.hidden),
141+ @"keepsMenuPresented": @(action.attributes.keepsMenuPresented),
142 },
143 @"subactions": subactionsArray,
144 };
145diff --git a/ios/OldArch/LegacyActionSheetView.swift b/ios/OldArch/LegacyActionSheetView.swift
146index 3cd1a2517624d99b51331c564fba9145d7c8d799..e458504bea6ef167d9b0db0c889f0f76ea965287 100644
147--- a/ios/OldArch/LegacyActionSheetView.swift
148+++ b/ios/OldArch/LegacyActionSheetView.swift
149@@ -3,6 +3,7 @@ public class LegacyActionSheetView: ActionSheetView {
150 @objc var onPressAction: RCTDirectEventBlock?
151 @objc var onCloseMenu: RCTDirectEventBlock?
152 @objc var onOpenMenu: RCTDirectEventBlock?
153+ @objc var onMenuInteractionStart: RCTDirectEventBlock?
154
155
156
157diff --git a/ios/OldArch/LegacyMenuViewImplementation.swift b/ios/OldArch/LegacyMenuViewImplementation.swift
158index 6a2a205f95c1001f3fdf6fb835815fa7c4c02c16..4f42b64349966fb1438f72439cec8256a76aab9c 100644
159--- a/ios/OldArch/LegacyMenuViewImplementation.swift
160+++ b/ios/OldArch/LegacyMenuViewImplementation.swift
161@@ -5,6 +5,7 @@ public class LegacyMenuViewImplementation: MenuViewImplementation {
162 @objc var onPressAction: RCTDirectEventBlock?
163 @objc var onCloseMenu: RCTDirectEventBlock?
164 @objc var onOpenMenu: RCTDirectEventBlock?
165+ @objc var onMenuInteractionStart: RCTDirectEventBlock?
166
167 @objc override func sendButtonAction(_ action: UIAction) {
168 if let onPress = onPressAction {
169@@ -24,4 +25,10 @@ public class LegacyMenuViewImplementation: MenuViewImplementation {
170 }
171 }
172
173+ @objc override func sendMenuInteractionStart() {
174+ if let onMenuInteractionStart = onMenuInteractionStart {
175+ onMenuInteractionStart([:])
176+ }
177+ }
178+
179 }
180diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift
181index 5c4e0da4292b15d3a27b5ea1555f11452a470815..475a99b2a3b1800c9d3e3e5f77a64f552fa35828 100644
182--- a/ios/Shared/MenuViewImplementation.swift
183+++ b/ios/Shared/MenuViewImplementation.swift
184@@ -60,17 +60,46 @@ public class MenuViewImplementation: UIButton {
185 }
186
187 public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
188- sendMenuOpen()
189+ // Flush updates deferred by the presented-guard before the action
190+ // provider snapshots self.menu (covers a stuck flag from an
191+ // interaction that never ended cleanly).
192+ if pendingMenu != nil {
193+ pendingMenu = nil
194+ isMenuPresented = false
195+ self.setup()
196+ }
197+ isMenuPresented = true
198+ sendMenuInteractionStart()
199 return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
200 guard let self = self else { return nil }
201 return self.menu
202 }
203 }
204-
205+
206+ public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
207+ // UIKit can request a configuration for a press that never opens a
208+ // menu. Notify React only once presentation actually begins, and
209+ // preserve UIButton's presentation behavior by calling super.
210+ super.contextMenuInteraction(interaction, willDisplayMenuFor: configuration, animator: animator)
211+ sendMenuOpen()
212+ }
213+
214 public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
215+ super.contextMenuInteraction(interaction, willEndFor: configuration, animator: animator)
216 sendMenuClose()
217+ isMenuPresented = false
218+ if pendingMenu != nil {
219+ // Re-run the full assignment now that presentation is over, so
220+ // any props deferred by the presented-guard (menu contents, press
221+ // mode) land on the button.
222+ pendingMenu = nil
223+ self.setup()
224+ }
225 }
226
227+ private var isMenuPresented = false
228+ private var pendingMenu: UIMenu?
229+
230 func setup () {
231 let menu = UIMenu(title: _title,
232 identifier: nil,
233@@ -86,8 +115,102 @@ public class MenuViewImplementation: UIButton {
234 }
235 }
236
237+ if isMenuPresented {
238+ // An action fired with keepsMenuPresented leaves the menu on
239+ // screen, and reassigning self.menu while it is presented makes
240+ // UIKit dismiss it — the update triggered by a selection would
241+ // defeat the keep-presented attribute. Update the visible copy in
242+ // place instead and defer the button assignment to dismissal.
243+ pendingMenu = menu
244+ self.refreshPresentedMenu(menu)
245+ return
246+ }
247+
248 self.menu = menu
249 self.showsMenuAsPrimaryAction = !shouldOpenOnLongPress
250+ // In long-press mode the button must not intercept touches: as a
251+ // full-bounds contentView it sits IN FRONT of the React children of
252+ // the hosting component view, which would swallow their taps (e.g. a
253+ // row Pressable wrapped by MenuView). The context-menu interaction is
254+ // hosted by the superview instead (see updateLongPressInteraction) so
255+ // long-press and pointer secondary-click still present the menu, with
256+ // the wrapped content as the zoom preview.
257+ self.isUserInteractionEnabled = !shouldOpenOnLongPress
258+ self.updateLongPressInteraction()
259+ }
260+
261+ private func refreshPresentedMenu(_ menu: UIMenu) {
262+ // Tap mode presents through the button's built-in interaction,
263+ // long-press mode through the superview-hosted one; cover both (plus
264+ // the interaction added in init) and dedupe by identity.
265+ var candidates = self.interactions.compactMap { $0 as? UIContextMenuInteraction }
266+ if let builtin = self.contextMenuInteraction {
267+ candidates.append(builtin)
268+ }
269+ if let host = longPressInteraction {
270+ candidates.append(host)
271+ }
272+ var visited: Set<ObjectIdentifier> = []
273+ for interaction in candidates where visited.insert(ObjectIdentifier(interaction)).inserted {
274+ // The block receives whichever menu level is currently on screen —
275+ // the navigated submenu when the user picked inside one, not the
276+ // root. Swap in the matching node from the rebuilt tree (stable
277+ // identifiers from the JS action ids) so that level updates in
278+ // place; returning an unrelated menu instead makes UIKit render it
279+ // as navigation into a foreign menu, with a stale or blank
280+ // expanded-submenu header row above the actions. The root carries
281+ // an auto-generated identifier that never matches, so it falls
282+ // through to a children-only replacement.
283+ interaction.updateVisibleMenu { [weak self] visibleMenu in
284+ guard let self = self else { return visibleMenu }
285+ if let replacement = self.menuMatching(visibleMenu.identifier, in: menu) {
286+ return replacement
287+ }
288+ return visibleMenu.replacingChildren(menu.children)
289+ }
290+ }
291+ }
292+
293+ private func menuMatching(_ identifier: UIMenu.Identifier, in menu: UIMenu) -> UIMenu? {
294+ if menu.identifier == identifier {
295+ return menu
296+ }
297+ for element in menu.children {
298+ if let submenu = element as? UIMenu, let match = menuMatching(identifier, in: submenu) {
299+ return match
300+ }
301+ }
302+ return nil
303+ }
304+
305+ private var longPressInteraction: UIContextMenuInteraction?
306+
307+ public override func didMoveToSuperview() {
308+ super.didMoveToSuperview()
309+ self.updateLongPressInteraction()
310+ }
311+
312+ private func updateLongPressInteraction() {
313+ if shouldOpenOnLongPress, let host = self.superview {
314+ if let existing = longPressInteraction, existing.view === host {
315+ return
316+ }
317+ if let existing = longPressInteraction {
318+ existing.view?.removeInteraction(existing)
319+ }
320+ // UIControl's built-in interaction presents a compact menu that
321+ // preserves the keyboard; a separate view interaction presents a
322+ // rich context menu and temporarily hides it.
323+ guard let interaction = self.contextMenuInteraction else { return }
324+ host.addInteraction(interaction)
325+ longPressInteraction = interaction
326+ return
327+ }
328+ if let existing = longPressInteraction {
329+ existing.view?.removeInteraction(existing)
330+ self.addInteraction(existing)
331+ longPressInteraction = nil
332+ }
333 }
334
335 public override func reactSetFrame(_ frame: CGRect) {
336@@ -124,4 +247,8 @@ public class MenuViewImplementation: UIButton {
337 @objc func sendMenuOpen() {
338 // NO-OP (should be overriden by parent)
339 }
340+
341+ @objc func sendMenuInteractionStart() {
342+ // NO-OP (should be overriden by parent)
343+ }
344 }
345diff --git a/ios/Shared/RCTMenuItem.swift b/ios/Shared/RCTMenuItem.swift
346index bb6bb2b7ad56135089f267587c974b166760539d..949b3f7ec49af7ba26d966a8923a51f619443d5a 100644
347--- a/ios/Shared/RCTMenuItem.swift
348+++ b/ios/Shared/RCTMenuItem.swift
349@@ -103,10 +103,18 @@ class RCTMenuAction {
350 subMenuActions.append(subaction.createUIMenuElement(handler))
351 }
352 var menu: UIMenu;
353+ // Stable identifiers let updateVisibleMenu match submenu nodes in
354+ // place, so a refresh while the menu is presented keeps the user's
355+ // current submenu level instead of popping back to the root.
356+ // Inline sections don't navigate, so they stay stock.
357+ let menuIdentifier = identifier.map { UIMenu.Identifier(rawValue: $0.rawValue) }
358 if self.displayInline {
359 menu = UIMenu(title: title, image: image, options: .displayInline, children: subMenuActions)
360 } else {
361- menu = UIMenu(title: title, image: image, children: subMenuActions)
362+ menu = UIMenu(title: title, image: image, identifier: menuIdentifier, children: subMenuActions)
363+ }
364+ if #available(iOS 15.0, *) {
365+ menu.subtitle = subtitle
366 }
367
368 if #available(iOS 16.0, *) {
369diff --git a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts
370index e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed99580776353 100644
371--- a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts
372+++ b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts
373@@ -48,6 +48,7 @@ export interface NativeProps extends ViewProps {
374 onPressAction?: DirectEventHandler<{ event: string }>;
375 onCloseMenu?: DirectEventHandler<{ event: string }>;
376 onOpenMenu?: DirectEventHandler<{ event: string }>;
377+ onMenuInteractionStart?: DirectEventHandler<{ event: string }>;
378 actions: Array<MenuAction>;
379 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)
380 title?: string;
381diff --git a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts
382index e6509355275596c451f9d082223cc16bfe504e1a..dd16e1a7403970cc3a404355f95ed99580776353 100644
383--- a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts
384+++ b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts
385@@ -48,6 +48,7 @@ export interface NativeProps extends ViewProps {
386 onPressAction?: DirectEventHandler<{ event: string }>;
387 onCloseMenu?: DirectEventHandler<{ event: string }>;
388 onOpenMenu?: DirectEventHandler<{ event: string }>;
389+ onMenuInteractionStart?: DirectEventHandler<{ event: string }>;
390 actions: Array<MenuAction>;
391 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)
392 title?: string;
393diff --git a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts
394index 24ee7fdcd9f81cfe2c813cfca9d4e276b846fb7b..c1b4ea53b29193283295d35c18e068f2836c6176 100644
395--- a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts
396+++ b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts
397@@ -41,6 +41,9 @@ export interface NativeProps extends ViewProps {
398 onOpenMenu?: DirectEventHandler<{
399 event: string;
400 }>;
401+ onMenuInteractionStart?: DirectEventHandler<{
402+ event: string;
403+ }>;
404 actions: Array<MenuAction>;
405 actionsHash: string;
406 title?: string;
407diff --git a/lib/typescript/src/index.d.ts b/lib/typescript/src/index.d.ts
408index 61d210aa9364f0354f77bddaecdc0a127b1cdbb4..0672bea3352b9ad0623124e308f740c70a7e6940 100644
409--- a/lib/typescript/src/index.d.ts
410+++ b/lib/typescript/src/index.d.ts
411@@ -4,6 +4,7 @@ declare const MenuView: import("react").ForwardRefExoticComponent<{
412 onPressAction?: ({ nativeEvent }: NativeActionEvent) => void;
413 onCloseMenu?: () => void;
414 onOpenMenu?: () => void;
415+ onMenuInteractionStart?: () => void;
416 actions: MenuAction[];
417 title?: string;
418 isAnchoredToRight?: boolean;
419diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts
420index eac7940ab7eb6a69ac329848c9f3eeffdcbaf43d..19e82df15b042bc0cda27349b2b8a8ee1d3f0b51 100644
421--- a/lib/typescript/src/types.d.ts
422+++ b/lib/typescript/src/types.d.ts
423@@ -113,6 +113,12 @@ type MenuComponentPropsBase = {
424 * Callback function that will be called when the menu opens.
425 */
426 onOpenMenu?: () => void;
427+ /**
428+ * Callback function that will be called when UIKit starts preparing a menu interaction.
429+ * Preparation can end without the menu opening.
430+ * @platform iOS
431+ */
432+ onMenuInteractionStart?: () => void;
433 /**
434 * Actions to be displayed in the menu.
435 */
436@@ -170,6 +176,7 @@ export type NativeMenuComponentProps = {
437 onPressAction?: ({ nativeEvent }: NativeActionEvent) => void;
438 onCloseMenu?: () => void;
439 onOpenMenu?: () => void;
440+ onMenuInteractionStart?: () => void;
441 actions: ProcessedMenuAction[];
442 actionsHash: string;
443 title?: string;
444diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.ts
445index e6509355275596c451f9d082223cc16bfe504e1a..604dba0d90a2545910f8724a80205565de2391d4 100644
446--- a/src/NativeModuleSpecs/UIMenuNativeComponent.ts
447+++ b/src/NativeModuleSpecs/UIMenuNativeComponent.ts
448@@ -13,6 +13,22 @@ import codegenNativeComponent from "react-native/Libraries/Utilities/codegenNati
449 types here, to avoid issues while `pod install` takes place.
450 */
451
452+type SubSubAction = {
453+ id?: string;
454+ title: string;
455+ titleColor?: Int32;
456+ subtitle?: string;
457+ state?: string;
458+ image?: string;
459+ imageColor?: Int32;
460+ displayInline?: boolean;
461+ attributes?: {
462+ destructive?: boolean;
463+ disabled?: boolean;
464+ hidden?: boolean;
465+ keepsMenuPresented?: boolean;
466+ };
467+};
468 type SubAction = {
469 id?: string;
470 title: string;
471@@ -26,7 +42,11 @@ type SubAction = {
472 destructive?: boolean;
473 disabled?: boolean;
474 hidden?: boolean;
475+ keepsMenuPresented?: boolean;
476 };
477+ // One extra nesting level (menu → submenu → nested submenu leaves); the
478+ // codegen structs can't recurse, so depth is capped explicitly.
479+ subactions?: Array<SubSubAction>;
480 };
481 type MenuAction = {
482 id?: string;
483@@ -41,6 +61,7 @@ type MenuAction = {
484 destructive?: boolean;
485 disabled?: boolean;
486 hidden?: boolean;
487+ keepsMenuPresented?: boolean;
488 };
489 subactions?: Array<SubAction>;
490 };
491@@ -48,6 +69,7 @@ export interface NativeProps extends ViewProps {
492 onPressAction?: DirectEventHandler<{ event: string }>;
493 onCloseMenu?: DirectEventHandler<{ event: string }>;
494 onOpenMenu?: DirectEventHandler<{ event: string }>;
495+ onMenuInteractionStart?: DirectEventHandler<{ event: string }>;
496 actions: Array<MenuAction>;
497 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)
498 title?: string;
499diff --git a/src/types.ts b/src/types.ts
500index 9c323d9c510e7359c83d4d6633affde673570632..5d5fdfb8b8235fcaa8295b85630443eaf5991a17 100644
501--- a/src/types.ts
502+++ b/src/types.ts
503@@ -123,6 +123,12 @@ type MenuComponentPropsBase = {
504 * Callback function that will be called when the menu opens.
505 */
506 onOpenMenu?: () => void;
507+ /**
508+ * Callback function that will be called when UIKit starts preparing a menu interaction.
509+ * Preparation can end without the menu opening.
510+ * @platform iOS
511+ */
512+ onMenuInteractionStart?: () => void;
513 /**
514 * Actions to be displayed in the menu.
515 */
516@@ -189,6 +195,7 @@ export type NativeMenuComponentProps = {
517 onPressAction?: ({ nativeEvent }: NativeActionEvent) => void;
518 onCloseMenu?: () => void;
519 onOpenMenu?: () => void;
520+ onMenuInteractionStart?: () => void;
521 actions: ProcessedMenuAction[];
522 actionsHash: string;
523 title?: string;