react-native-platform-components 0.1.1 → 0.3.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 (29) hide show
  1. package/PlatformComponents.podspec +1 -1
  2. package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +5 -7
  3. package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +273 -146
  4. package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +5 -4
  5. package/ios/PCDatePickerView.swift +2 -11
  6. package/ios/PCSelectionMenu.mm +0 -10
  7. package/ios/PCSelectionMenu.swift +145 -179
  8. package/lib/module/DatePicker.js +3 -1
  9. package/lib/module/DatePicker.js.map +1 -1
  10. package/lib/module/SelectionMenu.js +4 -10
  11. package/lib/module/SelectionMenu.js.map +1 -1
  12. package/lib/module/SelectionMenuNativeComponent.ts +0 -9
  13. package/lib/module/index.js +1 -1
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/typescript/src/DatePicker.d.ts +2 -0
  16. package/lib/typescript/src/DatePicker.d.ts.map +1 -1
  17. package/lib/typescript/src/SelectionMenu.d.ts +6 -10
  18. package/lib/typescript/src/SelectionMenu.d.ts.map +1 -1
  19. package/lib/typescript/src/SelectionMenuNativeComponent.d.ts +0 -7
  20. package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +1 -1
  21. package/package.json +16 -9
  22. package/src/DatePicker.tsx +5 -1
  23. package/src/SelectionMenu.tsx +8 -33
  24. package/src/SelectionMenuNativeComponent.ts +0 -9
  25. package/lib/module/SelectionMenu.web.js +0 -57
  26. package/lib/module/SelectionMenu.web.js.map +0 -1
  27. package/lib/typescript/src/SelectionMenu.web.d.ts +0 -19
  28. package/lib/typescript/src/SelectionMenu.web.d.ts.map +0 -1
  29. package/src/SelectionMenu.web.tsx +0 -93
@@ -1,9 +1,8 @@
1
1
  package com.platformcomponents
2
2
 
3
- import android.app.Activity
4
3
  import android.content.Context
5
- import android.content.ContextWrapper
6
4
  import android.text.InputType
5
+ import android.util.Log
7
6
  import android.view.View
8
7
  import android.view.ViewGroup
9
8
  import android.widget.AdapterView
@@ -11,10 +10,7 @@ import android.widget.ArrayAdapter
11
10
  import android.widget.FrameLayout
12
11
  import android.widget.LinearLayout
13
12
  import android.widget.Spinner
14
- import android.content.DialogInterface
15
- import androidx.fragment.app.FragmentActivity
16
- import com.facebook.react.uimanager.ThemedReactContext
17
- import com.google.android.material.dialog.MaterialAlertDialogBuilder
13
+ import androidx.appcompat.widget.PopupMenu
18
14
  import com.google.android.material.textfield.MaterialAutoCompleteTextView
19
15
  import com.google.android.material.textfield.TextInputLayout
20
16
 
@@ -22,6 +18,10 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
22
18
 
23
19
  data class Option(val label: String, val data: String)
24
20
 
21
+ companion object {
22
+ private const val TAG = "PCSelectionMenu"
23
+ }
24
+
25
25
  // --- Props ---
26
26
  var options: List<Option> = emptyList()
27
27
  var selectedData: String = "" // sentinel for none
@@ -32,9 +32,6 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
32
32
  var anchorMode: String = "headless" // "inline" | "headless"
33
33
  var visible: String = "closed" // "open" | "closed" (headless only)
34
34
 
35
- // Kept for backward compatibility with your interface; headless uses Spinner regardless.
36
- var presentation: String = "auto" // "auto" | "popover" | "sheet"
37
-
38
35
  // Only used to choose inline rendering style.
39
36
  var androidMaterial: String? = "system" // "system" | "m3"
40
37
 
@@ -46,12 +43,14 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
46
43
  private var inlineLayout: TextInputLayout? = null
47
44
  private var inlineText: MaterialAutoCompleteTextView? = null
48
45
  private var inlineSpinner: Spinner? = null
46
+ private var inlineDropdownOverlay: View? = null
47
+ private var inlineSpinnerSuppressCount = 0
49
48
 
50
49
  // --- Headless UI (true picker) ---
