react-native-platform-components 0.0.2

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 (63) hide show
  1. package/LICENSE +20 -0
  2. package/PlatformComponents.podspec +20 -0
  3. package/README.md +233 -0
  4. package/android/build.gradle +78 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/platformcomponents/DateConstraints.kt +83 -0
  8. package/android/src/main/java/com/platformcomponents/Helper.kt +27 -0
  9. package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +684 -0
  10. package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +149 -0
  11. package/android/src/main/java/com/platformcomponents/PCMaterialMode.kt +16 -0
  12. package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +410 -0
  13. package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +114 -0
  14. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +22 -0
  15. package/ios/PCDatePicker.h +11 -0
  16. package/ios/PCDatePicker.mm +248 -0
  17. package/ios/PCDatePickerView.swift +405 -0
  18. package/ios/PCSelectionMenu.h +10 -0
  19. package/ios/PCSelectionMenu.mm +182 -0
  20. package/ios/PCSelectionMenu.swift +434 -0
  21. package/lib/module/DatePicker.js +74 -0
  22. package/lib/module/DatePicker.js.map +1 -0
  23. package/lib/module/DatePickerNativeComponent.ts +68 -0
  24. package/lib/module/SelectionMenu.js +79 -0
  25. package/lib/module/SelectionMenu.js.map +1 -0
  26. package/lib/module/SelectionMenu.web.js +57 -0
  27. package/lib/module/SelectionMenu.web.js.map +1 -0
  28. package/lib/module/SelectionMenuNativeComponent.ts +106 -0
  29. package/lib/module/index.js +6 -0
  30. package/lib/module/index.js.map +1 -0
  31. package/lib/module/package.json +1 -0
  32. package/lib/module/sharedTypes.js +4 -0
  33. package/lib/module/sharedTypes.js.map +1 -0
  34. package/lib/typescript/package.json +1 -0
  35. package/lib/typescript/src/DatePicker.d.ts +38 -0
  36. package/lib/typescript/src/DatePicker.d.ts.map +1 -0
  37. package/lib/typescript/src/DatePickerNativeComponent.d.ts +53 -0
  38. package/lib/typescript/src/DatePickerNativeComponent.d.ts.map +1 -0
  39. package/lib/typescript/src/SelectionMenu.d.ts +50 -0
  40. package/lib/typescript/src/SelectionMenu.d.ts.map +1 -0
  41. package/lib/typescript/src/SelectionMenu.web.d.ts +19 -0
  42. package/lib/typescript/src/SelectionMenu.web.d.ts.map +1 -0
  43. package/lib/typescript/src/SelectionMenuNativeComponent.d.ts +85 -0
  44. package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +1 -0
  45. package/lib/typescript/src/index.d.ts +4 -0
  46. package/lib/typescript/src/index.d.ts.map +1 -0
  47. package/lib/typescript/src/sharedTypes.d.ts +10 -0
  48. package/lib/typescript/src/sharedTypes.d.ts.map +1 -0
  49. package/package.json +178 -0
  50. package/shared/PCDatePickerComponentDescriptors-custom.h +52 -0
  51. package/shared/PCDatePickerShadowNode-custom.cpp +1 -0
  52. package/shared/PCDatePickerShadowNode-custom.h +27 -0
  53. package/shared/PCDatePickerState-custom.h +13 -0
  54. package/shared/PCSelectionMenuComponentDescriptors-custom.h +25 -0
  55. package/shared/PCSelectionMenuShadowNode-custom.cpp +36 -0
  56. package/shared/PCSelectionMenuShadowNode-custom.h +46 -0
  57. package/src/DatePicker.tsx +146 -0
  58. package/src/DatePickerNativeComponent.ts +68 -0
  59. package/src/SelectionMenu.tsx +170 -0
  60. package/src/SelectionMenu.web.tsx +93 -0
  61. package/src/SelectionMenuNativeComponent.ts +106 -0
  62. package/src/index.tsx +3 -0
  63. package/src/sharedTypes.ts +14 -0
