react-native-platform-components 0.5.5 → 0.6.1

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.
Files changed (78) hide show
  1. package/README.md +342 -72
  2. package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +419 -0
  3. package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +200 -0
  4. package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +4 -0
  5. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
  6. package/app.plugin.cjs +4 -0
  7. package/expo-module.config.json +4 -0
  8. package/ios/PCContextMenu.h +12 -0
  9. package/ios/PCContextMenu.mm +247 -0
  10. package/ios/PCContextMenu.swift +389 -0
  11. package/ios/PCDatePickerView.swift +25 -11
  12. package/lib/commonjs/ContextMenu.js +118 -0
  13. package/lib/commonjs/ContextMenu.js.map +1 -0
  14. package/lib/commonjs/ContextMenuNativeComponent.ts +141 -0
  15. package/lib/commonjs/DatePicker.js +86 -0
  16. package/lib/commonjs/DatePicker.js.map +1 -0
  17. package/lib/commonjs/DatePickerNativeComponent.ts +69 -0
  18. package/lib/commonjs/SelectionMenu.js +73 -0
  19. package/lib/commonjs/SelectionMenu.js.map +1 -0
  20. package/lib/commonjs/SelectionMenuNativeComponent.ts +97 -0
  21. package/lib/commonjs/index.js +50 -0
  22. package/lib/commonjs/index.js.map +1 -0
  23. package/lib/commonjs/package.json +1 -0
  24. package/lib/commonjs/sharedTypes.js +6 -0
  25. package/lib/commonjs/sharedTypes.js.map +1 -0
  26. package/lib/module/ContextMenu.js +111 -0
  27. package/lib/module/ContextMenu.js.map +1 -0
  28. package/lib/module/ContextMenuNativeComponent.ts +141 -0
  29. package/lib/module/index.js +1 -0
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/typescript/commonjs/package.json +1 -0
  32. package/lib/typescript/commonjs/src/ContextMenu.d.ts +79 -0
  33. package/lib/typescript/commonjs/src/ContextMenu.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +122 -0
  35. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -0
  36. package/lib/typescript/commonjs/src/DatePicker.d.ts.map +1 -0
  37. package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/src/SelectionMenu.d.ts.map +1 -0
  39. package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts.map +1 -0
  40. package/lib/typescript/{src → commonjs/src}/index.d.ts +1 -0
  41. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  42. package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -0
  43. package/lib/typescript/module/src/ContextMenu.d.ts +79 -0
  44. package/lib/typescript/module/src/ContextMenu.d.ts.map +1 -0
  45. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +122 -0
  46. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -0
  47. package/lib/typescript/module/src/DatePicker.d.ts +40 -0
  48. package/lib/typescript/module/src/DatePicker.d.ts.map +1 -0
  49. package/lib/typescript/module/src/DatePickerNativeComponent.d.ts +54 -0
  50. package/lib/typescript/module/src/DatePickerNativeComponent.d.ts.map +1 -0
  51. package/lib/typescript/module/src/SelectionMenu.d.ts +47 -0
  52. package/lib/typescript/module/src/SelectionMenu.d.ts.map +1 -0
  53. package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts +78 -0
  54. package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts.map +1 -0
  55. package/lib/typescript/module/src/index.d.ts +5 -0
  56. package/lib/typescript/module/src/index.d.ts.map +1 -0
  57. package/lib/typescript/module/src/sharedTypes.d.ts +12 -0
  58. package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -0
  59. package/package.json +32 -11
  60. package/plugin/build/index.cjs +26 -0
  61. package/plugin/build/index.d.ts +22 -0
  62. package/plugin/build/index.d.ts.map +1 -0
  63. package/plugin/tsconfig.json +16 -0
  64. package/src/ContextMenu.tsx +209 -0
  65. package/src/ContextMenuNativeComponent.ts +141 -0
  66. package/src/index.tsx +1 -0
  67. package/lib/typescript/src/DatePicker.d.ts.map +0 -1
  68. package/lib/typescript/src/DatePickerNativeComponent.d.ts.map +0 -1
  69. package/lib/typescript/src/SelectionMenu.d.ts.map +0 -1
  70. package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +0 -1
  71. package/lib/typescript/src/index.d.ts.map +0 -1
  72. package/lib/typescript/src/sharedTypes.d.ts.map +0 -1
  73. /package/lib/typescript/{src → commonjs/src}/DatePicker.d.ts +0 -0
  74. /package/lib/typescript/{src → commonjs/src}/DatePickerNativeComponent.d.ts +0 -0
  75. /package/lib/typescript/{src → commonjs/src}/SelectionMenu.d.ts +0 -0
  76. /package/lib/typescript/{src → commonjs/src}/SelectionMenuNativeComponent.d.ts +0 -0
  77. /package/lib/typescript/{src → commonjs/src}/sharedTypes.d.ts +0 -0
  78. /package/lib/typescript/{package.json → module/package.json} +0 -0
