react-native-platform-components 0.0.3 → 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.
- package/PlatformComponents.podspec +1 -1
- package/README.md +2 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +5 -7
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +273 -146
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +5 -4
- package/ios/PCDatePickerView.swift +2 -11
- package/ios/PCSelectionMenu.mm +0 -10
- package/ios/PCSelectionMenu.swift +145 -179
- package/lib/module/DatePicker.js +3 -1
- package/lib/module/DatePicker.js.map +1 -1
- package/lib/module/SelectionMenu.js +4 -10
- package/lib/module/SelectionMenu.js.map +1 -1
- package/lib/module/SelectionMenuNativeComponent.ts +0 -9
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/DatePicker.d.ts +2 -0
- package/lib/typescript/src/DatePicker.d.ts.map +1 -1
- package/lib/typescript/src/SelectionMenu.d.ts +6 -10
- package/lib/typescript/src/SelectionMenu.d.ts.map +1 -1
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts +0 -7
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +1 -1
- package/package.json +16 -9
- package/src/DatePicker.tsx +5 -1
- package/src/SelectionMenu.tsx +8 -33
- package/src/SelectionMenuNativeComponent.ts +0 -9
- package/lib/module/SelectionMenu.web.js +0 -57
- package/lib/module/SelectionMenu.web.js.map +0 -1
- package/lib/typescript/src/SelectionMenu.web.d.ts +0 -19
- package/lib/typescript/src/SelectionMenu.web.d.ts.map +0 -1
- 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
|
|
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
|
|
52
|
-
private var
|
|
53
|
-
|
|
54
|
-
private var
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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")
|
|
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
|
|
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.
|
|
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 =
|
|
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
|
-
//
|
|
199
|
-
inputType = InputType.
|
|
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
|
-
|
|
206
|
-
|
|
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
|
|
222
|
-
val sp = Spinner(context, Spinner.MODE_DROPDOWN)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
271
|
-
|
|
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
|
-
|
|
371
|
+
val adapter = ArrayAdapter(actv.context, android.R.layout.simple_list_item_1, labels)
|
|
372
|
+
actv.setAdapter(adapter)
|
|
287
373
|
}
|
|
288
374
|
|
|
289
|
-
|
|
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
|
-
|
|
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?.
|
|
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
|
-
|
|
398
|
+
inlineSpinner?.let { sp ->
|
|
306
399
|
if (options.isEmpty()) return
|
|
307
400
|
val target = if (idx >= 0) idx else 0
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
319
|
-
|
|
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
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|