@@ -0,0 +1,22 @@
1
+ package com.platformcomponents
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+ import java.util.ArrayList
8
+
9
+ class PlatformComponentsViewPackage : ReactPackage {
10
+ override fun createViewManagers(
11
+ reactContext: ReactApplicationContext
12
+ ): List<ViewManager<*, *>> {
13
+ return listOf(
14
+ PCSelectionMenuViewManager(),
15
+ PCDatePickerViewManager(),
16
+ )
17
+ }
18
+
19
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
20
+ return emptyList()
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ // ios/RCTDatePicker.h
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+ #import <UIKit/UIKit.h>
5
+
6
+ NS_ASSUME_NONNULL_BEGIN
7
+
8
+ @interface PCDatePicker : RCTViewComponentView
9
+ @end
10
+
11
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,248 @@
1
+ // ios/PCDatePicker.mm
2
+
3
+ #import "PCDatePicker.h"
4
+
5
+ #import <React/RCTConversions.h>
6
+ #import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
7
+ #import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
8
+ #import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
9
+ #import <react/renderer/components/PlatformComponentsViewSpec/RCTComponentViewHelpers.h>
10
+ #import <react/renderer/core/LayoutPrimitives.h>
11
+
12
+ #if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
13
+ #import <PlatformComponents/PlatformComponents-Swift.h>
14
+ #else
15
+ #import "PlatformComponents-Swift.h"
16
+ #endif
17
+
18
+ #import "PCDatePickerComponentDescriptors-custom.h"
19
+ #import "PCDatePickerShadowNode-custom.h"
20
+ #import "PCDatePickerState-custom.h"
21
+ #import "RCTFabricComponentsPlugins.h"
22
+
23
+ using namespace facebook::react;
24
+
25
+ @interface PCDatePicker () <RCTPCDatePickerViewProtocol>
26
+
27
+ - (void)updateMeasurements;
28
+ - (const PCDatePickerEventEmitter &)eventEmitterTyped;
29
+
30
+ @end
31
+
32
+ @implementation PCDatePicker {
33
+ PCDatePickerView *_datePickerView;
34
+ MeasuringPCDatePickerShadowNode::ConcreteState::Shared _state;
35
+ }
36
+
37
+ + (ComponentDescriptorProvider)componentDescriptorProvider {
38
+ return concreteComponentDescriptorProvider<
39
+ MeasuringPCDatePickerComponentDescriptor>();
40
+ }
41
+
42
+ - (instancetype)initWithFrame:(CGRect)frame {
43
+ if (self = [super initWithFrame:frame]) {
44
+ _datePickerView = [PCDatePickerView new];
45
+ _datePickerView.translatesAutoresizingMaskIntoConstraints = NO;
46
+
47
+ __weak __typeof(self) weakSelf = self;
48
+
49
+ _datePickerView.onChangeHandler = ^(NSNumber *ms) {
50
+ __typeof(self) strongSelf = weakSelf;
51
+ if (!strongSelf)
52
+ return;
53
+
54
+ PCDatePickerEventEmitter::OnConfirm event{};
55
+ event.timestampMs = ms.doubleValue;
56
+
57
+ strongSelf.eventEmitterTyped.onConfirm(event);
58
+ };
59
+
60
+ _datePickerView.onCancelHandler = ^{
61
+ __typeof(self) strongSelf = weakSelf;
62
+ if (!strongSelf)
63
+ return;
64
+
65
+ PCDatePickerEventEmitter::OnClosed event{};
66
+ strongSelf.eventEmitterTyped.onClosed(event);
67
+ };
68
+
69
+ self.contentView = _datePickerView;
70
+
71
+ [NSLayoutConstraint activateConstraints:@[
72
+ [_datePickerView.topAnchor
73
+ constraintEqualToAnchor:self.contentView.topAnchor],
74
+ [_datePickerView.bottomAnchor
75
+ constraintEqualToAnchor:self.contentView.bottomAnchor],
76
+ [_datePickerView.leadingAnchor
77
+ constraintEqualToAnchor:self.contentView.leadingAnchor],
78
+ [_datePickerView.trailingAnchor
79
+ constraintEqualToAnchor:self.contentView.trailingAnchor],
80
+ ]];
81
+ }
82
+
83
+ return self;
84
+ }
85
+
86
+ - (void)layoutSubviews {
87
+ [super layoutSubviews];
88
+ _datePickerView.frame = self.bounds;
89
+ }
90
+
91
+ #pragma mark - Props
92
+
93
+ - (void)updateProps:(Props::Shared const &)props
94
+ oldProps:(Props::Shared const &)oldProps {
95
+ const auto &oldViewProps =
96
+ *std::static_pointer_cast<const PCDatePickerProps>(_props);
97
+ const auto &newViewProps =
98
+ *std::static_pointer_cast<const PCDatePickerProps>(props);
99
+
100
+ BOOL needsToUpdateMeasurements = NO;
101
+
102
+ // presentation (default "modal")
103
+ NSString *newPresentation =
104
+ newViewProps.presentation.empty()
105
+ ? @"modal"
106
+ : [NSString stringWithUTF8String:newViewProps.presentation.c_str()];
107
+
108
+ if (![_datePickerView.presentation isEqualToString:newPresentation]) {
109
+ _datePickerView.presentation = newPresentation;
110
+ needsToUpdateMeasurements = YES;
111
+ }
112
+
113
+ // visible: treat only "open" as open; everything else as closed
114
+ BOOL shouldOpen = (newViewProps.visible == "open");
115
+ NSNumber *newOpen = @(shouldOpen);
116
+ if (![_datePickerView.open isEqual:newOpen]) {
117
+ _datePickerView.open = newOpen;
118
+ }
119
+
120
+ // dateMs (sentinel -1)
121
+ if (oldViewProps.dateMs != newViewProps.dateMs) {
122
+ _datePickerView.dateMs =
123
+ (newViewProps.dateMs >= 0) ? @(newViewProps.dateMs) : nil;
124
+ }
125
+
126
+ // min/max (sentinel -1)
127
+ if (oldViewProps.minDateMs != newViewProps.minDateMs) {
128
+ _datePickerView.minDateMs =
129
+ (newViewProps.minDateMs >= 0) ? @(newViewProps.minDateMs) : nil;
130
+ }
131
+ if (oldViewProps.maxDateMs != newViewProps.maxDateMs) {
132
+ _datePickerView.maxDateMs =
133
+ (newViewProps.maxDateMs >= 0) ? @(newViewProps.maxDateMs) : nil;
134
+ }
135
+
136
+ // locale
137
+ if (oldViewProps.locale != newViewProps.locale) {
138
+ _datePickerView.localeIdentifier =
139
+ (!newViewProps.locale.empty())
140
+ ? [NSString stringWithUTF8String:newViewProps.locale.c_str()]
141
+ : nil;
142
+ }
143
+
144
+ // time zone
145
+ if (oldViewProps.timeZoneName != newViewProps.timeZoneName) {
146
+ _datePickerView.timeZoneName =
147
+ (!newViewProps.timeZoneName.empty())
148
+ ? [NSString stringWithUTF8String:newViewProps.timeZoneName.c_str()]
149
+ : nil;
150
+ }
151
+
152
+ // mode
153
+ if (oldViewProps.mode != newViewProps.mode) {
154
+ _datePickerView.mode =
155
+ (!newViewProps.mode.empty())
156
+ ? [NSString stringWithUTF8String:newViewProps.mode.c_str()]
157
+ : @"date";
158
+ needsToUpdateMeasurements = YES;
159
+ }
160
+
161
+ // ----- iOS nested props -----
162
+ const auto &oldIos = oldViewProps.ios;
163
+ const auto &newIos = newViewProps.ios;
164
+
165
+ if (oldIos.preferredStyle != newIos.preferredStyle) {
166
+ _datePickerView.preferredStyle =
167
+ (!newIos.preferredStyle.empty())
168
+ ? [NSString stringWithUTF8String:newIos.preferredStyle.c_str()]
169
+ : nil;
170
+ needsToUpdateMeasurements = YES;
171
+ }
172
+
173
+ if (oldIos.countDownDurationSeconds != newIos.countDownDurationSeconds) {
174
+ _datePickerView.countDownDurationSeconds =
175
+ @(newIos.countDownDurationSeconds);
176
+ }
177
+
178
+ if (oldIos.minuteInterval != newIos.minuteInterval) {
179
+ _datePickerView.minuteIntervalValue = @(newIos.minuteInterval);
180
+ }
181
+
182
+ // Expecting: "inherit" | "round" | "noRound"
183
+ if (oldIos.roundsToMinuteInterval != newIos.roundsToMinuteInterval) {
184
+ if (!newIos.roundsToMinuteInterval.empty()) {
185
+ _datePickerView.roundsToMinuteIntervalMode =
186
+ [NSString stringWithUTF8String:newIos.roundsToMinuteInterval.c_str()];
187
+ } else {
188
+ _datePickerView.roundsToMinuteIntervalMode = @"inherit";
189
+ }
190
+ }
191
+
192
+ if (needsToUpdateMeasurements) {
193
+ [self updateMeasurements];
194
+ }
195
+
196
+ [super updateProps:props oldProps:oldProps];
197
+ }
198
+
199
+ - (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
200
+ oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
201
+ [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
202
+
203
+ // Fill whatever Yoga decided the content frame is.
204
+ _datePickerView.frame = self.contentView.bounds;
205
+ }
206
+
207
+ #pragma mark - State (Measuring)
208
+
209
+ - (void)updateState:(const State::Shared &)state
210
+ oldState:(const State::Shared &)oldState {
211
+ _state = std::static_pointer_cast<
212
+ const MeasuringPCDatePickerShadowNode::ConcreteState>(state);
213
+
214
+ if (oldState == nullptr) {
215
+ // First time: compute initial size.
216
+ [self updateMeasurements];
217
+ }
218
+
219
+ [super updateState:state oldState:oldState];
220
+ }
221
+
222
+ - (void)updateMeasurements {
223
+ if (_state == nullptr)
224
+ return;
225
+
226
+ // Use the real width Yoga gave us (bounds is correct here after layoutMetrics
227
+ // update)
228
+ const CGFloat w = self.bounds.size.width > 1 ? self.bounds.size.width : 320;
229
+
230
+ CGSize size =
231
+ [_datePickerView sizeForLayoutWithConstrainedTo:CGSizeMake(w, 0)];
232
+
233
+ PCDatePickerStateFrameSize next;
234
+ next.frameSize = {(Float)size.width, (Float)size.height};
235
+ _state->updateState(std::move(next));
236
+ }
237
+
238
+ #pragma mark - Strongly typed emitter
239
+
240
+ - (const PCDatePickerEventEmitter &)eventEmitterTyped {
241
+ return static_cast<const PCDatePickerEventEmitter &>(*_eventEmitter);
242
+ }
243
+
244
+ @end
245
+
246
+ Class<RCTComponentViewProtocol> RCTPCDatePickerCls(void) {
247
+ return PCDatePicker.class;
248
+ }
@@ -0,0 +1,405 @@
1
+ import UIKit
2
+
3
+ @objcMembers
4
+ public final class PCDatePickerView: UIControl,
5
+ UIPopoverPresentationControllerDelegate,
6
+ UIAdaptivePresentationControllerDelegate
7
+ {
8
+ // MARK: - UI
9
+ private let picker = UIDatePicker()
10
+ private var modalVC: UIViewController?
11
+
12
+ // Suppress “programmatic” valueChanged events (apply props / initial present settle).
13
+ private var suppressChangeEvents = false
14
+ private func suppressNextChangesBriefly() {
15
+ suppressChangeEvents = true
16
+ // Clear on next runloop tick (usually enough to skip the “settle” event).
17
+ DispatchQueue.main.async { [weak self] in
18
+ self?.suppressChangeEvents = false
19
+ }
20
+ }
21
+
22
+ // MARK: - Events (wired from ObjC++)
23
+ public var onChangeHandler: ((NSNumber) -> Void)?
24
+ public var onCancelHandler: (() -> Void)?
25
+
26
+ // MARK: - Props
27
+
28
+ /// "date" | "time" | "dateAndTime" | "countDownTimer"
29
+ public var mode: String = "date" {
30
+ didSet {
31
+ if oldValue != mode {
32
+ applyMode()
33
+ invalidateSize()
34
+ }
35
+ }
36
+ }
37
+
38
+ /// "modal" | "inline"
39
+ public var presentation: String = "modal" {
40
+ didSet {
41
+ if oldValue != presentation {
42
+ applyPresentation()
43
+ invalidateSize()
44
+ }
45
+ }
46
+ }
47
+
48
+ /// modal only: NSNumber(0/1)
49
+ public var open: NSNumber? {
50
+ didSet {
51
+ guard oldValue != open else { return }
52
+ applyOpen()
53
+ }
54
+ }
55
+
56
+ public var dateMs: NSNumber? {
57
+ didSet { applyDateMs(animated: false) }
58
+ }
59
+
60
+ public var minDateMs: NSNumber? { didSet { applyMinMax() } }
61
+ public var maxDateMs: NSNumber? { didSet { applyMinMax() } }
62
+
63
+ public var localeIdentifier: String? { didSet { applyLocale() } }
64
+ public var timeZoneName: String? { didSet { applyTimeZone() } }
65
+
66
+ /// "wheels" | "compact" | "inline" | "automatic"
67
+ public var preferredStyle: String? {
68
+ didSet {
69
+ if oldValue != preferredStyle {
70
+ applyPreferredStyle()
71
+ invalidateSize()
72
+ }
73
+ }
74
+ }
75
+
76
+ public var countDownDurationSeconds: NSNumber? { didSet { applyCountDownDuration() } }
77
+ public var minuteIntervalValue: NSNumber? { didSet { applyMinuteInterval() } }
78
+
79
+ /// "inherit" | "round" | "noRound"
80
+ public var roundsToMinuteIntervalMode: String = "inherit" {
81
+ didSet { if oldValue != roundsToMinuteIntervalMode { applyRoundsMode() } }
82
+ }
83
+
84
+ // MARK: - Init
85
+
86
+ public override init(frame: CGRect) {
87
+ super.init(frame: frame)
88
+ commonInit()
89
+ }
90
+
91
+ public required init?(coder: NSCoder) {
92
+ super.init(coder: coder)
93
+ commonInit()
94
+ }
95
+
96
+ private func commonInit() {
97
+ backgroundColor = .clear
98
+ isOpaque = false
99
+
100
+ isUserInteractionEnabled = true
101
+ picker.isUserInteractionEnabled = true
102
+
103
+ picker.translatesAutoresizingMaskIntoConstraints = false
104
+ picker.addTarget(self, action: #selector(handleValueChanged), for: .valueChanged)
105
+
106
+ applyMode()
107
+ applyPreferredStyle()
108
+ applyLocale()
109
+ applyTimeZone()
110
+ applyMinMax()
111
+ applyMinuteInterval()
112
+ applyCountDownDuration()
113
+ applyRoundsMode()
114
+ applyPresentation()
115
+ }
116
+
117
+ // MARK: - Layout / Sizing
118
+
119
+ private func invalidateSize() {
120
+ invalidateIntrinsicContentSize()
121
+ setNeedsLayout()
122
+ superview?.setNeedsLayout()
123
+ }
124
+
125
+ /// ✅ Inline only takes space; modal/headless must be 0 height.
126
+ public override var intrinsicContentSize: CGSize {
127
+ guard presentation == "embedded" else {
128
+ return CGSize(width: UIView.noIntrinsicMetric, height: 0)
129
+ }
130
+
131
+ picker.setNeedsLayout()
132
+ picker.layoutIfNeeded()
133
+ let fitted = picker.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
134
+ return CGSize(width: UIView.noIntrinsicMetric, height: max(44, fitted.height))
135
+ }
136
+
137
+ /// ✅ Called by your measuring pipeline.
138
+ /// Modal/headless should be zero so Yoga reserves nothing.
139
+ @objc public func sizeForLayout(withConstrainedTo constrainedSize: CGSize) -> CGSize {
140
+ guard presentation == "embedded" else { return .zero }
141
+
142
+ picker.setNeedsLayout()
143
+ picker.layoutIfNeeded()
144
+
145
+ let width =
146
+ (constrainedSize.width.isFinite && constrainedSize.width > 1)
147
+ ? constrainedSize.width : 320
148
+ let fitted = picker.systemLayoutSizeFitting(
149
+ CGSize(width: width, height: UIView.layoutFittingCompressedSize.height),
150
+ withHorizontalFittingPriority: .required,
151
+ verticalFittingPriority: .fittingSizeLevel
152
+ )
153
+ return CGSize(width: constrainedSize.width, height: max(44, fitted.height))
154
+ }
155
+
156
+ /// Separate sizing for popover content.
157
+ private func popoverContentSize() -> CGSize {
158
+ picker.setNeedsLayout()
159
+ picker.layoutIfNeeded()
160
+
161
+ let fitted = picker.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
162
+ let minH: CGFloat = (preferredStyle == "wheels") ? 216 : 160
163
+ let minW: CGFloat = 280
164
+ return CGSize(width: max(minW, fitted.width), height: max(minH, fitted.height))
165
+ }
166
+
167
+ // MARK: - Presentation
168
+
169
+ private func applyPresentation() {
170
+ if presentation == "embedded" {
171
+ dismissIfNeeded(emitCancel: false)
172
+ attachInlinePickerIfNeeded()
173
+ } else {
174
+ detachInlinePickerIfNeeded()
175
+ applyOpen()
176
+ }
177
+ }
178
+
179
+ private func attachInlinePickerIfNeeded() {
180
+ guard picker.superview !== self else { return }
181
+
182
+ picker.removeFromSuperview()
183
+ addSubview(picker)
184
+
185
+ NSLayoutConstraint.activate([
186
+ picker.topAnchor.constraint(equalTo: topAnchor),
187
+ picker.bottomAnchor.constraint(equalTo: bottomAnchor),
188
+ picker.leadingAnchor.constraint(equalTo: leadingAnchor),
189
+ picker.trailingAnchor.constraint(equalTo: trailingAnchor),
190
+ ])
191
+ }
192
+
193
+ private func detachInlinePickerIfNeeded() {
194
+ if picker.superview === self {
195
+ picker.removeFromSuperview()
196
+ }
197
+ }
198
+
199
+ private func applyOpen() {
200
+ guard presentation == "modal" else { return }
201
+ let shouldOpen = open?.boolValue ?? false
202
+ if shouldOpen { presentIfNeeded() } else { dismissIfNeeded(emitCancel: false) }
203
+ }
204
+
205
+ // MARK: - Modal Popover
206
+
207
+ private func presentIfNeeded() {
208
+ guard modalVC == nil else { return }
209
+ guard let top = topViewController() else { return }
210
+
211
+ // Prevent “settle” events right as we present.
212
+ suppressNextChangesBriefly()
213
+
214
+ // Ensure picker is not inline.
215
+ detachInlinePickerIfNeeded()
216
+
217
+ let vc = UIViewController()
218
+ vc.view.backgroundColor = .clear
219
+ vc.view.isOpaque = false
220
+
221
+ picker.translatesAutoresizingMaskIntoConstraints = false
222
+ vc.view.addSubview(picker)
223
+
224
+ NSLayoutConstraint.activate([
225
+ picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
226
+ picker.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor),
227
+ picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
228
+ picker.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor),
229
+ ])
230
+
231
+ let size = popoverContentSize()
232
+ vc.preferredContentSize = size
233
+
234
+ // ✅ Anchored popover-style (not a full sheet)
235
+ vc.modalPresentationStyle = .popover
236
+ vc.presentationController?.delegate = self
237
+
238
+ if let pop = vc.popoverPresentationController {
239
+ pop.delegate = self
240
+ pop.sourceView = self
241
+ pop.sourceRect = bounds
242
+ pop.permittedArrowDirections = [.up, .down]
243
+ }
244
+
245
+ modalVC = vc
246
+ top.present(vc, animated: true)
247
+ }
248
+
249
+ private func dismissIfNeeded(emitCancel: Bool) {
250
+ guard let vc = modalVC else { return }
251
+ modalVC = nil
252
+ vc.dismiss(animated: true) { [weak self] in
253
+ guard let self else { return }
254
+ if emitCancel { self.onCancelHandler?() }
255
+ }
256
+ }
257
+
258
+ // ✅ Critical: prevent popover → sheet adaptation in compact environments
259
+ public func adaptivePresentationStyle(for controller: UIPresentationController)
260
+ -> UIModalPresentationStyle
261
+ {
262
+ return .none
263
+ }
264
+
265
+ // Tap outside / dismiss
266
+ public func presentationControllerDidDismiss(_ presentationController: UIPresentationController)
267
+ {
268
+ modalVC = nil
269
+ onCancelHandler?()
270
+ }
271
+
272
+ public func popoverPresentationControllerDidDismissPopover(
273
+ _ popoverPresentationController: UIPopoverPresentationController
274
+ ) {
275
+ modalVC = nil
276
+ onCancelHandler?()
277
+ }
278
+
279
+ // MARK: - Value changes
280
+
281
+ @objc private func handleValueChanged() {
282
+ // Skip “programmatic/settle” changes
283
+ if suppressChangeEvents { return }
284
+
285
+ let ms = picker.date.timeIntervalSince1970 * 1000.0
286
+ onChangeHandler?(NSNumber(value: ms))
287
+ }
288
+
289
+ // MARK: - Apply props (avoid firing valueChanged)
290
+
291
+ private func applyDateMs(animated: Bool) {
292
+ guard let ms = dateMs?.doubleValue else { return }
293
+ suppressNextChangesBriefly()
294
+ picker.setDate(Date(timeIntervalSince1970: ms / 1000.0), animated: animated)
295
+ }
296
+
297
+ private func applyMinMax() {
298
+ suppressNextChangesBriefly()
299
+
300
+ if let ms = minDateMs?.doubleValue {
301
+ picker.minimumDate = Date(timeIntervalSince1970: ms / 1000.0)
302
+ } else {
303
+ picker.minimumDate = nil
304
+ }
305
+
306
+ if let ms = maxDateMs?.doubleValue {
307
+ picker.maximumDate = Date(timeIntervalSince1970: ms / 1000.0)
308
+ } else {
309
+ picker.maximumDate = nil
310
+ }
311
+ }
312
+
313
+ private func applyLocale() {
314
+ suppressNextChangesBriefly()
315
+ if let id = localeIdentifier, !id.isEmpty {
316
+ picker.locale = Locale(identifier: id)
317
+ } else {
318
+ picker.locale = nil
319
+ }
320
+ }
321
+
322
+ private func applyTimeZone() {
323
+ suppressNextChangesBriefly()
324
+ if let name = timeZoneName, let tz = TimeZone(identifier: name) {
325
+ picker.timeZone = tz
326
+ } else {
327
+ picker.timeZone = nil
328
+ }
329
+ }
330
+
331
+ private func applyMode() {
332
+ suppressNextChangesBriefly()
333
+ switch mode {
334
+ case "date": picker.datePickerMode = .date
335
+ case "time": picker.datePickerMode = .time
336
+ case "dateAndTime": picker.datePickerMode = .dateAndTime
337
+ case "countDownTimer": picker.datePickerMode = .countDownTimer
338
+ default: picker.datePickerMode = .date
339
+ }
340
+ }
341
+
342
+ private func applyPreferredStyle() {
343
+ guard #available(iOS 13.4, *) else { return }
344
+ suppressNextChangesBriefly()
345
+ let s = preferredStyle ?? "automatic"
346
+ switch s {
347
+ case "wheels": picker.preferredDatePickerStyle = .wheels
348
+ case "compact": picker.preferredDatePickerStyle = .compact
349
+ case "inline": picker.preferredDatePickerStyle = .inline
350
+ default: picker.preferredDatePickerStyle = .automatic
351
+ }
352
+ }
353
+
354
+ private func applyCountDownDuration() {
355
+ guard picker.datePickerMode == .countDownTimer else { return }
356
+ suppressNextChangesBriefly()
357
+ if let secs = countDownDurationSeconds?.doubleValue {
358
+ picker.countDownDuration = secs
359
+ }
360
+ }
361
+
362
+ private func applyMinuteInterval() {
363
+ guard let v = minuteIntervalValue?.intValue else { return }
364
+ suppressNextChangesBriefly()
365
+ picker.minuteInterval = max(1, min(30, v))
366
+ }
367
+
368
+ private func applyRoundsMode() {
369
+ guard #available(iOS 14.0, *) else { return }
370
+ suppressNextChangesBriefly()
371
+ switch roundsToMinuteIntervalMode {
372
+ case "round": picker.roundsToMinuteInterval = true
373
+ case "noRound": picker.roundsToMinuteInterval = false
374
+ default: break // inherit
375
+ }
376
+ }
377
+
378
+ // MARK: - Top VC (same approach you were using)
379
+
380
+ private func topViewController() -> UIViewController? {
381
+ guard
382
+ let scene = UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene })
383
+ .first,
384
+ let root = scene.windows.first(where: { $0.isKeyWindow })?.rootViewController
385
+ else { return nil }
386
+
387
+ var top = root
388
+ while true {
389
+ if let presented = top.presentedViewController {
390
+ top = presented
391
+ continue
392
+ }
393
+ if let nav = top as? UINavigationController, let visible = nav.visibleViewController {
394
+ top = visible
395
+ continue
396
+ }
397
+ if let tab = top as? UITabBarController, let selected = tab.selectedViewController {
398
+ top = selected
399
+ continue
400
+ }
401
+ break
402
+ }
403
+ return top
404
+ }
405
+ }
@@ -0,0 +1,10 @@
1
+ // SelectionMenu.h
2
+
3
+ #import <React/RCTViewComponentView.h>
4
+
5
+ NS_ASSUME_NONNULL_BEGIN
6
+
7
+ @interface PCSelectionMenu : RCTViewComponentView
8
+ @end
9
+
10
+ NS_ASSUME_NONNULL_END