react-native-platform-components 0.4.1 → 0.5.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.
package/README.md CHANGED
@@ -1,20 +1,48 @@
1
1
  # react-native-platform-components
2
2
 
3
- > 🚧 In development — not ready for public use.
4
-
5
- High-quality **native UI components for React Native**, implemented with platform-first APIs and exposed through clean, typed JavaScript interfaces.
6
-
7
- This library focuses on **true native behavior**, not JavaScript re-implementations — providing:
8
-
9
- - **SelectionMenu** – native selection menus (Material on Android, system menus on iOS)
10
- - **DatePicker** – native date & time pickers with modal and embedded presentations
11
-
12
- The goal is to provide components that:
13
-
14
- - Feel **100% native** on each platform
15
- - Support modern platform design systems (Material 3 on Android, system pickers on iOS)
16
- - Offer **headless** and **inline** modes for maximum layout control
17
- - Integrate cleanly with **React Native Codegen / Fabric**
3
+ [![npm version](https://img.shields.io/npm/v/react-native-platform-components.svg)](https://www.npmjs.com/package/react-native-platform-components)
4
+ [![npm downloads](https://img.shields.io/npm/dm/react-native-platform-components.svg)](https://www.npmjs.com/package/react-native-platform-components)
5
+
6
+ <table>
7
+ <tr>
8
+ <td valign="top">
9
+ <table>
10
+ <tr>
11
+ <td align="center"><strong>iOS DatePicker</strong></td>
12
+ <td align="center"><strong>Android DatePicker</strong></td>
13
+ </tr>
14
+ <tr>
15
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-datepicker.gif" height="350" /></td>
16
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-datepicker.gif" height="350" /></td>
17
+ </tr>
18
+ <tr>
19
+ <td align="center"><strong>iOS SelectionMenu</strong></td>
20
+ <td align="center"><strong>Android SelectionMenu</strong></td>
21
+ </tr>
22
+ <tr>
23
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/ios-selectionmenu.gif" height="350" /></td>
24
+ <td><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/android-selectionmenu.gif" height="350" /></td>
25
+ </tr>
26
+ </table>
27
+ </td>
28
+ <td valign="top">
29
+ <blockquote>🚧 In development — not ready for public use.</blockquote>
30
+ <p>High-quality <strong>native UI components for React Native</strong>, implemented with platform-first APIs and exposed through clean, typed JavaScript interfaces.</p>
31
+ <p>This library focuses on <strong>true native behavior</strong>, not JavaScript re-implementations — providing:</p>
32
+ <ul>
33
+ <li><strong>SelectionMenu</strong> – native selection menus (Material on Android, system menus on iOS)</li>
34
+ <li><strong>DatePicker</strong> – native date & time pickers with modal and embedded presentations</li>
35
+ </ul>
36
+ <p>The goal is to provide components that:</p>
37
+ <ul>
38
+ <li>Feel <strong>100% native</strong> on each platform</li>
39
+ <li>Support modern platform design systems (Material 3 on Android, system pickers on iOS)</li>
40
+ <li>Offer <strong>headless</strong> and <strong>inline</strong> modes for maximum layout control</li>
41
+ <li>Integrate cleanly with <strong>React Native Codegen / Fabric</strong></li>
42
+ </ul>
43
+ </td>
44
+ </tr>
45
+ </table>
18
46
 
19
47
  ---
20
48
 
@@ -183,6 +211,8 @@ Native selection menu with **inline** and **headless** modes.
183
211
  - **Headless mode** (default): Menu visibility controlled by `visible` prop. Use for custom trigger UI.
184
212
  - **Inline mode** (`inlineMode={true}`): Native picker UI rendered inline. Menu managed internally.
185
213
 
214
+ > **Note:** On iOS, headless mode uses a custom popover to enable programmatic presentation. For the full native menu experience (system animations, scroll physics), use inline mode. This is an intentional trade-off: headless gives you control over the trigger UI, inline gives you the complete system menu behavior.
215
+
186
216
  ---
187
217
 
188
218
  ## DatePicker
@@ -218,7 +248,7 @@ Native date & time picker using **platform system pickers**.
218
248
  | Prop | Type | Description |
219
249
  |------|------|-------------|
220
250
  | `firstDayOfWeek` | `number` | First day of week (1-7, Sunday=1) |
221
- | `material` | `'system' \| 'm3'` | Material Design style |
251
+ | `material` | `'system' \| 'm3'` | Material Design style (modal only; embedded always uses system picker) |
222
252
  | `dialogTitle` | `string` | Custom dialog title |
223
253
  | `positiveButtonTitle` | `string` | Custom confirm button text |
224
254
  | `negativeButtonTitle` | `string` | Custom cancel button text |
@@ -235,6 +265,17 @@ Native date & time picker using **platform system pickers**.
235
265
 
236
266
  ---
237
267
 
268
+ ## Theming
269
+
270
+ This library does not expose theming props. Components inherit their appearance from your app's native platform theme.
271
+
272
+ - **iOS**: Components follow system appearance (light/dark mode) and use system-defined styles (e.g., `UIBlurEffect` for menu backgrounds). These are not customizable per-component.
273
+ - **Android**: Components respect your app's Material Theme. Customize via your `styles.xml` or Material 3 theme configuration.
274
+
275
+ This is intentional. The goal is native fidelity, not pixel-level customization. If you need custom styling beyond what the platform theme provides, this library may not be the right fit.
276
+
277
+ ---
278
+
238
279
  ## Contributing
239
280
 
240
281
  See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
@@ -136,14 +136,16 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
136
136
  minDateMs = value
137
137
  // clamp if needed
138
138
  dateMs = clamp(dateMs ?: System.currentTimeMillis())
139
- syncInlineFromState()
139
+ // Rebuild inline picker to apply new min date (avoids CalendarView bugs)
140
+ if (isInline()) rebuildUI() else syncInlineFromState()
140
141
  }
141
142
 
142
143
  fun applyMaxDateMs(value: Long?) {
143
144
  maxDateMs = value
144
145
  // clamp if needed
145
146
  dateMs = clamp(dateMs ?: System.currentTimeMillis())
146
- syncInlineFromState()
147
+ // Rebuild inline picker to apply new max date (avoids CalendarView bugs)
148
+ if (isInline()) rebuildUI() else syncInlineFromState()
147
149
  }
148
150
 
149
151
  /**
@@ -205,8 +207,11 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
205
207
  ViewGroup.LayoutParams.WRAP_CONTENT,
206
208
  ViewGroup.LayoutParams.WRAP_CONTENT
207
209
  )
208
- calendarViewShown = true
209
- spinnersShown = false
210
+ // Use spinner mode to avoid CalendarView rendering bugs when scrolling months
211
+ calendarViewShown = false
212
+ spinnersShown = true
213
+ minDateMs?.let { minDate = it }
214
+ maxDateMs?.let { maxDate = it }
210
215
  }
211
216
  container.addView(dp)
212
217
  inlineDatePicker = dp
@@ -239,7 +244,14 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
239
244
  inlineContainer = container
240
245
 
241
246
  syncInlineFromState()
242
- requestLayout()
247
+
248
+ // Force layout refresh - post to ensure React Native's layout system picks it up
249
+ post {
250
+ requestLayout()
251
+ invalidate()
252
+ // Also request layout from parent to notify React Native
253
+ (parent as? ViewGroup)?.requestLayout()
254
+ }
243
255
  }
244
256
 
245
257
  private fun syncInlineFromState() {
@@ -253,10 +265,8 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
253
265
  suppressInlineCallbacks = true
254
266
  try {
255
267
  inlineDatePicker?.let { dp ->
256
- // Apply min/max bounds on the widget itself where possible
257
- minDateMs?.let { dp.minDate = it }
258
- maxDateMs?.let { dp.maxDate = it }
259
-
268
+ // Note: min/max dates are set during picker creation in rebuildUI()
269
+ // to avoid CalendarView rendering bugs from repeated updates
260
270
  val y = cal.get(Calendar.YEAR)
261
271
  val m = cal.get(Calendar.MONTH)
262
272
  val d = cal.get(Calendar.DAY_OF_MONTH)
@@ -319,20 +329,28 @@ class PCDatePickerView(context: Context) : FrameLayout(context) {
319
329
 
320
330
  private fun presentIfNeeded() {
321
331
  if (showingModal) return
322
- val act = findFragmentActivity() ?: run {
323
- Log.w(TAG, "presentIfNeeded: no FragmentActivity found")
324
- onCancel?.invoke()
325
- showingModal = false
326
- return
327
- }
328
-
329
- Log.d(TAG, "presentIfNeeded mode=$mode material=$androidMaterialMode")
330
332
  showingModal = true
331
333
 
332
- when (mode) {
333
- "time" -> presentTime(act)
334
- "dateAndTime" -> presentDateThenTime(act)
335
- else -> presentDate(act)
334
+ // Defer presentation to the next frame to ensure all props from the current
335
+ // React Native batch are applied first. This guarantees dateMs reflects the
336
+ // latest value from React Native before we create the dialog.
337
+ post {
338
+ if (!showingModal) return@post
339
+
340
+ val act = findFragmentActivity() ?: run {
341
+ Log.w(TAG, "presentIfNeeded: no FragmentActivity found")
342
+ onCancel?.invoke()
343
+ showingModal = false
344
+ return@post
345
+ }
346
+
347
+ Log.d(TAG, "presentIfNeeded mode=$mode material=$androidMaterialMode")
348
+
349
+ when (mode) {
350
+ "time" -> presentTime(act)
351
+ "dateAndTime" -> presentDateThenTime(act)
352
+ else -> presentDate(act)
353
+ }
336
354
  }
337
355
  }
338
356
 
@@ -60,17 +60,19 @@ class PCDatePickerViewManager :
60
60
  view.applyTimeZoneName(value)
61
61
  }
62
62
 
63
- // WithDefault<double,-1> comes through as primitive Double
63
+ // Sentinel is MIN_SAFE_INTEGER to allow negative timestamps for pre-1970 dates
64
+ private val noDateSentinel = -9007199254740991.0
65
+
64
66
  override fun setDateMs(view: PCDatePickerView, value: Double) {
65
- view.applyDateMs(if (value >= 0.0) value.toLong() else null)
67
+ view.applyDateMs(if (value > noDateSentinel) value.toLong() else null)
66
68
  }
67
69
 
68
70
  override fun setMinDateMs(view: PCDatePickerView, value: Double) {
69
- view.applyMinDateMs(if (value >= 0.0) value.toLong() else null)
71
+ view.applyMinDateMs(if (value > noDateSentinel) value.toLong() else null)
70
72
  }
71
73
 
72
74
  override fun setMaxDateMs(view: PCDatePickerView, value: Double) {
73
- view.applyMaxDateMs(if (value >= 0.0) value.toLong() else null)
75
+ view.applyMaxDateMs(if (value > noDateSentinel) value.toLong() else null)
74
76
  }
75
77
 
76
78
  // --- platform objects ---
@@ -51,6 +51,7 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
51
51
  private var headlessMenuShowing = false
52
52
  private var headlessDismissProgrammatic = false
53
53
  private var headlessDismissAfterSelect = false
54
+ private var headlessOpenToken = 0
54
55
 
55
56
  init {
56
57
  minimumHeight = 0
@@ -129,6 +130,13 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
129
130
  if (anchorMode == newMode) return
130
131
  anchorMode = newMode
131
132
  Log.d(TAG, "applyAnchorMode anchorMode=$anchorMode")
133
+ if (anchorMode != "headless") {
134
+ headlessOpenToken += 1
135
+ if (headlessMenuShowing) {
136
+ headlessDismissProgrammatic = true
137
+ headlessMenu?.dismiss()
138
+ }
139
+ }
132
140
  rebuildUI()
133
141
  }
134
142
 
@@ -138,11 +146,13 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
138
146
  else -> "closed"
139
147
  }
140
148
  Log.d(TAG, "applyVisible visible=$visible anchorMode=$anchorMode")
149
+ headlessOpenToken += 1
150
+ val token = headlessOpenToken
141
151
 
142
152
  if (anchorMode != "headless") return
143
153
 
144
154
  if (visible == "open") {
145
- presentHeadlessIfNeeded()
155
+ presentHeadlessIfNeeded(token)
146
156
  } else {
147
157
  Log.d(TAG, "applyVisible close -> dismiss")
148
158
  if (headlessMenuShowing) {
@@ -162,6 +172,10 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
162
172
  // ---- UI building ----
163
173
 
164
174
  private fun rebuildUI() {
175
+ if (headlessMenuShowing) {
176
+ headlessDismissProgrammatic = true
177
+ headlessMenu?.dismiss()
178
+ }
165
179
  inlineText?.dismissDropDown()
166
180
  detachInlineDropdownOverlay()
167
181
  inlineDropdownOverlay = null
@@ -465,7 +479,7 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
465
479
 
466
480
  // ---- Headless open ----
467
481
 
468
- private fun presentHeadlessIfNeeded() {
482
+ private fun presentHeadlessIfNeeded(token: Int) {
469
483
  val popup = headlessMenu ?: return
470
484
  if (interactivity != "enabled") {
471
485
  Log.d(TAG, "presentHeadlessIfNeeded interactivity=$interactivity -> requestClose")
@@ -473,6 +487,18 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
473
487
  return
474
488
  }
475
489
  post {
490
+ if (token != headlessOpenToken) {
491
+ Log.d(TAG, "presentHeadlessIfNeeded stale token -> skip")
492
+ return@post
493
+ }
494
+ if (anchorMode != "headless" || visible != "open") {
495
+ Log.d(TAG, "presentHeadlessIfNeeded no longer open -> skip")
496
+ return@post
497
+ }
498
+ if (interactivity != "enabled") {
499
+ Log.d(TAG, "presentHeadlessIfNeeded disabled -> skip")
500
+ return@post
501
+ }
476
502
  if (!isAttachedToWindow) {
477
503
  Log.d(TAG, "presentHeadlessIfNeeded not attached -> requestClose")
478
504
  onRequestClose?.invoke()
@@ -16,8 +16,15 @@ enum PCConstants {
16
16
  /// Maximum popover height before scrolling
17
17
  static let popoverMaxHeight: CGFloat = 400
18
18
 
19
- /// Row height in selection menu popover
20
- static let popoverRowHeight: CGFloat = 44
19
+ /// Minimum row height in selection menu popover (used as baseline)
20
+ static let popoverRowHeightMin: CGFloat = 44
21
+
22
+ /// Dynamic row height that respects user's preferred content size
23
+ static var popoverRowHeight: CGFloat {
24
+ let bodyFont = UIFont.preferredFont(forTextStyle: .body)
25
+ // Row height = font line height + vertical padding (16pt total)
26
+ return max(popoverRowHeightMin, ceil(bodyFont.lineHeight) + 16)
27
+ }
21
28
 
22
29
  /// Vertical padding in selection menu popover (top + bottom)
23
30
  static let popoverVerticalPadding: CGFloat = 16
@@ -117,20 +117,21 @@ using namespace facebook::react;
117
117
  _datePickerView.open = newOpen;
118
118
  }
119
119
 
120
- // dateMs (sentinel -1)
120
+ // dateMs (sentinel is MIN_SAFE_INTEGER to allow negative timestamps for pre-1970 dates)
121
+ static const double kNoDateSentinel = -9007199254740991.0;
121
122
  if (oldViewProps.dateMs != newViewProps.dateMs) {
122
123
  _datePickerView.dateMs =
123
- (newViewProps.dateMs >= 0) ? @(newViewProps.dateMs) : nil;
124
+ (newViewProps.dateMs > kNoDateSentinel) ? @(newViewProps.dateMs) : nil;
124
125
  }
125
126
 
126
- // min/max (sentinel -1)
127
+ // min/max (same sentinel)
127
128
  if (oldViewProps.minDateMs != newViewProps.minDateMs) {
128
129
  _datePickerView.minDateMs =
129
- (newViewProps.minDateMs >= 0) ? @(newViewProps.minDateMs) : nil;
130
+ (newViewProps.minDateMs > kNoDateSentinel) ? @(newViewProps.minDateMs) : nil;
130
131
  }
131
132
  if (oldViewProps.maxDateMs != newViewProps.maxDateMs) {
132
133
  _datePickerView.maxDateMs =
133
- (newViewProps.maxDateMs >= 0) ? @(newViewProps.maxDateMs) : nil;
134
+ (newViewProps.maxDateMs > kNoDateSentinel) ? @(newViewProps.maxDateMs) : nil;
134
135
  }
135
136
 
136
137
  // locale
@@ -1,5 +1,5 @@
1
- import os.log
2
1
  import UIKit
2
+ import os.log
3
3
 
4
4
  private let logger = Logger(subsystem: "com.platformcomponents", category: "DatePicker")
5
5
 
@@ -134,7 +134,9 @@ public final class PCDatePickerView: UIControl,
134
134
  picker.setNeedsLayout()
135
135
  picker.layoutIfNeeded()
136
136
  let fitted = picker.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
137
- return CGSize(width: UIView.noIntrinsicMetric, height: max(PCConstants.minTouchTargetHeight, fitted.height))
137
+ return CGSize(
138
+ width: UIView.noIntrinsicMetric,
139
+ height: max(PCConstants.minTouchTargetHeight, fitted.height))
138
140
  }
139
141
 
140
142
  /// ✅ Called by your measuring pipeline.
@@ -153,7 +155,9 @@ public final class PCDatePickerView: UIControl,
153
155
  withHorizontalFittingPriority: .required,
154
156
  verticalFittingPriority: .fittingSizeLevel
155
157
  )
156
- return CGSize(width: constrainedSize.width, height: max(PCConstants.minTouchTargetHeight, fitted.height))
158
+ return CGSize(
159
+ width: constrainedSize.width,
160
+ height: max(PCConstants.minTouchTargetHeight, fitted.height))
157
161
  }
158
162
 
159
163
  /// Separate sizing for popover content.
@@ -213,12 +217,16 @@ public final class PCDatePickerView: UIControl,
213
217
  }
214
218
  logger.debug("presentIfNeeded: presenting modal picker")
215
219
 
216
- // Prevent “settle” events right as we present.
217
- suppressNextChangesBriefly()
218
-
219
220
  // Ensure picker is not inline.
220
221
  detachInlinePickerIfNeeded()
221
222
 
223
+ // Sync the picker's date to the current prop value before presenting.
224
+ // This ensures React Native's date is always respected as the source of truth.
225
+ applyDateMs(animated: false)
226
+
227
+ // Prevent "settle" events right as we present.
228
+ suppressNextChangesBriefly()
229
+
222
230
  let vc = UIViewController()
223
231
  vc.view.backgroundColor = .clear
224
232
  vc.view.isOpaque = false
@@ -226,8 +234,14 @@ public final class PCDatePickerView: UIControl,
226
234
  picker.translatesAutoresizingMaskIntoConstraints = false
227
235
  vc.view.addSubview(picker)
228
236
 
237
+ // Position picker at top-leading (constraints required to avoid freeze with inline style)
238
+ NSLayoutConstraint.activate([
239
+ picker.topAnchor.constraint(equalTo: vc.view.topAnchor),
240
+ picker.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
241
+ ])
242
+
229
243
  let size = popoverContentSize()
230
- vc.preferredContentSize = size
244
+ vc.preferredContentSize = size
231
245
 
232
246
  // ✅ Anchored popover-style (not a full sheet)
233
247
  vc.modalPresentationStyle = .popover
@@ -236,7 +250,15 @@ public final class PCDatePickerView: UIControl,
236
250
  if let pop = vc.popoverPresentationController {
237
251
  pop.delegate = self
238
252
  pop.sourceView = self
239
- pop.sourceRect = bounds
253
+ // Use a minimum height for sourceRect to help popover positioning
254
+ // (modal presentation views have zero intrinsic height)
255
+ let sourceRect = CGRect(
256
+ x: bounds.minX,
257
+ y: bounds.minY,
258
+ width: max(bounds.width, 44),
259
+ height: max(bounds.height, 44)
260
+ )
261
+ pop.sourceRect = sourceRect
240
262
  pop.permittedArrowDirections = [.up, .down]
241
263
  }
242
264
 
@@ -288,9 +310,14 @@ public final class PCDatePickerView: UIControl,
288
310
  // MARK: - Apply props (avoid firing valueChanged)
289
311
 
290
312
  private func applyDateMs(animated: Bool) {
291
- guard let ms = dateMs?.doubleValue else { return }
292
313
  suppressNextChangesBriefly()
293
- picker.setDate(Date(timeIntervalSince1970: ms / 1000.0), animated: animated)
314
+ if let ms = dateMs?.doubleValue {
315
+ picker.setDate(Date(timeIntervalSince1970: ms / 1000.0), animated: animated)
316
+ } else {
317
+ // When no date is provided, default to now to avoid layout issues
318
+ // (especially with inline style in modal presentation)
319
+ picker.setDate(Date(), animated: animated)
320
+ }
294
321
  }
295
322
 
296
323
  private func applyMinMax() {
@@ -41,6 +41,7 @@ private struct PCSelectionMenuInlinePickerView: View {
41
41
  } label: {
42
42
  HStack(spacing: 8) {
43
43
  Text(model.displayTitle)
44
+ .font(.body)
44
45
  .lineLimit(1)
45
46
  .truncationMode(.tail)
46
47
 
@@ -97,6 +98,8 @@ public final class PCSelectionMenuView: UIControl {
97
98
  private let model = PCSelectionMenuModel()
98
99
  private var hostingController: UIHostingController<AnyView>?
99
100
  private var headlessMenuView: UIView?
101
+ private var headlessMenuVC: UIViewController?
102
+ private var headlessPresentationToken: Int = 0
100
103
 
101
104
  private var parsedOptions: [PCSelectionMenuOption] {
102
105
  options.compactMap { any in
@@ -131,12 +134,16 @@ public final class PCSelectionMenuView: UIControl {
131
134
  alpha = disabled ? 0.5 : 1.0
132
135
  isUserInteractionEnabled = !disabled
133
136
  accessibilityTraits = disabled ? [.notEnabled] : [.button]
137
+ if disabled {
138
+ dismissHeadlessIfNeeded()
139
+ }
134
140
  }
135
141
 
136
142
  // MARK: - Inline vs headless
137
143
 
138
144
  private func updateAnchorMode() {
139
145
  if anchorMode == "inline" {
146
+ dismissHeadlessIfNeeded()
140
147
  uninstallHeadlessIfNeeded()
141
148
  installInlineIfNeeded()
142
149
  sync()
@@ -247,15 +254,22 @@ public final class PCSelectionMenuView: UIControl {
247
254
 
248
255
  private func updatePresentation() {
249
256
  guard anchorMode != "inline" else { return }
250
- guard interactivity != "disabled" else { return }
257
+ headlessPresentationToken += 1
251
258
 
252
- if visible == "open" {
253
- presentHeadlessMenuIfNeeded()
259
+ if visible == "open" && interactivity != "disabled" {
260
+ presentHeadlessMenuIfNeeded(token: headlessPresentationToken)
261
+ } else {
262
+ dismissHeadlessIfNeeded()
254
263
  }
255
- // Note: dismissal is handled by the menu itself calling onRequestClose
256
264
  }
257
265
 
258
- private func presentHeadlessMenuIfNeeded() {
266
+ private func dismissHeadlessIfNeeded() {
267
+ guard let vc = headlessMenuVC else { return }
268
+ headlessMenuVC = nil
269
+ vc.dismiss(animated: true)
270
+ }
271
+
272
+ private func presentHeadlessMenuIfNeeded(token: Int) {
259
273
  guard headlessMenuView != nil else { return }
260
274
  guard let vc = nearestViewController() else { return }
261
275
 
@@ -263,7 +277,15 @@ public final class PCSelectionMenuView: UIControl {
263
277
  guard !opts.isEmpty else { return }
264
278
 
265
279
  logger.debug("presentHeadlessMenuIfNeeded: scheduling presentation with \(opts.count) options")
266
- DispatchQueue.main.asyncAfter(deadline: .now() + PCConstants.headlessPresentationDelay) {
280
+ DispatchQueue.main.asyncAfter(deadline: .now() + PCConstants.headlessPresentationDelay) { [weak self] in
281
+ guard let self else { return }
282
+ guard self.headlessPresentationToken == token else { return }
283
+ guard self.visible == "open" else { return }
284
+ guard self.anchorMode != "inline" else { return }
285
+ guard self.interactivity != "disabled" else { return }
286
+ guard self.headlessMenuVC == nil else { return }
287
+ guard self.window != nil else { return }
288
+
267
289
  let menuVC = PCMenuViewController(
268
290
  options: opts,
269
291
  onSelect: { [weak self] idx in
@@ -276,26 +298,50 @@ public final class PCSelectionMenuView: UIControl {
276
298
  onCancel: { [weak self] in
277
299
  logger.debug("headless menu cancelled")
278
300
  self?.onRequestClose?()
301
+ },
302
+ onDismiss: { [weak self] in
303
+ self?.headlessMenuVC = nil
279
304
  }
280
305
  )
281
306
 
282
- menuVC.modalPresentationStyle = .popover
307
+ // Calculate menu position relative to source view
308
+ let sourceFrame = self.convert(self.bounds, to: vc.view)
309
+ let screenBounds = vc.view.bounds
283
310
  let popoverHeight = min(
284
311
  CGFloat(opts.count) * PCConstants.popoverRowHeight + PCConstants.popoverVerticalPadding,
285
312
  PCConstants.popoverMaxHeight
286
313
  )
287
- menuVC.preferredContentSize = CGSize(
314
+ let spacing: CGFloat = 8
315
+
316
+ // Check if menu fits below the source view
317
+ let rowHeight = PCConstants.popoverRowHeight
318
+ let wouldExtendBeyondBottom = sourceFrame.maxY + spacing + popoverHeight > screenBounds.maxY - 20
319
+
320
+ let menuY: CGFloat
321
+ if wouldExtendBeyondBottom {
322
+ // Position above the source view (no overlap offset)
323
+ menuY = sourceFrame.minY - spacing - popoverHeight
324
+ } else {
325
+ // Position below, but shift up by one row to overlap trigger (like system menu)
326
+ menuY = sourceFrame.maxY + spacing - rowHeight
327
+ }
328
+
329
+ // Center horizontally, but keep within screen bounds
330
+ var menuX = sourceFrame.midX - PCConstants.popoverWidth / 2
331
+ menuX = max(16, min(menuX, screenBounds.maxX - PCConstants.popoverWidth - 16))
332
+
333
+ let menuFrame = CGRect(
334
+ x: menuX,
335
+ y: menuY,
288
336
  width: PCConstants.popoverWidth,
289
337
  height: popoverHeight
290
338
  )
291
339
 
292
- if let popover = menuVC.popoverPresentationController {
293
- popover.sourceView = self
294
- popover.sourceRect = self.bounds
295
- popover.permittedArrowDirections = [] // Remove arrow to match inline
296
- popover.delegate = menuVC
297
- }
340
+ menuVC.modalPresentationStyle = .overCurrentContext
341
+ menuVC.modalTransitionStyle = .crossDissolve
342
+ menuVC.menuFrame = menuFrame
298
343
 
344
+ self.headlessMenuVC = menuVC
299
345
  vc.present(menuVC, animated: true)
300
346
  }
301
347
  }
@@ -326,18 +372,52 @@ public final class PCSelectionMenuView: UIControl {
326
372
  }
327
373
  }
328
374
 
375
+ // MARK: - Glass Menu Cell
376
+
377
+ private class PCGlassMenuCell: UITableViewCell {
378
+ static let reuseIdentifier = "PCGlassMenuCell"
379
+
380
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
381
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
382
+ setupCell()
383
+ }
384
+
385
+ required init?(coder: NSCoder) {
386
+ super.init(coder: coder)
387
+ setupCell()
388
+ }
389
+
390
+ private func setupCell() {
391
+ backgroundColor = .clear
392
+ contentView.backgroundColor = .clear
393
+ selectionStyle = .none
394
+ textLabel?.font = .preferredFont(forTextStyle: .body)
395
+ textLabel?.adjustsFontForContentSizeCategory = true
396
+ }
397
+ }
398
+
329
399
  // MARK: - Custom Menu View Controller (matches SwiftUI Menu appearance)
330
400
 
331
- private class PCMenuViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIPopoverPresentationControllerDelegate {
401
+ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
332
402
  private let options: [PCSelectionMenuOption]
333
403
  private let onSelect: (Int) -> Void
334
404
  private let onCancel: () -> Void
405
+ private let onDismiss: () -> Void
335
406
  private var tableView: UITableView!
407
+ private var menuContainer: UIView!
408
+
409
+ var menuFrame: CGRect = .zero
336
410
 
337
- init(options: [PCSelectionMenuOption], onSelect: @escaping (Int) -> Void, onCancel: @escaping () -> Void) {
411
+ init(
412
+ options: [PCSelectionMenuOption],
413
+ onSelect: @escaping (Int) -> Void,
414
+ onCancel: @escaping () -> Void,
415
+ onDismiss: @escaping () -> Void
416
+ ) {
338
417
  self.options = options
339
418
  self.onSelect = onSelect
340
419
  self.onCancel = onCancel
420
+ self.onDismiss = onDismiss
341
421
  super.init(nibName: nil, bundle: nil)
342
422
  }
343
423
 
@@ -348,37 +428,61 @@ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITab
348
428
  override func viewDidLoad() {
349
429
  super.viewDidLoad()
350
430
 
351
- // Add blur effect (liquid glass)
352
- let blurEffect = UIBlurEffect(style: .systemMaterial)
353
- let blurView = UIVisualEffectView(effect: blurEffect)
354
- blurView.translatesAutoresizingMaskIntoConstraints = false
355
- view.addSubview(blurView)
431
+ view.backgroundColor = .clear
432
+
433
+ // Tap outside to dismiss
434
+ let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleBackgroundTap))
435
+ tapGesture.cancelsTouchesInView = false
436
+ view.addGestureRecognizer(tapGesture)
437
+
438
+ // Menu container positioned at menuFrame
439
+ menuContainer = UIView(frame: menuFrame)
440
+ menuContainer.backgroundColor = .clear
441
+ menuContainer.layer.cornerRadius = 12
442
+ menuContainer.clipsToBounds = true
443
+ view.addSubview(menuContainer)
444
+
445
+ // Use liquid glass on iOS 26+, fall back to system material blur on older versions
446
+ let effectView: UIVisualEffectView
447
+ if #available(iOS 26, *) {
448
+ var glassEffect = UIGlassEffect()
449
+ glassEffect.isInteractive = true
450
+ effectView = UIVisualEffectView(effect: glassEffect)
451
+ } else {
452
+ let blurEffect = UIBlurEffect(style: .systemMaterial)
453
+ effectView = UIVisualEffectView(effect: blurEffect)
454
+ }
455
+ effectView.frame = menuContainer.bounds
456
+ effectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
457
+ menuContainer.addSubview(effectView)
356
458
 
357
- tableView = UITableView(frame: .zero, style: .plain)
459
+ tableView = UITableView(frame: menuContainer.bounds, style: .plain)
358
460
  tableView.delegate = self
359
461
  tableView.dataSource = self
360
- tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
462
+ tableView.register(PCGlassMenuCell.self, forCellReuseIdentifier: PCGlassMenuCell.reuseIdentifier)
361
463
  tableView.backgroundColor = .clear
362
464
  tableView.separatorStyle = .none
363
465
  tableView.isScrollEnabled = true
364
466
  tableView.rowHeight = PCConstants.popoverRowHeight
467
+ tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
365
468
  let verticalPad = PCConstants.popoverVerticalPadding / 2
366
469
  tableView.contentInset = UIEdgeInsets(top: verticalPad, left: 0, bottom: verticalPad, right: 0)
367
- tableView.translatesAutoresizingMaskIntoConstraints = false
368
470
 
369
- blurView.contentView.addSubview(tableView)
471
+ effectView.contentView.addSubview(tableView)
472
+ }
370
473
 
371
- NSLayoutConstraint.activate([
372
- blurView.topAnchor.constraint(equalTo: view.topAnchor),
373
- blurView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
374
- blurView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
375
- blurView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
376
-
377
- tableView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
378
- tableView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
379
- tableView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
380
- tableView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
381
- ])
474
+ override func viewDidDisappear(_ animated: Bool) {
475
+ super.viewDidDisappear(animated)
476
+ onDismiss()
477
+ }
478
+
479
+ @objc private func handleBackgroundTap(_ gesture: UITapGestureRecognizer) {
480
+ let location = gesture.location(in: view)
481
+ if !menuContainer.frame.contains(location) {
482
+ dismiss(animated: true) { [weak self] in
483
+ self?.onCancel()
484
+ }
485
+ }
382
486
  }
383
487
 
384
488
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
@@ -386,11 +490,8 @@ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITab
386
490
  }
387
491
 
388
492
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
389
- let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
493
+ let cell = tableView.dequeueReusableCell(withIdentifier: PCGlassMenuCell.reuseIdentifier, for: indexPath)
390
494
  cell.textLabel?.text = options[indexPath.row].label
391
- cell.textLabel?.font = .systemFont(ofSize: 17)
392
- cell.backgroundColor = .clear
393
- cell.selectionStyle = .default
394
495
  return cell
395
496
  }
396
497
 
@@ -400,13 +501,4 @@ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITab
400
501
  self?.onSelect(indexPath.row)
401
502
  }
402
503
  }
403
-
404
- func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
405
- onCancel()
406
- }
407
-
408
- func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
409
- return .none
410
- }
411
504
  }
412
-
@@ -1,12 +1,17 @@
1
1
  "use strict";
2
2
 
3
3
  // DatePicker.tsx
4
- import React, { useCallback, useMemo } from 'react';
4
+ import React, { useCallback } from 'react';
5
5
  import { StyleSheet } from 'react-native';
6
6
  import NativeDatePicker from './DatePickerNativeComponent';
7
7
  import { jsx as _jsx } from "react/jsx-runtime";
8
- function dateToMsOrMinusOne(d) {
9
- return d ? d.getTime() : -1;
8
+ // Sentinel value for "no date". Using MIN_SAFE_INTEGER ensures we don't
9
+ // conflict with valid negative timestamps (dates before 1970).
10
+ const NO_DATE_SENTINEL = Number.MIN_SAFE_INTEGER;
11
+ function dateToMsOrSentinel(d) {
12
+ if (!d) return NO_DATE_SENTINEL;
13
+ const ms = d.getTime();
14
+ return Number.isFinite(ms) ? ms : NO_DATE_SENTINEL;
10
15
  }
11
16
  function normalizeVisible(presentation, visible) {
12
17
  // Only meaningful in modal presentation. Keep undefined for inline to avoid noise.
@@ -30,13 +35,13 @@ export function DatePicker(props) {
30
35
  android,
31
36
  testID
32
37
  } = props;
38
+ const isModal = presentation === 'modal';
33
39
  const handleConfirm = useCallback(e => {
34
40
  onConfirm?.(new Date(e.nativeEvent.timestampMs));
35
41
  }, [onConfirm]);
36
42
  const handleClosed = useCallback(() => {
37
43
  onClosed?.();
38
44
  }, [onClosed]);
39
- const styles = useMemo(() => createStyles(), []);
40
45
  const nativeProps = {
41
46
  style: [styles.picker, style],
42
47
  mode,
@@ -44,11 +49,11 @@ export function DatePicker(props) {
44
49
  timeZoneName,
45
50
  presentation,
46
51
  visible: normalizeVisible(presentation, visible),
47
- dateMs: dateToMsOrMinusOne(date),
48
- minDateMs: dateToMsOrMinusOne(minDate ?? null),
49
- maxDateMs: dateToMsOrMinusOne(maxDate ?? null),
52
+ dateMs: dateToMsOrSentinel(date),
53
+ minDateMs: dateToMsOrSentinel(minDate),
54
+ maxDateMs: dateToMsOrSentinel(maxDate),
50
55
  onConfirm: onConfirm ? handleConfirm : undefined,
51
- onClosed: onClosed ? handleClosed : undefined,
56
+ onClosed: isModal && onClosed ? handleClosed : undefined,
52
57
  ios: ios ? {
53
58
  preferredStyle: ios.preferredStyle,
54
59
  countDownDurationSeconds: ios.countDownDurationSeconds,
@@ -68,9 +73,7 @@ export function DatePicker(props) {
68
73
  ...nativeProps
69
74
  });
70
75
  }
71
- function createStyles() {
72
- return StyleSheet.create({
73
- picker: {}
74
- });
75
- }
76
+ const styles = StyleSheet.create({
77
+ picker: {}
78
+ });
76
79
  //# sourceMappingURL=DatePicker.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["React","useCallback","useMemo","StyleSheet","NativeDatePicker","jsx","_jsx","dateToMsOrMinusOne","d","getTime","normalizeVisible","presentation","visible","undefined","DatePicker","props","style","date","minDate","maxDate","locale","timeZoneName","mode","onConfirm","onClosed","ios","android","testID","handleConfirm","e","Date","nativeEvent","timestampMs","handleClosed","styles","createStyles","nativeProps","picker","dateMs","minDateMs","maxDateMs","preferredStyle","countDownDurationSeconds","minuteInterval","roundsToMinuteInterval","firstDayOfWeek","material","dialogTitle","positiveButtonTitle","negativeButtonTitle","create"],"sourceRoot":"../../src","sources":["DatePicker.tsx"],"mappings":";;AAAA;AACA,OAAOA,KAAK,IAAIC,WAAW,EAAEC,OAAO,QAAQ,OAAO;AAEnD,SAASC,UAAU,QAAQ,cAAc;AAEzC,OAAOC,gBAAgB,MAShB,6BAA6B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AA+CrC,SAASC,kBAAkBA,CAACC,CAA0B,EAAU;EAC9D,OAAOA,CAAC,GAAGA,CAAC,CAACC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7B;AAEA,SAASC,gBAAgBA,CACvBC,YAA+D,EAC/DC,OAA4B,EACP;EACrB;EACA,IAAID,YAAY,KAAK,OAAO,EAAE,OAAOE,SAAS;EAC9C,OAAOD,OAAO,GAAG,MAAM,GAAG,QAAQ;AACpC;AAEA,OAAO,SAASE,UAAUA,CAACC,KAAsB,EAAsB;EACrE,MAAM;IACJC,KAAK;IACLC,IAAI;IACJC,OAAO;IACPC,OAAO;IACPC,MAAM;IACNC,YAAY;IACZC,IAAI;IACJX,YAAY,GAAG,OAAO;IACtBC,OAAO;IACPW,SAAS;IACTC,QAAQ;IACRC,GAAG;IACHC,OAAO;IACPC;EACF,CAAC,GAAGZ,KAAK;EAET,MAAMa,aAAa,GAAG3B,WAAW,CAC9B4B,CAAwC,IAAK;IAC5CN,SAAS,GAAG,IAAIO,IAAI,CAACD,CAAC,CAACE,WAAW,CAACC,WAAW,CAAC,CAAC;EAClD,CAAC,EACD,CAACT,SAAS,CACZ,CAAC;EAED,MAAMU,YAAY,GAAGhC,WAAW,CAAC,MAAM;IACrCuB,QAAQ,GAAG,CAAC;EACd,CAAC,EAAE,CAACA,QAAQ,CAAC,CAAC;EAEd,MAAMU,MAAM,GAAGhC,OAAO,CAAC,MAAMiC,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC;EAEhD,MAAMC,WAAkC,GAAG;IACzCpB,KAAK,EAAE,CAACkB,MAAM,CAACG,MAAM,EAAErB,KAAK,CAAQ;IAEpCM,IAAI;IACJF,MAAM;IACNC,YAAY;IAEZV,YAAY;IACZC,OAAO,EAAEF,gBAAgB,CAACC,YAAY,EAAEC,OAAO,CAAQ;IAEvD0B,MAAM,EAAE/B,kBAAkB,CAACU,IAAI,CAAQ;IACvCsB,SAAS,EAAEhC,kBAAkB,CAACW,OAAO,IAAI,IAAI,CAAQ;IACrDsB,SAAS,EAAEjC,kBAAkB,CAACY,OAAO,IAAI,IAAI,CAAQ;IAErDI,SAAS,EAAEA,SAAS,GAAGK,aAAa,GAAGf,SAAS;IAChDW,QAAQ,EAAEA,QAAQ,GAAGS,YAAY,GAAGpB,SAAS;IAE7CY,GAAG,EAAEA,GAAG,GACJ;MACEgB,cAAc,EAAEhB,GAAG,CAACgB,cAAc;MAClCC,wBAAwB,EAAEjB,GAAG,CAACiB,wBAAwB;MACtDC,cAAc,EAAElB,GAAG,CAACkB,cAAc;MAClCC,sBAAsB,EAAEnB,GAAG,CAACmB;IAC9B,CAAC,GACD/B,SAAS;IAEba,OAAO,EAAEA,OAAO,GACZ;MACEmB,cAAc,EAAEnB,OAAO,CAACmB,cAAc;MACtCC,QAAQ,EAAEpB,OAAO,CAACoB,QAAe;MACjCC,WAAW,EAAErB,OAAO,CAACqB,WAAW;MAChCC,mBAAmB,EAAEtB,OAAO,CAACsB,mBAAmB;MAChDC,mBAAmB,EAAEvB,OAAO,CAACuB;IAC/B,CAAC,GACDpC;EACN,CAAC;EAED,oBAAOP,IAAA,CAACF,gBAAgB;IAACuB,MAAM,EAAEA,MAAO;IAAA,GAAKS;EAAW,CAAG,CAAC;AAC9D;AAEA,SAASD,YAAYA,CAAA,EAAG;EACtB,OAAOhC,UAAU,CAAC+C,MAAM,CAAC;IACvBb,MAAM,EAAE,CAAC;EACX,CAAC,CAAC;AACJ","ignoreList":[]}
1
+ {"version":3,"names":["React","useCallback","StyleSheet","NativeDatePicker","jsx","_jsx","NO_DATE_SENTINEL","Number","MIN_SAFE_INTEGER","dateToMsOrSentinel","d","ms","getTime","isFinite","normalizeVisible","presentation","visible","undefined","DatePicker","props","style","date","minDate","maxDate","locale","timeZoneName","mode","onConfirm","onClosed","ios","android","testID","isModal","handleConfirm","e","Date","nativeEvent","timestampMs","handleClosed","nativeProps","styles","picker","dateMs","minDateMs","maxDateMs","preferredStyle","countDownDurationSeconds","minuteInterval","roundsToMinuteInterval","firstDayOfWeek","material","dialogTitle","positiveButtonTitle","negativeButtonTitle","create"],"sourceRoot":"../../src","sources":["DatePicker.tsx"],"mappings":";;AAAA;AACA,OAAOA,KAAK,IAAIC,WAAW,QAAQ,OAAO;AAE1C,SAASC,UAAU,QAAQ,cAAc;AAEzC,OAAOC,gBAAgB,MAShB,6BAA6B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AA+CrC;AACA;AACA,MAAMC,gBAAgB,GAAGC,MAAM,CAACC,gBAAgB;AAEhD,SAASC,kBAAkBA,CAACC,CAA0B,EAAU;EAC9D,IAAI,CAACA,CAAC,EAAE,OAAOJ,gBAAgB;EAC/B,MAAMK,EAAE,GAAGD,CAAC,CAACE,OAAO,CAAC,CAAC;EACtB,OAAOL,MAAM,CAACM,QAAQ,CAACF,EAAE,CAAC,GAAGA,EAAE,GAAGL,gBAAgB;AACpD;AAEA,SAASQ,gBAAgBA,CACvBC,YAA+D,EAC/DC,OAA4B,EACP;EACrB;EACA,IAAID,YAAY,KAAK,OAAO,EAAE,OAAOE,SAAS;EAC9C,OAAOD,OAAO,GAAG,MAAM,GAAG,QAAQ;AACpC;AAEA,OAAO,SAASE,UAAUA,CAACC,KAAsB,EAAsB;EACrE,MAAM;IACJC,KAAK;IACLC,IAAI;IACJC,OAAO;IACPC,OAAO;IACPC,MAAM;IACNC,YAAY;IACZC,IAAI;IACJX,YAAY,GAAG,OAAO;IACtBC,OAAO;IACPW,SAAS;IACTC,QAAQ;IACRC,GAAG;IACHC,OAAO;IACPC;EACF,CAAC,GAAGZ,KAAK;EAET,MAAMa,OAAO,GAAGjB,YAAY,KAAK,OAAO;EAExC,MAAMkB,aAAa,GAAGhC,WAAW,CAC9BiC,CAAwC,IAAK;IAC5CP,SAAS,GAAG,IAAIQ,IAAI,CAACD,CAAC,CAACE,WAAW,CAACC,WAAW,CAAC,CAAC;EAClD,CAAC,EACD,CAACV,SAAS,CACZ,CAAC;EAED,MAAMW,YAAY,GAAGrC,WAAW,CAAC,MAAM;IACrC2B,QAAQ,GAAG,CAAC;EACd,CAAC,EAAE,CAACA,QAAQ,CAAC,CAAC;EAEd,MAAMW,WAAkC,GAAG;IACzCnB,KAAK,EAAE,CAACoB,MAAM,CAACC,MAAM,EAAErB,KAAK,CAAC;IAE7BM,IAAI;IACJF,MAAM;IACNC,YAAY;IAEZV,YAAY;IACZC,OAAO,EAAEF,gBAAgB,CAACC,YAAY,EAAEC,OAAO,CAAC;IAEhD0B,MAAM,EAAEjC,kBAAkB,CAACY,IAAI,CAAC;IAChCsB,SAAS,EAAElC,kBAAkB,CAACa,OAAO,CAAC;IACtCsB,SAAS,EAAEnC,kBAAkB,CAACc,OAAO,CAAC;IAEtCI,SAAS,EAAEA,SAAS,GAAGM,aAAa,GAAGhB,SAAS;IAChDW,QAAQ,EAAEI,OAAO,IAAIJ,QAAQ,GAAGU,YAAY,GAAGrB,SAAS;IAExDY,GAAG,EAAEA,GAAG,GACJ;MACEgB,cAAc,EAAEhB,GAAG,CAACgB,cAAc;MAClCC,wBAAwB,EAAEjB,GAAG,CAACiB,wBAAwB;MACtDC,cAAc,EAAElB,GAAG,CAACkB,cAAc;MAClCC,sBAAsB,EAAEnB,GAAG,CAACmB;IAC9B,CAAC,GACD/B,SAAS;IAEba,OAAO,EAAEA,OAAO,GACZ;MACEmB,cAAc,EAAEnB,OAAO,CAACmB,cAAc;MACtCC,QAAQ,EAAEpB,OAAO,CAACoB,QAAQ;MAC1BC,WAAW,EAAErB,OAAO,CAACqB,WAAW;MAChCC,mBAAmB,EAAEtB,OAAO,CAACsB,mBAAmB;MAChDC,mBAAmB,EAAEvB,OAAO,CAACuB;IAC/B,CAAC,GACDpC;EACN,CAAC;EAED,oBAAOZ,IAAA,CAACF,gBAAgB;IAAC4B,MAAM,EAAEA,MAAO;IAAA,GAAKQ;EAAW,CAAG,CAAC;AAC9D;AAEA,MAAMC,MAAM,GAAGtC,UAAU,CAACoD,MAAM,CAAC;EAC/Bb,MAAM,EAAE,CAAC;AACX,CAAC,CAAC","ignoreList":[]}
@@ -35,14 +35,15 @@ export type MacOSProps = Readonly<{}>;
35
35
 
36
36
  /**
37
37
  * Sentinel convention:
38
- * - `-1` means "no value / unbounded / unset".
38
+ * - `Number.MIN_SAFE_INTEGER` means "no value / unbounded / unset".
39
+ * (Allows negative timestamps for pre-1970 dates.)
39
40
  */
40
41
  export type CommonProps = {
41
42
  mode?: string; // DatePickerMode
42
43
 
43
- dateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
44
- minDateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
45
- maxDateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
44
+ dateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
45
+ minDateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
46
+ maxDateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
46
47
 
47
48
  locale?: string;
48
49
  timeZoneName?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"DatePicker.d.ts","sourceRoot":"","sources":["../../../src/DatePicker.tsx"],"names":[],"mappings":"AACA,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,KAAK,EAAwB,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG/E,OAAyB,EAGvB,KAAK,QAAQ,IAAI,cAAc,EAC/B,KAAK,YAAY,IAAI,kBAAkB,EACvC,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACxB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,mBAAmB,EAAW,MAAM,eAAe,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAE7B,2DAA2D;IAC3D,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAElB,mDAAmD;IACnD,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAEtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,GAAG,CAAC,EAAE;QACJ,cAAc,CAAC,EAAE,kBAAkB,CAAC;QACpC,wBAAwB,CAAC,EAAE,cAAc,CAAC,0BAA0B,CAAC,CAAC;QACtE,cAAc,CAAC,EAAE,cAAc,CAAC,gBAAgB,CAAC,CAAC;QAClD,sBAAsB,CAAC,EAAE,yBAAyB,CAAC;KACpD,CAAC;IAEF,OAAO,CAAC,EAAE;QACR,cAAc,CAAC,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;QACtD,QAAQ,CAAC,EAAE,mBAAmB,CAAC;QAC/B,WAAW,CAAC,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC;QAChD,mBAAmB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;QAChE,mBAAmB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;KACjE,CAAC;CACH,CAAC;AAeF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAqErE"}
1
+ {"version":3,"file":"DatePicker.d.ts","sourceRoot":"","sources":["../../../src/DatePicker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAsB,MAAM,OAAO,CAAC;AAC3C,OAAO,KAAK,EAAwB,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG/E,OAAyB,EAGvB,KAAK,QAAQ,IAAI,cAAc,EAC/B,KAAK,YAAY,IAAI,kBAAkB,EACvC,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,yBAAyB,EAC9B,KAAK,kBAAkB,EACxB,MAAM,6BAA6B,CAAC;AAErC,OAAO,KAAK,EAAE,mBAAmB,EAAW,MAAM,eAAe,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAE7B,2DAA2D;IAC3D,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAElB,mDAAmD;IACnD,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAEtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,YAAY,CAAC,EAAE,sBAAsB,CAAC;IAEtC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,KAAK,IAAI,CAAC;IACrC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtB,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,GAAG,CAAC,EAAE;QACJ,cAAc,CAAC,EAAE,kBAAkB,CAAC;QACpC,wBAAwB,CAAC,EAAE,cAAc,CAAC,0BAA0B,CAAC,CAAC;QACtE,cAAc,CAAC,EAAE,cAAc,CAAC,gBAAgB,CAAC,CAAC;QAClD,sBAAsB,CAAC,EAAE,yBAAyB,CAAC;KACpD,CAAC;IAEF,OAAO,CAAC,EAAE;QACR,cAAc,CAAC,EAAE,kBAAkB,CAAC,gBAAgB,CAAC,CAAC;QACtD,QAAQ,CAAC,EAAE,mBAAmB,CAAC;QAC/B,WAAW,CAAC,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC;QAChD,mBAAmB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;QAChE,mBAAmB,CAAC,EAAE,kBAAkB,CAAC,qBAAqB,CAAC,CAAC;KACjE,CAAC;CACH,CAAC;AAqBF,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,KAAK,CAAC,YAAY,CAqErE"}
@@ -25,13 +25,14 @@ export type WindowsProps = Readonly<{}>;
25
25
  export type MacOSProps = Readonly<{}>;
26
26
  /**
27
27
  * Sentinel convention:
28
- * - `-1` means "no value / unbounded / unset".
28
+ * - `Number.MIN_SAFE_INTEGER` means "no value / unbounded / unset".
29
+ * (Allows negative timestamps for pre-1970 dates.)
29
30
  */
30
31
  export type CommonProps = {
31
32
  mode?: string;
32
- dateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
33
- minDateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
34
- maxDateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
33
+ dateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
34
+ minDateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
35
+ maxDateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
35
36
  locale?: string;
36
37
  timeZoneName?: string;
37
38
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"DatePickerNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/DatePickerNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG5D,MAAM,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,YAAY,CAAC,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAChF,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC/E,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,QAAQ,GAAG;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wBAAwB,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;IAC/C,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;AACpC,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;AACxC,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;AAEtC;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,MAAM,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IACnD,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IACtD,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC;IAEtD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,WAAY,SAAQ,SAAS,EAAE,WAAW;IACzD,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,SAAS,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,eAAe,CAAC,CAAC;IAC/D,QAAQ,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5D;;AAED,wBAAmE"}
1
+ {"version":3,"file":"DatePickerNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/DatePickerNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG5D,MAAM,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,EAAE,YAAY,CAAC,MAAM,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,gBAAgB,CAAC;AAChF,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,UAAU,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAC/E,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,QAAQ,GAAG;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,wBAAwB,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC;IAC/C,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,CAAC;IACpC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;AACpC,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;AACxC,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;AAEtC;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,MAAM,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,gBAAgB,CAAC,CAAC;IAClE,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,gBAAgB,CAAC,CAAC;IACrE,SAAS,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,gBAAgB,CAAC,CAAC;IAErE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,WAAW,WAAY,SAAQ,SAAS,EAAE,WAAW;IACzD,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,OAAO,CAAC,EAAE,YAAY,CAAC;IAEvB,SAAS,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,eAAe,CAAC,CAAC;IAC/D,QAAQ,CAAC,EAAE,YAAY,CAAC,oBAAoB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC;CAC5D;;AAED,wBAAmE"}
@@ -36,7 +36,7 @@ export interface SelectionMenuProps extends ViewProps {
36
36
  */
37
37
  ios?: {};
38
38
  android?: {
39
- /** Material preference ("auto" | "m2" | "m3"). */
39
+ /** Material preference ('system' | 'm3'). */
40
40
  material?: AndroidMaterialMode;
41
41
  };
42
42
  /** Test identifier */
@@ -1 +1 @@
1
- {"version":3,"file":"SelectionMenu.d.ts","sourceRoot":"","sources":["../../../src/SelectionMenu.tsx"],"names":[],"mappings":"AACA,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,OAA4B,EAC1B,KAAK,mBAAmB,EAEzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,WAAW,kBAAmB,SAAQ,SAAS;IACnD,yCAAyC;IACzC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAExC;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAE5B;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,CAAC;IAET,OAAO,CAAC,EAAE;QACR,kDAAkD;QAClD,QAAQ,CAAC,EAAE,mBAAmB,CAAC;KAChC,CAAC;IAEF,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAkE3E"}
1
+ {"version":3,"file":"SelectionMenu.d.ts","sourceRoot":"","sources":["../../../src/SelectionMenu.tsx"],"names":[],"mappings":"AACA,OAAO,KAA+B,MAAM,OAAO,CAAC;AACpD,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,OAA4B,EAC1B,KAAK,mBAAmB,EAEzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEzD,MAAM,WAAW,kBAAmB,SAAQ,SAAS;IACnD,yCAAyC;IACzC,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IAExC;;;OAGG;IACH,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAExB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAElB;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhE;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAE5B;;OAEG;IACH,GAAG,CAAC,EAAE,EAAE,CAAC;IAET,OAAO,CAAC,EAAE;QACR,6CAA6C;QAC7C,QAAQ,CAAC,EAAE,mBAAmB,CAAC;KAChC,CAAC;IAEF,sBAAsB;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,KAAK,CAAC,YAAY,CAkE3E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-platform-components",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "A cross-platform toolkit of native UI components for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -1,5 +1,5 @@
1
1
  #include <react/renderer/core/LayoutConstraints.h>
2
- #include "PCSelectionMenuShadowNode-Custom.h"
2
+ #include "PCSelectionMenuShadowNode-custom.h"
3
3
 
4
4
  #include <algorithm>
5
5
 
@@ -1,5 +1,5 @@
1
1
  // DatePicker.tsx
2
- import React, { useCallback, useMemo } from 'react';
2
+ import React, { useCallback } from 'react';
3
3
  import type { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native';
4
4
  import { StyleSheet } from 'react-native';
5
5
 
@@ -59,8 +59,14 @@ export type DatePickerProps = {
59
59
  };
60
60
  };
61
61
 
62
- function dateToMsOrMinusOne(d: Date | null | undefined): number {
63
- return d ? d.getTime() : -1;
62
+ // Sentinel value for "no date". Using MIN_SAFE_INTEGER ensures we don't
63
+ // conflict with valid negative timestamps (dates before 1970).
64
+ const NO_DATE_SENTINEL = Number.MIN_SAFE_INTEGER;
65
+
66
+ function dateToMsOrSentinel(d: Date | null | undefined): number {
67
+ if (!d) return NO_DATE_SENTINEL;
68
+ const ms = d.getTime();
69
+ return Number.isFinite(ms) ? ms : NO_DATE_SENTINEL;
64
70
  }
65
71
 
66
72
  function normalizeVisible(
@@ -90,6 +96,8 @@ export function DatePicker(props: DatePickerProps): React.ReactElement {
90
96
  testID,
91
97
  } = props;
92
98
 
99
+ const isModal = presentation === 'modal';
100
+
93
101
  const handleConfirm = useCallback(
94
102
  (e: NativeSyntheticEvent<DateChangeEvent>) => {
95
103
  onConfirm?.(new Date(e.nativeEvent.timestampMs));
@@ -101,24 +109,22 @@ export function DatePicker(props: DatePickerProps): React.ReactElement {
101
109
  onClosed?.();
102
110
  }, [onClosed]);
103
111
 
104
- const styles = useMemo(() => createStyles(), []);
105
-
106
112
  const nativeProps: NativeDatePickerProps = {
107
- style: [styles.picker, style] as any,
113
+ style: [styles.picker, style],
108
114
 
109
115
  mode,
110
116
  locale,
111
117
  timeZoneName,
112
118
 
113
119
  presentation,
114
- visible: normalizeVisible(presentation, visible) as any,
120
+ visible: normalizeVisible(presentation, visible),
115
121
 
116
- dateMs: dateToMsOrMinusOne(date) as any,
117
- minDateMs: dateToMsOrMinusOne(minDate ?? null) as any,
118
- maxDateMs: dateToMsOrMinusOne(maxDate ?? null) as any,
122
+ dateMs: dateToMsOrSentinel(date),
123
+ minDateMs: dateToMsOrSentinel(minDate),
124
+ maxDateMs: dateToMsOrSentinel(maxDate),
119
125
 
120
126
  onConfirm: onConfirm ? handleConfirm : undefined,
121
- onClosed: onClosed ? handleClosed : undefined,
127
+ onClosed: isModal && onClosed ? handleClosed : undefined,
122
128
 
123
129
  ios: ios
124
130
  ? {
@@ -132,7 +138,7 @@ export function DatePicker(props: DatePickerProps): React.ReactElement {
132
138
  android: android
133
139
  ? {
134
140
  firstDayOfWeek: android.firstDayOfWeek,
135
- material: android.material as any,
141
+ material: android.material,
136
142
  dialogTitle: android.dialogTitle,
137
143
  positiveButtonTitle: android.positiveButtonTitle,
138
144
  negativeButtonTitle: android.negativeButtonTitle,
@@ -143,8 +149,6 @@ export function DatePicker(props: DatePickerProps): React.ReactElement {
143
149
  return <NativeDatePicker testID={testID} {...nativeProps} />;
144
150
  }
145
151
 
146
- function createStyles() {
147
- return StyleSheet.create({
148
- picker: {},
149
- });
150
- }
152
+ const styles = StyleSheet.create({
153
+ picker: {},
154
+ });
@@ -35,14 +35,15 @@ export type MacOSProps = Readonly<{}>;
35
35
 
36
36
  /**
37
37
  * Sentinel convention:
38
- * - `-1` means "no value / unbounded / unset".
38
+ * - `Number.MIN_SAFE_INTEGER` means "no value / unbounded / unset".
39
+ * (Allows negative timestamps for pre-1970 dates.)
39
40
  */
40
41
  export type CommonProps = {
41
42
  mode?: string; // DatePickerMode
42
43
 
43
- dateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
44
- minDateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
45
- maxDateMs?: CodegenTypes.WithDefault<TimestampMs, -1>;
44
+ dateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
45
+ minDateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
46
+ maxDateMs?: CodegenTypes.WithDefault<TimestampMs, -9007199254740991>;
46
47
 
47
48
  locale?: string;
48
49
  timeZoneName?: string;
@@ -51,7 +51,7 @@ export interface SelectionMenuProps extends ViewProps {
51
51
  ios?: {};
52
52
 
53
53
  android?: {
54
- /** Material preference ("auto" | "m2" | "m3"). */
54
+ /** Material preference ('system' | 'm3'). */
55
55
  material?: AndroidMaterialMode;
56
56
  };
57
57