| 1 | diff --git a/ios/MenuViewManager.mm b/ios/MenuViewManager.mm |
| 2 | index 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 | */ |
| 24 | diff --git a/ios/NewArch/FabricActionSheetView.swift b/ios/NewArch/FabricActionSheetView.swift |
| 25 | index 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 { |
| 36 | diff --git a/ios/NewArch/FabricMenuViewImplementation.swift b/ios/NewArch/FabricMenuViewImplementation.swift |
| 37 | index 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 | } |
| 59 | diff --git a/ios/NewArch/FabricViewImplementationProtocol.swift b/ios/NewArch/FabricViewImplementationProtocol.swift |
| 60 | index 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 | } |
| 69 | diff --git a/ios/NewArch/MenuView.mm b/ios/NewArch/MenuView.mm |
| 70 | index 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 | }; |
| 145 | diff --git a/ios/OldArch/LegacyActionSheetView.swift b/ios/OldArch/LegacyActionSheetView.swift |
| 146 | index 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 | |
| 157 | diff --git a/ios/OldArch/LegacyMenuViewImplementation.swift b/ios/OldArch/LegacyMenuViewImplementation.swift |
| 158 | index 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 | } |
| 180 | diff --git a/ios/Shared/MenuViewImplementation.swift b/ios/Shared/MenuViewImplementation.swift |
| 181 | index 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 | } |
| 345 | diff --git a/ios/Shared/RCTMenuItem.swift b/ios/Shared/RCTMenuItem.swift |
| 346 | index 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, *) { |
| 369 | diff --git a/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/commonjs/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 370 | index 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; |
| 381 | diff --git a/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts b/lib/module/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 382 | index 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; |
| 393 | diff --git a/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts b/lib/typescript/src/NativeModuleSpecs/UIMenuNativeComponent.d.ts |
| 394 | index 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; |
| 407 | diff --git a/lib/typescript/src/index.d.ts b/lib/typescript/src/index.d.ts |
| 408 | index 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; |
| 419 | diff --git a/lib/typescript/src/types.d.ts b/lib/typescript/src/types.d.ts |
| 420 | index 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; |
| 444 | diff --git a/src/NativeModuleSpecs/UIMenuNativeComponent.ts b/src/NativeModuleSpecs/UIMenuNativeComponent.ts |
| 445 | index 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; |
| 499 | diff --git a/src/types.ts b/src/types.ts |
| 500 | index 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; |