51
- private var headlessSpinner: Spinner? = null
52
- private var headlessDismissHooked: Boolean = false
53
-
54
- private var suppressSpinnerSelection = false
50
+ private var headlessMenu: PopupMenu? = null
51
+ private var headlessMenuShowing = false
52
+ private var headlessDismissProgrammatic = false
53
+ private var headlessDismissAfterSelect = false
55
54
 
56
55
  init {
57
56
  minimumHeight = 0
@@ -83,23 +82,35 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
83
82
  // ---- Public apply* (called by manager) ----
84
83
 
85
84
  fun applyOptions(newOptions: List<Option>) {
85
+ if (options == newOptions) return
86
86
  options = newOptions
87
+ Log.d(TAG, "applyOptions size=${options.size}")
87
88
  refreshAdapters()
88
89
  refreshSelections()
89
90
  }
90
91
 
91
92
  fun applySelectedData(data: String?) {
92
- selectedData = data ?: ""
93
+ val next = data ?: ""
94
+ if (selectedData == next) return
95
+ selectedData = next
96
+ Log.d(TAG, "applySelectedData selectedData=$selectedData")
93
97
  refreshSelections()
94
98
  }
95
99
 
96
100
  fun applyInteractivity(value: String?) {
97
101
  interactivity = if (value == "disabled") "disabled" else "enabled"
102
+ Log.d(TAG, "applyInteractivity interactivity=$interactivity")
98
103
  updateEnabledState()
99
104
 
100
105
  // If disabled while open, request close.
101
106
  if (interactivity != "enabled" && visible == "open") {
102
- onRequestClose?.invoke()
107
+ Log.d(TAG, "applyInteractivity disabled while open -> requestClose")
108
+ if (anchorMode == "headless" && headlessMenuShowing) {
109
+ headlessDismissProgrammatic = true
110
+ headlessMenu?.dismiss()
111
+ } else {
112
+ onRequestClose?.invoke()
113
+ }
103
114
  }
104
115
  }
105
116
 
@@ -107,6 +118,7 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
107
118
  if (placeholder == value) return
108
119
  placeholder = value
109
120
  inlineLayout?.hint = placeholder
121
+ // Spinner doesn't support placeholder
110
122
  }
111
123
 
112
124
  fun applyAnchorMode(value: String?) {
@@ -116,6 +128,7 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
116
128
  }
117
129
  if (anchorMode == newMode) return
118
130
  anchorMode = newMode
131
+ Log.d(TAG, "applyAnchorMode anchorMode=$anchorMode")
119
132
  rebuildUI()
120
133
  }
121
134
 
@@ -124,21 +137,18 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
124
137
  "open", "closed" -> value
125
138
  else -> "closed"
126
139
  }
140
+ Log.d(TAG, "applyVisible visible=$visible anchorMode=$anchorMode")
127
141
 
128
142
  if (anchorMode != "headless") return
129
143
 
130
144
  if (visible == "open") {
131
145
  presentHeadlessIfNeeded()
132
146
  } else {
133
- // We can't reliably force-close a Spinner dropdown, but we can at least signal state.
134
- // Most apps will set visible=closed after onSelect/onRequestClose anyway.
135
- }
136
- }
137
-
138
- fun applyPresentation(value: String?) {
139
- presentation = when (value) {
140
- "auto", "popover", "sheet" -> value
141
- else -> "auto"
147
+ Log.d(TAG, "applyVisible close -> dismiss")
148
+ if (headlessMenuShowing) {
149
+ headlessDismissProgrammatic = true
150
+ headlessMenu?.dismiss()
151
+ }
142
152
  }
143
153
  }
144
154
 
@@ -152,15 +162,22 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
152
162
  // ---- UI building ----
153
163
 
