react-native-platform-components 1.5.0 → 1.7.0
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 +19 -18
- package/android/src/main/java/com/platformcomponents/PCActionSheetView.kt +309 -0
- package/android/src/main/java/com/platformcomponents/PCActionSheetViewManager.kt +131 -0
- package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +5 -1
- package/android/src/main/java/com/platformcomponents/PCButtonView.kt +5 -1
- package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +82 -27
- package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +12 -13
- package/android/src/main/java/com/platformcomponents/PCMenuSupport.kt +66 -2
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
- package/android/src/main/res/values/ids.xml +2 -0
- package/ios/PCActionSheet.h +12 -0
- package/ios/PCActionSheet.mm +160 -0
- package/ios/PCActionSheetView.swift +280 -0
- package/ios/PCContextMenu.mm +22 -1
- package/ios/PCContextMenu.swift +218 -15
- package/ios/PCMenuItems.h +2 -1
- package/ios/PCMenuSupport.swift +22 -3
- package/lib/commonjs/ActionSheet.js +85 -0
- package/lib/commonjs/ActionSheet.js.map +1 -0
- package/lib/commonjs/ActionSheetNativeComponent.ts +94 -0
- package/lib/commonjs/ButtonGroupNativeComponent.ts +2 -0
- package/lib/commonjs/ButtonNativeComponent.ts +2 -0
- package/lib/commonjs/ContextMenu.js +50 -4
- package/lib/commonjs/ContextMenu.js.map +1 -1
- package/lib/commonjs/ContextMenuNativeComponent.ts +29 -8
- package/lib/commonjs/index.js +11 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +7 -0
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/jest.js +92 -5
- package/lib/commonjs/jest.js.map +1 -1
- package/lib/commonjs/menuItems.js +2 -1
- package/lib/commonjs/menuItems.js.map +1 -1
- package/lib/commonjs/selectionIdentifiers.js.map +1 -1
- package/lib/commonjs/web/ActionSheet.js +131 -0
- package/lib/commonjs/web/ActionSheet.js.map +1 -0
- package/lib/commonjs/web/components.js +3 -1
- package/lib/commonjs/web/components.js.map +1 -1
- package/lib/commonjs/web/containers.js +1 -0
- package/lib/commonjs/web/containers.js.map +1 -1
- package/lib/module/ActionSheet.js +78 -0
- package/lib/module/ActionSheet.js.map +1 -0
- package/lib/module/ActionSheetNativeComponent.ts +94 -0
- package/lib/module/ButtonGroupNativeComponent.ts +2 -0
- package/lib/module/ButtonNativeComponent.ts +2 -0
- package/lib/module/ContextMenu.js +51 -4
- package/lib/module/ContextMenu.js.map +1 -1
- package/lib/module/ContextMenuNativeComponent.ts +29 -8
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +1 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/jest.js +90 -5
- package/lib/module/jest.js.map +1 -1
- package/lib/module/menuItems.js +2 -1
- package/lib/module/menuItems.js.map +1 -1
- package/lib/module/selectionIdentifiers.js.map +1 -1
- package/lib/module/web/ActionSheet.js +126 -0
- package/lib/module/web/ActionSheet.js.map +1 -0
- package/lib/module/web/components.js +2 -0
- package/lib/module/web/components.js.map +1 -1
- package/lib/module/web/containers.js +1 -0
- package/lib/module/web/containers.js.map +1 -1
- package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ActionSheet.d.ts +98 -0
- package/lib/typescript/commonjs/src/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ActionSheetNativeComponent.d.ts +78 -0
- package/lib/typescript/commonjs/src/ActionSheetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +2 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +2 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ContextMenu.d.ts +22 -6
- package/lib/typescript/commonjs/src/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +20 -7
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.d.ts +1 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.web.d.ts +2 -1
- package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/jest.d.ts +17 -3
- package/lib/typescript/commonjs/src/jest.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/menuItems.d.ts +12 -0
- package/lib/typescript/commonjs/src/menuItems.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts +1 -1
- package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/ActionSheet.d.ts +11 -0
- package/lib/typescript/commonjs/src/web/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/components.d.ts +1 -0
- package/lib/typescript/commonjs/src/web/components.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/web/containers.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/webComponents.d.ts +2 -0
- package/lib/typescript/commonjs/src/webComponents.d.ts.map +1 -1
- package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/module/src/ActionSheet.d.ts +98 -0
- package/lib/typescript/module/src/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/module/src/ActionSheetNativeComponent.d.ts +78 -0
- package/lib/typescript/module/src/ActionSheetNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +2 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts +2 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/ContextMenu.d.ts +22 -6
- package/lib/typescript/module/src/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +20 -7
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +1 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.web.d.ts +2 -1
- package/lib/typescript/module/src/index.web.d.ts.map +1 -1
- package/lib/typescript/module/src/jest.d.ts +17 -3
- package/lib/typescript/module/src/jest.d.ts.map +1 -1
- package/lib/typescript/module/src/menuItems.d.ts +12 -0
- package/lib/typescript/module/src/menuItems.d.ts.map +1 -1
- package/lib/typescript/module/src/selectionIdentifiers.d.ts +1 -1
- package/lib/typescript/module/src/selectionIdentifiers.d.ts.map +1 -1
- package/lib/typescript/module/src/web/ActionSheet.d.ts +11 -0
- package/lib/typescript/module/src/web/ActionSheet.d.ts.map +1 -0
- package/lib/typescript/module/src/web/components.d.ts +1 -0
- package/lib/typescript/module/src/web/components.d.ts.map +1 -1
- package/lib/typescript/module/src/web/containers.d.ts.map +1 -1
- package/lib/typescript/module/src/webComponents.d.ts +2 -0
- package/lib/typescript/module/src/webComponents.d.ts.map +1 -1
- package/package.json +9 -2
- package/plugin/build/index.cjs +79 -6
- package/plugin/build/index.d.ts +24 -0
- package/plugin/build/index.d.ts.map +1 -1
- package/plugin/tsconfig.json +2 -1
- package/src/ActionSheet.tsx +193 -0
- package/src/ActionSheetNativeComponent.ts +94 -0
- package/src/ButtonGroupNativeComponent.ts +2 -0
- package/src/ButtonNativeComponent.ts +2 -0
- package/src/ContextMenu.tsx +71 -12
- package/src/ContextMenuNativeComponent.ts +29 -8
- package/src/index.tsx +1 -0
- package/src/index.web.tsx +2 -0
- package/src/jest.tsx +108 -4
- package/src/menuItems.ts +13 -0
- package/src/selectionIdentifiers.ts +1 -1
- package/src/web/ActionSheet.tsx +130 -0
- package/src/web/components.tsx +5 -0
- package/src/web/containers.tsx +1 -0
- package/src/webComponents.ts +2 -0
|
@@ -7,7 +7,10 @@ import android.view.Gravity
|
|
|
7
7
|
import android.view.HapticFeedbackConstants
|
|
8
8
|
import android.view.MotionEvent
|
|
9
9
|
import android.view.ViewConfiguration
|
|
10
|
+
import android.view.ViewGroup
|
|
10
11
|
import androidx.appcompat.widget.PopupMenu
|
|
12
|
+
import com.facebook.react.uimanager.RootView
|
|
13
|
+
import com.facebook.react.uimanager.events.NativeGestureUtil
|
|
11
14
|
import com.facebook.react.views.view.ReactViewGroup
|
|
12
15
|
|
|
13
16
|
class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
@@ -21,8 +24,8 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
21
24
|
/** The flattened menu items (see PCMenuSupport) */
|
|
22
25
|
var actions: List<PCMenuSupport.Item> = emptyList()
|
|
23
26
|
var interactivity: String = "enabled" // "enabled" | "disabled"
|
|
24
|
-
var trigger: String = "longPress" // "longPress" | "tap"
|
|
25
|
-
var
|
|
27
|
+
var trigger: String = "longPress" // "longPress" | "tap" | "none"
|
|
28
|
+
var visible: String = "closed" // "open" | "closed" (programmatic)
|
|
26
29
|
var androidAnchorPosition: String? = "left" // "left" | "right"
|
|
27
30
|
|
|
28
31
|
/** The `haptics` prop, played when an action is picked (an action's own `haptics` wins). */
|
|
@@ -42,8 +45,12 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
42
45
|
|
|
43
46
|
// Long-press detection
|
|
44
47
|
private var longPressRunnable: Runnable? = null
|
|
48
|
+
/** A long press opened the menu and cancelled the touch in JS. */
|
|
49
|
+
private var nativeGestureStarted = false
|
|
45
50
|
private var touchDownX = 0f
|
|
46
51
|
private var touchDownY = 0f
|
|
52
|
+
/** Tap mode: the last touch went down on a native control (see handleTap). */
|
|
53
|
+
private var downOnNativeControl = false
|
|
47
54
|
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
|
48
55
|
private val longPressTimeout = ViewConfiguration.getLongPressTimeout().toLong()
|
|
49
56
|
|
|
@@ -51,8 +58,8 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
51
58
|
// Enable long-click for longPress mode
|
|
52
59
|
isLongClickable = true
|
|
53
60
|
setOnLongClickListener { handleLongClick() }
|
|
54
|
-
// Enable click for tap mode
|
|
55
|
-
setOnClickListener {
|
|
61
|
+
// Enable click for tap mode (accessibility services; touches go through JS)
|
|
62
|
+
setOnClickListener { openTapMenu() }
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
@@ -64,6 +71,7 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
64
71
|
MotionEvent.ACTION_DOWN -> {
|
|
65
72
|
touchDownX = ev.x
|
|
66
73
|
touchDownY = ev.y
|
|
74
|
+
downOnNativeControl = trigger == "tap" && isOnNativeControl(this, ev.x, ev.y)
|
|
67
75
|
// In longPress mode, schedule long-press detection
|
|
68
76
|
if (trigger == "longPress") {
|
|
69
77
|
// Dispatch recycles MotionEvent instances before this callback runs.
|
|
@@ -76,12 +84,18 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
76
84
|
// The long-press haptic View.performLongClick plays, which this
|
|
77
85
|
// path bypasses; off with haptics "none" (isHapticFeedbackEnabled)
|
|
78
86
|
if (popupShowing) performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
|
79
|
-
// Cancel any pending touch events on children
|
|
87
|
+
// Cancel any pending touch events on children, and the touch in
|
|
88
|
+
// JS, so a pressable under the finger doesn't fire onPress when
|
|
89
|
+
// it lifts (as a ScrollView does when it starts scrolling)
|
|
80
90
|
val cancel = MotionEvent.obtain(
|
|
81
91
|
downTime, SystemClock.uptimeMillis(),
|
|
82
92
|
MotionEvent.ACTION_CANCEL, downX, downY, 0
|
|
83
93
|
)
|
|
84
94
|
super.dispatchTouchEvent(cancel)
|
|
95
|
+
if (hasReactRoot()) {
|
|
96
|
+
NativeGestureUtil.notifyNativeGestureStarted(this, cancel)
|
|
97
|
+
nativeGestureStarted = true
|
|
98
|
+
}
|
|
85
99
|
cancel.recycle()
|
|
86
100
|
}
|
|
87
101
|
}
|
|
@@ -95,24 +109,36 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
95
109
|
cancelPendingLongPress()
|
|
96
110
|
}
|
|
97
111
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (trigger == "tap" &&
|
|
102
|
-
Math.abs(ev.x - touchDownX) <= touchSlop &&
|
|
103
|
-
Math.abs(ev.y - touchDownY) <= touchSlop) {
|
|
104
|
-
handleTap()
|
|
105
|
-
return true // Consume the event
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
MotionEvent.ACTION_CANCEL -> {
|
|
112
|
+
// Tap mode opens the menu from JS (handleTap), for taps that no inner
|
|
113
|
+
// pressable took
|
|
114
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
109
115
|
cancelPendingLongPress()
|
|
116
|
+
endNativeGesture(ev)
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
return super.onInterceptTouchEvent(ev)
|
|
114
121
|
}
|
|
115
122
|
|
|
123
|
+
private fun endNativeGesture(ev: MotionEvent) {
|
|
124
|
+
if (!nativeGestureStarted) return
|
|
125
|
+
nativeGestureStarted = false
|
|
126
|
+
if (hasReactRoot()) NativeGestureUtil.notifyNativeGestureEnded(this, ev)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether a React root (a RootView) is above this view. NativeGestureUtil
|
|
131
|
+
* asserts on one, and crashes for a view hosted outside React Native.
|
|
132
|
+
*/
|
|
133
|
+
private fun hasReactRoot(): Boolean {
|
|
134
|
+
var parent = parent
|
|
135
|
+
while (parent != null) {
|
|
136
|
+
if (parent is RootView) return true
|
|
137
|
+
parent = parent.parent
|
|
138
|
+
}
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
|
|
116
142
|
private fun cancelPendingLongPress() {
|
|
117
143
|
longPressRunnable?.let {
|
|
118
144
|
removeCallbacks(it)
|
|
@@ -128,13 +154,39 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
128
154
|
return true
|
|
129
155
|
}
|
|
130
156
|
|
|
131
|
-
|
|
157
|
+
/**
|
|
158
|
+
* Tap mode: JS calls this for a tap on the content that no inner pressable
|
|
159
|
+
* took (React Native's responder system knows: the deepest responder wins).
|
|
160
|
+
*/
|
|
161
|
+
fun handleTap() {
|
|
162
|
+
// A native control (this library's Button) handles its own taps,
|
|
163
|
+
// outside the JS responder system
|
|
164
|
+
if (downOnNativeControl) return
|
|
165
|
+
openTapMenu()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Tap mode: JS taps (handleTap), and TalkBack through the click listener. */
|
|
169
|
+
private fun openTapMenu() {
|
|
132
170
|
if (trigger != "tap") return
|
|
133
171
|
if (interactivity != "enabled") return
|
|
134
172
|
|
|
135
173
|
showPopupMenu()
|
|
136
174
|
}
|
|
137
175
|
|
|
176
|
+
/** Whether (x, y) of `group` lands on a descendant with its own click listener. */
|
|
177
|
+
private fun isOnNativeControl(group: ViewGroup, x: Float, y: Float): Boolean {
|
|
178
|
+
for (i in group.childCount - 1 downTo 0) {
|
|
179
|
+
val child = group.getChildAt(i)
|
|
180
|
+
if (child.visibility != VISIBLE) continue
|
|
181
|
+
val childX = x + group.scrollX - child.left - child.translationX
|
|
182
|
+
val childY = y + group.scrollY - child.top - child.translationY
|
|
183
|
+
if (childX < 0 || childY < 0 || childX >= child.width || childY >= child.height) continue
|
|
184
|
+
if (child.isEnabled && child.hasOnClickListeners()) return true
|
|
185
|
+
return child is ViewGroup && isOnNativeControl(child, childX, childY)
|
|
186
|
+
}
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
|
|
138
190
|
// ---- Public apply* (called by manager) ----
|
|
139
191
|
|
|
140
192
|
fun applyMenuTitle(value: String?) {
|
|
@@ -161,23 +213,22 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
161
213
|
|
|
162
214
|
fun applyTrigger(value: String?) {
|
|
163
215
|
trigger = when (value) {
|
|
164
|
-
|
|
216
|
+
// "none": only `visible` opens the menu; no touch is intercepted
|
|
217
|
+
"longPress", "tap", "none" -> value
|
|
165
218
|
else -> "longPress"
|
|
166
219
|
}
|
|
167
220
|
Log.d(TAG, "applyTrigger trigger=$trigger")
|
|
168
221
|
updateTriggerState()
|
|
169
222
|
}
|
|
170
223
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
Log.d(TAG, "applyAndroidVisible androidVisible=$androidVisible")
|
|
224
|
+
/** Called only when the prop changes, so a menu the user opened stays open. */
|
|
225
|
+
fun applyVisible(value: String?) {
|
|
226
|
+
visible = if (value == "open") "open" else "closed"
|
|
227
|
+
Log.d(TAG, "applyVisible visible=$visible")
|
|
177
228
|
openToken += 1
|
|
178
229
|
val token = openToken
|
|
179
230
|
|
|
180
|
-
if (
|
|
231
|
+
if (visible == "open") {
|
|
181
232
|
presentIfNeeded(token)
|
|
182
233
|
} else {
|
|
183
234
|
if (popupShowing) {
|
|
@@ -217,7 +268,7 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
217
268
|
Log.d(TAG, "presentIfNeeded stale token -> skip")
|
|
218
269
|
return@post
|
|
219
270
|
}
|
|
220
|
-
if (
|
|
271
|
+
if (visible != "open") {
|
|
221
272
|
Log.d(TAG, "presentIfNeeded no longer open -> skip")
|
|
222
273
|
return@post
|
|
223
274
|
}
|
|
@@ -257,7 +308,10 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
257
308
|
popup.setOnMenuItemClickListener { menuItem ->
|
|
258
309
|
// Submenu headers open their submenu; they aren't actions.
|
|
259
310
|
val item = PCMenuSupport.itemFor(menuItem, items)
|
|
260
|
-
if (item == null || !item.isAction)
|
|
311
|
+
if (item == null || !item.isAction) {
|
|
312
|
+
post { PCMenuSupport.tagPopupRows(popup.menu, items) }
|
|
313
|
+
return@setOnMenuItemClickListener false
|
|
314
|
+
}
|
|
261
315
|
Log.d(TAG, "popup onMenuItemClick")
|
|
262
316
|
dismissAfterSelect = true
|
|
263
317
|
PCHaptics.perform(this, item.haptics.ifEmpty { haptics })
|
|
@@ -285,6 +339,7 @@ class PCContextMenuView(context: Context) : ReactViewGroup(context) {
|
|
|
285
339
|
|
|
286
340
|
onMenuOpen?.invoke()
|
|
287
341
|
popup.show()
|
|
342
|
+
PCMenuSupport.tagPopupRows(popup.menu, items)
|
|
288
343
|
}
|
|
289
344
|
|
|
290
345
|
override fun onDetachedFromWindow() {
|
|
@@ -64,6 +64,11 @@ class PCContextMenuViewManager :
|
|
|
64
64
|
view.applyTrigger(value)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// "open" | "closed" | "" (unset, treated as closed)
|
|
68
|
+
override fun setVisible(view: PCContextMenuView, value: String?) {
|
|
69
|
+
view.applyVisible(value)
|
|
70
|
+
}
|
|
71
|
+
|
|
67
72
|
override fun setIos(view: PCContextMenuView, value: ReadableMap?) {
|
|
68
73
|
// Android ignores iOS config
|
|
69
74
|
}
|
|
@@ -74,25 +79,19 @@ class PCContextMenuViewManager :
|
|
|
74
79
|
}
|
|
75
80
|
|
|
76
81
|
override fun setAndroid(view: PCContextMenuView, value: ReadableMap?) {
|
|
77
|
-
if (value
|
|
78
|
-
view.applyAndroidAnchorPosition(null)
|
|
79
|
-
view.applyAndroidVisible(null)
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
val anchorPosition = if (value.hasKey("anchorPosition") && !value.isNull("anchorPosition")) {
|
|
82
|
+
val anchorPosition = if (value != null && value.hasKey("anchorPosition") && !value.isNull("anchorPosition")) {
|
|
84
83
|
value.getString("anchorPosition")
|
|
85
84
|
} else {
|
|
86
85
|
null
|
|
87
86
|
}
|
|
88
87
|
view.applyAndroidAnchorPosition(anchorPosition)
|
|
88
|
+
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
view.applyAndroidVisible(visible)
|
|
90
|
+
// --- Commands ---
|
|
91
|
+
|
|
92
|
+
// trigger "tap": a tap on the content that no inner pressable took
|
|
93
|
+
override fun handleTap(view: PCContextMenuView) {
|
|
94
|
+
view.handleTap()
|
|
96
95
|
}
|
|
97
96
|
|
|
98
97
|
// --- ViewGroupManager child management ---
|
|
@@ -5,6 +5,7 @@ import android.graphics.Rect
|
|
|
5
5
|
import android.graphics.drawable.BitmapDrawable
|
|
6
6
|
import android.graphics.drawable.Drawable
|
|
7
7
|
import android.graphics.drawable.DrawableWrapper
|
|
8
|
+
import android.os.Build
|
|
8
9
|
import android.text.SpannableString
|
|
9
10
|
import android.text.Spanned
|
|
10
11
|
import android.text.style.ForegroundColorSpan
|
|
@@ -12,6 +13,12 @@ import android.util.Log
|
|
|
12
13
|
import android.util.TypedValue
|
|
13
14
|
import android.view.Menu
|
|
14
15
|
import android.view.MenuItem
|
|
16
|
+
import android.view.View
|
|
17
|
+
import android.view.ViewGroup
|
|
18
|
+
import android.view.ViewTreeObserver
|
|
19
|
+
import android.view.WindowManager
|
|
20
|
+
import android.view.inspector.WindowInspector
|
|
21
|
+
import android.widget.ListView
|
|
15
22
|
import androidx.appcompat.R as AppCompatR
|
|
16
23
|
import androidx.core.content.ContextCompat
|
|
17
24
|
import androidx.core.graphics.drawable.DrawableCompat
|
|
@@ -57,7 +64,9 @@ object PCMenuSupport {
|
|
|
57
64
|
/** "" | "off" | "on" | "mixed" */
|
|
58
65
|
val state: String,
|
|
59
66
|
/** The item's own `haptics`; "" = the component's */
|
|
60
|
-
val haptics: String = ""
|
|
67
|
+
val haptics: String = "",
|
|
68
|
+
/** Tag of the item's popup row, for UI tests; "" = none */
|
|
69
|
+
val testID: String = ""
|
|
61
70
|
) {
|
|
62
71
|
val isAction: Boolean get() = kind != "menu" && kind != "section"
|
|
63
72
|
}
|
|
@@ -82,7 +91,8 @@ object PCMenuSupport {
|
|
|
82
91
|
disabled = map.stringOr("disabled", "false") == "true",
|
|
83
92
|
keepsMenuPresented = map.stringOr("keepsMenuPresented", "false") == "true",
|
|
84
93
|
state = map.stringOr("state", ""),
|
|
85
|
-
haptics = map.stringOr("haptics", "")
|
|
94
|
+
haptics = map.stringOr("haptics", ""),
|
|
95
|
+
testID = map.stringOr("testID", "")
|
|
86
96
|
)
|
|
87
97
|
)
|
|
88
98
|
}
|
|
@@ -165,6 +175,60 @@ object PCMenuSupport {
|
|
|
165
175
|
/** The item behind a clicked MenuItem. */
|
|
166
176
|
fun itemFor(menuItem: MenuItem, items: List<Item>): Item? = items.getOrNull(menuItem.itemId)
|
|
167
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Tags the rows of [menu]'s showing popups with their items' testIDs:
|
|
180
|
+
* Detox and Espresso match `by.id` against the view tag. AppCompat builds
|
|
181
|
+
* the rows in the popup's own window and keeps its list private, so the
|
|
182
|
+
* list is found among the app's windows (WindowInspector, Android 10+;
|
|
183
|
+
* earlier versions leave the rows untagged). Call it after the popup shows,
|
|
184
|
+
* and after a submenu header is picked: the submenu opens in a new popup.
|
|
185
|
+
*/
|
|
186
|
+
fun tagPopupRows(menu: Menu, items: List<Item>) {
|
|
187
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return
|
|
188
|
+
if (items.none { it.testID.isNotEmpty() }) return
|
|
189
|
+
for (root in WindowInspector.getGlobalWindowViews()) {
|
|
190
|
+
// Popups are sub-windows of the activity's window
|
|
191
|
+
val type = (root.layoutParams as? WindowManager.LayoutParams)?.type ?: continue
|
|
192
|
+
if (type < WindowManager.LayoutParams.FIRST_SUB_WINDOW ||
|
|
193
|
+
type > WindowManager.LayoutParams.LAST_SUB_WINDOW) continue
|
|
194
|
+
forEachListView(root) { list -> watchRows(list, menu, items) }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private fun forEachListView(view: View, action: (ListView) -> Unit) {
|
|
199
|
+
if (view is ListView) {
|
|
200
|
+
action(view)
|
|
201
|
+
} else if (view is ViewGroup) {
|
|
202
|
+
for (i in 0 until view.childCount) forEachListView(view.getChildAt(i), action)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Tags [list]'s rows if it lists [menu]'s items, and again before every
|
|
208
|
+
* frame: the list binds rows to items as it lays out and scrolls.
|
|
209
|
+
*/
|
|
210
|
+
private fun watchRows(list: ListView, menu: Menu, items: List<Item>) {
|
|
211
|
+
if (list.getTag(R.id.pc_menu_rows_tagged) != null) return
|
|
212
|
+
val first = (0 until list.count).firstNotNullOfOrNull { list.getItemAtPosition(it) as? MenuItem }
|
|
213
|
+
if (first == null || menu.findItem(first.itemId) !== first) return
|
|
214
|
+
|
|
215
|
+
fun tagRows() {
|
|
216
|
+
for (i in 0 until list.childCount) {
|
|
217
|
+
val menuItem = list.getItemAtPosition(list.firstVisiblePosition + i) as? MenuItem
|
|
218
|
+
val testID = menuItem?.let { itemFor(it, items)?.testID }.orEmpty()
|
|
219
|
+
list.getChildAt(i).tag = testID.ifEmpty { null }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
list.setTag(R.id.pc_menu_rows_tagged, true)
|
|
223
|
+
list.viewTreeObserver.addOnPreDrawListener(
|
|
224
|
+
ViewTreeObserver.OnPreDrawListener {
|
|
225
|
+
tagRows()
|
|
226
|
+
true
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
tagRows()
|
|
230
|
+
}
|
|
231
|
+
|
|
168
232
|
/** Destructive titles are drawn in the theme's error color. */
|
|
169
233
|
private fun title(context: Context, item: Item): CharSequence {
|
|
170
234
|
if (!item.destructive) return item.title
|
|
@@ -15,6 +15,7 @@ class PlatformComponentsViewPackage : BaseReactPackage() {
|
|
|
15
15
|
PCSelectionMenuViewManager(),
|
|
16
16
|
PCDatePickerViewManager(),
|
|
17
17
|
PCContextMenuViewManager(),
|
|
18
|
+
PCActionSheetViewManager(),
|
|
18
19
|
PCSegmentedControlViewManager(),
|
|
19
20
|
PCButtonViewManager(),
|
|
20
21
|
PCButtonGroupViewManager(),
|
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
<resources>
|
|
3
3
|
<!-- The text size a view was capped at (maxFontSizeMultiplier); see PCThemeSupport.capTextSize -->
|
|
4
4
|
<item name="pc_capped_text_size" type="id" />
|
|
5
|
+
<!-- A menu popup's list whose rows carry their items' testIDs; see PCMenuSupport.tagPopupRows -->
|
|
6
|
+
<item name="pc_menu_rows_tagged" type="id" />
|
|
5
7
|
</resources>
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// PCActionSheet.mm
|
|
2
|
+
|
|
3
|
+
#import "PCActionSheet.h"
|
|
4
|
+
|
|
5
|
+
#import <React/RCTComponentViewFactory.h>
|
|
6
|
+
#import <React/RCTConversions.h>
|
|
7
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
8
|
+
|
|
9
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
|
|
10
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
|
|
11
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
|
|
12
|
+
|
|
13
|
+
#if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
|
|
14
|
+
#import <PlatformComponents/PlatformComponents-Swift.h>
|
|
15
|
+
#else
|
|
16
|
+
#import "PlatformComponents-Swift.h"
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
using namespace facebook::react;
|
|
20
|
+
|
|
21
|
+
namespace {
|
|
22
|
+
static inline NSString *ToNSString(const std::string &value) {
|
|
23
|
+
return value.empty() ? @"" : [NSString stringWithUTF8String:value.c_str()];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static inline bool ActionsEqual(
|
|
27
|
+
const std::vector<PCActionSheetActionsStruct> &a,
|
|
28
|
+
const std::vector<PCActionSheetActionsStruct> &b) {
|
|
29
|
+
if (a.size() != b.size()) return false;
|
|
30
|
+
for (size_t i = 0; i < a.size(); i++) {
|
|
31
|
+
const auto &x = a[i];
|
|
32
|
+
const auto &y = b[i];
|
|
33
|
+
if (x.id != y.id || x.title != y.title || x.iconType != y.iconType ||
|
|
34
|
+
x.iconName != y.iconName || x.iconRequest != y.iconRequest || x.iconUri != y.iconUri ||
|
|
35
|
+
x.iconScale != y.iconScale || x.iconTinted != y.iconTinted ||
|
|
36
|
+
x.imageColor != y.imageColor || x.destructive != y.destructive ||
|
|
37
|
+
x.disabled != y.disabled || x.haptics != y.haptics || x.testID != y.testID) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
} // namespace
|
|
44
|
+
|
|
45
|
+
@implementation PCActionSheet {
|
|
46
|
+
PCActionSheetView *_view;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
50
|
+
return concreteComponentDescriptorProvider<PCActionSheetComponentDescriptor>();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
54
|
+
if (self = [super initWithFrame:frame]) {
|
|
55
|
+
// Fabric views seed _props with their own defaults: RCTViewComponentView
|
|
56
|
+
// asserts on it in debug builds of React Native core, and the first
|
|
57
|
+
// updateProps would otherwise read the base ViewProps as PCActionSheetProps.
|
|
58
|
+
static const auto defaultProps = std::make_shared<const PCActionSheetProps>();
|
|
59
|
+
_props = defaultProps;
|
|
60
|
+
|
|
61
|
+
// Covers the component's frame without taking touches; the children
|
|
62
|
+
// mount next to it, and the sheet's popover points at them.
|
|
63
|
+
_view = [PCActionSheetView new];
|
|
64
|
+
self.contentView = _view;
|
|
65
|
+
|
|
66
|
+
__weak __typeof(self) weakSelf = self;
|
|
67
|
+
|
|
68
|
+
_view.onSelect = ^(NSString *actionId) {
|
|
69
|
+
__typeof(self) strongSelf = weakSelf;
|
|
70
|
+
if (!strongSelf) return;
|
|
71
|
+
|
|
72
|
+
auto eventEmitter =
|
|
73
|
+
std::static_pointer_cast<const PCActionSheetEventEmitter>(
|
|
74
|
+
strongSelf->_eventEmitter);
|
|
75
|
+
if (!eventEmitter) return;
|
|
76
|
+
|
|
77
|
+
PCActionSheetEventEmitter::OnSelect payload = {
|
|
78
|
+
.actionId = actionId.UTF8String,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
eventEmitter->onSelect(payload);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
_view.onCancel = ^{
|
|
85
|
+
__typeof(self) strongSelf = weakSelf;
|
|
86
|
+
if (!strongSelf) return;
|
|
87
|
+
|
|
88
|
+
auto eventEmitter =
|
|
89
|
+
std::static_pointer_cast<const PCActionSheetEventEmitter>(
|
|
90
|
+
strongSelf->_eventEmitter);
|
|
91
|
+
if (!eventEmitter) return;
|
|
92
|
+
|
|
93
|
+
eventEmitter->onCancel({});
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return self;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
100
|
+
oldProps:(Props::Shared const &)oldProps {
|
|
101
|
+
const auto &newProps =
|
|
102
|
+
*std::static_pointer_cast<const PCActionSheetProps>(props);
|
|
103
|
+
const auto prevProps =
|
|
104
|
+
std::static_pointer_cast<const PCActionSheetProps>(oldProps);
|
|
105
|
+
|
|
106
|
+
if (!prevProps || newProps.title != prevProps->title) {
|
|
107
|
+
_view.sheetTitle = ToNSString(newProps.title);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!prevProps || newProps.message != prevProps->message) {
|
|
111
|
+
_view.message = ToNSString(newProps.message);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// actions: [{id, title, icon fields, imageColor, destructive, disabled, haptics, testID}]
|
|
115
|
+
if (!prevProps || !ActionsEqual(newProps.actions, prevProps->actions)) {
|
|
116
|
+
NSMutableArray *arr = [NSMutableArray arrayWithCapacity:newProps.actions.size()];
|
|
117
|
+
for (const auto &action : newProps.actions) {
|
|
118
|
+
[arr addObject:@{
|
|
119
|
+
@"id" : ToNSString(action.id),
|
|
120
|
+
@"title" : ToNSString(action.title),
|
|
121
|
+
@"destructive" : ToNSString(action.destructive),
|
|
122
|
+
@"disabled" : ToNSString(action.disabled),
|
|
123
|
+
@"haptics" : ToNSString(action.haptics),
|
|
124
|
+
@"testID" : ToNSString(action.testID),
|
|
125
|
+
}];
|
|
126
|
+
}
|
|
127
|
+
_view.actions = arr;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!prevProps || newProps.cancelTitle != prevProps->cancelTitle) {
|
|
131
|
+
_view.cancelTitle = ToNSString(newProps.cancelTitle);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!prevProps || newProps.cancelTestID != prevProps->cancelTestID) {
|
|
135
|
+
_view.cancelTestID = ToNSString(newProps.cancelTestID);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!prevProps || newProps.haptics != prevProps->haptics) {
|
|
139
|
+
_view.haptics.kind = ToNSString(newProps.haptics);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// visible last, so a sheet opened by this update has the new actions.
|
|
143
|
+
// "open" | "closed"
|
|
144
|
+
if (!prevProps || newProps.visible != prevProps->visible) {
|
|
145
|
+
_view.visible = newProps.visible.empty() ? @"closed" : ToNSString(newProps.visible);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
[super updateProps:props oldProps:oldProps];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
- (void)prepareForRecycle {
|
|
152
|
+
[super prepareForRecycle];
|
|
153
|
+
[_view resetForRecycle];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@end
|
|
157
|
+
|
|
158
|
+
Class<RCTComponentViewProtocol> PCActionSheetCls(void) {
|
|
159
|
+
return PCActionSheet.class;
|
|
160
|
+
}
|