@@ -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,389 @@
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
+
118
+
119
+ private func updateEnabled() {
120
+ let disabled = (interactivity == "disabled")
121
+ alpha = disabled ? 0.5 : 1.0
122
+ isUserInteractionEnabled = !disabled
123
+ accessibilityTraits = disabled ? [.notEnabled] : []
124
+ }
125
+
126
+ private func sync() {
127
+ // Re-setup trigger mode if needed
128
+ updateTrigger()
129
+ // Update tap menu content if in tap mode
130
+ if trigger == "tap" {
131
+ updateTapMenuButton()
132
+ }
133
+ }
134
+
135
+ // MARK: - Trigger Mode
136
+
137
+ private func updateTrigger() {
138
+ if trigger == "longPress" {
139
+ // Install context menu interaction for long-press
140
+ installContextMenuInteraction()
141
+ removeTapMenuButton()
142
+ } else {
143
+ // Tap mode: use UIButton with UIMenu for tap-to-show
144
+ removeContextMenuInteraction()
145
+ installTapMenuButton()
146
+ }
147
+ }
148
+
149
+ private func installContextMenuInteraction() {
150
+ guard contextMenuInteraction == nil else { return }
151
+
152
+ // Install interaction on self so that long-press on our view triggers the menu.
153
+ // We use UITargetedPreview in the delegate methods to show the parent view
154
+ // (which contains the React content) as the preview.
155
+ let interaction = UIContextMenuInteraction(delegate: self)
156
+ addInteraction(interaction)
157
+ contextMenuInteraction = interaction
158
+ logger.debug("Installed UIContextMenuInteraction on PCContextMenuView")
159
+ }
160
+
161
+ private func removeContextMenuInteraction() {
162
+ guard let interaction = contextMenuInteraction else { return }
163
+ removeInteraction(interaction)
164
+ contextMenuInteraction = nil
165
+ logger.debug("Removed UIContextMenuInteraction")
166
+ }
167
+
168
+ // MARK: - Tap Mode Button
169
+
170
+ private func installTapMenuButton() {
171
+ guard tapMenuButton == nil else {
172
+ updateTapMenuButton()
173
+ return
174
+ }
175
+
176
+ let button = UIButton(type: .system)
177
+ button.backgroundColor = .clear
178
+ button.showsMenuAsPrimaryAction = true
179
+ button.translatesAutoresizingMaskIntoConstraints = false
180
+ // Make button invisible but still tappable
181
+ button.tintColor = .clear
182
+
183
+ addSubview(button)
184
+ NSLayoutConstraint.activate([
185
+ button.topAnchor.constraint(equalTo: topAnchor),
186
+ button.bottomAnchor.constraint(equalTo: bottomAnchor),
187
+ button.leadingAnchor.constraint(equalTo: leadingAnchor),
188
+ button.trailingAnchor.constraint(equalTo: trailingAnchor)
189
+ ])
190
+
191
+ tapMenuButton = button
192
+ updateTapMenuButton()
193
+ logger.debug("Installed tap menu button")
194
+ }
195
+
196
+ private func removeTapMenuButton() {
197
+ tapMenuButton?.removeFromSuperview()
198
+ tapMenuButton = nil
199
+ }
200
+
201
+ private func updateTapMenuButton() {
202
+ guard let button = tapMenuButton else { return }
203
+
204
+ let actions = parsedActions.filter { !$0.hidden }
205
+ guard !actions.isEmpty else {
206
+ button.menu = nil
207
+ return
208
+ }
209
+
210
+ let menu = buildMenu(from: actions, title: menuTitle)
211
+ button.menu = menu
212
+ }
213
+
214
+ // MARK: - UIContextMenuInteractionDelegate
215
+
216
+ public func contextMenuInteraction(
217
+ _ interaction: UIContextMenuInteraction,
218
+ configurationForMenuAtLocation location: CGPoint
219
+ ) -> UIContextMenuConfiguration? {
220
+ guard interactivity != "disabled" else { return nil }
221
+
222
+ let actions = parsedActions.filter { !$0.hidden }
223
+ guard !actions.isEmpty else { return nil }
224
+
225
+ let actionCountStr = String(actions.count)
226
+ logger.debug("contextMenuInteraction: creating configuration with \(actionCountStr) actions")
227
+
228
+ // Use nil previewProvider - we'll use UITargetedPreview via delegate instead
229
+ return UIContextMenuConfiguration(
230
+ identifier: nil,
231
+ previewProvider: nil,
232
+ actionProvider: { [weak self] suggestedActions in
233
+ guard let self else { return nil }
234
+ return self.buildMenu(from: actions, title: self.menuTitle)
235
+ }
236
+ )
237
+ }
238
+
239
+ /// Get the parent component view (PCContextMenu) which contains all React content
240
+ private func getParentComponentView() -> UIView? {
241
+ return superview
242
+ }
243
+
244
+ public func contextMenuInteraction(
245
+ _ interaction: UIContextMenuInteraction,
246
+ previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
247
+ ) -> UITargetedPreview? {
248
+ let parameters = UIPreviewParameters()
249
+ parameters.backgroundColor = .clear
250
+
251
+ // Check if preview is enabled via the enablePreview prop
252
+ if enablePreview == "true" {
253
+ // Target the parent view (PCContextMenu) which contains the React content
254
+ guard let parentView = getParentComponentView() else {
255
+ logger.debug("previewForHighlighting: no parent found")
256
+ return nil
257
+ }
258
+ let parentType = String(describing: type(of: parentView))
259
+ logger.debug("previewForHighlighting: targeting parent \(parentType)")
260
+ parameters.visiblePath = UIBezierPath(roundedRect: parentView.bounds, cornerRadius: 8)
261
+ return UITargetedPreview(view: parentView, parameters: parameters)
262
+ } else {
263
+ // When preview is disabled, target self with zero-size path
264
+ // This prevents iOS from manipulating the parent view and causing white flashes
265
+ logger.debug("previewForHighlighting: preview disabled, targeting self with zero path")
266
+ parameters.visiblePath = UIBezierPath(rect: .zero)
267
+ return UITargetedPreview(view: self, parameters: parameters)
268
+ }
269
+ }
270
+
271
+ public func contextMenuInteraction(
272
+ _ interaction: UIContextMenuInteraction,
273
+ previewForDismissingMenuWithConfiguration configuration: UIContextMenuConfiguration
274
+ ) -> UITargetedPreview? {
275
+ let parameters = UIPreviewParameters()
276
+ parameters.backgroundColor = .clear
277
+
278
+ if enablePreview == "true" {
279
+ guard let parentView = getParentComponentView() else {
280
+ return nil
281
+ }
282
+ parameters.visiblePath = UIBezierPath(roundedRect: parentView.bounds, cornerRadius: 8)
283
+ return UITargetedPreview(view: parentView, parameters: parameters)
284
+ } else {
285
+ parameters.visiblePath = UIBezierPath(rect: .zero)
286
+ return UITargetedPreview(view: self, parameters: parameters)
287
+ }
288
+ }
289
+
290
+ public func contextMenuInteraction(
291
+ _ interaction: UIContextMenuInteraction,
292
+ willDisplayMenuFor configuration: UIContextMenuConfiguration,
293
+ animator: UIContextMenuInteractionAnimating?
294
+ ) {
295
+ logger.debug("contextMenuInteraction: willDisplayMenu")
296
+ onMenuOpen?()
297
+ }
298
+
299
+ public func contextMenuInteraction(
300
+ _ interaction: UIContextMenuInteraction,
301
+ willEndFor configuration: UIContextMenuConfiguration,
302
+ animator: UIContextMenuInteractionAnimating?
303
+ ) {
304
+ logger.debug("contextMenuInteraction: willEnd")
305
+ onMenuClose?()
306
+ }
307
+
308
+ // MARK: - Menu Building
309
+
310
+ private func buildMenu(from actions: [PCContextMenuAction], title: String?) -> UIMenu {
311
+ let menuElements = actions.compactMap { buildMenuElement(from: $0) }
312
+
313
+ return UIMenu(
314
+ title: title ?? "",
315
+ children: menuElements
316
+ )
317
+ }
318
+
319
+ private func buildMenuElement(from action: PCContextMenuAction) -> UIMenuElement? {
320
+ guard !action.hidden else { return nil }
321
+
322
+ // If has subactions, create a submenu
323
+ if !action.subactions.isEmpty {
324
+ let children = action.subactions.compactMap { buildMenuElement(from: $0) }
325
+ return UIMenu(
326
+ title: action.title,
327
+ image: imageForAction(action),
328
+ children: children
329
+ )
330
+ }
331
+
332
+ // Build action attributes
333
+ var attributes: UIMenuElement.Attributes = []
334
+ if action.destructive { attributes.insert(.destructive) }
335
+ if action.disabled { attributes.insert(.disabled) }
336
+
337
+ // Build state
338
+ let state: UIMenuElement.State
339
+ switch action.state {
340
+ case "on": state = .on
341
+ case "mixed": state = .mixed
342
+ default: state = .off
343
+ }
344
+
345
+ let uiAction = UIAction(
346
+ title: action.title,
347
+ subtitle: action.subtitle,
348
+ image: imageForAction(action),
349
+ attributes: attributes,
350
+ state: state
351
+ ) { [weak self] _ in
352
+ logger.debug("UIAction selected: id=\(action.id), title=\(action.title)")
353
+ self?.onPressAction?(action.id, action.title)
354
+ }
355
+
356
+ return uiAction
357
+ }
358
+
359
+ private func imageForAction(_ action: PCContextMenuAction) -> UIImage? {
360
+ guard let imageName = action.image, !imageName.isEmpty else { return nil }
361
+
362
+ var image = UIImage(systemName: imageName)
363
+
364
+ // Apply tint color if specified
365
+ if let colorStr = action.imageColor, !colorStr.isEmpty, let color = colorFromString(colorStr) {
366
+ image = image?.withTintColor(color, renderingMode: .alwaysOriginal)
367
+ }
368
+
369
+ return image
370
+ }
371
+
372
+ private func colorFromString(_ str: String) -> UIColor? {
373
+ // Support hex colors like "#FF0000" or "FF0000"
374
+ var hex = str.trimmingCharacters(in: .whitespacesAndNewlines)
375
+ if hex.hasPrefix("#") {
376
+ hex = String(hex.dropFirst())
377
+ }
378
+
379
+ guard hex.count == 6, let rgbValue = UInt64(hex, radix: 16) else {
380
+ return nil
381
+ }
382
+
383
+ let r = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
384
+ let g = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
385
+ let b = CGFloat(rgbValue & 0x0000FF) / 255.0
386
+
387
+ return UIColor(red: r, green: g, blue: b, alpha: 1.0)
388
+ }
389
+ }
@@ -283,31 +283,45 @@ public final class PCDatePickerView: UIControl,
283
283
  // Prevent "settle" events right as we present.
