react-native-platform-components 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ // PCContextMenu.mm
2
+
3
+ #import "PCContextMenu.h"
4
+
5
+ #import <React/RCTComponentViewFactory.h>
6
+ #import <React/RCTConversions.h>
7
+ #import <React/RCTFabricComponentsPlugins.h>
8
+
9
+ #import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
10
+ #import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
11
+ #import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
12
+
13
+ #if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
14
+ #import <PlatformComponents/PlatformComponents-Swift.h>
15
+ #else
16
+ #import "PlatformComponents-Swift.h"
17
+ #endif
18
+
19
+ using namespace facebook::react;
20
+
21
+ namespace {
22
+ // Helper to convert subaction struct to NSDictionary
23
+ static NSDictionary *SubactionToDict(const PCContextMenuActionsSubactionsStruct &action) {
24
+ NSMutableDictionary *dict = [NSMutableDictionary new];
25
+
26
+ dict[@"id"] = action.id.empty() ? @"" : [NSString stringWithUTF8String:action.id.c_str()];
27
+ dict[@"title"] = action.title.empty() ? @"" : [NSString stringWithUTF8String:action.title.c_str()];
28
+
29
+ if (!action.subtitle.empty()) {
30
+ dict[@"subtitle"] = [NSString stringWithUTF8String:action.subtitle.c_str()];
31
+ }
32
+
33
+ if (!action.image.empty()) {
34
+ dict[@"image"] = [NSString stringWithUTF8String:action.image.c_str()];
35
+ }
36
+
37
+ if (!action.imageColor.empty()) {
38
+ dict[@"imageColor"] = [NSString stringWithUTF8String:action.imageColor.c_str()];
39
+ }
40
+
41
+ // Check if any attributes field is non-empty
42
+ if (!action.attributes.destructive.empty() ||
43
+ !action.attributes.disabled.empty() ||
44
+ !action.attributes.hidden.empty()) {
45
+ NSMutableDictionary *attrs = [NSMutableDictionary new];
46
+ attrs[@"destructive"] = action.attributes.destructive.empty() ? @"false" :
47
+ [NSString stringWithUTF8String:action.attributes.destructive.c_str()];
48
+ attrs[@"disabled"] = action.attributes.disabled.empty() ? @"false" :
49
+ [NSString stringWithUTF8String:action.attributes.disabled.c_str()];
50
+ attrs[@"hidden"] = action.attributes.hidden.empty() ? @"false" :
51
+ [NSString stringWithUTF8String:action.attributes.hidden.c_str()];
52
+ dict[@"attributes"] = attrs;
53
+ }
54
+
55
+ if (!action.state.empty()) {
56
+ dict[@"state"] = [NSString stringWithUTF8String:action.state.c_str()];
57
+ }
58
+
59
+ return dict;
60
+ }
61
+
62
+ // Helper to convert action struct to NSDictionary
63
+ static NSDictionary *ActionToDict(const PCContextMenuActionsStruct &action) {
64
+ NSMutableDictionary *dict = [NSMutableDictionary new];
65
+
66
+ dict[@"id"] = action.id.empty() ? @"" : [NSString stringWithUTF8String:action.id.c_str()];
67
+ dict[@"title"] = action.title.empty() ? @"" : [NSString stringWithUTF8String:action.title.c_str()];
68
+
69
+ if (!action.subtitle.empty()) {
70
+ dict[@"subtitle"] = [NSString stringWithUTF8String:action.subtitle.c_str()];
71
+ }
72
+
73
+ if (!action.image.empty()) {
74
+ dict[@"image"] = [NSString stringWithUTF8String:action.image.c_str()];
75
+ }
76
+
77
+ if (!action.imageColor.empty()) {
78
+ dict[@"imageColor"] = [NSString stringWithUTF8String:action.imageColor.c_str()];
79
+ }
80
+
81
+ // Check if any attributes field is non-empty
82
+ if (!action.attributes.destructive.empty() ||
83
+ !action.attributes.disabled.empty() ||
84
+ !action.attributes.hidden.empty()) {
85
+ NSMutableDictionary *attrs = [NSMutableDictionary new];
86
+ attrs[@"destructive"] = action.attributes.destructive.empty() ? @"false" :
87
+ [NSString stringWithUTF8String:action.attributes.destructive.c_str()];
88
+ attrs[@"disabled"] = action.attributes.disabled.empty() ? @"false" :
89
+ [NSString stringWithUTF8String:action.attributes.disabled.c_str()];
90
+ attrs[@"hidden"] = action.attributes.hidden.empty() ? @"false" :
91
+ [NSString stringWithUTF8String:action.attributes.hidden.c_str()];
92
+ dict[@"attributes"] = attrs;
93
+ }
94
+
95
+ if (!action.state.empty()) {
96
+ dict[@"state"] = [NSString stringWithUTF8String:action.state.c_str()];
97
+ }
98
+
99
+ // Convert subactions (vector, not optional)
100
+ if (!action.subactions.empty()) {
101
+ NSMutableArray *subs = [NSMutableArray new];
102
+ for (const auto &sub : action.subactions) {
103
+ [subs addObject:SubactionToDict(sub)];
104
+ }
105
+ dict[@"subactions"] = subs;
106
+ }
107
+
108
+ return dict;
109
+ }
110
+
111
+ static bool ActionsEqual(
112
+ const std::vector<PCContextMenuActionsStruct> &a,
113
+ const std::vector<PCContextMenuActionsStruct> &b) {
114
+ if (a.size() != b.size()) return false;
115
+ for (size_t i = 0; i < a.size(); i++) {
116
+ if (a[i].id != b[i].id) return false;
117
+ if (a[i].title != b[i].title) return false;
118
+ // Simplified comparison - in production, compare all fields
119
+ }
120
+ return true;
121
+ }
122
+ } // namespace
123
+
124
+ @implementation PCContextMenu {
125
+ PCContextMenuView *_view;
126
+ }
127
+
128
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
129
+ return concreteComponentDescriptorProvider<PCContextMenuComponentDescriptor>();
130
+ }
131
+
132
+ - (instancetype)initWithFrame:(CGRect)frame {
133
+ if (self = [super initWithFrame:frame]) {
134
+ _view = [PCContextMenuView new];
135
+ self.contentView = _view;
136
+
137
+ __weak __typeof(self) weakSelf = self;
138
+
139
+ _view.onPressAction = ^(NSString *actionId, NSString *actionTitle) {
140
+ __typeof(self) strongSelf = weakSelf;
141
+ if (!strongSelf) return;
142
+
143
+ auto eventEmitter =
144
+ std::static_pointer_cast<const PCContextMenuEventEmitter>(
145
+ strongSelf->_eventEmitter);
146
+ if (!eventEmitter) return;
147
+
148
+ PCContextMenuEventEmitter::OnPressAction payload = {
149
+ .actionId = actionId.UTF8String,
150
+ .actionTitle = actionTitle.UTF8String,
151
+ };
152
+
153
+ eventEmitter->onPressAction(payload);
154
+ };
155
+
156
+ _view.onMenuOpen = ^{
157
+ __typeof(self) strongSelf = weakSelf;
158
+ if (!strongSelf) return;
159
+
160
+ auto eventEmitter =
161
+ std::static_pointer_cast<const PCContextMenuEventEmitter>(
162
+ strongSelf->_eventEmitter);
163
+ if (!eventEmitter) return;
164
+
165
+ eventEmitter->onMenuOpen({});
166
+ };
167
+
168
+ _view.onMenuClose = ^{
169
+ __typeof(self) strongSelf = weakSelf;
170
+ if (!strongSelf) return;
171
+
172
+ auto eventEmitter =
173
+ std::static_pointer_cast<const PCContextMenuEventEmitter>(
174
+ strongSelf->_eventEmitter);
175
+ if (!eventEmitter) return;
176
+
177
+ eventEmitter->onMenuClose({});
178
+ };
179
+ }
180
+ return self;
181
+ }
182
+
183
+ - (void)updateProps:(Props::Shared const &)props
184
+ oldProps:(Props::Shared const &)oldProps {
185
+ const auto &newProps =
186
+ *std::static_pointer_cast<const PCContextMenuProps>(props);
187
+ const auto prevProps =
188
+ std::static_pointer_cast<const PCContextMenuProps>(oldProps);
189
+
190
+ // title
191
+ if (!prevProps || newProps.title != prevProps->title) {
192
+ if (!newProps.title.empty()) {
193
+ _view.menuTitle = [NSString stringWithUTF8String:newProps.title.c_str()];
194
+ } else {
195
+ _view.menuTitle = nil;
196
+ }
197
+ }
198
+
199
+ // actions
200
+ if (!prevProps || !ActionsEqual(newProps.actions, prevProps->actions)) {
201
+ NSMutableArray *arr = [NSMutableArray new];
202
+ for (const auto &action : newProps.actions) {
203
+ [arr addObject:ActionToDict(action)];
204
+ }
205
+ _view.actions = arr;
206
+ }
207
+
208
+ // interactivity: "enabled" | "disabled"
209
+ if (!prevProps || newProps.interactivity != prevProps->interactivity) {
210
+ if (!newProps.interactivity.empty()) {
211
+ _view.interactivity =
212
+ [NSString stringWithUTF8String:newProps.interactivity.c_str()];
213
+ } else {
214
+ _view.interactivity = @"enabled";
215
+ }
216
+ }
217
+
218
+ // trigger: "longPress" | "tap"
219
+ if (!prevProps || newProps.trigger != prevProps->trigger) {
220
+ if (!newProps.trigger.empty()) {
221
+ _view.trigger =
222
+ [NSString stringWithUTF8String:newProps.trigger.c_str()];
223
+ } else {
224
+ _view.trigger = @"longPress";
225
+ }
226
+ }
227
+
228
+ // iOS-specific props
229
+ const auto &newIOS = newProps.ios;
230
+ const auto &oldIOS = prevProps ? prevProps->ios : PCContextMenuIosStruct{};
231
+ if (!prevProps || newIOS.enablePreview != oldIOS.enablePreview) {
232
+ if (!newIOS.enablePreview.empty()) {
233
+ _view.enablePreview =
234
+ [NSString stringWithUTF8String:newIOS.enablePreview.c_str()];
235
+ } else {
236
+ _view.enablePreview = @"false";
237
+ }
238
+ }
239
+
240
+ [super updateProps:props oldProps:oldProps];
241
+ }
242
+
243
+ @end
244
+
245
+ Class<RCTComponentViewProtocol> PCContextMenuCls(void) {
246
+ return PCContextMenu.class;
247
+ }
@@ -0,0 +1,346 @@
1
+ import os.log
2
+ import UIKit
3
+
4
+ private let logger = Logger(subsystem: "com.platformcomponents", category: "ContextMenu")
5
+
6
+ // MARK: - Action model (bridged from ObjC++ as dictionaries)
7
+
8
+ struct PCContextMenuAction {
9
+ let id: String
10
+ let title: String
11
+ let subtitle: String?
12
+ let image: String?
13
+ let imageColor: String?
14
+ let destructive: Bool
15
+ let disabled: Bool
16
+ let hidden: Bool
17
+ let state: String? // "off" | "on" | "mixed"
18
+ let subactions: [PCContextMenuAction]
19
+
20
+ init(from dict: [String: Any]) {
21
+ self.id = (dict["id"] as? String) ?? ""
22
+ self.title = (dict["title"] as? String) ?? ""
23
+ self.subtitle = dict["subtitle"] as? String
24
+ self.image = dict["image"] as? String
25
+ self.imageColor = dict["imageColor"] as? String
26
+
27
+ // Parse attributes
28
+ if let attrs = dict["attributes"] as? [String: Any] {
29
+ self.destructive = (attrs["destructive"] as? String) == "true"
30
+ self.disabled = (attrs["disabled"] as? String) == "true"
31
+ self.hidden = (attrs["hidden"] as? String) == "true"
32
+ } else {
33
+ self.destructive = false
34
+ self.disabled = false
35
+ self.hidden = false
36
+ }
37
+
38
+ self.state = dict["state"] as? String
39
+
40
+ // Parse subactions recursively
41
+ if let subs = dict["subactions"] as? [[String: Any]] {
42
+ self.subactions = subs.map { PCContextMenuAction(from: $0) }
43
+ } else {
44
+ self.subactions = []
45
+ }
46
+ }
47
+ }
48
+
49
+ // MARK: - Main View
50
+
51
+ @objcMembers
52
+ public final class PCContextMenuView: UIView, UIContextMenuInteractionDelegate {
53
+ // MARK: - Props (set from ObjC++)
54
+
55
+ /// Menu title (shown as header on iOS)
56
+ public var menuTitle: String? { didSet { sync() } }
57
+
58
+ /// ObjC++ sets this as an array of dictionaries
59
+ public var actions: [Any] = [] { didSet { sync() } }
60
+
61
+ /// "enabled" | "disabled"
62
+ public var interactivity: String = "enabled" {
63
+ didSet {
64
+ updateEnabled()
65
+ sync()
66
+ }
67
+ }
68
+
69
+ /// "longPress" | "tap"
70
+ public var trigger: String = "longPress" { didSet { updateTrigger() } }
71
+
72
+ /// iOS-specific: enable preview
73
+ public var enablePreview: String = "false"
74
+
75
+ // MARK: - Events back to ObjC++
76
+
77
+ public var onPressAction: ((String, String) -> Void)? // (id, title)
78
+ public var onMenuOpen: (() -> Void)?
79
+ public var onMenuClose: (() -> Void)?
80
+
81
+ // MARK: - Internal
82
+
83
+ private var contextMenuInteraction: UIContextMenuInteraction?
84
+ private var parsedActions: [PCContextMenuAction] {
85
+ (actions as? [[String: Any]])?.map { PCContextMenuAction(from: $0) } ?? []
86
+ }
87
+
88
+ // Tap mode: UIButton with UIMenu for tap-to-show
89
+ private var tapMenuButton: UIButton?
90
+
91
+ // MARK: - Init
92
+
93
+ public override init(frame: CGRect) {
94
+ super.init(frame: frame)
95
+ setup()
96
+ }
97
+
98
+ public required init?(coder: NSCoder) {
99
+ super.init(coder: coder)
100
+ setup()
101
+ }
102
+
103
+ private func setup() {
104
+ backgroundColor = .clear
105
+ updateEnabled()
106
+ updateTrigger()
107
+ }
108
+
109
+ public override func layoutSubviews() {
110
+ super.layoutSubviews()
111
+ // Ensure tap button stays on top of React Native children
112
+ if let button = tapMenuButton {
113
+ bringSubviewToFront(button)
114
+ }
115
+ }
116
+
117
+ private func updateEnabled() {
118
+ let disabled = (interactivity == "disabled")
119
+ alpha = disabled ? 0.5 : 1.0
120
+ isUserInteractionEnabled = !disabled
121
+ accessibilityTraits = disabled ? [.notEnabled] : []
122
+ }
123
+
124
+ private func sync() {
125
+ // Re-setup trigger mode if needed
126
+ updateTrigger()
127
+ // Update tap menu content if in tap mode
128
+ if trigger == "tap" {
129
+ updateTapMenuButton()
130
+ }
131
+ }
132
+
133
+ // MARK: - Trigger Mode
134
+
135
+ private func updateTrigger() {
136
+ if trigger == "longPress" {
137
+ // Install context menu interaction for long-press
138
+ installContextMenuInteraction()
139
+ removeTapMenuButton()
140
+ } else {
141
+ // Tap mode: use UIButton with UIMenu for tap-to-show
142
+ removeContextMenuInteraction()
143
+ installTapMenuButton()
144
+ }
145
+ }
146
+
147
+ private func installContextMenuInteraction() {
148
+ guard contextMenuInteraction == nil else { return }
149
+ let interaction = UIContextMenuInteraction(delegate: self)
150
+ addInteraction(interaction)
151
+ contextMenuInteraction = interaction
152
+ logger.debug("Installed UIContextMenuInteraction")
153
+ }
154
+
155
+ private func removeContextMenuInteraction() {
156
+ guard let interaction = contextMenuInteraction else { return }
157
+ removeInteraction(interaction)
158
+ contextMenuInteraction = nil
159
+ logger.debug("Removed UIContextMenuInteraction")
160
+ }
161
+
162
+ // MARK: - Tap Mode Button
163
+
164
+ private func installTapMenuButton() {
165
+ guard tapMenuButton == nil else {
166
+ updateTapMenuButton()
167
+ return
168
+ }
169
+
170
+ let button = UIButton(type: .custom)
171
+ button.backgroundColor = .clear
172
+ button.showsMenuAsPrimaryAction = true
173
+ button.translatesAutoresizingMaskIntoConstraints = false
174
+
175
+ // Add context menu interaction to track menu lifecycle
176
+ let interaction = UIContextMenuInteraction(delegate: self)
177
+ button.addInteraction(interaction)
178
+
179
+ addSubview(button)
180
+ NSLayoutConstraint.activate([
181
+ button.topAnchor.constraint(equalTo: topAnchor),
182
+ button.bottomAnchor.constraint(equalTo: bottomAnchor),
183
+ button.leadingAnchor.constraint(equalTo: leadingAnchor),
184
+ button.trailingAnchor.constraint(equalTo: trailingAnchor)
185
+ ])
186
+
187
+ tapMenuButton = button
188
+ updateTapMenuButton()
189
+ logger.debug("Installed tap menu button")
190
+ }
191
+
192
+ private func removeTapMenuButton() {
193
+ tapMenuButton?.removeFromSuperview()
194
+ tapMenuButton = nil
195
+ }
196
+
197
+ private func updateTapMenuButton() {
198
+ guard let button = tapMenuButton else { return }
199
+
200
+ let actions = parsedActions.filter { !$0.hidden }
201
+ guard !actions.isEmpty else {
202
+ button.menu = nil
203
+ return
204
+ }
205
+
206
+ let menu = buildMenu(from: actions, title: menuTitle)
207
+ button.menu = menu
208
+ }
209
+
210
+ // MARK: - UIContextMenuInteractionDelegate
211
+
212
+ public func contextMenuInteraction(
213
+ _ interaction: UIContextMenuInteraction,
214
+ configurationForMenuAtLocation location: CGPoint
215
+ ) -> UIContextMenuConfiguration? {
216
+ guard interactivity != "disabled" else { return nil }
217
+
218
+ let actions = parsedActions.filter { !$0.hidden }
219
+ guard !actions.isEmpty else { return nil }
220
+
221
+ logger.debug("contextMenuInteraction: creating configuration with \(actions.count) actions")
222
+
223
+ return UIContextMenuConfiguration(
224
+ identifier: nil,
225
+ previewProvider: enablePreview == "true" ? { [weak self] in
226
+ // Return a preview controller showing the content
227
+ guard let self else { return nil }
228
+ let preview = UIViewController()
229
+ preview.view.backgroundColor = .clear
230
+
231
+ // Snapshot the current view for preview
232
+ let snapshot = self.snapshotView(afterScreenUpdates: false)
233
+ if let snap = snapshot {
234
+ snap.frame = CGRect(origin: .zero, size: self.bounds.size)
235
+ preview.view.addSubview(snap)
236
+ preview.preferredContentSize = self.bounds.size
237
+ }
238
+ return preview
239
+ } : nil,
240
+ actionProvider: { [weak self] suggestedActions in
241
+ guard let self else { return nil }
242
+ return self.buildMenu(from: actions, title: self.menuTitle)
243
+ }
244
+ )
245
+ }
246
+
247
+ public func contextMenuInteraction(
248
+ _ interaction: UIContextMenuInteraction,
249
+ willDisplayMenuFor configuration: UIContextMenuConfiguration,
250
+ animator: UIContextMenuInteractionAnimating?
251
+ ) {
252
+ logger.debug("contextMenuInteraction: willDisplayMenu")
253
+ onMenuOpen?()
254
+ }
255
+
256
+ public func contextMenuInteraction(
257
+ _ interaction: UIContextMenuInteraction,
258
+ willEndFor configuration: UIContextMenuConfiguration,
259
+ animator: UIContextMenuInteractionAnimating?
260
+ ) {
261
+ logger.debug("contextMenuInteraction: willEnd")
262
+ onMenuClose?()
263
+ }
264
+
265
+ // MARK: - Menu Building
266
+
267
+ private func buildMenu(from actions: [PCContextMenuAction], title: String?) -> UIMenu {
268
+ let menuElements = actions.compactMap { buildMenuElement(from: $0) }
269
+
270
+ return UIMenu(
271
+ title: title ?? "",
272
+ children: menuElements
273
+ )
274
+ }
275
+
276
+ private func buildMenuElement(from action: PCContextMenuAction) -> UIMenuElement? {
277
+ guard !action.hidden else { return nil }
278
+
279
+ // If has subactions, create a submenu
280
+ if !action.subactions.isEmpty {
281
+ let children = action.subactions.compactMap { buildMenuElement(from: $0) }
282
+ return UIMenu(
283
+ title: action.title,
284
+ image: imageForAction(action),
285
+ children: children
286
+ )
287
+ }
288
+
289
+ // Build action attributes
290
+ var attributes: UIMenuElement.Attributes = []
291
+ if action.destructive { attributes.insert(.destructive) }
292
+ if action.disabled { attributes.insert(.disabled) }
293
+
294
+ // Build state
295
+ let state: UIMenuElement.State
296
+ switch action.state {
297
+ case "on": state = .on
298
+ case "mixed": state = .mixed
299
+ default: state = .off
300
+ }
301
+
302
+ let uiAction = UIAction(
303
+ title: action.title,
304
+ subtitle: action.subtitle,
305
+ image: imageForAction(action),
306
+ attributes: attributes,
307
+ state: state
308
+ ) { [weak self] _ in
309
+ logger.debug("UIAction selected: id=\(action.id), title=\(action.title)")
310
+ self?.onPressAction?(action.id, action.title)
311
+ }
312
+
313
+ return uiAction
314
+ }
315
+
316
+ private func imageForAction(_ action: PCContextMenuAction) -> UIImage? {
317
+ guard let imageName = action.image, !imageName.isEmpty else { return nil }
318
+
319
+ var image = UIImage(systemName: imageName)
320
+
321
+ // Apply tint color if specified
322
+ if let colorStr = action.imageColor, !colorStr.isEmpty, let color = colorFromString(colorStr) {
323
+ image = image?.withTintColor(color, renderingMode: .alwaysOriginal)
324
+ }
325
+
326
+ return image
327
+ }
328
+
329
+ private func colorFromString(_ str: String) -> UIColor? {
330
+ // Support hex colors like "#FF0000" or "FF0000"
331
+ var hex = str.trimmingCharacters(in: .whitespacesAndNewlines)
332
+ if hex.hasPrefix("#") {
333
+ hex = String(hex.dropFirst())
334
+ }
335
+
336
+ guard hex.count == 6, let rgbValue = UInt64(hex, radix: 16) else {
337
+ return nil
338
+ }
339
+
340
+ let r = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
341
+ let g = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
342
+ let b = CGFloat(rgbValue & 0x0000FF) / 255.0
343
+
344
+ return UIColor(red: r, green: g, blue: b, alpha: 1.0)
345
+ }
346
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ // ContextMenu.tsx
4
+ import React, { useCallback, useMemo } from 'react';
5
+ import NativeContextMenu from './ContextMenuNativeComponent';
6
+
7
+ /**
8
+ * Attributes for a context menu action.
9
+ */
10
+
11
+ /**
12
+ * A single action in the context menu.
13
+ */
14
+ import { jsx as _jsx } from "react/jsx-runtime";
15
+ /**
16
+ * Convert user-friendly subaction to native format (no further nesting).
17
+ */
18
+ function normalizeSubaction(action) {
19
+ return {
20
+ id: action.id,
21
+ title: action.title,
22
+ subtitle: action.subtitle,
23
+ image: action.image,
24
+ imageColor: action.imageColor,
25
+ attributes: action.attributes ? {
26
+ destructive: action.attributes.destructive ? 'true' : 'false',
27
+ disabled: action.attributes.disabled ? 'true' : 'false',
28
+ hidden: action.attributes.hidden ? 'true' : 'false'
29
+ } : undefined,
30
+ state: action.state
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Convert user-friendly action to native format.
36
+ * Note: Only one level of nesting is supported by the native component.
37
+ */
38
+ function normalizeAction(action) {
39
+ return {
40
+ id: action.id,
41
+ title: action.title,
42
+ subtitle: action.subtitle,
43
+ image: action.image,
44
+ imageColor: action.imageColor,
45
+ attributes: action.attributes ? {
46
+ destructive: action.attributes.destructive ? 'true' : 'false',
47
+ disabled: action.attributes.disabled ? 'true' : 'false',
48
+ hidden: action.attributes.hidden ? 'true' : 'false'
49
+ } : undefined,
50
+ state: action.state,
51
+ subactions: action.subactions?.map(normalizeSubaction)
52
+ };
53
+ }
54
+ export function ContextMenu(props) {
55
+ const {
56
+ style,
57
+ title,
58
+ actions,
59
+ disabled,
60
+ trigger = 'longPress',
61
+ onPressAction,
62
+ onMenuOpen,
63
+ onMenuClose,
64
+ children,
65
+ ios,
66
+ android,
67
+ ...viewProps
68
+ } = props;
69
+ const nativeActions = useMemo(() => actions.map(normalizeAction), [actions]);
70
+ const handlePressAction = useCallback(e => {
71
+ const {
72
+ actionId,
73
+ actionTitle
74
+ } = e.nativeEvent;
75
+ onPressAction?.(actionId, actionTitle);
76
+ }, [onPressAction]);
77
+ const handleMenuOpen = useCallback(() => {
78
+ onMenuOpen?.();
79
+ }, [onMenuOpen]);
80
+ const handleMenuClose = useCallback(() => {
81
+ onMenuClose?.();
82
+ }, [onMenuClose]);
83
+ const nativeIOS = useMemo(() => {
84
+ if (!ios) return undefined;
85
+ return {
86
+ enablePreview: ios.enablePreview ? 'true' : 'false'
87
+ };
88
+ }, [ios]);
89
+ const nativeAndroid = useMemo(() => {
90
+ if (!android) return undefined;
91
+ return {
92
+ anchorPosition: android.anchorPosition,
93
+ visible: android.visible ? 'open' : 'closed'
94
+ };
95
+ }, [android]);
96
+ return /*#__PURE__*/_jsx(NativeContextMenu, {
97
+ style: style,
98
+ title: title,
99
+ actions: nativeActions,
100
+ interactivity: disabled ? 'disabled' : 'enabled',
101
+ trigger: trigger,
102
+ onPressAction: onPressAction ? handlePressAction : undefined,
103
+ onMenuOpen: onMenuOpen ? handleMenuOpen : undefined,
104
+ onMenuClose: onMenuClose ? handleMenuClose : undefined,
105
+ ios: nativeIOS,
106
+ android: nativeAndroid,
107
+ ...viewProps,
108
+ children: children
109
+ });
110
+ }
111
+ //# sourceMappingURL=ContextMenu.js.map