craft-native 0.0.484 → 0.0.487
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.
|
@@ -150,8 +150,8 @@ private fun craftNativeBoolean(value: Any?, fallback: Boolean = false): Boolean
|
|
|
150
150
|
private fun craftNativeInt(value: Any?, fallback: Int): Int = (value as? Number)?.toInt() ?: fallback
|
|
151
151
|
|
|
152
152
|
private fun craftNativePickerValue(value: Any?): String? = when (value) {
|
|
153
|
-
is String -> value
|
|
154
|
-
is Number -> value.toDouble().takeIf { it.isFinite() }?.toString()
|
|
153
|
+
is String -> value
|
|
154
|
+
is Number -> value.toDouble().takeIf { it.isFinite() }?.let { value.toString() }
|
|
155
155
|
else -> null
|
|
156
156
|
}
|
|
157
157
|
|
|
@@ -389,13 +389,172 @@ private object CraftNativeSkewTransforms {
|
|
|
389
389
|
}
|
|
390
390
|
|
|
391
391
|
/** A DOM-free SVG surface for the geometry emitted by the shared STX native protocol. */
|
|
392
|
+
/** Local native chart selection: no JS events are needed while scrubbing. */
|
|
393
|
+
private class CraftNativeChartInspection(private val view: View) {
|
|
394
|
+
private var fingerprint = ""
|
|
395
|
+
private var title = ""
|
|
396
|
+
private var points = emptyList<JSONObject>()
|
|
397
|
+
private var selected = -1
|
|
398
|
+
private var startX = 0f
|
|
399
|
+
private var startY = 0f
|
|
400
|
+
private var dragging = false
|
|
401
|
+
private var abandoned = false
|
|
402
|
+
private val slop = android.view.ViewConfiguration.get(view.context).scaledTouchSlop
|
|
403
|
+
private val hold = Runnable { if (!abandoned) selectAt(startX) }
|
|
404
|
+
val enabled: Boolean get() = points.isNotEmpty()
|
|
405
|
+
private val density get() = view.resources.displayMetrics.density
|
|
406
|
+
private fun dp(value: Float) = value * density
|
|
407
|
+
fun configure(raw: JSONObject?) {
|
|
408
|
+
val next = raw?.toString().orEmpty()
|
|
409
|
+
if (next == fingerprint) return
|
|
410
|
+
fingerprint = next
|
|
411
|
+
title = raw?.optString("title").orEmpty()
|
|
412
|
+
val list = raw?.optJSONArray("points") ?: JSONArray()
|
|
413
|
+
points = (0 until list.length()).mapNotNull { list.optJSONObject(it) }.filter {
|
|
414
|
+
val x = it.optDouble("x", Double.NaN)
|
|
415
|
+
x.isFinite() && x in 0.0..1.0 && it.opt("label") is String
|
|
416
|
+
}
|
|
417
|
+
selected = -1
|
|
418
|
+
view.isFocusable = enabled
|
|
419
|
+
view.invalidate()
|
|
420
|
+
}
|
|
421
|
+
private fun announcement(point: JSONObject): String {
|
|
422
|
+
val rows = point.optJSONArray("rows") ?: JSONArray()
|
|
423
|
+
return (listOf(point.optString("label")) + (0 until rows.length()).mapNotNull { rows.optJSONObject(it)?.let { row -> row.optString("label") + ": " + row.optString("value") } }).joinToString(". ")
|
|
424
|
+
}
|
|
425
|
+
private fun select(index: Int) {
|
|
426
|
+
if (!enabled) return
|
|
427
|
+
val next = index.coerceIn(0, points.lastIndex)
|
|
428
|
+
if (next == selected) return
|
|
429
|
+
selected = next
|
|
430
|
+
view.performHapticFeedback(android.view.HapticFeedbackConstants.CLOCK_TICK)
|
|
431
|
+
view.sendAccessibilityEvent(android.view.accessibility.AccessibilityEvent.TYPE_VIEW_SELECTED)
|
|
432
|
+
view.invalidate()
|
|
433
|
+
}
|
|
434
|
+
private fun selectAt(x: Float) {
|
|
435
|
+
if (view.width <= 0) return
|
|
436
|
+
val fraction = (x / view.width).coerceIn(0f, 1f)
|
|
437
|
+
points.indices.minByOrNull { kotlin.math.abs(points[it].optDouble("x") - fraction) }?.let { select(it) }
|
|
438
|
+
}
|
|
439
|
+
fun touch(event: MotionEvent): Boolean {
|
|
440
|
+
if (!enabled) return false
|
|
441
|
+
when (event.actionMasked) {
|
|
442
|
+
MotionEvent.ACTION_DOWN -> {
|
|
443
|
+
startX = event.x; startY = event.y; dragging = false; abandoned = false
|
|
444
|
+
view.postDelayed(hold, 250)
|
|
445
|
+
}
|
|
446
|
+
MotionEvent.ACTION_MOVE -> {
|
|
447
|
+
val dx = kotlin.math.abs(event.x - startX); val dy = kotlin.math.abs(event.y - startY)
|
|
448
|
+
if (!dragging && maxOf(dx, dy) > slop) {
|
|
449
|
+
view.removeCallbacks(hold)
|
|
450
|
+
if (dy > dx) { abandoned = true; return false }
|
|
451
|
+
dragging = true
|
|
452
|
+
view.parent?.requestDisallowInterceptTouchEvent(true)
|
|
453
|
+
}
|
|
454
|
+
if (dragging && !abandoned) selectAt(event.x)
|
|
455
|
+
}
|
|
456
|
+
MotionEvent.ACTION_UP -> {
|
|
457
|
+
view.removeCallbacks(hold)
|
|
458
|
+
if (!abandoned) { view.performClick(); selectAt(event.x) }
|
|
459
|
+
view.parent?.requestDisallowInterceptTouchEvent(false)
|
|
460
|
+
}
|
|
461
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
462
|
+
view.removeCallbacks(hold); abandoned = true; selected = -1
|
|
463
|
+
view.parent?.requestDisallowInterceptTouchEvent(false); view.invalidate()
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return !abandoned
|
|
467
|
+
}
|
|
468
|
+
fun detach() { view.removeCallbacks(hold); selected = -1 }
|
|
469
|
+
fun accessibility(info: android.view.accessibility.AccessibilityNodeInfo) {
|
|
470
|
+
if (!enabled) return
|
|
471
|
+
info.className = "android.widget.SeekBar"
|
|
472
|
+
info.isScrollable = true
|
|
473
|
+
info.contentDescription = listOf(title, announcement(points[if (selected >= 0) selected else points.lastIndex])).filter { it.isNotBlank() }.joinToString(". ")
|
|
474
|
+
info.addAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD)
|
|
475
|
+
info.addAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD)
|
|
476
|
+
if (selected >= 0) info.addAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS)
|
|
477
|
+
}
|
|
478
|
+
fun action(action: Int): Boolean {
|
|
479
|
+
if (!enabled) return false
|
|
480
|
+
when (action) {
|
|
481
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD -> select(if (selected < 0) 0 else selected + 1)
|
|
482
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD -> select(if (selected < 0) points.lastIndex else selected - 1)
|
|
483
|
+
android.view.accessibility.AccessibilityNodeInfo.ACTION_DISMISS -> { selected = -1; view.invalidate() }
|
|
484
|
+
else -> return false
|
|
485
|
+
}
|
|
486
|
+
return true
|
|
487
|
+
}
|
|
488
|
+
fun draw(canvas: Canvas) {
|
|
489
|
+
val point = points.getOrNull(selected) ?: return
|
|
490
|
+
val dark = view.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
|
491
|
+
val ink = if (dark) Color.rgb(249, 250, 251) else Color.rgb(17, 24, 39)
|
|
492
|
+
val secondary = if (dark) Color.rgb(209, 213, 219) else Color.rgb(75, 85, 99)
|
|
493
|
+
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
|
494
|
+
val x = (point.optDouble("x") * view.width).toFloat()
|
|
495
|
+
paint.color = secondary; paint.alpha = 160; paint.strokeWidth = dp(1f); paint.style = Paint.Style.STROKE
|
|
496
|
+
paint.pathEffect = DashPathEffect(floatArrayOf(dp(2f), dp(3f)), 0f)
|
|
497
|
+
canvas.drawLine(x, 0f, x, view.height.toFloat(), paint)
|
|
498
|
+
paint.pathEffect = null; paint.alpha = 255; paint.style = Paint.Style.FILL
|
|
499
|
+
val y = point.optDouble("y", Double.NaN)
|
|
500
|
+
if (y.isFinite() && y in 0.0..1.0) {
|
|
501
|
+
paint.color = runCatching { craftNativeColor(point.optString("color", "#3b82f6")) }.getOrDefault(Color.BLUE)
|
|
502
|
+
canvas.drawCircle(x, (y * view.height).toFloat(), dp(5f), paint)
|
|
503
|
+
paint.color = if (dark) Color.BLACK else Color.WHITE; paint.style = Paint.Style.STROKE; paint.strokeWidth = dp(2f)
|
|
504
|
+
canvas.drawCircle(x, (y * view.height).toFloat(), dp(5f), paint)
|
|
505
|
+
}
|
|
506
|
+
val rows = point.optJSONArray("rows") ?: JSONArray()
|
|
507
|
+
val textSize = minOf(dp(20f), 12f * view.resources.displayMetrics.scaledDensity)
|
|
508
|
+
val rowHeight = textSize * 1.25f + dp(7f)
|
|
509
|
+
val width = minOf(dp(264f), view.width - dp(16f))
|
|
510
|
+
if (width <= 0 || view.height < dp(24f)) return
|
|
511
|
+
val height = minOf(view.height - dp(16f), dp(24f) + rowHeight * (rows.length() + if (title.isBlank()) 1 else 2))
|
|
512
|
+
val left = (if (x > view.width / 2f) x - width - dp(12f) else x + dp(12f)).coerceIn(dp(8f), view.width - width - dp(8f))
|
|
513
|
+
val panel = RectF(left, dp(8f), left + width, dp(8f) + height)
|
|
514
|
+
paint.style = Paint.Style.FILL; paint.color = if (dark) Color.rgb(36, 36, 38) else Color.WHITE
|
|
515
|
+
canvas.drawRoundRect(panel, dp(12f), dp(12f), paint)
|
|
516
|
+
paint.style = Paint.Style.STROKE; paint.strokeWidth = dp(1f); paint.color = if (dark) Color.rgb(70, 70, 74) else Color.rgb(226, 232, 240)
|
|
517
|
+
canvas.drawRoundRect(panel, dp(12f), dp(12f), paint)
|
|
518
|
+
canvas.save(); canvas.clipRect(panel.left + dp(10f), panel.top + dp(8f), panel.right - dp(10f), panel.bottom - dp(8f))
|
|
519
|
+
paint.style = Paint.Style.FILL; paint.textSize = textSize
|
|
520
|
+
fun text(value: String, left: Float, top: Float, width: Float, bold: Boolean = false, right: Boolean = false, muted: Boolean = false) {
|
|
521
|
+
paint.color = if (muted) secondary else ink
|
|
522
|
+
paint.typeface = if (bold) Typeface.DEFAULT_BOLD else Typeface.DEFAULT
|
|
523
|
+
val shown = TextUtils.ellipsize(value, android.text.TextPaint(paint), maxOf(0f, width), TextUtils.TruncateAt.END).toString()
|
|
524
|
+
canvas.drawText(shown, if (right) left + width - paint.measureText(shown) else left, top - paint.fontMetrics.ascent, paint)
|
|
525
|
+
}
|
|
526
|
+
var top = panel.top + dp(12f)
|
|
527
|
+
if (title.isNotBlank()) { text(title, left + dp(12f), top, width - dp(24f), bold = true); top += rowHeight }
|
|
528
|
+
text(point.optString("label"), left + dp(12f), top, width - dp(24f), muted = true); top += rowHeight
|
|
529
|
+
for (i in 0 until rows.length()) {
|
|
530
|
+
val row = rows.optJSONObject(i) ?: continue
|
|
531
|
+
var labelLeft = left + dp(12f)
|
|
532
|
+
if (row.opt("color") is String) {
|
|
533
|
+
paint.color = runCatching { craftNativeColor(row.optString("color")) }.getOrDefault(ink)
|
|
534
|
+
canvas.drawRoundRect(RectF(labelLeft, top + dp(4f), labelLeft + dp(8f), top + dp(12f)), dp(1f), dp(1f), paint); labelLeft += dp(14f)
|
|
535
|
+
}
|
|
536
|
+
text(row.optString("label"), labelLeft, top, width * 0.47f - (labelLeft - left), muted = true)
|
|
537
|
+
text(row.optString("value"), left + width * 0.48f, top, width * 0.52f - dp(12f), bold = true, right = true)
|
|
538
|
+
top += rowHeight
|
|
539
|
+
}
|
|
540
|
+
canvas.restore()
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
392
544
|
private class CraftNativeSvgView(context: Context) : View(context) {
|
|
545
|
+
private val inspection = CraftNativeChartInspection(this)
|
|
546
|
+
override fun onTouchEvent(event: MotionEvent): Boolean = inspection.touch(event) || super.onTouchEvent(event)
|
|
547
|
+
override fun performClick(): Boolean = super.performClick()
|
|
548
|
+
override fun onInitializeAccessibilityNodeInfo(info: android.view.accessibility.AccessibilityNodeInfo) { super.onInitializeAccessibilityNodeInfo(info); inspection.accessibility(info) }
|
|
549
|
+
override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean = inspection.action(action) || super.performAccessibilityAction(action, arguments)
|
|
550
|
+
|
|
393
551
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean =
|
|
394
552
|
CraftNativePointers.dispatch(this, event) { super.dispatchTouchEvent(it) }
|
|
395
553
|
|
|
396
554
|
var drawing: JSONObject = JSONObject()
|
|
397
555
|
set(value) {
|
|
398
556
|
field = value
|
|
557
|
+
inspection.configure(value.optJSONObject("props")?.optJSONObject("inspection"))
|
|
399
558
|
animates = hasAnimation(value.optJSONArray("children"))
|
|
400
559
|
invalidate()
|
|
401
560
|
requestLayout()
|
|
@@ -419,6 +578,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
419
578
|
}
|
|
420
579
|
|
|
421
580
|
override fun onDetachedFromWindow() {
|
|
581
|
+
inspection.detach()
|
|
422
582
|
super.onDetachedFromWindow()
|
|
423
583
|
updateFrames()
|
|
424
584
|
}
|
|
@@ -633,6 +793,7 @@ private class CraftNativeSvgView(context: Context) : View(context) {
|
|
|
633
793
|
canvas.concat(viewTransform)
|
|
634
794
|
drawNodes(canvas, drawing.optJSONArray("children") ?: JSONArray(), SvgPaint().merge(drawing))
|
|
635
795
|
canvas.restore()
|
|
796
|
+
inspection.draw(canvas)
|
|
636
797
|
}
|
|
637
798
|
|
|
638
799
|
private fun viewBox(): Quadruple<Float, Float, Float, Float>? {
|
package/dist/cli.js
CHANGED
|
@@ -3243,7 +3243,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
3243
3243
|
`);
|
|
3244
3244
|
}
|
|
3245
3245
|
// package.json
|
|
3246
|
-
var version = "0.0.
|
|
3246
|
+
var version = "0.0.487";
|
|
3247
3247
|
|
|
3248
3248
|
// bin/cli.ts
|
|
3249
3249
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
package/dist/index.cjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -168,8 +168,8 @@ private func craftNativeFontWeight(_ value: Any?) -> String? {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
private func craftNativePickerValue(_ value: Any?) -> String? {
|
|
171
|
-
if let value = value as? String
|
|
172
|
-
if let value = craftNativeNumber(value), value.doubleValue.isFinite { return
|
|
171
|
+
if let value = value as? String { return value }
|
|
172
|
+
if let value = craftNativeNumber(value), value.doubleValue.isFinite { return value.stringValue }
|
|
173
173
|
return nil
|
|
174
174
|
}
|
|
175
175
|
|
|
@@ -5192,7 +5192,164 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
5192
5192
|
/// 3 quadratic, 4 close, in viewBox units - and scaled to the view. Paint is
|
|
5193
5193
|
/// inherited down the tree as SVG does; `dark:` classes arrive already
|
|
5194
5194
|
/// resolved for the appearance, so a redraw is all a theme change needs.
|
|
5195
|
+
/// Native inspection of an SVG plot. Coordinates are fractions of the plot's
|
|
5196
|
+
/// bounds; labels and values are already localized by the application. No JS
|
|
5197
|
+
/// callback or bridge round trip is needed while a finger moves.
|
|
5198
|
+
final class CraftChartInspection: NSObject, UIGestureRecognizerDelegate {
|
|
5199
|
+
struct Reading {
|
|
5200
|
+
let key: String
|
|
5201
|
+
let x: CGFloat
|
|
5202
|
+
let y: CGFloat?
|
|
5203
|
+
let color: UIColor
|
|
5204
|
+
let label: String
|
|
5205
|
+
let rows: [(String, String, UIColor?)]
|
|
5206
|
+
var announcement: String { ([label] + rows.map { $0.0 + ": " + $0.1 }).joined(separator: ". ") }
|
|
5207
|
+
}
|
|
5208
|
+
private weak var view: UIView?
|
|
5209
|
+
private var configuration: NSDictionary = [:]
|
|
5210
|
+
private(set) var readings: [Reading] = []
|
|
5211
|
+
private(set) var selectedIndex = -1
|
|
5212
|
+
private var title = ""
|
|
5213
|
+
private let feedback = UISelectionFeedbackGenerator()
|
|
5214
|
+
private lazy var tap = UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
|
|
5215
|
+
private lazy var pan = UIPanGestureRecognizer(target: self, action: #selector(panned(_:)))
|
|
5216
|
+
private lazy var hold = UILongPressGestureRecognizer(target: self, action: #selector(held(_:)))
|
|
5217
|
+
|
|
5218
|
+
init(view: UIView) {
|
|
5219
|
+
self.view = view
|
|
5220
|
+
super.init()
|
|
5221
|
+
pan.delegate = self
|
|
5222
|
+
hold.minimumPressDuration = 0.25
|
|
5223
|
+
[tap, pan, hold].forEach { view.addGestureRecognizer($0); $0.isEnabled = false }
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
func configure(_ raw: Any?) {
|
|
5227
|
+
let next = raw as? [String: Any] ?? [:]
|
|
5228
|
+
guard !configuration.isEqual(to: next) else { return }
|
|
5229
|
+
configuration = next as NSDictionary
|
|
5230
|
+
title = next["title"] as? String ?? ""
|
|
5231
|
+
readings = (next["points"] as? [[String: Any]] ?? []).compactMap { point in
|
|
5232
|
+
guard let x = craftNativeNumber(point["x"])?.doubleValue, x.isFinite, x >= 0, x <= 1,
|
|
5233
|
+
let label = point["label"] as? String else { return nil }
|
|
5234
|
+
let rawY = craftNativeNumber(point["y"])?.doubleValue
|
|
5235
|
+
let y = rawY.flatMap { $0.isFinite && $0 >= 0 && $0 <= 1 ? CGFloat($0) : nil }
|
|
5236
|
+
let rows = (point["rows"] as? [[String: Any]] ?? []).compactMap { row -> (String, String, UIColor?)? in
|
|
5237
|
+
guard let label = row["label"] as? String, let value = row["value"] as? String else { return nil }
|
|
5238
|
+
return (label, value, (row["color"] as? String).flatMap { UIColor(css: $0) })
|
|
5239
|
+
}
|
|
5240
|
+
return Reading(key: point["key"] as? String ?? label, x: CGFloat(x), y: y,
|
|
5241
|
+
color: (point["color"] as? String).flatMap { UIColor(css: $0) } ?? .systemBlue, label: label, rows: rows)
|
|
5242
|
+
}
|
|
5243
|
+
selectedIndex = -1
|
|
5244
|
+
view?.isUserInteractionEnabled = !readings.isEmpty
|
|
5245
|
+
if readings.isEmpty { view?.accessibilityTraits.remove(.adjustable); view?.accessibilityValue = nil }
|
|
5246
|
+
[tap, pan, hold].forEach { $0.isEnabled = !readings.isEmpty }
|
|
5247
|
+
configureAccessibility()
|
|
5248
|
+
view?.setNeedsDisplay()
|
|
5249
|
+
}
|
|
5250
|
+
|
|
5251
|
+
func configureAccessibility() {
|
|
5252
|
+
guard let view, !readings.isEmpty else { return }
|
|
5253
|
+
view.isAccessibilityElement = true
|
|
5254
|
+
view.accessibilityTraits.insert(.adjustable)
|
|
5255
|
+
view.accessibilityValue = readings[selectedIndex >= 0 ? selectedIndex : readings.count - 1].announcement
|
|
5256
|
+
}
|
|
5257
|
+
|
|
5258
|
+
func select(_ index: Int, haptic: Bool = true) {
|
|
5259
|
+
guard !readings.isEmpty else { return }
|
|
5260
|
+
let next = max(0, min(readings.count - 1, index))
|
|
5261
|
+
guard next != selectedIndex else { return }
|
|
5262
|
+
selectedIndex = next
|
|
5263
|
+
if haptic { feedback.selectionChanged() }
|
|
5264
|
+
configureAccessibility()
|
|
5265
|
+
view?.setNeedsDisplay()
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5268
|
+
func adjust(_ direction: Int) {
|
|
5269
|
+
select(selectedIndex < 0 ? (direction < 0 ? readings.count - 1 : 0) : selectedIndex + direction)
|
|
5270
|
+
}
|
|
5271
|
+
|
|
5272
|
+
@discardableResult func dismiss() -> Bool {
|
|
5273
|
+
guard selectedIndex >= 0 else { return false }
|
|
5274
|
+
selectedIndex = -1
|
|
5275
|
+
configureAccessibility()
|
|
5276
|
+
view?.setNeedsDisplay()
|
|
5277
|
+
return true
|
|
5278
|
+
}
|
|
5279
|
+
|
|
5280
|
+
private func select(at point: CGPoint) {
|
|
5281
|
+
guard let view, view.bounds.width > 0 else { return }
|
|
5282
|
+
let fraction = max(0, min(1, point.x / view.bounds.width))
|
|
5283
|
+
if let nearest = readings.indices.min(by: { abs(readings[$0].x - fraction) < abs(readings[$1].x - fraction) }) { select(nearest) }
|
|
5284
|
+
}
|
|
5285
|
+
@objc private func tapped(_ gesture: UITapGestureRecognizer) { select(at: gesture.location(in: view)) }
|
|
5286
|
+
@objc private func panned(_ gesture: UIPanGestureRecognizer) {
|
|
5287
|
+
if gesture.state == .began || gesture.state == .changed { select(at: gesture.location(in: view)) }
|
|
5288
|
+
if gesture.state == .cancelled { dismiss() }
|
|
5289
|
+
}
|
|
5290
|
+
@objc private func held(_ gesture: UILongPressGestureRecognizer) {
|
|
5291
|
+
if gesture.state == .began || gesture.state == .changed { select(at: gesture.location(in: view)) }
|
|
5292
|
+
if gesture.state == .cancelled { dismiss() }
|
|
5293
|
+
}
|
|
5294
|
+
func gestureRecognizerShouldBegin(_ gesture: UIGestureRecognizer) -> Bool {
|
|
5295
|
+
guard gesture === pan else { return true }
|
|
5296
|
+
let velocity = pan.velocity(in: view)
|
|
5297
|
+
return abs(velocity.x) > abs(velocity.y)
|
|
5298
|
+
}
|
|
5299
|
+
func gestureRecognizer(_ gesture: UIGestureRecognizer, shouldBeRequiredToFailBy other: UIGestureRecognizer) -> Bool {
|
|
5300
|
+
gesture === pan && other.view is UIScrollView
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
func draw(in context: CGContext, bounds: CGRect) {
|
|
5304
|
+
guard readings.indices.contains(selectedIndex), bounds.width > 24, bounds.height > 24 else { return }
|
|
5305
|
+
let reading = readings[selectedIndex], x = bounds.width * reading.x
|
|
5306
|
+
context.saveGState()
|
|
5307
|
+
defer { context.restoreGState() }
|
|
5308
|
+
context.setStrokeColor(UIColor.secondaryLabel.withAlphaComponent(0.65).cgColor)
|
|
5309
|
+
context.setLineWidth(1)
|
|
5310
|
+
context.setLineDash(phase: 0, lengths: [2, 3])
|
|
5311
|
+
context.move(to: CGPoint(x: x, y: 0)); context.addLine(to: CGPoint(x: x, y: bounds.height)); context.strokePath()
|
|
5312
|
+
context.setLineDash(phase: 0, lengths: [])
|
|
5313
|
+
if let y = reading.y {
|
|
5314
|
+
let dot = CGRect(x: x - 5, y: bounds.height * y - 5, width: 10, height: 10)
|
|
5315
|
+
context.setFillColor(reading.color.cgColor); context.fillEllipse(in: dot)
|
|
5316
|
+
context.setStrokeColor(UIColor.systemBackground.cgColor); context.setLineWidth(2); context.strokeEllipse(in: dot)
|
|
5317
|
+
}
|
|
5318
|
+
let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12), maximumPointSize: 20)
|
|
5319
|
+
let bold = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 12, weight: .semibold), maximumPointSize: 20)
|
|
5320
|
+
let rowHeight = ceil(font.lineHeight) + 7
|
|
5321
|
+
let width = min(264, bounds.width - 16)
|
|
5322
|
+
let height = min(bounds.height - 16, 24 + rowHeight * CGFloat(reading.rows.count + (title.isEmpty ? 1 : 2)))
|
|
5323
|
+
let left = max(8, min(bounds.width - width - 8, x > bounds.midX ? x - width - 12 : x + 12))
|
|
5324
|
+
let panel = CGRect(x: left, y: 8, width: width, height: height)
|
|
5325
|
+
context.setShadow(offset: CGSize(width: 0, height: 4), blur: 12, color: UIColor.black.withAlphaComponent(0.16).cgColor)
|
|
5326
|
+
UIColor.secondarySystemGroupedBackground.setFill(); UIBezierPath(roundedRect: panel, cornerRadius: 12).fill()
|
|
5327
|
+
context.setShadow(offset: .zero, blur: 0, color: nil)
|
|
5328
|
+
UIColor.separator.withAlphaComponent(0.5).setStroke(); let border = UIBezierPath(roundedRect: panel.insetBy(dx: 0.5, dy: 0.5), cornerRadius: 12); border.lineWidth = 1; border.stroke()
|
|
5329
|
+
context.clip(to: panel.insetBy(dx: 10, dy: 8))
|
|
5330
|
+
let paragraph = NSMutableParagraphStyle(); paragraph.lineBreakMode = .byTruncatingTail
|
|
5331
|
+
func text(_ value: String, rect: CGRect, strong: Bool = false, right: Bool = false, secondary: Bool = false) {
|
|
5332
|
+
paragraph.alignment = right ? .right : .left
|
|
5333
|
+
(value as NSString).draw(in: rect, withAttributes: [.font: strong ? bold : font, .foregroundColor: secondary ? UIColor.label.withAlphaComponent(0.72) : UIColor.label, .paragraphStyle: paragraph])
|
|
5334
|
+
}
|
|
5335
|
+
var top = panel.minY + 12
|
|
5336
|
+
if !title.isEmpty { text(title, rect: CGRect(x: left + 12, y: top, width: width - 24, height: rowHeight), strong: true); top += rowHeight }
|
|
5337
|
+
text(reading.label, rect: CGRect(x: left + 12, y: top, width: width - 24, height: rowHeight), secondary: true); top += rowHeight
|
|
5338
|
+
for row in reading.rows {
|
|
5339
|
+
var labelLeft = left + 12
|
|
5340
|
+
if let color = row.2 { color.setFill(); UIBezierPath(roundedRect: CGRect(x: labelLeft, y: top + 4, width: 8, height: 8), cornerRadius: 1).fill(); labelLeft += 14 }
|
|
5341
|
+
text(row.0, rect: CGRect(x: labelLeft, y: top, width: width * 0.47 - (labelLeft - left), height: rowHeight), secondary: true)
|
|
5342
|
+
text(row.1, rect: CGRect(x: left + width * 0.48, y: top, width: width * 0.52 - 12, height: rowHeight), strong: true, right: true)
|
|
5343
|
+
top += rowHeight
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
|
|
5195
5348
|
final class CraftNativeSvgView: UIView {
|
|
5349
|
+
lazy var inspection = CraftChartInspection(view: self)
|
|
5350
|
+
override func accessibilityIncrement() { inspection.adjust(1) }
|
|
5351
|
+
override func accessibilityDecrement() { inspection.adjust(-1) }
|
|
5352
|
+
override func accessibilityPerformEscape() -> Bool { inspection.dismiss() }
|
|
5196
5353
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
5197
5354
|
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
5198
5355
|
}
|
|
@@ -5200,6 +5357,7 @@ final class CraftNativeSvgView: UIView {
|
|
|
5200
5357
|
var drawing: [String: Any] = [:] {
|
|
5201
5358
|
didSet {
|
|
5202
5359
|
animates = Self.hasAnimation(drawing["children"] as? [Any] ?? [])
|
|
5360
|
+
inspection.configure((drawing["props"] as? [String: Any])?["inspection"])
|
|
5203
5361
|
setNeedsDisplay()
|
|
5204
5362
|
updateFrames()
|
|
5205
5363
|
}
|
|
@@ -5426,14 +5584,17 @@ final class CraftNativeSvgView: UIView {
|
|
|
5426
5584
|
|
|
5427
5585
|
override func draw(_ rect: CGRect) {
|
|
5428
5586
|
guard let context = UIGraphicsGetCurrentContext() else { return }
|
|
5587
|
+
context.saveGState()
|
|
5429
5588
|
viewTransform = .identity
|
|
5430
5589
|
if let box = viewBox {
|
|
5431
5590
|
let props = drawing["props"] as? [String: Any] ?? [:]
|
|
5432
|
-
guard let transform = CraftNativeSvgView.transform(box: box, bounds: bounds.size, preserveAspectRatio: props["preserveAspectRatio"] as? String) else { return }
|
|
5591
|
+
guard let transform = CraftNativeSvgView.transform(box: box, bounds: bounds.size, preserveAspectRatio: props["preserveAspectRatio"] as? String) else { context.restoreGState(); return }
|
|
5433
5592
|
viewTransform = transform
|
|
5434
5593
|
context.concatenate(transform)
|
|
5435
5594
|
}
|
|
5436
5595
|
draw(children: drawing["children"] as? [Any] ?? [], paint: Paint().merged(drawing), in: context)
|
|
5596
|
+
context.restoreGState()
|
|
5597
|
+
inspection.draw(in: context, bounds: bounds)
|
|
5437
5598
|
}
|
|
5438
5599
|
|
|
5439
5600
|
private func draw(children: [Any], paint: Paint, in context: CGContext) {
|
|
@@ -6434,11 +6595,108 @@ enum CraftNativeBundle {
|
|
|
6434
6595
|
}
|
|
6435
6596
|
}
|
|
6436
6597
|
|
|
6437
|
-
|
|
6598
|
+
/// One shared single-choice control, compact like a form field. UIKit owns
|
|
6599
|
+
/// presentation, dismissal, checkmarks, keyboard navigation and VoiceOver.
|
|
6600
|
+
final class CraftNativePickerView: UIButton {
|
|
6601
|
+
struct Option: Equatable {
|
|
6602
|
+
let value: String
|
|
6603
|
+
let label: String
|
|
6604
|
+
let disabled: Bool
|
|
6605
|
+
}
|
|
6606
|
+
private var options: [Option] = []
|
|
6607
|
+
private var selectedValue: String?
|
|
6608
|
+
private var handlerIdentity: String?
|
|
6609
|
+
private var dispatch: ((String, Int) -> Void)?
|
|
6610
|
+
private var revision = 0
|
|
6611
|
+
private(set) var selectedLabel = ""
|
|
6612
|
+
|
|
6613
|
+
override init(frame: CGRect) {
|
|
6614
|
+
super.init(frame: frame)
|
|
6615
|
+
showsMenuAsPrimaryAction = true
|
|
6616
|
+
contentHorizontalAlignment = .fill
|
|
6617
|
+
titleLabel?.adjustsFontForContentSizeCategory = true
|
|
6618
|
+
layer.cornerRadius = 10
|
|
6619
|
+
}
|
|
6620
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
6621
|
+
|
|
6622
|
+
func update(options: [Option], selected: String?, enabled: Bool, handler: String?,
|
|
6623
|
+
font: UIFont, foreground: UIColor, dispatch: @escaping (String, Int) -> Void) {
|
|
6624
|
+
let candidate = selected ?? self.selectedValue
|
|
6625
|
+
let resolved = options.contains(where: { $0.value == candidate }) ? candidate : options.first(where: { !$0.disabled })?.value
|
|
6626
|
+
if self.options != options || self.selectedValue != resolved || self.isEnabled != enabled || self.handlerIdentity != handler {
|
|
6627
|
+
revision += 1
|
|
6628
|
+
interactions.compactMap { $0 as? UIContextMenuInteraction }.forEach { $0.dismissMenu() }
|
|
6629
|
+
}
|
|
6630
|
+
self.options = options
|
|
6631
|
+
self.selectedValue = resolved
|
|
6632
|
+
self.handlerIdentity = handler
|
|
6633
|
+
self.dispatch = dispatch
|
|
6634
|
+
isEnabled = enabled && options.contains(where: { !$0.disabled })
|
|
6635
|
+
selectedLabel = options.first(where: { $0.value == resolved })?.label ?? ""
|
|
6636
|
+
var configuration = UIButton.Configuration.plain()
|
|
6637
|
+
configuration.title = selectedLabel
|
|
6638
|
+
configuration.image = UIImage(systemName: "chevron.down")
|
|
6639
|
+
configuration.imagePlacement = .trailing
|
|
6640
|
+
configuration.imagePadding = 12
|
|
6641
|
+
configuration.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 12, bottom: 10, trailing: 12)
|
|
6642
|
+
configuration.titleAlignment = .leading
|
|
6643
|
+
configuration.baseForegroundColor = foreground
|
|
6644
|
+
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attributes in
|
|
6645
|
+
var result = attributes
|
|
6646
|
+
result.font = font
|
|
6647
|
+
return result
|
|
6648
|
+
}
|
|
6649
|
+
configuration.imageColorTransformer = UIConfigurationColorTransformer { _ in .secondaryLabel }
|
|
6650
|
+
self.configuration = configuration
|
|
6651
|
+
refreshMenu()
|
|
6652
|
+
invalidateIntrinsicContentSize()
|
|
6653
|
+
}
|
|
6654
|
+
|
|
6655
|
+
private func refreshMenu() {
|
|
6656
|
+
menu = UIMenu(children: options.enumerated().map { index, option in
|
|
6657
|
+
let choose = selection(index)
|
|
6658
|
+
return UIAction(title: option.label, attributes: option.disabled ? [.disabled] : [],
|
|
6659
|
+
state: option.value == selectedValue ? .on : .off) { _ in choose() }
|
|
6660
|
+
})
|
|
6661
|
+
}
|
|
6662
|
+
|
|
6663
|
+
/// An open menu must not apply an old option to a rebound or disabled field.
|
|
6664
|
+
func selection(_ index: Int) -> () -> Void {
|
|
6665
|
+
let expected = revision
|
|
6666
|
+
return { [weak self] in
|
|
6667
|
+
guard let self, self.revision == expected, self.isEnabled,
|
|
6668
|
+
self.options.indices.contains(index), !self.options[index].disabled else { return }
|
|
6669
|
+
let value = self.options[index].value
|
|
6670
|
+
guard value != self.selectedValue else { return }
|
|
6671
|
+
self.selectedValue = value
|
|
6672
|
+
self.selectedLabel = self.options[index].label
|
|
6673
|
+
self.configuration?.title = self.selectedLabel
|
|
6674
|
+
self.accessibilityValue = self.selectedLabel
|
|
6675
|
+
self.revision += 1
|
|
6676
|
+
self.refreshMenu()
|
|
6677
|
+
self.invalidateIntrinsicContentSize()
|
|
6678
|
+
self.dispatch?(value, index)
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
|
|
6682
|
+
func detach() {
|
|
6683
|
+
revision += 1
|
|
6684
|
+
dispatch = nil
|
|
6685
|
+
interactions.compactMap { $0 as? UIContextMenuInteraction }.forEach { $0.dismissMenu() }
|
|
6686
|
+
menu = nil
|
|
6687
|
+
}
|
|
6688
|
+
|
|
6689
|
+
override var intrinsicContentSize: CGSize {
|
|
6690
|
+
let size = super.intrinsicContentSize
|
|
6691
|
+
return CGSize(width: max(44, size.width), height: max(44, size.height))
|
|
6692
|
+
}
|
|
6693
|
+
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
|
6694
|
+
let fitted = super.sizeThatFits(size)
|
|
6695
|
+
return CGSize(width: fitted.width, height: max(44, fitted.height))
|
|
6696
|
+
}
|
|
6438
6697
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
6439
6698
|
craftPointerHitTest(point, with: event) { super.hitTest(point, with: event) }
|
|
6440
6699
|
}
|
|
6441
|
-
|
|
6442
6700
|
}
|
|
6443
6701
|
|
|
6444
6702
|
private final class CraftNativeDatePickerView: UIDatePicker {
|
|
@@ -6489,7 +6747,7 @@ private final class CraftNativeScreenRootView: UIView {
|
|
|
6489
6747
|
}
|
|
6490
6748
|
}
|
|
6491
6749
|
|
|
6492
|
-
final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, UITextFieldDelegate, UITextViewDelegate
|
|
6750
|
+
final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate, UITextFieldDelegate, UITextViewDelegate {
|
|
6493
6751
|
private final class RenderedNode {
|
|
6494
6752
|
/// A recycled list row's root takes the identity of the row it shows next.
|
|
6495
6753
|
var identity: String
|
|
@@ -6553,7 +6811,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6553
6811
|
/// the user's tap. Remember that native value briefly so the echo does not
|
|
6554
6812
|
/// interrupt the animation and leave the thumb painted at its old edge.
|
|
6555
6813
|
private var switchUserValues: [ObjectIdentifier: Bool] = [:]
|
|
6556
|
-
private var pickerOptions: [ObjectIdentifier: [(value: String, label: String)]] = [:]
|
|
6557
6814
|
private var modalVisibility: [ObjectIdentifier: Bool] = [:]
|
|
6558
6815
|
private var modalFocusRestore: [ObjectIdentifier: UIView] = [:]
|
|
6559
6816
|
private var layoutHandlers: [ObjectIdentifier: String] = [:]
|
|
@@ -9433,31 +9690,33 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9433
9690
|
control.configureAccessibility(items: items)
|
|
9434
9691
|
updateHandler(nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"]), for: control)
|
|
9435
9692
|
case "Picker":
|
|
9436
|
-
let picker = result as!
|
|
9437
|
-
let
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
guard let node = child as? [String: Any], node["type"] as? String == "Text" else { return nil }
|
|
9444
|
-
let optionProps = node["props"] as? [String: Any] ?? [:]
|
|
9445
|
-
let label = (node["children"] as? [Any] ?? []).compactMap { $0 as? String }.joined()
|
|
9446
|
-
guard !label.isEmpty else { return nil }
|
|
9447
|
-
return (value: craftNativePickerValue(optionProps["value"]) ?? label, label: label)
|
|
9693
|
+
let picker = result as! CraftNativePickerView
|
|
9694
|
+
let rawItems = props["items"] as? [[String: Any]]
|
|
9695
|
+
let options: [CraftNativePickerView.Option] = rawItems.map { items in
|
|
9696
|
+
items.compactMap { item in
|
|
9697
|
+
guard let label = item["label"] as? String, !label.isEmpty else { return nil }
|
|
9698
|
+
return CraftNativePickerView.Option(value: craftNativePickerValue(item["value"]) ?? label,
|
|
9699
|
+
label: label, disabled: item["disabled"] as? Bool == true)
|
|
9448
9700
|
}
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
}
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
picker.
|
|
9460
|
-
|
|
9701
|
+
} ?? children.compactMap { child in
|
|
9702
|
+
guard let node = child as? [String: Any], node["type"] as? String == "Text" else { return nil }
|
|
9703
|
+
let optionProps = node["props"] as? [String: Any] ?? [:]
|
|
9704
|
+
let label = (node["children"] as? [Any] ?? []).compactMap { $0 as? String }.joined()
|
|
9705
|
+
guard !label.isEmpty else { return nil }
|
|
9706
|
+
return CraftNativePickerView.Option(value: craftNativePickerValue(optionProps["value"]) ?? label,
|
|
9707
|
+
label: label, disabled: optionProps["disabled"] as? Bool == true)
|
|
9708
|
+
}
|
|
9709
|
+
let handler = nonEmptyHandler(events["onValueChange"]) ?? nonEmptyHandler(events["onChange"])
|
|
9710
|
+
updateHandler(handler, for: picker)
|
|
9711
|
+
picker.update(options: options,
|
|
9712
|
+
selected: craftNativePickerValue(props["selectedValue"]) ?? craftNativePickerValue(props["value"]),
|
|
9713
|
+
enabled: props["disabled"] as? Bool != true,
|
|
9714
|
+
handler: handler, font: textFont(style, default: .preferredFont(forTextStyle: .body)),
|
|
9715
|
+
foreground: color(style["color"]) ?? .label) { [weak self, weak picker] value, index in
|
|
9716
|
+
guard let self, let picker, let handler = self.handlers[ObjectIdentifier(picker)] else { return }
|
|
9717
|
+
self.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": value, "index": index]])
|
|
9718
|
+
}
|
|
9719
|
+
if style["backgroundColor"] == nil { picker.backgroundColor = .tertiarySystemFill }
|
|
9461
9720
|
case "DatePicker":
|
|
9462
9721
|
let picker = result as! UIDatePicker
|
|
9463
9722
|
let mode = Self.datePickerMode(props["mode"])
|
|
@@ -9832,6 +10091,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9832
10091
|
}
|
|
9833
10092
|
result.craftPointerEvents = CraftNativePointerEvents.resolve(props: props, style: style)
|
|
9834
10093
|
applyAccessibility(props, type: type, to: result)
|
|
10094
|
+
(result as? CraftNativeSvgView)?.inspection.configureAccessibility()
|
|
10095
|
+
if let picker = result as? CraftNativePickerView, props["accessibilityValue"] == nil {
|
|
10096
|
+
picker.accessibilityValue = picker.selectedLabel
|
|
10097
|
+
}
|
|
9835
10098
|
// After applyAccessibility: it settles whether the view is an element
|
|
9836
10099
|
// at all, and an action on a non-element is unreachable.
|
|
9837
10100
|
updateAccessibilityActions(props["accessibilityActions"], handler: menuDisabled ? nil : events["onAccessibilityAction"], for: result)
|
|
@@ -9891,10 +10154,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
9891
10154
|
control.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
|
|
9892
10155
|
return control
|
|
9893
10156
|
case "Picker":
|
|
9894
|
-
|
|
9895
|
-
picker.dataSource = self
|
|
9896
|
-
picker.delegate = self
|
|
9897
|
-
return picker
|
|
10157
|
+
return CraftNativePickerView(frame: .zero)
|
|
9898
10158
|
case "DatePicker":
|
|
9899
10159
|
let picker = CraftNativeDatePickerView()
|
|
9900
10160
|
picker.preferredDatePickerStyle = .compact
|
|
@@ -10282,10 +10542,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10282
10542
|
// its subtree is gone.
|
|
10283
10543
|
if !preservingInputDrafts {
|
|
10284
10544
|
if let textView = node.view as? UITextView { textView.delegate = nil }
|
|
10285
|
-
|
|
10286
|
-
picker.delegate = nil
|
|
10287
|
-
picker.dataSource = nil
|
|
10288
|
-
}
|
|
10545
|
+
(node.view as? CraftNativePickerView)?.detach()
|
|
10289
10546
|
if let scroll = node.view as? UIScrollView { scroll.delegate = nil }
|
|
10290
10547
|
(node.view as? UIControl)?.removeTarget(self, action: nil, for: .allEvents)
|
|
10291
10548
|
}
|
|
@@ -10340,7 +10597,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10340
10597
|
scrollMomentumEndHandlers.removeValue(forKey: id)
|
|
10341
10598
|
sliderCompleteHandlers.removeValue(forKey: id)
|
|
10342
10599
|
sliderSteps.removeValue(forKey: id)
|
|
10343
|
-
pickerOptions.removeValue(forKey: id)
|
|
10344
10600
|
modalVisibility.removeValue(forKey: id)
|
|
10345
10601
|
if let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
10346
10602
|
node.view.removeGestureRecognizer(recognizer)
|
|
@@ -10474,12 +10730,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
10474
10730
|
/// Views that take their own touches whatever handlers the screen gives
|
|
10475
10731
|
/// them: native controls, a scroll view, a modal, and a video's controls (with
|
|
10476
10732
|
/// interaction off, its play, scrubber and full screen never answered).
|
|
10477
|
-
///
|
|
10478
|
-
///
|
|
10479
|
-
/// not be panned, zoomed, or have a pin tapped; a picker could not be spun.
|
|
10733
|
+
/// Maps too: a map without a long-press handler must still pan, zoom,
|
|
10734
|
+
/// and take taps on its pins.
|
|
10480
10735
|
private func ownsTouches(_ view: UIView) -> Bool {
|
|
10481
10736
|
view is UIControl || view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
10482
|
-
|| view is MKMapView || view
|
|
10737
|
+
|| view is MKMapView || ((view as? CraftNativeSvgView)?.inspection.readings.isEmpty == false)
|
|
10483
10738
|
}
|
|
10484
10739
|
|
|
10485
10740
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|
|
@@ -11468,9 +11723,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11468
11723
|
// A video with its system controls is a group too: VoiceOver reaches
|
|
11469
11724
|
// play, the scrubber, skipping and full screen inside it.
|
|
11470
11725
|
let controlledVideo = type == "Video" && (props["controls"] as? Bool) == true
|
|
11471
|
-
//
|
|
11472
|
-
//
|
|
11473
|
-
let container = ["ScrollView", "FlatList", "SectionList", "
|
|
11726
|
+
// Compact single-choice pickers are accessible menu buttons. Controls
|
|
11727
|
+
// containing several independent elements remain semantic groups.
|
|
11728
|
+
let container = ["ScrollView", "FlatList", "SectionList", "SegmentedControl", "Modal", "Drawer"].contains(type)
|
|
11474
11729
|
|| ["tablist", "radiogroup", "list", "menu", "menubar", "toolbar"].contains(role ?? "")
|
|
11475
11730
|
|| controlledVideo
|
|
11476
11731
|
view.isAccessibilityElement = !container && role != "none" && (view.accessibilityLabel != nil || role != nil || ["Text", "Button", "Link", "Checkbox", "Image", "ImageBackground", "Video", "TextInput", "Picker", "DatePicker", "Switch", "Slider"].contains(type))
|
|
@@ -11482,6 +11737,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11482
11737
|
var traits: UIAccessibilityTraits = []
|
|
11483
11738
|
switch type {
|
|
11484
11739
|
case "Slider", "DatePicker": traits.insert(.adjustable)
|
|
11740
|
+
case "Picker": traits.insert(.button)
|
|
11485
11741
|
default: break
|
|
11486
11742
|
}
|
|
11487
11743
|
switch role ?? type.lowercased() {
|
|
@@ -11894,27 +12150,6 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
11894
12150
|
send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["value": Self.sliderEventValue(snappedSliderValue(sender.value, for: sender), step: sliderSteps[ObjectIdentifier(sender)])]])
|
|
11895
12151
|
}
|
|
11896
12152
|
|
|
11897
|
-
func numberOfComponents(in pickerView: UIPickerView) -> Int { 1 }
|
|
11898
|
-
|
|
11899
|
-
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
|
11900
|
-
pickerOptions[ObjectIdentifier(pickerView)]?.count ?? 0
|
|
11901
|
-
}
|
|
11902
|
-
|
|
11903
|
-
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
|
11904
|
-
guard let options = pickerOptions[ObjectIdentifier(pickerView)], options.indices.contains(row) else { return nil }
|
|
11905
|
-
return options[row].label
|
|
11906
|
-
}
|
|
11907
|
-
|
|
11908
|
-
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
|
11909
|
-
guard let options = pickerOptions[ObjectIdentifier(pickerView)], options.indices.contains(row),
|
|
11910
|
-
let handler = handlers[ObjectIdentifier(pickerView)] else { return }
|
|
11911
|
-
let option = options[row]
|
|
11912
|
-
send(type: "EVENT", payload: [
|
|
11913
|
-
"handlerName": handler,
|
|
11914
|
-
"nativeEvent": ["value": option.value, "index": row],
|
|
11915
|
-
])
|
|
11916
|
-
}
|
|
11917
|
-
|
|
11918
12153
|
@objc private func textFocused(_ sender: UITextField) {
|
|
11919
12154
|
lastFocusedInput = sender
|
|
11920
12155
|
// Moving between fields does not move the keyboard, so nothing else
|