284
284
  suppressNextChangesBriefly()
285
285
 
286
- let vc = UIViewController()
287
- vc.view.backgroundColor = .clear
288
- vc.view.isOpaque = false
286
+ // Check if using inline style (full calendar) - needs larger popover size
287
+ var isInlineStyle = false
288
+ if #available(iOS 13.4, *) {
289
+ isInlineStyle = picker.preferredDatePickerStyle == .inline
290
+ }
289
291
 
292
+ let vc = UIViewController()
290
293
  picker.translatesAutoresizingMaskIntoConstraints = false
291
294
  vc.view.addSubview(picker)
292
295
 
293
- // Position picker at top-leading (constraints required to avoid freeze with inline style)
294
- NSLayoutConstraint.activate([
295
- picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
296
- picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
297
- ])
296
+ // For inline style, use system background and constrain all edges
297
+ // For other styles, use clear background and only top/leading constraints
298
+ if isInlineStyle {
299
+ vc.view.backgroundColor = .systemBackground
300
+ vc.view.isOpaque = true
301
+ NSLayoutConstraint.activate([
302
+ picker.topAnchor.constraint(equalTo: vc.view.topAnchor, constant: 8),
303
+ picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor, constant: 8),
304
+ picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor, constant: -8),
305
+ picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor, constant: -8),
306
+ ])
307
+ } else {
308
+ vc.view.backgroundColor = .clear
309
+ vc.view.isOpaque = false
310
+ NSLayoutConstraint.activate([
311
+ picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
312
+ picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
313
+ ])
314
+ }
298
315
 
299
316
  let size = popoverContentSize()
300
317
  vc.preferredContentSize = size
301
318
 
302
- // ✅ Anchored popover-style (not a full sheet)
303
319
  vc.modalPresentationStyle = .popover
304
320
  vc.presentationController?.delegate = self
305
321
 
306
322
  if let pop = vc.popoverPresentationController {
307
323
  pop.delegate = self
308
324
  pop.sourceView = self
309
- // Use a minimum height for sourceRect to help popover positioning
310
- // (modal presentation views have zero intrinsic height)
311
325
  let sourceRect = CGRect(
312
326
  x: bounds.minX,
313
327
  y: bounds.minY,