Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions ios/NewArch/MenuView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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()],
Expand All @@ -118,7 +139,9 @@ - (void)onOpenMenu {
@"destructive": @(subaction.attributes.destructive),
@"disabled": @(subaction.attributes.disabled),
@"hidden": @(subaction.attributes.hidden),
@"keepsMenuPresented": @(subaction.attributes.keepsMenuPresented),
},
@"subactions": subSubactionsArray,
};
[subactionsArray addObject:subactionDict];
}
Expand All @@ -138,6 +161,7 @@ - (void)onOpenMenu {
@"destructive": @(action.attributes.destructive),
@"disabled": @(action.attributes.disabled),
@"hidden": @(action.attributes.hidden),
@"keepsMenuPresented": @(action.attributes.keepsMenuPresented),
},
@"subactions": subactionsArray,
};
Expand Down
67 changes: 67 additions & 0 deletions ios/Shared/MenuViewImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,21 @@ public class MenuViewImplementation: UIButton {
self.setup()
}

// Presentation is tracked from the two delegate methods the class already
// overrode. Overriding willDisplayMenuFor as well (even for bookkeeping)
// shadows UIButton's own implementation and degrades the button-anchored
// presentation into generic context-menu chrome — an empty header row with
// a dismiss chevron appears above the actions.
public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
// 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
sendMenuOpen()
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
guard let self = self else { return nil }
Expand All @@ -69,8 +83,16 @@ public class MenuViewImplementation: UIButton {

public override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionAnimating?) {
sendMenuClose()
isMenuPresented = false
if pendingMenu != nil {
pendingMenu = nil
self.setup()
}
}

private var isMenuPresented = false
private var pendingMenu: UIMenu?

func setup () {
let menu = UIMenu(title: _title,
identifier: nil,
Expand All @@ -86,10 +108,55 @@ public class MenuViewImplementation: UIButton {
}
}

if isMenuPresented {
pendingMenu = menu
self.refreshPresentedMenu(menu)
return
}

self.menu = menu
self.showsMenuAsPrimaryAction = !shouldOpenOnLongPress
}

private func refreshPresentedMenu(_ menu: UIMenu) {
var candidates = self.interactions.compactMap { $0 as? UIContextMenuInteraction }
if let interaction = self.contextMenuInteraction {
candidates.append(interaction)
}

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
}

public override func reactSetFrame(_ frame: CGRect) {
super.reactSetFrame(frame);
}
Expand Down
19 changes: 19 additions & 0 deletions src/NativeModuleSpecs/UIMenuNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,7 +42,9 @@ type SubAction = {
destructive?: boolean;
disabled?: boolean;
hidden?: boolean;
keepsMenuPresented?: boolean;
};
subactions?: Array<SubSubAction>;
};
type MenuAction = {
id?: string;
Expand All @@ -41,6 +59,7 @@ type MenuAction = {
destructive?: boolean;
disabled?: boolean;
hidden?: boolean;
keepsMenuPresented?: boolean;
};
subactions?: Array<SubAction>;
};
Expand Down