154
164
  private fun rebuildUI() {
165
+ inlineText?.dismissDropDown()
166
+ detachInlineDropdownOverlay()
167
+ inlineDropdownOverlay = null
155
168
  removeAllViews()
156
169
  inlineLayout = null
157
170
  inlineText = null
158
171
  inlineSpinner = null
159
- headlessSpinner = null
160
- headlessDismissHooked = false
172
+ inlineSpinnerSuppressCount = 0
173
+ headlessMenu = null
174
+ headlessMenuShowing = false
175
+ headlessDismissProgrammatic = false
176
+ headlessDismissAfterSelect = false
161
177
 
162
178
  // Headless should be invisible but anchorable.
163
- alpha = if (anchorMode == "headless") 0f else 1f
179
+ alpha = if (anchorMode == "headless") 0.01f else 1f
180
+ Log.d(TAG, "rebuildUI anchorMode=$anchorMode alpha=$alpha")
164
181
 
165
182
  if (anchorMode == "inline") {
166
183
  buildInline()
@@ -178,63 +195,129 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
178
195
  val mode = parseMaterial(androidMaterial)
179
196
 
180
197
  if (mode == MaterialMode.M3) {
181
- // M3 inline = exposed dropdown look, but forced read-only
198
+ // M3 exposed dropdown menu - the standard Material 3 way
182
199
  val til = TextInputLayout(context).apply {
183
200
  layoutParams = FrameLayout.LayoutParams(
184
201
  FrameLayout.LayoutParams.MATCH_PARENT,
185
- FrameLayout.LayoutParams.MATCH_PARENT
202
+ FrameLayout.LayoutParams.WRAP_CONTENT
186
203
  )
187
204
  hint = placeholder
205
+ endIconMode = TextInputLayout.END_ICON_DROPDOWN_MENU
188
206
  }
189
207
 
190
- val actv = MaterialAutoCompleteTextView(til.context).apply {
208
+ val actv = InlineAutoCompleteTextView(til.context)
209
+ inlineText = actv
210
+ actv.apply {
191
211
  layoutParams = LinearLayout.LayoutParams(
192
212
  ViewGroup.LayoutParams.MATCH_PARENT,
193
213
  ViewGroup.LayoutParams.WRAP_CONTENT
194
214
  )
195
- isSingleLine = true
196
- threshold = 0
197
215
 
198
- // behave like picker
199
- inputType = InputType.TYPE_NULL
216
+ // Keep it a real text editor so the popup behaves modally
217
+ inputType = InputType.TYPE_CLASS_TEXT
218
+
219
+ // Prevent keyboard
220
+ showSoftInputOnFocus = false
221
+
222
+ // Optional: keep it from being typed into
200
223
  keyListener = null
201
224
  isCursorVisible = false
202
- isFocusable = false
203
- isFocusableInTouchMode = false
204
225
 
205
- setOnClickListener {
206
- if (interactivity == "enabled") showDropDown()
207
- }
226
+ // Nice UX: click anywhere opens dropdown
227
+ setOnClickListener { showDropDown() }
208
228
 
209
229
  setOnItemClickListener { _, _, position, _ ->
210
230
  val opt = options.getOrNull(position) ?: return@setOnItemClickListener
211
231
  selectedData = opt.data
212
232
  onSelect?.invoke(position, opt.label, opt.data)
233
+ detachInlineDropdownOverlay()
234
+ }
235
+
236
+ setOnTouchListener { _, e ->
237
+ if (e.action == android.view.MotionEvent.ACTION_UP) {
238
+ showDropDown()
239
+ }
240
+ false // let default handling run
213
241
  }
214
242
  }
215
243
 
216
244
  til.addView(actv)
217
245
  addView(til)
218
246
  inlineLayout = til
219
- inlineText = actv
220
247
  } else {
221
- // SYSTEM inline = native spinner
222
- val sp = Spinner(context, Spinner.MODE_DROPDOWN).apply {
223
- layoutParams = FrameLayout.LayoutParams(
224
- FrameLayout.LayoutParams.MATCH_PARENT,
225
- FrameLayout.LayoutParams.WRAP_CONTENT
226
- )
248
+ // SYSTEM mode: Use custom Spinner with dropdown mode to ensure callbacks fire
249
+ val sp = object : Spinner(context, null, android.R.attr.spinnerStyle, Spinner.MODE_DROPDOWN) {
250
+ override fun setSelection(position: Int, animate: Boolean) {
251
+ val oldPos = selectedItemPosition
252
+ super.setSelection(position, animate)
253
+
254
+ // Manually trigger onItemSelectedListener if selection changed
255
+ // This is needed because Spinner doesn't always trigger the callback when
256
+ // the selection changes via user interaction with the dropdown
257
+ if (position != oldPos && onItemSelectedListener != null) {
258
+ post {
259
+ onItemSelectedListener?.onItemSelected(
260
+ this,
261
+ selectedView,
262
+ position,
263
+ getItemIdAtPosition(position)
264
+ )
265
+ }
266
+ }
267
+ }
268
+
269
+ override fun setSelection(position: Int) {
270
+ val oldPos = selectedItemPosition
271
+ super.setSelection(position)
272
+
273
+ // Manually trigger onItemSelectedListener if selection changed
274
+ if (position != oldPos && onItemSelectedListener != null) {
275
+ post {
276
+ onItemSelectedListener?.onItemSelected(
277
+ this,
278
+ selectedView,
279
+ position,
280
+ getItemIdAtPosition(position)
281
+ )
282
+ }
283
+ }
284
+ }
227
285
  }
228
286
 
287
+ // Set listener FIRST, before adapter
229
288
  sp.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
230
- override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
231
- if (suppressSpinnerSelection) return
289
+ override fun onItemSelected(
290
+ parent: AdapterView<*>,
291
+ view: View?,
292
+ position: Int,
293
+ id: Long
294
+ ) {
295
+ // If suppress count > 0, this is a programmatic change (ignore it)
296
+ if (inlineSpinnerSuppressCount > 0) return
297
+
298
+ if (interactivity != "enabled") return
299
+
232
300
  val opt = options.getOrNull(position) ?: return
301
+
302
+ // Only fire callback if selection actually changed
233
303
  if (opt.data == selectedData) return
234
- selectedData = opt.data
304
+
305
+ // Don't update selectedData here - let applySelectedData handle it
306
+ // This ensures refreshSelections() is called to update the Spinner's display
235
307
  onSelect?.invoke(position, opt.label, opt.data)
236
308
  }
237
- override fun onNothingSelected(parent: AdapterView<*>?) = Unit
309
+
310
+ override fun onNothingSelected(parent: AdapterView<*>) {
311
+ // No-op
312
+ }
313
+ }
314
+
315
+ sp.apply {
316
+ layoutParams = FrameLayout.LayoutParams(
317
+ FrameLayout.LayoutParams.MATCH_PARENT,
318
+ FrameLayout.LayoutParams.WRAP_CONTENT
319
+ )
320
+ visibility = View.VISIBLE
238
321
  }
239
322
 
240
323
  addView(sp)
@@ -243,32 +326,35 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
243
326
  }
244
327
 
245
328
  private fun buildHeadless() {
246
- // Headless = TRUE picker using Spinner dropdown
247
- val sp = Spinner(context, Spinner.MODE_DROPDOWN).apply {
248
- layoutParams = FrameLayout.LayoutParams(1, 1)
249
- // keep it anchorable but not interactive unless we programmatically open it
250
- isClickable = true
251
- isFocusable = false
252
- isFocusableInTouchMode = false
253
- }
254
-
255
- sp.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
256
- override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
257
- if (suppressSpinnerSelection) return
258
- val opt = options.getOrNull(position) ?: return
259
-
260
- selectedData = opt.data
261
- onSelect?.invoke(position, opt.label, opt.data)
262
-
263
- // After selection, close contract.
264
- onRequestClose?.invoke()
329
+ val popup = PopupMenu(context, this@PCSelectionMenuView).apply {
330
+ setOnMenuItemClickListener { item ->
331
+ val index = item.itemId
332
+ val opt = options.getOrNull(index)
333
+ Log.d(
334
+ TAG,
335
+ "headless onMenuItemClick index=$index optData=${opt?.data} selectedData=$selectedData"
336
+ )
337
+ headlessDismissAfterSelect = true
338
+ handleHeadlessSelection(index)
339
+ true
340
+ }
341
+ setOnDismissListener {
342
+ val programmatic = headlessDismissProgrammatic || headlessDismissAfterSelect
343
+ headlessDismissProgrammatic = false
344
+ headlessDismissAfterSelect = false
345
+ headlessMenuShowing = false
346
+ if (programmatic) {
347
+ Log.d(TAG, "headless onDismiss programmatic")
348
+ } else {
349
+ Log.d(TAG, "headless onDismiss -> requestClose")
350
+ onRequestClose?.invoke()
351
+ }
265
352
  }
266
-
267
- override fun onNothingSelected(parent: AdapterView<*>?) = Unit
268
353
  }
269
354
 
270
- addView(sp)
271
- headlessSpinner = sp
355
+ headlessMenu = popup
356
+ Log.d(TAG, "buildHeadless menu=${System.identityHashCode(popup)}")
357
+ refreshHeadlessMenu()
272
358
  }
273
359
 
274
360
  private fun updateEnabledState() {
@@ -276,108 +362,166 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
276
362
  inlineLayout?.isEnabled = enabled
277
363
  inlineText?.isEnabled = enabled
278
364
  inlineSpinner?.isEnabled = enabled
279
- headlessSpinner?.isEnabled = enabled
280
365
  }
281
366
 
282
367
  private fun refreshAdapters() {
283
368
  val labels = options.map { it.label }
284
369
 
285
370
  inlineText?.let { actv ->
286
- actv.setAdapter(ArrayAdapter(actv.context, android.R.layout.simple_list_item_1, labels))
371
+ val adapter = ArrayAdapter(actv.context, android.R.layout.simple_list_item_1, labels)
372
+ actv.setAdapter(adapter)
287
373
  }
288
374
 
289
- fun applySpinnerAdapter(sp: Spinner) {
375
+ inlineSpinner?.let { sp ->
376
+ suppressInlineSpinnerCallbacks(sp)
290
377
  val adapter = ArrayAdapter(sp.context, android.R.layout.simple_spinner_item, labels)
291
378
  adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
292
379
  sp.adapter = adapter
293
380
  }
294
381
 
295
- inlineSpinner?.let { applySpinnerAdapter(it) }
296
- headlessSpinner?.let { applySpinnerAdapter(it) }
382
+ refreshHeadlessMenu()
297
383
  }
298
384
 
299
385
  private fun refreshSelections() {
300
386
  val idx = options.indexOfFirst { it.data == selectedData }
301
- val label = if (idx >= 0) options[idx].label else ""
302
387
 
303
- inlineText?.setText(label, false)
388
+ inlineText?.let { actv ->
389
+ if (idx >= 0) {
390
+ // Show selected value
391
+ actv.setText(options[idx].label, false)
392
+ } else {
393
+ // Clear text to show placeholder
394
+ actv.setText("", false)
395
+ }
396
+ }
304
397
 
305
- fun setSpinnerSelection(sp: Spinner) {
398
+ inlineSpinner?.let { sp ->
306
399
  if (options.isEmpty()) return
307
400
  val target = if (idx >= 0) idx else 0
308
- if (sp.selectedItemPosition == target) return
401
+ // Always call setSelection to ensure the view is refreshed
402
+ // Even if the position hasn't changed, we need to update the displayed text
403
+ suppressInlineSpinnerCallbacks(sp)
404
+ sp.setSelection(target, false)
405
+ }
406
+ }
309
407
 
310
- suppressSpinnerSelection = true
311
- try {
312
- sp.setSelection(target, false)
313
- } finally {
314
- suppressSpinnerSelection = false
408
+ // ---- Inline dropdown overlay ----
409
+
410
+ private inner class InlineAutoCompleteTextView(context: Context) :
411
+ MaterialAutoCompleteTextView(context) {
412
+ override fun showDropDown() {
413
+ if (interactivity != "enabled" || !isEnabled) return
414
+ attachInlineDropdownOverlay()
415
+ super.showDropDown()
416
+ post {
417
+ if (!isPopupShowing) {
418
+ detachInlineDropdownOverlay()
419
+ }
315
420
  }
316
421
  }
317
422
 
318
- inlineSpinner?.let { setSpinnerSelection(it) }
319
- headlessSpinner?.let { setSpinnerSelection(it) }
423
+ override fun dismissDropDown() {
424
+ super.dismissDropDown()
425
+ detachInlineDropdownOverlay()
426
+ }
427
+ }
428
+
429
+ private fun attachInlineDropdownOverlay() {
430
+ if (inlineDropdownOverlay?.parent != null) return
431
+ val parent = findInlineOverlayParent() ?: return
432
+ // Fullscreen touch guard to dismiss without leaking taps to underlying views.
433
+ val overlay = inlineDropdownOverlay
434
+ ?: View(parent.context).apply {
435
+ layoutParams = ViewGroup.LayoutParams(
436
+ ViewGroup.LayoutParams.MATCH_PARENT,
437
+ ViewGroup.LayoutParams.MATCH_PARENT
438
+ )
439
+ isClickable = true
440
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
441
+ setOnTouchListener { _, event ->
442
+ if (event.action == android.view.MotionEvent.ACTION_DOWN) {
443
+ inlineText?.dismissDropDown()
444
+ detachInlineDropdownOverlay()
445
+ }
446
+ true
447
+ }
448
+ }.also { inlineDropdownOverlay = it }
449
+ parent.addView(overlay)
450
+ }
451
+
452
+ private fun detachInlineDropdownOverlay() {
453
+ val overlay = inlineDropdownOverlay ?: return
454
+ (overlay.parent as? ViewGroup)?.removeView(overlay)
455
+ }
456
+
457
+ private fun findInlineOverlayParent(): ViewGroup? {
458
+ val activity = context.findActivity()
459
+ val contentRoot = activity?.findViewById<ViewGroup>(android.R.id.content)
460
+ if (contentRoot != null) return contentRoot
461
+ val activityRoot = activity?.window?.decorView as? ViewGroup
462
+ if (activityRoot != null) return activityRoot
463
+ return rootView as? ViewGroup
320
464
  }
321
465
 
322
466
  // ---- Headless open ----
323
467
 
324
468
  private fun presentHeadlessIfNeeded() {
325
- val sp = headlessSpinner ?: return
469
+ val popup = headlessMenu ?: return
326
470
  if (interactivity != "enabled") {
471
+ Log.d(TAG, "presentHeadlessIfNeeded interactivity=$interactivity -> requestClose")
327
472
  onRequestClose?.invoke()
328
473
  return
329
474
  }
330
475
  post {
331
476
  if (!isAttachedToWindow) {
477
+ Log.d(TAG, "presentHeadlessIfNeeded not attached -> requestClose")
332
478
  onRequestClose?.invoke()
333
479
  return@post
334
480
  }
335
-
336
- // Make dropdown match the anchor view width
337
- val anchorW = this@PCSelectionMenuView.width
338
- if (anchorW > 0) {
339
- sp.dropDownWidth = anchorW
481
+ Log.d(
482
+ TAG,
483
+ "presentHeadlessIfNeeded attached width=${this@PCSelectionMenuView.width} alpha=$alpha"
484
+ )
485
+
486
+ refreshHeadlessMenu()
487
+ if (!headlessMenuShowing) {
488
+ Log.d(TAG, "presentHeadlessIfNeeded show items=${options.size}")
489
+ headlessDismissProgrammatic = false
490
+ headlessDismissAfterSelect = false
491
+ headlessMenuShowing = true
492
+ popup.show()
340
493
  }
494
+ }
495
+ }
341
496
 
342
- // Align dropdown’s left edge with the anchor’s left edge.
343
- // (X positioning comes from the anchor itself; this is just extra explicit.)
344
- sp.dropDownHorizontalOffset = 0
345
- sp.dropDownVerticalOffset = 0
497
+ private fun handleHeadlessSelection(position: Int) {
498
+ val opt = options.getOrNull(position) ?: return
499
+ Log.d(TAG, "handleHeadlessSelection pos=$position data=${opt.data}")
500
+ selectedData = opt.data
501
+ onSelect?.invoke(position, opt.label, opt.data)
502
+ }
346
503
 
347
- hookSpinnerDismiss(sp)
348
- sp.performClick()
504
+ private fun refreshHeadlessMenu() {
505
+ val menu = headlessMenu?.menu ?: return
506
+ menu.clear()
507
+ options.forEachIndexed { index, opt ->
508
+ menu.add(0, index, index, opt.label)
349
509
  }
350
510
  }
351
511
 
352
- /**
353
- * Spinner doesn't expose dismiss callbacks publicly. Many Android builds use an internal
354
- * DropdownPopup (ListPopupWindow) stored in a private field. We hook it if possible.
355
- *
356
- * If this fails, selection still works, but you may not get an "onRequestClose" when dismissed.
357
- */
358
- private fun hookSpinnerDismiss(sp: Spinner) {
359
- if (headlessDismissHooked) return
360
-
361
- try {
362
- val f = Spinner::class.java.getDeclaredField("mPopup")
363
- f.isAccessible = true
364
- val popupObj = f.get(sp)
365
-
366
- // AOSP DropdownPopup extends ListPopupWindow on many versions.
367
- val setOnDismiss = popupObj?.javaClass?.methods?.firstOrNull { m ->
368
- m.name == "setOnDismissListener" && m.parameterTypes.size == 1
369
- }
370
-
371
- if (setOnDismiss != null) {
372
- val listener = DialogInterface.OnDismissListener {
373
- onRequestClose?.invoke()
374
- }
375
- setOnDismiss.invoke(popupObj, listener)
376
- headlessDismissHooked = true
377
- }
378
- } catch (_: Throwable) {
379
- // ignore; no dismiss hook available
512
+ private fun suppressInlineSpinnerCallbacks(sp: Spinner) {
513
+ inlineSpinnerSuppressCount += 1
514
+ val posted = sp.post {
515
+ inlineSpinnerSuppressCount = (inlineSpinnerSuppressCount - 1).coerceAtLeast(0)
380
516
  }
517
+ if (!posted) {
518
+ inlineSpinnerSuppressCount = (inlineSpinnerSuppressCount - 1).coerceAtLeast(0)
519
+ }
520
+ }
521
+
522
+ override fun onDetachedFromWindow() {
523
+ detachInlineDropdownOverlay()
524
+ super.onDetachedFromWindow()
381
525
  }
382
526
 
383
527
  // ---- Helpers ----
@@ -390,21 +534,4 @@ class PCSelectionMenuView(context: Context) : FrameLayout(context) {
390
534
  "system", null -> MaterialMode.SYSTEM
391
535
  else -> MaterialMode.SYSTEM
392
536
  }
393
-
394
- private fun findFragmentActivity(): FragmentActivity? {
395
- val trc = context as? ThemedReactContext
396
- val a1 = trc?.currentActivity
397
- if (a1 is FragmentActivity) return a1
398
-
399
- var c: Context? = context
400
- while (c is ContextWrapper) {
401
- if (c is FragmentActivity) return c
402
- val base = (c as ContextWrapper).baseContext
403
- if (base == c) break
404
- c = base
405
- }
406
-
407
- val a2 = (context as? Activity)
408
- return a2 as? FragmentActivity
409
- }
410
537
  }
@@ -1,5 +1,6 @@
1
1
  package com.platformcomponents
2
2
 
3
+ import android.util.Log
3
4
  import com.facebook.react.bridge.ReadableArray
4
5
  import com.facebook.react.bridge.ReadableMap
5
6
  import com.facebook.react.uimanager.SimpleViewManager
@@ -15,6 +16,10 @@ class PCSelectionMenuViewManager :
15
16
  SimpleViewManager<PCSelectionMenuView>(),
16
17
  PCSelectionMenuManagerInterface<PCSelectionMenuView> {
17
18
 
19
+ companion object {
20
+ private const val TAG = "PCSelectionMenu"
21
+ }
22
+
18
23
  private val delegate: ViewManagerDelegate<PCSelectionMenuView> =
19
24
  PCSelectionMenuManagerDelegate(this)
20
25
 
@@ -73,10 +78,6 @@ class PCSelectionMenuViewManager :
73
78
  view.applyVisible(value)
74
79
  }
75
80
 
76
- override fun setPresentation(view: PCSelectionMenuView, value: String?) {
77
- view.applyPresentation(value)
78
- }
79
-
80
81
  override fun setAndroid(view: PCSelectionMenuView, value: ReadableMap?) {
81
82
  val material =
82
83
  if (value != null && value.hasKey("material") && !value.isNull("material")) value.getString("material") else null
@@ -159,9 +159,7 @@ public final class PCDatePickerView: UIControl,
159
159
  picker.layoutIfNeeded()
160
160
 
161
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))
162
+ return fitted
165
163
  }
166
164
 
167
165
  // MARK: - Presentation
@@ -221,15 +219,8 @@ public final class PCDatePickerView: UIControl,
221
219
  picker.translatesAutoresizingMaskIntoConstraints = false
222
220
  vc.view.addSubview(picker)
223
221
 
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
222
  let size = popoverContentSize()
232
- vc.preferredContentSize = size
223
+ vc.preferredContentSize = size
233
224
 
234
225
  // ✅ Anchored popover-style (not a full sheet)
235
226
  vc.modalPresentationStyle = .popover