@craft-native/android 0.0.451 → 0.0.467
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 -1
- package/dist/index.d.ts +1 -350
- package/dist/index.js +228 -31
- package/dist/index.js.map +7 -5
- package/dist/internal/android/src/index.d.ts +358 -0
- package/dist/internal/android/src/testing.d.ts +2 -0
- package/dist/internal/typescript/src/native-notifications.d.ts +17 -0
- package/dist/internal/typescript/src/native-routing.d.ts +26 -0
- package/package.json +1 -1
- package/templates/CraftNativeBackground.kt.template +65 -25
- package/templates/CraftNativeCapabilities.kt.template +3 -1
- package/templates/CraftNativeMessagingService.kt.template +8 -28
- package/templates/CraftNativeNotificationReceiver.kt.template +213 -15
- package/templates/CraftNativeServices.kt.template +23 -5
- package/templates/CraftNativeShell.kt.template +201 -50
- package/templates/MainActivityNative.kt.template +227 -12
- package/dist/testing.d.ts +0 -2
- /package/dist/{emulator.d.ts → internal/android/src/emulator.d.ts} +0 -0
- /package/dist/{native-map.d.ts → internal/android/src/native-map.d.ts} +0 -0
- /package/dist/{promise-runtime.d.ts → internal/android/src/promise-runtime.d.ts} +0 -0
|
@@ -286,6 +286,26 @@ private data class CraftNativeLayoutStyle(
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/** Component scope nodes preserve identity but lay out through their single non-modal root. */
|
|
290
|
+
internal fun craftNativeComponentLayoutStyle(node: JSONObject): JSONObject {
|
|
291
|
+
val own = node.optJSONObject("style") ?: JSONObject()
|
|
292
|
+
if (node.optJSONObject("props")?.has("data-stx-component-wrapper") != true) return own
|
|
293
|
+
val children = node.optJSONArray("children") ?: return own
|
|
294
|
+
if (children.length() != 1) return own
|
|
295
|
+
val root = children.optJSONObject(0) ?: return own
|
|
296
|
+
if (root.optString("type").equals("Modal", ignoreCase = true)) return own
|
|
297
|
+
if (root.optJSONObject("style")?.optString("display") == "none") return own
|
|
298
|
+
val inner = craftNativeComponentLayoutStyle(root)
|
|
299
|
+
val merged = JSONObject(own.toString())
|
|
300
|
+
for (key in listOf("width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", "aspectRatio",
|
|
301
|
+
"flex", "flexGrow", "flexShrink", "flexBasis", "alignSelf", "order", "position", "zIndex",
|
|
302
|
+
"left", "right", "top", "bottom", "margin", "marginTop", "marginRight", "marginBottom", "marginLeft",
|
|
303
|
+
"marginHorizontal", "marginVertical")) {
|
|
304
|
+
if (!merged.has(key) && inner.has(key)) merged.put(key, inner.opt(key))
|
|
305
|
+
}
|
|
306
|
+
return merged
|
|
307
|
+
}
|
|
308
|
+
|
|
289
309
|
private fun craftNativeGridColumnCount(value: Any?): Int {
|
|
290
310
|
if (value is Number) return max(1, value.toInt())
|
|
291
311
|
val text = value as? String ?: return 1
|
|
@@ -1196,6 +1216,13 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1196
1216
|
private var applyingPadding = false
|
|
1197
1217
|
private var safeAreaEdges = setOf("top", "right", "bottom", "left")
|
|
1198
1218
|
private var overflowTouchTarget: View? = null
|
|
1219
|
+
var componentScope = false
|
|
1220
|
+
|
|
1221
|
+
private fun componentRoot(): View? {
|
|
1222
|
+
if (!componentScope || childCount != 1) return null
|
|
1223
|
+
return getChildAt(0).takeIf { it !is CraftNativeModalView && it.visibility != View.GONE }
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1199
1226
|
|
|
1200
1227
|
private fun childEvent(child: View, event: MotionEvent): MotionEvent = MotionEvent.obtain(event).also {
|
|
1201
1228
|
it.offsetLocation((scrollX - child.left).toFloat(), (scrollY - child.top).toFloat())
|
|
@@ -1877,6 +1904,7 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
1877
1904
|
|
|
1878
1905
|
/** Flatten a flex or equal-track grid subtree for the shared ABI. */
|
|
1879
1906
|
private fun sharedTreeChildren(width: Int, height: Int): List<Entry>? {
|
|
1907
|
+
if (componentRoot() != null) return null
|
|
1880
1908
|
if (grid) {
|
|
1881
1909
|
val layout = gridLayout(width, height)
|
|
1882
1910
|
if (layout.absolute.isNotEmpty()) return null
|
|
@@ -2229,6 +2257,11 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
2229
2257
|
}
|
|
2230
2258
|
|
|
2231
2259
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
2260
|
+
componentRoot()?.let { root ->
|
|
2261
|
+
root.measure(widthMeasureSpec, heightMeasureSpec)
|
|
2262
|
+
setMeasuredDimension(root.measuredWidth, root.measuredHeight)
|
|
2263
|
+
return
|
|
2264
|
+
}
|
|
2232
2265
|
pageViewport?.let { (pageWidth, pageHeight) ->
|
|
2233
2266
|
val pages = (0 until childCount).map { getChildAt(it) }.filter { it.visibility != View.GONE }
|
|
2234
2267
|
pages.forEach { it.measure(MeasureSpec.makeMeasureSpec(pageWidth, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(pageHeight, MeasureSpec.EXACTLY)) }
|
|
@@ -2271,6 +2304,11 @@ internal class CraftNativeFlexLayout(context: Context) : LinearLayout(context) {
|
|
|
2271
2304
|
|
|
2272
2305
|
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
2273
2306
|
celebration?.layout()
|
|
2307
|
+
componentRoot()?.let { root ->
|
|
2308
|
+
place(root, 0, 0, right - left, bottom - top)
|
|
2309
|
+
onLayoutChanged?.invoke()
|
|
2310
|
+
return
|
|
2311
|
+
}
|
|
2274
2312
|
pageViewport?.let { (pageWidth, pageHeight) ->
|
|
2275
2313
|
val pages = (0 until childCount).map { getChildAt(it) }.filter { it.visibility != View.GONE }
|
|
2276
2314
|
pages.forEachIndexed { index, child -> child.layout(index * pageWidth, 0, (index + 1) * pageWidth, pageHeight) }
|
|
@@ -3350,6 +3388,104 @@ private class CraftNativeTextField(context: Context) : EditText(context) {
|
|
|
3350
3388
|
}
|
|
3351
3389
|
}
|
|
3352
3390
|
|
|
3391
|
+
/** Hide only siblings behind the top blocking modal, restoring authored values on every update. */
|
|
3392
|
+
private fun craftNativeModalAccessibility(root: ViewGroup, saved: MutableMap<View, Int>, blocks: (View) -> Boolean) {
|
|
3393
|
+
saved.forEach { (view, previous) ->
|
|
3394
|
+
// Reconciliation may already have replaced the old value with a new role.
|
|
3395
|
+
if (view.importantForAccessibility == View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
|
3396
|
+
view.importantForAccessibility = previous
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
saved.clear()
|
|
3400
|
+
val top = (0 until root.childCount).lastOrNull { blocks(root.getChildAt(it)) } ?: return
|
|
3401
|
+
for (index in 0 until top) {
|
|
3402
|
+
val view = root.getChildAt(index)
|
|
3403
|
+
saved[view] = view.importantForAccessibility
|
|
3404
|
+
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
private fun craftNativeAccessibilityLanguage(value: Any?): Locale? =
|
|
3409
|
+
(value as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let(Locale::forLanguageTag)?.takeIf { it.language.isNotEmpty() }
|
|
3410
|
+
|
|
3411
|
+
private fun craftNativeSpokenText(text: CharSequence?, language: Locale?): CharSequence? {
|
|
3412
|
+
if (text == null || language == null || text.isEmpty()) return text
|
|
3413
|
+
return SpannableString(text).apply {
|
|
3414
|
+
setSpan(android.text.style.LocaleSpan(language), 0, length, android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
internal data class CraftNativeMenuItem(val id: String, val title: String, val symbol: String = "", val destructive: Boolean = false, val disabled: Boolean = false)
|
|
3419
|
+
|
|
3420
|
+
/** One anchored popup per retained view; a reconciled popup can never dispatch. */
|
|
3421
|
+
internal class CraftNativeContextMenu(private val anchor: View) {
|
|
3422
|
+
private var popup: android.widget.PopupMenu? = null
|
|
3423
|
+
private var revision = 0
|
|
3424
|
+
private var items: List<CraftNativeMenuItem> = emptyList()
|
|
3425
|
+
private var dispatch: ((String) -> Unit)? = null
|
|
3426
|
+
private var handlerIdentity: String? = null
|
|
3427
|
+
|
|
3428
|
+
fun update(next: List<CraftNativeMenuItem>, identity: String? = null, callback: (String) -> Unit) {
|
|
3429
|
+
val valid = next.filter { it.id.isNotBlank() && it.title.isNotBlank() }.distinctBy { it.id }
|
|
3430
|
+
if (identity != null && identity == handlerIdentity && valid == items) {
|
|
3431
|
+
dispatch = callback
|
|
3432
|
+
return
|
|
3433
|
+
}
|
|
3434
|
+
clear()
|
|
3435
|
+
handlerIdentity = identity
|
|
3436
|
+
items = valid
|
|
3437
|
+
dispatch = callback
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
fun clear() {
|
|
3441
|
+
revision++
|
|
3442
|
+
popup?.dismiss()
|
|
3443
|
+
popup = null
|
|
3444
|
+
items = emptyList()
|
|
3445
|
+
dispatch = null
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
fun show(decorate: (android.view.MenuItem, CraftNativeMenuItem) -> Unit): Boolean {
|
|
3449
|
+
if (items.isEmpty() || dispatch == null || !anchor.isEnabled) return false
|
|
3450
|
+
popup?.dismiss()
|
|
3451
|
+
val expected = revision
|
|
3452
|
+
val shown = android.widget.PopupMenu(anchor.context, anchor)
|
|
3453
|
+
popup = shown
|
|
3454
|
+
if (Build.VERSION.SDK_INT >= 29) shown.setForceShowIcon(true)
|
|
3455
|
+
items.forEachIndexed { index, item ->
|
|
3456
|
+
shown.menu.add(0, index, index, item.title).also {
|
|
3457
|
+
it.isEnabled = !item.disabled
|
|
3458
|
+
decorate(it, item)
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
shown.setOnMenuItemClickListener { choice ->
|
|
3462
|
+
val item = items.getOrNull(choice.itemId)
|
|
3463
|
+
if (revision != expected || item == null || item.disabled || !anchor.isEnabled) false
|
|
3464
|
+
else { dispatch?.invoke(item.id); true }
|
|
3465
|
+
}
|
|
3466
|
+
shown.setOnDismissListener { if (popup === shown) popup = null }
|
|
3467
|
+
shown.show()
|
|
3468
|
+
return true
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
private fun craftNativeAnnounce(args: JSONArray, announce: (String) -> Unit): CraftNativeCapabilityResult {
|
|
3473
|
+
val text = args.opt(0) as? String
|
|
3474
|
+
if (text.isNullOrBlank()) return CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Accessibility.announce needs nonempty text")
|
|
3475
|
+
val options = args.opt(1)
|
|
3476
|
+
if (options != null && options != JSONObject.NULL && options !is JSONObject) {
|
|
3477
|
+
return CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Accessibility.announce options must be an object")
|
|
3478
|
+
}
|
|
3479
|
+
val polite = (options as? JSONObject)?.opt("polite")
|
|
3480
|
+
if (polite != null && polite !is Boolean) {
|
|
3481
|
+
return CraftNativeCapabilityResult(code = "INVALID_ARGUMENT", message = "Accessibility.announce polite must be a Boolean")
|
|
3482
|
+
}
|
|
3483
|
+
// Android's announcement event has no queue-priority field. The service
|
|
3484
|
+
// owns speech scheduling; never move focus to force it to speak.
|
|
3485
|
+
announce(text)
|
|
3486
|
+
return CraftNativeCapabilityResult(data = true)
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3353
3489
|
private class CraftNativeModalView(context: Context) : FrameLayout(context) {
|
|
3354
3490
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
3355
3491
|
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
@@ -4563,7 +4699,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4563
4699
|
val pressUnderlayShowHandlers = mutableMapOf<View, String>()
|
|
4564
4700
|
val pressUnderlayHideHandlers = mutableMapOf<View, String>()
|
|
4565
4701
|
val longPressHandlers = mutableMapOf<View, String>()
|
|
4702
|
+
val contextMenus = mutableMapOf<View, CraftNativeContextMenu>()
|
|
4566
4703
|
val accessibilityActionHandlers = mutableMapOf<View, String>()
|
|
4704
|
+
val accessibilityActionRevisions = mutableMapOf<View, Any>()
|
|
4567
4705
|
// TalkBack actions are added by id, so removing them on a reconcile
|
|
4568
4706
|
// needs the ids back; without this a renamed action stacks up beside
|
|
4569
4707
|
// the old one every render.
|
|
@@ -4588,6 +4726,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
4588
4726
|
val pressUnderlayColors = mutableMapOf<View, Int>()
|
|
4589
4727
|
val pressBaseBackgrounds = mutableMapOf<View, Drawable?>()
|
|
4590
4728
|
val modalVisibility = mutableMapOf<View, Boolean>()
|
|
4729
|
+
val modalAccessibility = mutableMapOf<View, Int>()
|
|
4591
4730
|
val modalCloseHandlers = mutableMapOf<View, String>()
|
|
4592
4731
|
val layoutHandlers = mutableMapOf<View, String>()
|
|
4593
4732
|
val layoutListeners = mutableMapOf<View, View.OnLayoutChangeListener>()
|
|
@@ -4749,7 +4888,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4749
4888
|
initialDeepLink = savedInstanceState?.getString("craft.initialDeepLink")?.let(Uri::parse) ?: intent?.data
|
|
4750
4889
|
initialDeepLinkClaimed = savedInstanceState?.getBoolean("craft.initialDeepLinkClaimed", false) ?: false
|
|
4751
4890
|
deliveredInitialDeepLink = savedInstanceState?.getBoolean("craft.deliveredInitialDeepLink", false) ?: false
|
|
4752
|
-
if (savedInstanceState == null
|
|
4891
|
+
if (savedInstanceState == null && !CraftNativeNotifications.receiveAction(this, intent)) {
|
|
4892
|
+
pendingNotificationTap = notificationTap(intent)
|
|
4893
|
+
}
|
|
4753
4894
|
setContentView(R.layout.activity_main)
|
|
4754
4895
|
root = findViewById(R.id.native_root)
|
|
4755
4896
|
root.foreground = navigationBarProtection
|
|
@@ -5428,12 +5569,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
5428
5569
|
"if (typeof globalThis.__craftInstallNativeDialog === 'function') globalThis.__craftInstallNativeDialog();"
|
|
5429
5570
|
).get()
|
|
5430
5571
|
if (activeScreen.headless) {
|
|
5431
|
-
CraftNativeBackground.foreground = { event, id, done ->
|
|
5572
|
+
CraftNativeBackground.foreground = { event, id, detail, done ->
|
|
5432
5573
|
val app = appScreen
|
|
5433
5574
|
if (app == null || !app.active) false
|
|
5434
5575
|
else {
|
|
5435
5576
|
backgroundWaiters[id] = done
|
|
5436
|
-
send(app, "CRAFT_EVENT", JSONObject().put("event", event).put("detail", JSONObject().put("id", id)))
|
|
5577
|
+
send(app, "CRAFT_EVENT", JSONObject().put("event", event).put("detail", JSONObject(detail.toString()).put("id", id)))
|
|
5437
5578
|
true
|
|
5438
5579
|
}
|
|
5439
5580
|
}
|
|
@@ -5589,6 +5730,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5589
5730
|
put("viewNonModalPresentation")
|
|
5590
5731
|
put("nativePlatformControls")
|
|
5591
5732
|
put("nativePager")
|
|
5733
|
+
put("accessibility")
|
|
5592
5734
|
}
|
|
5593
5735
|
|
|
5594
5736
|
private fun appVersion(): Pair<String, String> = runCatching {
|
|
@@ -5897,7 +6039,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5897
6039
|
makeView(screen, document, path, screen.controls, inheritedFontFamily = screen.inheritedFontFamilies[target])
|
|
5898
6040
|
val updated = screen.nodeViews[target] ?: return false
|
|
5899
6041
|
screen.lastLayoutBounds.remove(updated)
|
|
5900
|
-
if (!updateParentLayoutStyle(updated, document
|
|
6042
|
+
if (!updateParentLayoutStyle(updated, craftNativeComponentLayoutStyle(document))) return false
|
|
5901
6043
|
}
|
|
5902
6044
|
if (applied.isNotEmpty()) {
|
|
5903
6045
|
// Updating a modal's content may reattach its nested popups to
|
|
@@ -5913,7 +6055,20 @@ class MainActivity : AppCompatActivity() {
|
|
|
5913
6055
|
var current: String? = id
|
|
5914
6056
|
while (current != null) {
|
|
5915
6057
|
val target = screen.flatListOwners[current] ?: current
|
|
5916
|
-
if (screen.nodeViews[target] != null)
|
|
6058
|
+
if (screen.nodeViews[target] != null) {
|
|
6059
|
+
// A root style mutation also changes its scope's layout in the
|
|
6060
|
+
// outer parent. Reconcile that owner, including nested scopes.
|
|
6061
|
+
var owner = target
|
|
6062
|
+
while (true) {
|
|
6063
|
+
val parentId = screen.mutations.parentId(owner) ?: break
|
|
6064
|
+
val parentNode = screen.mutations.node(parentId) ?: break
|
|
6065
|
+
if (parentNode.optJSONObject("props")?.has("data-stx-component-wrapper") != true) break
|
|
6066
|
+
val children = parentNode.optJSONArray("children") ?: break
|
|
6067
|
+
if (children.length() != 1 || parentNode.optJSONArray("children")?.optJSONObject(0)?.optString("type")?.equals("Modal", ignoreCase = true) == true) break
|
|
6068
|
+
owner = parentId
|
|
6069
|
+
}
|
|
6070
|
+
return owner
|
|
6071
|
+
}
|
|
5917
6072
|
current = screen.mutations.parentId(current)
|
|
5918
6073
|
}
|
|
5919
6074
|
return null
|
|
@@ -5993,7 +6148,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5993
6148
|
screen.controls.putAll(next)
|
|
5994
6149
|
screen.handlers.keys.retainAll(retainedViews(screen))
|
|
5995
6150
|
screen.view.setViewportChild(tree)
|
|
5996
|
-
screen.view.setChildStyle(tree, document
|
|
6151
|
+
screen.view.setChildStyle(tree, craftNativeComponentLayoutStyle(document), resources.displayMetrics.density)
|
|
5997
6152
|
promoteModalOverlays(screen)
|
|
5998
6153
|
updateSafeAreaViews()
|
|
5999
6154
|
// A full render can change a view's custom layout style without
|
|
@@ -6005,7 +6160,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
6005
6160
|
|
|
6006
6161
|
/** Keep Modal nodes in the screen viewport even when declared in a clipped subtree. */
|
|
6007
6162
|
private fun promoteModalOverlays(screen: Screen) {
|
|
6008
|
-
if (screen.modalVisibility.isEmpty())
|
|
6163
|
+
if (screen.modalVisibility.isEmpty()) {
|
|
6164
|
+
craftNativeModalAccessibility(screen.view, screen.modalAccessibility) { false }
|
|
6165
|
+
return
|
|
6166
|
+
}
|
|
6009
6167
|
// Snapshot parent-before-child order before moving anything. The
|
|
6010
6168
|
// controls map registers descendants first, which would place a
|
|
6011
6169
|
// parent modal above its own popup when brought forward in that order.
|
|
@@ -6040,6 +6198,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
6040
6198
|
}
|
|
6041
6199
|
modal.bringToFront()
|
|
6042
6200
|
}
|
|
6201
|
+
craftNativeModalAccessibility(screen.view, screen.modalAccessibility) {
|
|
6202
|
+
it is CraftNativeModalView && it.blocksBackground && screen.modalVisibility[it] == true
|
|
6203
|
+
}
|
|
6043
6204
|
}
|
|
6044
6205
|
|
|
6045
6206
|
private fun updateStatusBarProps(screen: Screen, props: JSONObject) {
|
|
@@ -6066,6 +6227,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6066
6227
|
}
|
|
6067
6228
|
|
|
6068
6229
|
private fun clearRenderedTree(screen: Screen) {
|
|
6230
|
+
craftNativeModalAccessibility(screen.view, screen.modalAccessibility) { false }
|
|
6069
6231
|
releaseViewState(screen, retainedViews(screen).toList())
|
|
6070
6232
|
screen.view.removeAllViews()
|
|
6071
6233
|
screen.controls.clear()
|
|
@@ -6095,7 +6257,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6095
6257
|
): View {
|
|
6096
6258
|
val type = canonicalType(node.optString("type", "View"))
|
|
6097
6259
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
6098
|
-
val style = craftNativeInheritFontFamily(node
|
|
6260
|
+
val style = craftNativeInheritFontFamily(craftNativeComponentLayoutStyle(node), inheritedFontFamily)
|
|
6099
6261
|
val events = node.optJSONObject("events") ?: JSONObject()
|
|
6100
6262
|
val children = node.optJSONArray("children") ?: JSONArray()
|
|
6101
6263
|
val protocolId = node.optString("id").takeIf { it.isNotBlank() }
|
|
@@ -6885,6 +7047,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6885
7047
|
reconcileFlatList(screen, nativeList, children, identity, protocolId ?: identity, props, events, craftNativeInheritableFontFamily(style))
|
|
6886
7048
|
}
|
|
6887
7049
|
"View", "SafeAreaView" -> (previous as? CraftNativeFlexLayout ?: CraftNativeFlexLayout(this)).apply {
|
|
7050
|
+
componentScope = props.has("data-stx-component-wrapper")
|
|
6888
7051
|
configureContainer(this, style)
|
|
6889
7052
|
if (type == "SafeAreaView") setSafeAreaEdges(props.optJSONArray("edges"))
|
|
6890
7053
|
else setWindowSafeInsets(0, 0, 0, 0)
|
|
@@ -6997,6 +7160,33 @@ class MainActivity : AppCompatActivity() {
|
|
|
6997
7160
|
control.setOnLongClickListener(null)
|
|
6998
7161
|
control.isLongClickable = false
|
|
6999
7162
|
}
|
|
7163
|
+
// STX emits @contextmenu as onLongPress and lowercases unknown bound props.
|
|
7164
|
+
val menuHandler = craftNativeEventName(events, "onContextMenuAction").ifBlank { longPressHandler }.takeUnless { longPressDisabled }.orEmpty()
|
|
7165
|
+
val authoredMenu = props.optJSONArray("contextMenu") ?: props.optJSONArray("contextmenu")
|
|
7166
|
+
val menuItems = (0 until (authoredMenu?.length() ?: 0)).mapNotNull { index ->
|
|
7167
|
+
val item = authoredMenu?.optJSONObject(index) ?: return@mapNotNull null
|
|
7168
|
+
val id = craftNativeOptionalString(item.opt("id"))?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
|
7169
|
+
val title = craftNativeOptionalString(item.opt("title"))?.takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
|
7170
|
+
CraftNativeMenuItem(id, title, craftNativeOptionalString(item.opt("symbol")).orEmpty(),
|
|
7171
|
+
craftNativeBoolean(item.opt("destructive")), craftNativeBoolean(item.opt("disabled")))
|
|
7172
|
+
}
|
|
7173
|
+
if (menuHandler.isNotBlank() && menuItems.isNotEmpty()) {
|
|
7174
|
+
val menu = screen.contextMenus.getOrPut(control) { CraftNativeContextMenu(control) }
|
|
7175
|
+
menu.update(menuItems, menuHandler) { id ->
|
|
7176
|
+
send(screen, "EVENT", JSONObject().put("handlerName", menuHandler).put("nativeEvent", JSONObject().put("id", id)))
|
|
7177
|
+
}
|
|
7178
|
+
// A configured menu owns the hold, rather than firing onLongPress too.
|
|
7179
|
+
control.setOnLongClickListener {
|
|
7180
|
+
menu.show { nativeItem, item ->
|
|
7181
|
+
nativeItem.icon = CraftNativeIcons.drawable(this, "", item.symbol, defaultTextColor)
|
|
7182
|
+
if (item.destructive) nativeItem.title = SpannableString(item.title).apply {
|
|
7183
|
+
setSpan(android.text.style.ForegroundColorSpan(Color.RED), 0, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
} else {
|
|
7188
|
+
screen.contextMenus.remove(control)?.clear()
|
|
7189
|
+
}
|
|
7000
7190
|
CraftNativePointers.configure(control, props, style, control is CraftNativeFlexLayout || control is CraftNativeImageBackgroundView ||
|
|
7001
7191
|
control is CraftNativeKeyboardAvoidingView || control is CraftNativeScrollView || control is CraftNativeRefreshableFlatList || control is CraftNativeModalView)
|
|
7002
7192
|
applyAccessibility(control, props, type, explicitKey ?: protocolId ?: path)
|
|
@@ -7164,7 +7354,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7164
7354
|
}
|
|
7165
7355
|
if (childView != null) desired.add(childView)
|
|
7166
7356
|
if (childView != null && child is JSONObject) {
|
|
7167
|
-
val childStyle = child
|
|
7357
|
+
val childStyle = craftNativeComponentLayoutStyle(child)
|
|
7168
7358
|
layout.setChildStyle(childView, childStyle, resources.displayMetrics.density)
|
|
7169
7359
|
desiredOrder[childView] = craftNativeFiniteNumber(childStyle, "order", 0.0).toInt()
|
|
7170
7360
|
}
|
|
@@ -7916,6 +8106,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
7916
8106
|
* Runs after applyAccessibility, which installs its own delegate and
|
|
7917
8107
|
* would drop actions added before it.
|
|
7918
8108
|
*/
|
|
8109
|
+
private fun clearAccessibilityActions(screen: Screen, control: View) {
|
|
8110
|
+
screen.accessibilityActionIds.remove(control)?.forEach { ViewCompat.removeAccessibilityAction(control, it) }
|
|
8111
|
+
screen.accessibilityActionHandlers.remove(control)
|
|
8112
|
+
screen.accessibilityActionRevisions.remove(control)
|
|
8113
|
+
}
|
|
8114
|
+
|
|
7919
8115
|
private fun applyAccessibilityActions(
|
|
7920
8116
|
screen: Screen,
|
|
7921
8117
|
control: View,
|
|
@@ -7923,7 +8119,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7923
8119
|
events: JSONObject,
|
|
7924
8120
|
disabled: Boolean,
|
|
7925
8121
|
) {
|
|
7926
|
-
screen
|
|
8122
|
+
clearAccessibilityActions(screen, control)
|
|
7927
8123
|
val handler = craftNativeEventName(events, "onAccessibilityAction").takeUnless { disabled } ?: ""
|
|
7928
8124
|
val authored = props.optJSONArray("accessibilityActions")
|
|
7929
8125
|
if (handler.isBlank() || authored == null || authored.length() == 0) {
|
|
@@ -7931,6 +8127,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
7931
8127
|
return
|
|
7932
8128
|
}
|
|
7933
8129
|
screen.accessibilityActionHandlers[control] = handler
|
|
8130
|
+
val revision = Any()
|
|
8131
|
+
screen.accessibilityActionRevisions[control] = revision
|
|
7934
8132
|
val ids = mutableListOf<Int>()
|
|
7935
8133
|
for (index in 0 until authored.length()) {
|
|
7936
8134
|
val entry = authored.optJSONObject(index) ?: continue
|
|
@@ -7940,7 +8138,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
7940
8138
|
// Read the handler at firing time: a reconcile can rename it
|
|
7941
8139
|
// while the action sits in TalkBack's menu.
|
|
7942
8140
|
val current = screen.accessibilityActionHandlers[control]
|
|
7943
|
-
if (current.isNullOrBlank()) {
|
|
8141
|
+
if (current.isNullOrBlank() || screen.accessibilityActionRevisions[control] !== revision) {
|
|
7944
8142
|
false
|
|
7945
8143
|
} else {
|
|
7946
8144
|
send(screen, "EVENT", JSONObject().put("handlerName", current).put("nativeEvent", JSONObject().put("actionName", name)))
|
|
@@ -7952,6 +8150,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
7952
8150
|
}
|
|
7953
8151
|
|
|
7954
8152
|
private fun applyAccessibility(view: View, props: JSONObject, type: String, fallbackLabel: String) {
|
|
8153
|
+
val language = craftNativeAccessibilityLanguage(props.opt("accessibilityLanguage"))
|
|
8154
|
+
if (view is TextView) {
|
|
8155
|
+
view.textLocales = if (language == null) view.context.resources.configuration.locales else android.os.LocaleList(language)
|
|
8156
|
+
}
|
|
7955
8157
|
val testId = craftNativeOptionalString(props.opt("testID"))
|
|
7956
8158
|
view.tag = testId?.takeIf { it.isNotBlank() } ?: fallbackLabel
|
|
7957
8159
|
val label = craftNativeOptionalString(props.opt("accessibilityLabel"))
|
|
@@ -8020,6 +8222,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
8020
8222
|
if (state?.has("checked") == true) {
|
|
8021
8223
|
info.isChecked = craftNativeBoolean(state.opt("checked"))
|
|
8022
8224
|
}
|
|
8225
|
+
info.text = craftNativeSpokenText(info.text, language)
|
|
8226
|
+
info.contentDescription = craftNativeSpokenText(info.contentDescription, language)
|
|
8227
|
+
info.hintText = craftNativeSpokenText(info.hintText, language)
|
|
8023
8228
|
}
|
|
8024
8229
|
})
|
|
8025
8230
|
ViewCompat.setImportantForAccessibility(
|
|
@@ -8316,6 +8521,10 @@ class MainActivity : AppCompatActivity() {
|
|
|
8316
8521
|
|
|
8317
8522
|
internal fun performImmediateCapability(module: String, method: String, args: JSONArray): CraftNativeCapabilityResult =
|
|
8318
8523
|
when (module to method) {
|
|
8524
|
+
"Accessibility" to "announce" -> craftNativeAnnounce(args) { text ->
|
|
8525
|
+
@Suppress("DEPRECATION")
|
|
8526
|
+
window.decorView.announceForAccessibility(text)
|
|
8527
|
+
}
|
|
8319
8528
|
"Device" to "getInfo" -> CraftNativeCapabilityResult(data = JSONObject()
|
|
8320
8529
|
.put("platform", "android")
|
|
8321
8530
|
.put("osVersion", Build.VERSION.RELEASE)
|
|
@@ -8445,7 +8654,8 @@ class MainActivity : AppCompatActivity() {
|
|
|
8445
8654
|
override fun onNewIntent(intent: Intent) {
|
|
8446
8655
|
super.onNewIntent(intent)
|
|
8447
8656
|
setIntent(intent)
|
|
8448
|
-
|
|
8657
|
+
val notificationAction = CraftNativeNotifications.receiveAction(this, intent)
|
|
8658
|
+
(if (notificationAction) null else notificationTap(intent))?.let { tap ->
|
|
8449
8659
|
val app = appScreen
|
|
8450
8660
|
if (app != null && app.active) send(app, "CRAFT_EVENT", JSONObject().put("event", "craftNotificationResponse").put("detail", tap))
|
|
8451
8661
|
else pendingNotificationTap = tap
|
|
@@ -8469,6 +8679,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
8469
8679
|
private fun notificationTap(intent: Intent?): JSONObject? {
|
|
8470
8680
|
val extras = intent?.extras ?: return null
|
|
8471
8681
|
if (!extras.containsKey("google.message_id") && !extras.containsKey("craft.notification")) return null
|
|
8682
|
+
extras.getString("craft.notification.data")?.let { raw ->
|
|
8683
|
+
return runCatching { JSONObject(raw) }.getOrNull()
|
|
8684
|
+
}
|
|
8472
8685
|
val detail = JSONObject()
|
|
8473
8686
|
extras.keySet().forEach { key ->
|
|
8474
8687
|
if (key.startsWith("google.") || key.startsWith("gcm.") || key == "from" || key == "collapse_key" || key == "craft.notification") return@forEach
|
|
@@ -8809,6 +9022,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
8809
9022
|
// released/recycled rows so a later identity cannot inherit a closure
|
|
8810
9023
|
// that still captures the previous row's screen state.
|
|
8811
9024
|
released.forEach {
|
|
9025
|
+
clearAccessibilityActions(screen, it)
|
|
8812
9026
|
// Modals are promoted out of their logical layout into the
|
|
8813
9027
|
// screen overlay. Removing the logical node therefore does not
|
|
8814
9028
|
// remove the native view through ordinary child reconciliation;
|
|
@@ -8850,6 +9064,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
8850
9064
|
screen.pressUnderlayHideHandlers.keys.removeAll(released)
|
|
8851
9065
|
screen.pressUnderlayColors.keys.removeAll(released)
|
|
8852
9066
|
screen.pressBaseBackgrounds.keys.removeAll(released)
|
|
9067
|
+
released.forEach { screen.contextMenus.remove(it)?.clear() }
|
|
8853
9068
|
screen.longPressHandlers.keys.removeAll(released)
|
|
8854
9069
|
screen.layoutHandlers.keys.removeAll(released)
|
|
8855
9070
|
screen.layoutListeners.forEach { (view, listener) ->
|
package/dist/testing.d.ts
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|