craft-native 0.0.205 → 0.0.206
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/dist/android/templates/CraftNativeRefreshableFlatList.kt.template +7 -0
- package/dist/android/templates/MainActivityNative.kt.template +105 -53
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +5 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.js +11 -0
- package/dist/ios/src/testing.d.ts +2 -0
- package/dist/ios/templates/CraftNativeFlatList.swift +4 -0
- package/dist/ios/templates/CraftNativeScreen.swift +32 -3
- package/dist/ios/templates/testing/DriverTests.swift.template +26 -3
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package {{PACKAGE_NAME}}
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import org.json.JSONObject
|
|
4
5
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
5
6
|
|
|
6
7
|
/** Swipe-to-refresh host for a native FlatList. */
|
|
@@ -21,6 +22,12 @@ internal class CraftNativeRefreshableFlatList(context: Context) : SwipeRefreshLa
|
|
|
21
22
|
if (handler == null) isRefreshing = false
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
fun setRefreshAppearance(props: JSONObject, fallback: Int) {
|
|
26
|
+
val tint = props.optString("tintColor").takeIf { it.isNotBlank() }
|
|
27
|
+
val resolved = tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback
|
|
28
|
+
setColorSchemeColors(resolved)
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
override fun onDetachedFromWindow() {
|
|
25
32
|
refreshHandler = null
|
|
26
33
|
isRefreshing = false
|
|
@@ -149,8 +149,9 @@ private fun craftNativeOptionalString(value: Any?): String? = value
|
|
|
149
149
|
?.takeIf { it.isNotBlank() }
|
|
150
150
|
|
|
151
151
|
private fun craftNativePickerValue(value: Any?): String? = when (value) {
|
|
152
|
+
is String -> value.takeIf { it.isNotEmpty() }
|
|
152
153
|
is Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
|
|
153
|
-
else ->
|
|
154
|
+
else -> null
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
private fun craftNativeEventName(events: JSONObject, vararg names: String): String =
|
|
@@ -352,6 +353,14 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
352
353
|
val vectorEffect: String? = null,
|
|
353
354
|
val dominantBaseline: String? = null,
|
|
354
355
|
) {
|
|
356
|
+
private fun svgTypeface(weight: String?, inherited: Typeface): Typeface = when (weight?.lowercase()) {
|
|
357
|
+
"500" -> Typeface.create("sans-serif-medium", Typeface.NORMAL)
|
|
358
|
+
"600" -> Typeface.create("sans-serif", Typeface.BOLD)
|
|
359
|
+
"bold", "700", "800", "900" -> Typeface.DEFAULT_BOLD
|
|
360
|
+
"normal", "100", "200", "300", "400" -> Typeface.DEFAULT
|
|
361
|
+
else -> inherited
|
|
362
|
+
}
|
|
363
|
+
|
|
355
364
|
fun merge(node: JSONObject): SvgPaint {
|
|
356
365
|
val props = node.optJSONObject("props") ?: JSONObject()
|
|
357
366
|
val style = node.optJSONObject("style") ?: JSONObject()
|
|
@@ -387,7 +396,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
387
396
|
strokeDashoffset = number("strokeDashoffset") ?: strokeDashoffset,
|
|
388
397
|
strokeMiterlimit = number("strokeMiterlimit")?.takeIf { it > 0 } ?: strokeMiterlimit,
|
|
389
398
|
fillRule = value("fillRule") ?: fillRule,
|
|
390
|
-
typeface =
|
|
399
|
+
typeface = svgTypeface(weight, typeface),
|
|
391
400
|
strokeLinecap = value("strokeLinecap") ?: strokeLinecap,
|
|
392
401
|
strokeLinejoin = value("strokeLinejoin") ?: strokeLinejoin,
|
|
393
402
|
strokeDasharray = dashes(raw("strokeDasharray")) ?: strokeDasharray,
|
|
@@ -2209,13 +2218,13 @@ private class CraftNativeScrollView(context: Context) : SwipeRefreshLayout(conte
|
|
|
2209
2218
|
}
|
|
2210
2219
|
}
|
|
2211
2220
|
|
|
2212
|
-
fun setRefreshAppearance(props: JSONObject, density: Float) {
|
|
2221
|
+
fun setRefreshAppearance(props: JSONObject, density: Float, fallback: Int) {
|
|
2213
2222
|
val tint = craftNativeOptionalString(props.opt("tintColor"))
|
|
2214
|
-
|
|
2215
|
-
else setColorSchemeColors(Color.WHITE)
|
|
2223
|
+
setColorSchemeColors(tint?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: fallback)
|
|
2216
2224
|
val background = craftNativeOptionalString(props.opt("progressBackgroundColor"))
|
|
2217
|
-
|
|
2218
|
-
|
|
2225
|
+
setProgressBackgroundColorSchemeColor(
|
|
2226
|
+
background?.let { runCatching { craftNativeColor(it) }.getOrNull() } ?: Color.TRANSPARENT,
|
|
2227
|
+
)
|
|
2219
2228
|
val offset = props.optDouble("progressViewOffset", Double.NaN)
|
|
2220
2229
|
val requested = if (offset.isFinite()) (offset * density).roundToInt() else null
|
|
2221
2230
|
if (configuredProgressOffset != requested) {
|
|
@@ -2862,7 +2871,8 @@ private class CraftNativeDatePickerView(context: Context) : FrameLayout(context)
|
|
|
2862
2871
|
if (text.isBlank() || text == JSONObject.NULL.toString()) return null
|
|
2863
2872
|
val formats = listOf(
|
|
2864
2873
|
"HH:mm", "HH:mm:ss", "yyyy-MM-dd", "yyyy-MM-dd'T'HH:mm",
|
|
2865
|
-
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.
|
|
2874
|
+
"yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss.SSS",
|
|
2875
|
+
"yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssX",
|
|
2866
2876
|
)
|
|
2867
2877
|
formats.firstNotNullOfOrNull { format ->
|
|
2868
2878
|
runCatching {
|
|
@@ -4485,6 +4495,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
4485
4495
|
screen.view.setBackgroundColor(configured ?: ContextCompat.getColor(this, R.color.background))
|
|
4486
4496
|
}
|
|
4487
4497
|
|
|
4498
|
+
/** Refresh controls use a contrasting system tint when no color is authored. */
|
|
4499
|
+
private fun refreshTintFallback(screen: Screen): Int {
|
|
4500
|
+
val background = (screen.view.background as? ColorDrawable)?.color
|
|
4501
|
+
?: ContextCompat.getColor(this, R.color.background)
|
|
4502
|
+
return if (Color.luminance(background) < 0.5f) Color.WHITE else Color.BLACK
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4488
4505
|
/** The bridge's capability names: Craft's core set and this host's services. */
|
|
4489
4506
|
private fun capabilityNames(): JSONArray = capabilities.names().apply {
|
|
4490
4507
|
services.names().forEach { name -> put(name) }
|
|
@@ -4961,7 +4978,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
4961
4978
|
val title = craftNativeOptionalString(props.opt("title")) ?: childText(children)
|
|
4962
4979
|
isAllCaps = false
|
|
4963
4980
|
configureText(this, title, style)
|
|
4964
|
-
craftNativeOptionalString(props.opt("color"))
|
|
4981
|
+
val buttonColor = craftNativeOptionalString(props.opt("color"))
|
|
4982
|
+
?: craftNativeOptionalString(style.opt("color"))
|
|
4983
|
+
setTextColor(buttonColor?.let { color(it, defaultTextColor) } ?: defaultTextColor)
|
|
4965
4984
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4966
4985
|
val handler = craftNativeEventName(events, "onPress", "onClick")
|
|
4967
4986
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -4973,11 +4992,11 @@ class MainActivity : AppCompatActivity() {
|
|
|
4973
4992
|
"Checkbox" -> (previous as? CheckBox ?: CheckBox(this)).apply {
|
|
4974
4993
|
text = craftNativeOptionalString(props.opt("label")) ?: childText(children)
|
|
4975
4994
|
configureText(this, text.toString(), style)
|
|
4976
|
-
val
|
|
4977
|
-
val
|
|
4978
|
-
|
|
4979
|
-
is
|
|
4980
|
-
is
|
|
4995
|
+
val checkboxChecked = props.opt("checked")
|
|
4996
|
+
val checkboxValue = props.opt("value")
|
|
4997
|
+
val checked = when {
|
|
4998
|
+
checkboxChecked is Boolean -> checkboxChecked
|
|
4999
|
+
checkboxValue is Boolean -> checkboxValue
|
|
4981
5000
|
else -> false
|
|
4982
5001
|
}
|
|
4983
5002
|
setOnCheckedChangeListener(null)
|
|
@@ -4985,11 +5004,14 @@ class MainActivity : AppCompatActivity() {
|
|
|
4985
5004
|
// an indeterminate control must announce "mixed" and its next
|
|
4986
5005
|
// user toggle should transition to checked, not unchecked.
|
|
4987
5006
|
isChecked = checked
|
|
4988
|
-
|
|
5007
|
+
val indeterminate = (props.opt("indeterminate") as? Boolean) == true
|
|
5008
|
+
isActivated = indeterminate
|
|
4989
5009
|
isEnabled = !props.optBoolean("disabled", false)
|
|
4990
5010
|
val checkboxColor = craftNativeOptionalString(props.opt("color"))
|
|
4991
5011
|
?: craftNativeOptionalString(style.opt("color"))
|
|
4992
|
-
buttonTintList = checkboxColor
|
|
5012
|
+
buttonTintList = checkboxColor
|
|
5013
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5014
|
+
?.let { ColorStateList.valueOf(it) }
|
|
4993
5015
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
4994
5016
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
4995
5017
|
setOnCheckedChangeListener { _, value ->
|
|
@@ -5016,11 +5038,12 @@ class MainActivity : AppCompatActivity() {
|
|
|
5016
5038
|
hint = craftNativeOptionalString(props.opt("placeholder")).orEmpty()
|
|
5017
5039
|
setHintTextColor(
|
|
5018
5040
|
craftNativeOptionalString(props.opt("placeholderTextColor"))
|
|
5019
|
-
?.let {
|
|
5041
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5042
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5020
5043
|
?: defaultHintColors,
|
|
5021
5044
|
)
|
|
5022
5045
|
highlightColor = craftNativeOptionalString(props.opt("selectionColor"))
|
|
5023
|
-
?.let {
|
|
5046
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5024
5047
|
?: defaultHighlightColor
|
|
5025
5048
|
val maxLength = props.optInt("maxLength", 0)
|
|
5026
5049
|
val currentLength = (filters.firstOrNull { it is InputFilter.LengthFilter } as? InputFilter.LengthFilter)?.max ?: 0
|
|
@@ -5254,24 +5277,29 @@ class MainActivity : AppCompatActivity() {
|
|
|
5254
5277
|
val requestedValue = props.opt("value")
|
|
5255
5278
|
val requestedChecked = props.opt("checked")
|
|
5256
5279
|
isChecked = when {
|
|
5257
|
-
requestedValue != null && requestedValue != JSONObject.NULL ->
|
|
5258
|
-
requestedChecked == null || requestedChecked == JSONObject.NULL -> false
|
|
5280
|
+
requestedValue != null && requestedValue != JSONObject.NULL -> requestedValue as? Boolean ?: false
|
|
5259
5281
|
requestedChecked is Boolean -> requestedChecked
|
|
5260
|
-
|
|
5261
|
-
requestedChecked is String -> requestedChecked.isBlank() || requestedChecked.equals("true", ignoreCase = true)
|
|
5262
|
-
else -> props.optBoolean("checked", false)
|
|
5282
|
+
else -> false
|
|
5263
5283
|
}
|
|
5264
5284
|
isEnabled = !props.optBoolean("disabled", false)
|
|
5265
5285
|
props.optJSONObject("trackColor")?.let { track ->
|
|
5266
|
-
val
|
|
5267
|
-
val
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5286
|
+
val falseRaw = craftNativeOptionalString(track.opt("false"))
|
|
5287
|
+
val trueRaw = craftNativeOptionalString(track.opt("true"))
|
|
5288
|
+
val falseColor = falseRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5289
|
+
val trueColor = trueRaw?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5290
|
+
trackTintList = if ((falseRaw != null && falseColor == null) || (trueRaw != null && trueColor == null)) {
|
|
5291
|
+
null
|
|
5292
|
+
} else {
|
|
5293
|
+
val resolvedFalse = falseColor ?: Color.GRAY
|
|
5294
|
+
ColorStateList(
|
|
5295
|
+
arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
|
|
5296
|
+
intArrayOf(trueColor ?: resolvedFalse, resolvedFalse),
|
|
5297
|
+
)
|
|
5298
|
+
}
|
|
5272
5299
|
} ?: run { trackTintList = null }
|
|
5273
5300
|
craftNativeOptionalString(props.opt("thumbColor"))
|
|
5274
|
-
?.let {
|
|
5301
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5302
|
+
?.let { thumbTintList = ColorStateList.valueOf(it) }
|
|
5275
5303
|
?: run { thumbTintList = null }
|
|
5276
5304
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
5277
5305
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -5308,15 +5336,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
5308
5336
|
(((value - minimum) / (maximum - minimum)) * max).roundToInt().coerceIn(0, max)
|
|
5309
5337
|
}
|
|
5310
5338
|
if (step != null) screen.sliderSteps[this] = step else screen.sliderSteps.remove(this)
|
|
5311
|
-
craftNativeOptionalString(props.opt("minimumTrackTintColor"))
|
|
5312
|
-
?.let {
|
|
5313
|
-
|
|
5314
|
-
craftNativeOptionalString(props.opt("maximumTrackTintColor"))
|
|
5315
|
-
?.let {
|
|
5316
|
-
|
|
5317
|
-
craftNativeOptionalString(props.opt("thumbTintColor"))
|
|
5318
|
-
?.let {
|
|
5319
|
-
|
|
5339
|
+
progressTintList = craftNativeOptionalString(props.opt("minimumTrackTintColor"))
|
|
5340
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5341
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5342
|
+
progressBackgroundTintList = craftNativeOptionalString(props.opt("maximumTrackTintColor"))
|
|
5343
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5344
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5345
|
+
thumbTintList = craftNativeOptionalString(props.opt("thumbTintColor"))
|
|
5346
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5347
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5320
5348
|
isEnabled = !props.optBoolean("disabled", false)
|
|
5321
5349
|
val handler = craftNativeEventName(events, "onValueChange", "onChange")
|
|
5322
5350
|
if (handler.isNotBlank()) screen.handlers[this] = handler else screen.handlers.remove(this)
|
|
@@ -5354,9 +5382,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5354
5382
|
val animating = props.optBoolean("animating", true)
|
|
5355
5383
|
isIndeterminate = animating
|
|
5356
5384
|
visibility = if (!animating && props.optBoolean("hidesWhenStopped", true)) View.GONE else View.VISIBLE
|
|
5357
|
-
craftNativeOptionalString(props.opt("color"))
|
|
5358
|
-
?.let {
|
|
5359
|
-
|
|
5385
|
+
indeterminateTintList = craftNativeOptionalString(props.opt("color"))
|
|
5386
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5387
|
+
?.let { ColorStateList.valueOf(it) }
|
|
5360
5388
|
}
|
|
5361
5389
|
"Icon" -> (previous as? CraftNativeIconView ?: CraftNativeIconView(this)).apply {
|
|
5362
5390
|
configureIcon(this, props, style)
|
|
@@ -5386,9 +5414,9 @@ class MainActivity : AppCompatActivity() {
|
|
|
5386
5414
|
"center" -> ImageView.ScaleType.CENTER
|
|
5387
5415
|
else -> ImageView.ScaleType.CENTER_INSIDE
|
|
5388
5416
|
}
|
|
5389
|
-
craftNativeOptionalString(style.opt("tintColor"))
|
|
5390
|
-
?.let {
|
|
5391
|
-
|
|
5417
|
+
val tint = craftNativeOptionalString(style.opt("tintColor"))
|
|
5418
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
5419
|
+
if (tint == null) image.clearColorFilter() else image.setColorFilter(tint)
|
|
5392
5420
|
val onLoadStart = craftNativeEventName(events, "onLoadStart")
|
|
5393
5421
|
if (onLoadStart.isNotBlank()) screen.imageLoadStartHandlers[image] = onLoadStart else screen.imageLoadStartHandlers.remove(image)
|
|
5394
5422
|
val onLoad = craftNativeEventName(events, "onLoad")
|
|
@@ -5458,7 +5486,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5458
5486
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5459
5487
|
}
|
|
5460
5488
|
setRefreshHandler(refreshHandler, refreshProps.optBoolean("refreshing", props.optBoolean("refreshing", false)))
|
|
5461
|
-
setRefreshAppearance(refreshProps, resources.displayMetrics.density)
|
|
5489
|
+
setRefreshAppearance(refreshProps, resources.displayMetrics.density, refreshTintFallback(screen))
|
|
5462
5490
|
val onScroll = craftNativeEventName(events, "onScroll")
|
|
5463
5491
|
if (onScroll.isNotBlank()) screen.scrollHandlers[this] = onScroll else screen.scrollHandlers.remove(this)
|
|
5464
5492
|
val onBegin = craftNativeEventName(events, "onScrollBeginDrag")
|
|
@@ -5528,9 +5556,15 @@ class MainActivity : AppCompatActivity() {
|
|
|
5528
5556
|
}
|
|
5529
5557
|
"FlatList" -> (previous as? CraftNativeRefreshableFlatList ?: CraftNativeRefreshableFlatList(this)).apply {
|
|
5530
5558
|
val nativeList = list
|
|
5531
|
-
|
|
5559
|
+
val propRefresh = props.optJSONObject("refreshControl") ?: JSONObject()
|
|
5560
|
+
val propRefreshEvents = propRefresh.optJSONObject("events") ?: JSONObject()
|
|
5561
|
+
val refreshHandlerName = craftNativeEventName(propRefreshEvents, "onRefresh")
|
|
5562
|
+
.takeIf { it.isNotBlank() }
|
|
5563
|
+
?: craftNativeEventName(events, "onRefresh").takeIf { it.isNotBlank() }
|
|
5564
|
+
configureRefresh(refreshHandlerName?.let { handler ->
|
|
5532
5565
|
{ send(screen, "EVENT", JSONObject().put("handlerName", handler).put("nativeEvent", JSONObject())) }
|
|
5533
|
-
}, props.optBoolean("refreshing", false))
|
|
5566
|
+
}, propRefresh.optBoolean("refreshing", props.optBoolean("refreshing", false)))
|
|
5567
|
+
setRefreshAppearance(propRefresh, refreshTintFallback(screen))
|
|
5534
5568
|
nativeList.setKeyboardShouldPersistTaps(props.optString("keyboardShouldPersistTaps", "never"))
|
|
5535
5569
|
nativeList.setContentContainerStyle(props.optJSONObject("contentContainerStyle"), resources.displayMetrics.density)
|
|
5536
5570
|
nativeList.setKeyboardDismissMode(props.optString("keyboardDismissMode")) {
|
|
@@ -5800,8 +5834,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
5800
5834
|
is JSONObject -> makeView(screen, child, "$path.$index", next, previousControls)
|
|
5801
5835
|
is String -> TextView(this).apply {
|
|
5802
5836
|
includeFontPadding = false
|
|
5803
|
-
|
|
5804
|
-
setTextColor(defaultTextColor)
|
|
5837
|
+
configureText(this, child, craftNativeTypography(style))
|
|
5805
5838
|
}
|
|
5806
5839
|
else -> null
|
|
5807
5840
|
}
|
|
@@ -6046,10 +6079,13 @@ class MainActivity : AppCompatActivity() {
|
|
|
6046
6079
|
}
|
|
6047
6080
|
if (view is ImageView) {
|
|
6048
6081
|
val tint = style.opt("tintColor") as? String
|
|
6049
|
-
|
|
6082
|
+
val resolvedTint = tint
|
|
6083
|
+
?.takeUnless { it.isBlank() || it == "null" || it == "undefined" }
|
|
6084
|
+
?.let { runCatching { craftNativeColor(it) }.getOrNull() }
|
|
6085
|
+
if (resolvedTint == null) {
|
|
6050
6086
|
view.clearColorFilter()
|
|
6051
6087
|
} else {
|
|
6052
|
-
view.setColorFilter(
|
|
6088
|
+
view.setColorFilter(resolvedTint)
|
|
6053
6089
|
}
|
|
6054
6090
|
}
|
|
6055
6091
|
}
|
|
@@ -6120,6 +6156,22 @@ class MainActivity : AppCompatActivity() {
|
|
|
6120
6156
|
configureTextStyle(view, style)
|
|
6121
6157
|
}
|
|
6122
6158
|
|
|
6159
|
+
/**
|
|
6160
|
+
* Raw strings inside a native container are implicit Text nodes. Keep
|
|
6161
|
+
* their typography, but not the parent's padding or layout, just like
|
|
6162
|
+
* the iOS reconciler does for the same child shape.
|
|
6163
|
+
*/
|
|
6164
|
+
private fun craftNativeTypography(style: JSONObject): JSONObject = JSONObject().also { typography ->
|
|
6165
|
+
listOf(
|
|
6166
|
+
"color", "letterSpacing", "lineHeight", "fontSize", "fontFamily",
|
|
6167
|
+
"fontWeight", "fontStyle", "textAlign", "textTransform",
|
|
6168
|
+
"textDecorationLine", "textDecorationStyle", "textDecorationColor",
|
|
6169
|
+
"textShadowColor", "textShadowRadius", "textShadowOffset",
|
|
6170
|
+
).forEach { key ->
|
|
6171
|
+
if (style.has(key) && !style.isNull(key)) typography.put(key, style.opt(key))
|
|
6172
|
+
}
|
|
6173
|
+
}
|
|
6174
|
+
|
|
6123
6175
|
private fun configureIcon(view: CraftNativeIconView, props: JSONObject, style: JSONObject) {
|
|
6124
6176
|
val symbol = craftNativeOptionalString(props.opt("symbol"))
|
|
6125
6177
|
?: craftNativeOptionalString(props.opt("name")).orEmpty()
|
|
@@ -6466,7 +6518,7 @@ class MainActivity : AppCompatActivity() {
|
|
|
6466
6518
|
}
|
|
6467
6519
|
ViewCompat.setStateDescription(
|
|
6468
6520
|
view,
|
|
6469
|
-
if (type == "Checkbox" && props.
|
|
6521
|
+
if (type == "Checkbox" && (props.opt("indeterminate") as? Boolean) == true) "mixed" else null,
|
|
6470
6522
|
)
|
|
6471
6523
|
ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
|
|
6472
6524
|
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
|
package/dist/cli.js
CHANGED
|
@@ -3424,7 +3424,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3424
3424
|
`);
|
|
3425
3425
|
}
|
|
3426
3426
|
// package.json
|
|
3427
|
-
var version = "0.0.
|
|
3427
|
+
var version = "0.0.206";
|
|
3428
3428
|
|
|
3429
3429
|
// bin/cli.ts
|
|
3430
3430
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -527,6 +527,8 @@ export interface SvgPaintProps extends BaseProps {
|
|
|
527
527
|
vectorEffect?: 'none' | 'non-scaling-stroke';
|
|
528
528
|
color?: string;
|
|
529
529
|
opacity?: number;
|
|
530
|
+
/** Font weight inherited by descendant native SVG text. */
|
|
531
|
+
fontWeight?: string | number;
|
|
530
532
|
}
|
|
531
533
|
/** Root native SVG drawing surface. */
|
|
532
534
|
export interface SvgProps extends SvgPaintProps {
|
|
@@ -559,7 +561,6 @@ export interface SvgTextProps extends SvgPaintProps {
|
|
|
559
561
|
x?: number;
|
|
560
562
|
y?: number;
|
|
561
563
|
fontSize?: number;
|
|
562
|
-
fontWeight?: string | number;
|
|
563
564
|
textAnchor?: 'start' | 'middle' | 'end' | string;
|
|
564
565
|
dominantBaseline?: string;
|
|
565
566
|
}
|
|
@@ -679,6 +680,7 @@ export interface TextInputProps extends BaseProps {
|
|
|
679
680
|
export type KeyboardType = 'default' | 'email-address' | 'numeric' | 'phone-pad' | 'number-pad' | 'decimal-pad' | 'url' | 'web-search' | 'visible-password';
|
|
680
681
|
export type ReturnKeyType = 'done' | 'go' | 'next' | 'search' | 'send' | 'join' | 'route' | 'continue' | 'emergency-call' | 'default';
|
|
681
682
|
export interface SwitchProps extends BaseProps {
|
|
683
|
+
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
682
684
|
value?: boolean;
|
|
683
685
|
onValueChange?: (value: boolean) => void;
|
|
684
686
|
onChange?: (value: boolean) => void;
|
|
@@ -691,6 +693,7 @@ export interface SwitchProps extends BaseProps {
|
|
|
691
693
|
ios_backgroundColor?: string;
|
|
692
694
|
}
|
|
693
695
|
export interface CheckboxProps extends BaseProps {
|
|
696
|
+
/** Controlled boolean state; malformed native bridge values stay off. */
|
|
694
697
|
value?: boolean;
|
|
695
698
|
checked?: boolean;
|
|
696
699
|
indeterminate?: boolean;
|
|
@@ -715,6 +718,7 @@ export interface SliderProps extends BaseProps {
|
|
|
715
718
|
}
|
|
716
719
|
export interface PickerOption {
|
|
717
720
|
label: string;
|
|
721
|
+
/** String or finite number; other bridge values are ignored by native hosts. */
|
|
718
722
|
value: string | number;
|
|
719
723
|
}
|
|
720
724
|
export interface PickerProps extends BaseProps {
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
package/dist/ios/src/index.js
CHANGED
|
@@ -102,6 +102,17 @@ function initSimulatorDriver(options) {
|
|
|
102
102
|
if (previous !== content)
|
|
103
103
|
writeFileSync(path, content);
|
|
104
104
|
}
|
|
105
|
+
const script = join2(output, "UITests/journey.txt");
|
|
106
|
+
if (options.script === undefined)
|
|
107
|
+
rmSync(script, { force: true });
|
|
108
|
+
else {
|
|
109
|
+
let previous;
|
|
110
|
+
try {
|
|
111
|
+
previous = readFileSync(script, "utf8");
|
|
112
|
+
} catch {}
|
|
113
|
+
if (previous !== options.script)
|
|
114
|
+
writeFileSync(script, options.script);
|
|
115
|
+
}
|
|
105
116
|
}
|
|
106
117
|
function serializeSimulatorSteps(steps) {
|
|
107
118
|
const number = (value) => {
|
|
@@ -7,6 +7,8 @@ export interface SimulatorDriverOptions {
|
|
|
7
7
|
deploymentTarget?: string;
|
|
8
8
|
controlUrl?: string;
|
|
9
9
|
tabs?: readonly string[];
|
|
10
|
+
/** Bundle a wire-protocol journey and save native XCTest attachments. No control server is needed on physical devices. */
|
|
11
|
+
script?: string;
|
|
10
12
|
}
|
|
11
13
|
/** Generate the reusable XCUITest shell from package templates. No app-owned Swift. */
|
|
12
14
|
export declare function initSimulatorDriver(options: SimulatorDriverOptions): void;
|
|
@@ -348,6 +348,10 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
348
348
|
}
|
|
349
349
|
}
|
|
350
350
|
|
|
351
|
+
func setRefreshAppearance(_ props: [String: Any]) {
|
|
352
|
+
nativeRefreshControl.tintColor = (props["tintColor"] as? String).flatMap { UIColor(hex: $0) }
|
|
353
|
+
}
|
|
354
|
+
|
|
351
355
|
@objc private func refreshTriggered() {
|
|
352
356
|
refreshHandler?()
|
|
353
357
|
DispatchQueue.main.async { [weak self] in
|
|
@@ -3318,6 +3318,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3318
3318
|
var linejoin: String?
|
|
3319
3319
|
var dashes: [CGFloat]?
|
|
3320
3320
|
var vectorEffect: String?
|
|
3321
|
+
var fontWeight: String?
|
|
3321
3322
|
var dominantBaseline: String?
|
|
3322
3323
|
|
|
3323
3324
|
func merged(_ node: [String: Any]) -> Paint {
|
|
@@ -3330,6 +3331,11 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3330
3331
|
let value = CGFloat(truncating: number)
|
|
3331
3332
|
return value.isFinite ? value : nil
|
|
3332
3333
|
}
|
|
3334
|
+
func string(_ raw: Any?) -> String? {
|
|
3335
|
+
if let value = raw as? String { return value }
|
|
3336
|
+
if let number = raw as? NSNumber { return number.stringValue }
|
|
3337
|
+
return nil
|
|
3338
|
+
}
|
|
3333
3339
|
if let color = style["color"] as? String { next.color = color }
|
|
3334
3340
|
if let fill = style["fill"] as? String ?? props["fill"] as? String { next.fill = fill }
|
|
3335
3341
|
if let stroke = style["stroke"] as? String ?? props["stroke"] as? String { next.stroke = stroke }
|
|
@@ -3346,6 +3352,7 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3346
3352
|
if let join = value("strokeLinejoin") as? String { next.linejoin = join }
|
|
3347
3353
|
if let dashes = value("strokeDasharray") as? [Any] { next.dashes = dashes.compactMap(finite).filter { $0 >= 0 } }
|
|
3348
3354
|
if let vectorEffect = value("vectorEffect") as? String { next.vectorEffect = vectorEffect }
|
|
3355
|
+
if let fontWeight = string(value("fontWeight")) { next.fontWeight = fontWeight }
|
|
3349
3356
|
if let baseline = value("dominantBaseline") as? String { next.dominantBaseline = baseline }
|
|
3350
3357
|
return next
|
|
3351
3358
|
}
|
|
@@ -3446,7 +3453,14 @@ private final class CraftNativeSvgView: UIView {
|
|
|
3446
3453
|
let props = node["props"] as? [String: Any] ?? [:]
|
|
3447
3454
|
let style = node["style"] as? [String: Any] ?? [:]
|
|
3448
3455
|
let size = number(props["fontSize"]) ?? number(style["fontSize"]) ?? 12
|
|
3449
|
-
let weight: UIFont.Weight =
|
|
3456
|
+
let weight: UIFont.Weight = {
|
|
3457
|
+
switch paint.fontWeight?.lowercased() {
|
|
3458
|
+
case "500": return .medium
|
|
3459
|
+
case "600": return .semibold
|
|
3460
|
+
case "bold", "700", "800", "900": return .bold
|
|
3461
|
+
default: return .regular
|
|
3462
|
+
}
|
|
3463
|
+
}()
|
|
3450
3464
|
let font = UIFont.systemFont(ofSize: size, weight: weight)
|
|
3451
3465
|
let resolvedTextColor = paint.resolve(paint.fill) ?? UIColor.label
|
|
3452
3466
|
let textAlpha = max(0, min(1, paint.opacity * paint.fillOpacity))
|
|
@@ -5779,6 +5793,15 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5779
5793
|
updateImage(image, source: props["source"], defaultSource: props["defaultSource"])
|
|
5780
5794
|
case "ImageBackground":
|
|
5781
5795
|
let background = result as! CraftNativeImageBackgroundView
|
|
5796
|
+
let backgroundID = ObjectIdentifier(background)
|
|
5797
|
+
if let tint = color(style["tintColor"]) {
|
|
5798
|
+
tintedImages.insert(backgroundID)
|
|
5799
|
+
background.tintColor = tint
|
|
5800
|
+
} else {
|
|
5801
|
+
tintedImages.remove(backgroundID)
|
|
5802
|
+
background.tintColor = nil
|
|
5803
|
+
}
|
|
5804
|
+
if let current = background.image { background.image = imageForDisplay(current, view: background) }
|
|
5782
5805
|
let requestedResizeMode = style["resizeMode"] as? String ?? props["resizeMode"] as? String
|
|
5783
5806
|
background.contentMode = imageContentMode(requestedResizeMode)
|
|
5784
5807
|
background.clipsToBounds = background.contentMode == .scaleAspectFill
|
|
@@ -6139,6 +6162,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6139
6162
|
let owner = parent.protocolId ?? path
|
|
6140
6163
|
flatListOwners = flatListOwners.filter { $0.value != owner }
|
|
6141
6164
|
for child in children { registerFlatListOwnership(child, owner: owner) }
|
|
6165
|
+
let propRefresh = props["refreshControl"] as? [String: Any] ?? [:]
|
|
6166
|
+
let propRefreshEvents = propRefresh["events"] as? [String: String] ?? [:]
|
|
6167
|
+
let refreshHandler = nonEmptyHandler(propRefreshEvents["onRefresh"])
|
|
6168
|
+
?? nonEmptyHandler(events["onRefresh"])
|
|
6169
|
+
let refreshing = (propRefresh["refreshing"] ?? props["refreshing"]) as? Bool == true
|
|
6142
6170
|
list.bounces = props["bounces"] as? Bool ?? true
|
|
6143
6171
|
list.isScrollEnabled = props["scrollEnabled"] as? Bool != false
|
|
6144
6172
|
list.onFittingHeightChanged = { [weak list] in
|
|
@@ -6147,9 +6175,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6147
6175
|
}
|
|
6148
6176
|
list.setKeyboardShouldPersistTaps(props["keyboardShouldPersistTaps"])
|
|
6149
6177
|
list.setContentContainerStyle(props["contentContainerStyle"] as? [String: Any])
|
|
6150
|
-
list.setRefreshHandler(
|
|
6178
|
+
list.setRefreshHandler(refreshHandler.map { [weak self] handler in
|
|
6151
6179
|
{ self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": [:]]) }
|
|
6152
|
-
}, refreshing:
|
|
6180
|
+
}, refreshing: refreshing)
|
|
6181
|
+
list.setRefreshAppearance(propRefresh)
|
|
6153
6182
|
list.keyboardDismissMode = keyboardDismissMode(props["keyboardDismissMode"])
|
|
6154
6183
|
list.onScrollEvent = nonEmptyHandler(events["onScroll"]).map { handler in
|
|
6155
6184
|
{ [weak self] scrollView in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": self?.scrollEvent(scrollView) ?? [:]]) }
|
|
@@ -42,11 +42,14 @@ import XCTest
|
|
|
42
42
|
/// ifid <id> <command…> runs the command only when that element is there (a screen shown the first time)
|
|
43
43
|
/// log <text>
|
|
44
44
|
///
|
|
45
|
-
///
|
|
45
|
+
/// With a bundled journey.txt, evidence is kept as native XCTest attachments
|
|
46
|
+
/// and PASS/FAIL lines appear in the test log. Control-server journeys also
|
|
47
|
+
/// write driver.log; matrix.sh counts those checks.
|
|
46
48
|
final class DriverTests: XCTestCase {
|
|
47
49
|
let control = ProcessInfo.processInfo.environment["CRAFT_TEST_CONTROL"] ?? {{CONTROL_URL}}
|
|
48
50
|
let app = XCUIApplication(bundleIdentifier: {{BUNDLE_ID}})
|
|
49
51
|
let tabs: [String] = {{TABS}}
|
|
52
|
+
private var bundledJourney = false
|
|
50
53
|
|
|
51
54
|
func get(_ path: String, timeout: Double = 15) -> Data? {
|
|
52
55
|
var result: Data?
|
|
@@ -57,6 +60,15 @@ final class DriverTests: XCTestCase {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
func post(_ path: String, _ body: Data) {
|
|
63
|
+
if bundledJourney {
|
|
64
|
+
guard path.hasPrefix("/save?"),
|
|
65
|
+
let name = URLComponents(string: path)?.queryItems?.first(where: { $0.name == "name" })?.value else { return }
|
|
66
|
+
let attachment = XCTAttachment(data: body, uniformTypeIdentifier: name.hasSuffix(".png") ? "public.png" : "public.plain-text")
|
|
67
|
+
attachment.name = name
|
|
68
|
+
attachment.lifetime = .keepAlways
|
|
69
|
+
add(attachment)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
60
72
|
var request = URLRequest(url: URL(string: control + path)!)
|
|
61
73
|
request.httpMethod = "POST"
|
|
62
74
|
request.httpBody = body
|
|
@@ -67,7 +79,7 @@ final class DriverTests: XCTestCase {
|
|
|
67
79
|
|
|
68
80
|
func note(_ text: String) {
|
|
69
81
|
print("DRIVER \(text)")
|
|
70
|
-
post("/note", Data(text.utf8))
|
|
82
|
+
if !bundledJourney { post("/note", Data(text.utf8)) }
|
|
71
83
|
}
|
|
72
84
|
|
|
73
85
|
func check(_ ok: Bool, _ what: String) {
|
|
@@ -156,7 +168,14 @@ final class DriverTests: XCTestCase {
|
|
|
156
168
|
|
|
157
169
|
func testScript() throws {
|
|
158
170
|
continueAfterFailure = true
|
|
159
|
-
|
|
171
|
+
let data: Data?
|
|
172
|
+
if let resource = Bundle(for: DriverTests.self).url(forResource: "journey", withExtension: "txt") {
|
|
173
|
+
bundledJourney = true
|
|
174
|
+
data = try Data(contentsOf: resource)
|
|
175
|
+
} else {
|
|
176
|
+
data = get("/script")
|
|
177
|
+
}
|
|
178
|
+
guard let data, let script = String(data: data, encoding: .utf8) else {
|
|
160
179
|
XCTFail("no script")
|
|
161
180
|
return
|
|
162
181
|
}
|
|
@@ -171,6 +190,10 @@ final class DriverTests: XCTestCase {
|
|
|
171
190
|
let parts = line.split(separator: " ", maxSplits: 1).map(String.init)
|
|
172
191
|
let command = parts[0]
|
|
173
192
|
let arg = parts.count > 1 ? parts[1] : ""
|
|
193
|
+
if bundledJourney && ["net", "delay", "exec"].contains(command) {
|
|
194
|
+
check(false, "\(command) requires a control-server journey")
|
|
195
|
+
return
|
|
196
|
+
}
|
|
174
197
|
switch command {
|
|
175
198
|
case "ifid":
|
|
176
199
|
let rest = arg.split(separator: " ", maxSplits: 1).map(String.init)
|