@craft-native/ios 0.0.249 → 0.0.251
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/package.json
CHANGED
|
@@ -3788,7 +3788,6 @@ private final class CraftNativeModalView: UIView, UIGestureRecognizerDelegate {
|
|
|
3788
3788
|
private let blocker = UIView()
|
|
3789
3789
|
private let backdropTap = UITapGestureRecognizer()
|
|
3790
3790
|
private var renderedVisible = false
|
|
3791
|
-
private var dismissSuppressedByRequest = false
|
|
3792
3791
|
private var animationGeneration = 0
|
|
3793
3792
|
var onRequestClose: ((String) -> Void)?
|
|
3794
3793
|
|
|
@@ -3877,25 +3876,15 @@ private final class CraftNativeModalView: UIView, UIGestureRecognizerDelegate {
|
|
|
3877
3876
|
transform = .identity
|
|
3878
3877
|
alpha = 0
|
|
3879
3878
|
isUserInteractionEnabled = true
|
|
3880
|
-
dismissSuppressedByRequest = false
|
|
3881
3879
|
onRequestClose = nil
|
|
3882
3880
|
}
|
|
3883
3881
|
|
|
3884
3882
|
func requestClose(reason: String = "backdrop") {
|
|
3885
3883
|
guard let onRequestClose else { return }
|
|
3886
|
-
dismissSuppressedByRequest = true
|
|
3887
3884
|
isUserInteractionEnabled = false
|
|
3888
3885
|
onRequestClose(reason)
|
|
3889
3886
|
}
|
|
3890
3887
|
|
|
3891
|
-
func consumeDismissSuppression() -> Bool {
|
|
3892
|
-
let suppressed = dismissSuppressedByRequest
|
|
3893
|
-
dismissSuppressedByRequest = false
|
|
3894
|
-
return suppressed
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
func clearDismissSuppression() { dismissSuppressedByRequest = false }
|
|
3898
|
-
|
|
3899
3888
|
@objc private func backdropTapped() { requestClose(reason: "backdrop") }
|
|
3900
3889
|
|
|
3901
3890
|
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
@@ -3993,9 +3982,10 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele
|
|
|
3993
3982
|
required init?(coder: NSCoder) { nil }
|
|
3994
3983
|
|
|
3995
3984
|
override func touchesShouldCancel(in view: UIView) -> Bool {
|
|
3996
|
-
//
|
|
3997
|
-
//
|
|
3998
|
-
|
|
3985
|
+
// Buttons and text fields yield once a scroll drag begins; an
|
|
3986
|
+
// ordinary tap still reaches them immediately. Value controls such
|
|
3987
|
+
// as sliders, switches and date pickers keep their tracking gesture.
|
|
3988
|
+
if view is UIButton || view is UITextField { return true }
|
|
3999
3989
|
if view is UIControl { return false }
|
|
4000
3990
|
return super.touchesShouldCancel(in: view)
|
|
4001
3991
|
}
|
|
@@ -7366,11 +7356,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7366
7356
|
modal.onRequestClose = nonEmptyHandler(events["onRequestClose"]).map { handler in
|
|
7367
7357
|
{ [weak self] reason in self?.send(type: "EVENT", payload: ["handlerName": handler, "nativeEvent": ["reason": reason]]) }
|
|
7368
7358
|
}
|
|
7369
|
-
if visible { modal.clearDismissSuppression() }
|
|
7370
|
-
let suppressDismiss = !visible && modal.consumeDismissSuppression()
|
|
7371
7359
|
if wasVisible != visible, visible || wasVisible == true {
|
|
7372
7360
|
let lifecycleHandler = visible ? events["onShow"] : events["onDismiss"]
|
|
7373
|
-
if let lifecycleHandler = nonEmptyHandler(lifecycleHandler)
|
|
7361
|
+
if let lifecycleHandler = nonEmptyHandler(lifecycleHandler) {
|
|
7374
7362
|
send(type: "EVENT", payload: ["handlerName": lifecycleHandler, "nativeEvent": [:]])
|
|
7375
7363
|
}
|
|
7376
7364
|
}
|
|
@@ -7448,10 +7436,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
7448
7436
|
// already-stopped view changes hidesWhenStopped to false.
|
|
7449
7437
|
// Android applies the requested visibility on every render;
|
|
7450
7438
|
// keep the two hosts in lockstep for dynamic prop changes.
|
|
7451
|
-
indicator.isHidden = indicator.hidesWhenStopped
|
|
7439
|
+
indicator.isHidden = style["display"] as? String == "none" || indicator.hidesWhenStopped
|
|
7452
7440
|
} else {
|
|
7453
7441
|
indicator.startAnimating()
|
|
7454
|
-
indicator.isHidden =
|
|
7442
|
+
indicator.isHidden = style["display"] as? String == "none"
|
|
7455
7443
|
}
|
|
7456
7444
|
case "Svg":
|
|
7457
7445
|
let drawing = result as! CraftNativeSvgView
|
|
@@ -690,8 +690,15 @@ enum CraftNativePush {
|
|
|
690
690
|
|
|
691
691
|
// MARK: - Share sheet
|
|
692
692
|
|
|
693
|
-
/// A URL stays a URL in Messages
|
|
693
|
+
/// A URL stays a URL in Messages and AirDrop. Its title belongs in the
|
|
694
694
|
/// system preview and mail subject, rather than becoming a second text item.
|
|
695
|
+
///
|
|
696
|
+
/// Copy is the exception: it writes the address as text. A URL-only
|
|
697
|
+
/// pasteboard item has no textual representation, so `UIPasteboard.string`
|
|
698
|
+
/// is nil, a web page's `navigator.clipboard.readText()` gets nothing, and
|
|
699
|
+
/// so does the simulator's `pbpaste` - the shared link copied as nothing
|
|
700
|
+
/// anywhere text is read. Text that is an address is still detected as a
|
|
701
|
+
/// link wherever it is pasted.
|
|
695
702
|
final class CraftShareLink: NSObject, UIActivityItemSource {
|
|
696
703
|
let url: URL
|
|
697
704
|
let title: String
|
|
@@ -703,7 +710,9 @@ final class CraftShareLink: NSObject, UIActivityItemSource {
|
|
|
703
710
|
|
|
704
711
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { url }
|
|
705
712
|
|
|
706
|
-
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
|
713
|
+
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
|
714
|
+
activityType == .copyToPasteboard ? url.absoluteString : url
|
|
715
|
+
}
|
|
707
716
|
|
|
708
717
|
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { title }
|
|
709
718
|
|
|
@@ -139,6 +139,57 @@ final class DriverTests: XCTestCase {
|
|
|
139
139
|
return nil
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/// About a third of a screen of scrolling, as a held drag.
|
|
143
|
+
///
|
|
144
|
+
/// `app.swipeUp()` is a flick, and how far a flick throws a scroll view
|
|
145
|
+
/// depends on how its touches arrive: on a loaded machine, with a map
|
|
146
|
+
/// keeping the app busy, eight of them moved a trail page less than one
|
|
147
|
+
/// screen. A drag that stops before it lifts moves the content by its
|
|
148
|
+
/// own length every time. It runs between the middle of the screen and
|
|
149
|
+
/// the upper part, where a flick began too, so it stays off the keys when
|
|
150
|
+
/// a keyboard is up - an iOS 27 simulator reports its keyboard's frame
|
|
151
|
+
/// below the screen while drawing it over the lower half - and it stops
|
|
152
|
+
/// short of a keyboard whose frame is known.
|
|
153
|
+
func scrollPage(towardTop: Bool) {
|
|
154
|
+
let frame = app.frame
|
|
155
|
+
var lower = frame.minY + frame.height * 0.55
|
|
156
|
+
let keyboard = app.keyboards.firstMatch
|
|
157
|
+
if keyboard.exists, !keyboard.frame.isEmpty, keyboard.frame.minY < frame.maxY {
|
|
158
|
+
lower = min(lower, keyboard.frame.minY - 24)
|
|
159
|
+
}
|
|
160
|
+
let upper = max(frame.minY + frame.height * 0.15, lower - frame.height * 0.4)
|
|
161
|
+
let point = { (y: CGFloat) in self.app.coordinate(withNormalizedOffset: .zero).withOffset(CGVector(dx: frame.midX, dy: y)) }
|
|
162
|
+
let (from, to) = towardTop ? (point(upper), point(lower)) : (point(lower), point(upper))
|
|
163
|
+
from.press(forDuration: 0.05, thenDragTo: to, withVelocity: .default, thenHoldForDuration: 0.2)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Whether `target`, or a field inside it, holds the keyboard.
|
|
167
|
+
func hasKeyboardFocus(_ target: XCUIElement) -> Bool {
|
|
168
|
+
if (target.value(forKey: "hasKeyboardFocus") as? Bool) == true { return true }
|
|
169
|
+
return target.descendants(matching: .any).matching(NSPredicate(format: "hasKeyboardFocus == 1")).firstMatch.exists
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Taps `target` until it holds the keyboard, for up to `seconds`.
|
|
173
|
+
///
|
|
174
|
+
/// A tap is not focus. The first keyboard a freshly created simulator
|
|
175
|
+
/// shows takes its keyboard process many seconds to start - one CI run
|
|
176
|
+
/// spent 20s delivering the tap - and the field had not become first
|
|
177
|
+
/// responder when the text was typed, which failed with "Neither element
|
|
178
|
+
/// nor any descendant has keyboard focus". A later tap lands once the app
|
|
179
|
+
/// is idle again.
|
|
180
|
+
func focus(_ target: XCUIElement, seconds: Double = 15) -> Bool {
|
|
181
|
+
let end = Date().addingTimeInterval(seconds)
|
|
182
|
+
repeat {
|
|
183
|
+
target.tap()
|
|
184
|
+
let settle = Date().addingTimeInterval(3)
|
|
185
|
+
repeat {
|
|
186
|
+
if hasKeyboardFocus(target) { return true }
|
|
187
|
+
Thread.sleep(forTimeInterval: 0.25)
|
|
188
|
+
} while Date() < settle
|
|
189
|
+
} while Date() < end
|
|
190
|
+
return hasKeyboardFocus(target)
|
|
191
|
+
}
|
|
192
|
+
|
|
142
193
|
func waitFor(_ text: String, _ seconds: Double) -> XCUIElement? {
|
|
143
194
|
poll(seconds) { element(containing: text) }
|
|
144
195
|
}
|
|
@@ -375,8 +426,10 @@ final class DriverTests: XCTestCase {
|
|
|
375
426
|
let idParts = arg.split(separator: " ", maxSplits: 1).map(String.init)
|
|
376
427
|
let target = identified(idParts[0])
|
|
377
428
|
if target.waitForExistence(timeout: 10) {
|
|
378
|
-
target
|
|
379
|
-
|
|
429
|
+
guard focus(target) else {
|
|
430
|
+
check(false, "typeid \(idParts[0]) takes keyboard focus")
|
|
431
|
+
return
|
|
432
|
+
}
|
|
380
433
|
app.typeText(idParts.count > 1 ? idParts[1] : "")
|
|
381
434
|
note("typeid \(idParts[0]): ok")
|
|
382
435
|
} else { XCTFail("Element not found"); note("FAIL typeid \(idParts[0]): NOT FOUND") }
|
|
@@ -402,8 +455,10 @@ final class DriverTests: XCTestCase {
|
|
|
402
455
|
let index = Int(fieldParts[0]) ?? 0
|
|
403
456
|
let fields = app.textFields.allElementsBoundByIndex + app.secureTextFields.allElementsBoundByIndex
|
|
404
457
|
if index >= 0 && index < fields.count {
|
|
405
|
-
fields[index]
|
|
406
|
-
|
|
458
|
+
guard focus(fields[index]) else {
|
|
459
|
+
check(false, "field \(index) takes keyboard focus")
|
|
460
|
+
return
|
|
461
|
+
}
|
|
407
462
|
app.typeText(fieldParts.count > 1 ? fieldParts[1] : "")
|
|
408
463
|
note("field \(index): ok")
|
|
409
464
|
} else { XCTFail("Element not found"); note("FAIL field \(index): NOT FOUND (\(fields.count) fields)") }
|
|
@@ -447,14 +502,11 @@ final class DriverTests: XCTestCase {
|
|
|
447
502
|
else { XCTFail("Element not found"); note("FAIL tab \(arg): NOT FOUND") }
|
|
448
503
|
case "scrollto":
|
|
449
504
|
var found = element(containing: arg)
|
|
450
|
-
var
|
|
451
|
-
while (found == nil || !(found!.isHittable)) &&
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
app.swipeUp()
|
|
456
|
-
}
|
|
457
|
-
swipes += 1
|
|
505
|
+
var drags = 0
|
|
506
|
+
while (found == nil || !(found!.isHittable)) && drags < 12 {
|
|
507
|
+
let towardTop = found.map { !$0.frame.isEmpty && $0.frame.midY < app.frame.midY } ?? false
|
|
508
|
+
scrollPage(towardTop: towardTop)
|
|
509
|
+
drags += 1
|
|
458
510
|
found = element(containing: arg)
|
|
459
511
|
}
|
|
460
512
|
check(found != nil && found!.isHittable, "scrollto \(arg)")
|