craft-native 0.0.201 → 0.0.203
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/src/index.d.ts +1 -0
- package/dist/android/src/index.js +141 -85
- package/dist/android/src/testing.d.ts +2 -0
- package/dist/android/templates/MainActivityNative.kt.template +151 -55
- package/dist/android/templates/build.gradle.kts.app.template +2 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -1
- package/dist/ios/src/index.js +17 -4
- package/dist/ios/templates/CraftApp.swift +4 -2
- package/dist/ios/templates/CraftHybrid.swift +63 -4
- package/dist/ios/templates/CraftNativeFiles.swift +4 -2
- package/dist/ios/templates/CraftNativeFlatList.swift +62 -8
- package/dist/ios/templates/CraftNativeScreen.swift +38 -14
- package/package.json +1 -1
|
@@ -972,9 +972,26 @@ final class CraftHybridWebSlot: UIViewController {
|
|
|
972
972
|
/// page's stack is all the page's.
|
|
973
973
|
var depth: Int?
|
|
974
974
|
let isTabRoot: Bool
|
|
975
|
-
/// The page has not reached `path` yet:
|
|
976
|
-
///
|
|
975
|
+
/// The page has not reached `path` yet: retain its last rendered content
|
|
976
|
+
/// until the replacement is ready.
|
|
977
977
|
var awaitingPage = false
|
|
978
|
+
/// A slow page keeps the last rendered content until its replacement is
|
|
979
|
+
/// ready. This belongs to the incoming slot, beyond the push animation.
|
|
980
|
+
private(set) var waitingPageSnapshot: UIView?
|
|
981
|
+
|
|
982
|
+
func keepPageVisible(_ snapshot: UIView) {
|
|
983
|
+
waitingPageSnapshot?.removeFromSuperview()
|
|
984
|
+
snapshot.frame = view.bounds
|
|
985
|
+
snapshot.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
986
|
+
snapshot.accessibilityElementsHidden = true
|
|
987
|
+
view.addSubview(snapshot)
|
|
988
|
+
waitingPageSnapshot = snapshot
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
func pageBecameVisible() {
|
|
992
|
+
waitingPageSnapshot?.removeFromSuperview()
|
|
993
|
+
waitingPageSnapshot = nil
|
|
994
|
+
}
|
|
978
995
|
|
|
979
996
|
init(path: String, depth: Int?, isTabRoot: Bool) {
|
|
980
997
|
self.path = path
|
|
@@ -1023,6 +1040,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1023
1040
|
private var tabSwipeRecognizers: [UIGestureRecognizer] = []
|
|
1024
1041
|
private var activeTabSwipeTarget: String?
|
|
1025
1042
|
private var tabSwipeSettling = false
|
|
1043
|
+
private var tabSwipeGeneration: UInt = 0
|
|
1026
1044
|
/// The edge the incoming tab started from for the current gesture. Keep
|
|
1027
1045
|
/// this separate from the live frame: deriving it after the drag begins
|
|
1028
1046
|
/// makes the target jump when the translation crosses the midpoint.
|
|
@@ -1195,10 +1213,13 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1195
1213
|
let end = completed ? 0 : direction * width
|
|
1196
1214
|
let settlingDuration = completed ? 0.22 : 0.18
|
|
1197
1215
|
let duration = CraftChrome.motion(.easeOut(duration: settlingDuration)) == nil ? 0 : settlingDuration
|
|
1216
|
+
let generation = tabSwipeGeneration
|
|
1198
1217
|
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut, .beginFromCurrentState], animations: {
|
|
1199
1218
|
target.view.frame.origin.x = end
|
|
1200
1219
|
current.view.frame.origin.x = completed ? -direction * width : 0
|
|
1201
1220
|
}, completion: { _ in
|
|
1221
|
+
guard self.tabSwipeGeneration == generation,
|
|
1222
|
+
self.activeTabSwipeTarget == targetId else { return }
|
|
1202
1223
|
if completed {
|
|
1203
1224
|
current.view.isHidden = true
|
|
1204
1225
|
current.view.frame = self.view.bounds
|
|
@@ -1216,6 +1237,29 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1216
1237
|
})
|
|
1217
1238
|
}
|
|
1218
1239
|
|
|
1240
|
+
/// A newer navigation action wins immediately, including a tap back on
|
|
1241
|
+
/// the outgoing tab. Invalidate the old animation before removing it:
|
|
1242
|
+
/// UIKit may deliver its completion while the replacement is starting.
|
|
1243
|
+
private func interruptTabSwipe(restoringSelection: Bool) {
|
|
1244
|
+
guard let targetId = activeTabSwipeTarget else { return }
|
|
1245
|
+
tabSwipeGeneration &+= 1
|
|
1246
|
+
activeTabSwipeTarget = nil
|
|
1247
|
+
tabSwipeSettling = false
|
|
1248
|
+
for nav in [visibleStack, stacks[targetId]].compactMap({ $0 }) {
|
|
1249
|
+
nav.view.layer.removeAllAnimations()
|
|
1250
|
+
nav.view.frame = view.bounds
|
|
1251
|
+
nav.view.isHidden = nav !== visibleStack
|
|
1252
|
+
}
|
|
1253
|
+
// Cancel the current touch stream, so its later moved/ended events
|
|
1254
|
+
// cannot claim another tab after the explicit navigation.
|
|
1255
|
+
for recognizer in tabSwipeRecognizers {
|
|
1256
|
+
recognizer.isEnabled = false
|
|
1257
|
+
recognizer.isEnabled = true
|
|
1258
|
+
}
|
|
1259
|
+
CraftChrome.shared.cancelTabSwipe(restoringSelection: restoringSelection)
|
|
1260
|
+
if visibleStack?.topViewController is CraftNativeScreenController { parkWeb() }
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1219
1263
|
private func horizontalScrollAt(_ point: CGPoint, in root: UIView) -> Bool {
|
|
1220
1264
|
let local = root.convert(point, from: view)
|
|
1221
1265
|
guard root.bounds.contains(local), let hit = root.hitTest(local, with: nil) else { return false }
|
|
@@ -1304,6 +1348,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1304
1348
|
snapshot.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
1305
1349
|
source.view.addSubview(snapshot)
|
|
1306
1350
|
webTransitionSnapshots.append(snapshot)
|
|
1351
|
+
if slot.awaitingPage,
|
|
1352
|
+
let waiting = webHost.view.snapshotView(afterScreenUpdates: false) {
|
|
1353
|
+
slot.keepPageVisible(waiting)
|
|
1354
|
+
}
|
|
1307
1355
|
}
|
|
1308
1356
|
|
|
1309
1357
|
private var visibleStack: UINavigationController? { stacks[currentTab] }
|
|
@@ -1365,6 +1413,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1365
1413
|
}
|
|
1366
1414
|
|
|
1367
1415
|
private func show(tab id: String, rootPath: String? = nil) {
|
|
1416
|
+
interruptTabSwipe(restoringSelection: false)
|
|
1368
1417
|
cancelPendingTab()
|
|
1369
1418
|
let nav = stacks[id] ?? makeStack(tab: id, rootPath: rootPath ?? (id.isEmpty ? hybrid.startPath : id))
|
|
1370
1419
|
// A stack is hidden and shown rather than moved, so UIKit tells its
|
|
@@ -1395,6 +1444,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1395
1444
|
attachWeb()
|
|
1396
1445
|
moveWeb(to: slot, in: slot.view)
|
|
1397
1446
|
webHost.view.alpha = slot.awaitingPage ? 0 : 1
|
|
1447
|
+
if !slot.awaitingPage { slot.pageBecameVisible() }
|
|
1398
1448
|
hybrid.setPageBase(slot.isTabRoot ? nil : slot.depth)
|
|
1399
1449
|
}
|
|
1400
1450
|
|
|
@@ -1448,6 +1498,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1448
1498
|
/// it), a tab's root, or a new one pushed.
|
|
1449
1499
|
func showNative(path raw: String, replace: Bool, fromLink: Bool = false) {
|
|
1450
1500
|
guard let match = hybrid.routes.match(raw) else { return }
|
|
1501
|
+
interruptTabSwipe(restoringSelection: true)
|
|
1451
1502
|
let tabs = hybrid.tabIds
|
|
1452
1503
|
if let tab = tabs.first(where: { CraftHybridRoutes.normalize($0) == match.path }) {
|
|
1453
1504
|
if tab != currentTab {
|
|
@@ -1483,6 +1534,7 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1483
1534
|
/// slide, then the entry slides in over the screen that opened it.
|
|
1484
1535
|
private func pushWeb(_ path: String) {
|
|
1485
1536
|
guard hybrid.hasWeb, !openingWeb, let nav = visibleStack else { return }
|
|
1537
|
+
interruptTabSwipe(restoringSelection: true)
|
|
1486
1538
|
openingWeb = true
|
|
1487
1539
|
hybrid.navigatePage(path) { [weak self, weak nav] depth in
|
|
1488
1540
|
guard let self else { return }
|
|
@@ -1504,7 +1556,10 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1504
1556
|
private func reveal(_ slot: CraftHybridWebSlot) {
|
|
1505
1557
|
slot.awaitingPage = false
|
|
1506
1558
|
guard let webHost, webHost.view.superview === slot.view else { return }
|
|
1507
|
-
|
|
1559
|
+
// Commit the ready page and remove its placeholder in the same frame.
|
|
1560
|
+
// Fading it up from zero exposed the slot's empty background again.
|
|
1561
|
+
webHost.view.alpha = 1
|
|
1562
|
+
slot.pageBecameVisible()
|
|
1508
1563
|
}
|
|
1509
1564
|
|
|
1510
1565
|
/// Back from the web entry on top, as the page's Back at that entry asks.
|
|
@@ -1557,7 +1612,11 @@ final class CraftHybridController: UIViewController, UINavigationControllerDeleg
|
|
|
1557
1612
|
// MARK: Tabs
|
|
1558
1613
|
|
|
1559
1614
|
func tabTapped(_ id: String, reselect: Bool) -> Bool {
|
|
1560
|
-
|
|
1615
|
+
let interruptedSwipe = activeTabSwipeTarget != nil
|
|
1616
|
+
interruptTabSwipe(restoringSelection: false)
|
|
1617
|
+
// The incoming tab is only a preview until the swipe commits, so a
|
|
1618
|
+
// tap on its already-lit indicator is a selection, not a reselect.
|
|
1619
|
+
if (reselect && !interruptedSwipe) || id == currentTab {
|
|
1561
1620
|
guard let nav = stacks[id] else { return true }
|
|
1562
1621
|
if nav.viewControllers.count > 1 {
|
|
1563
1622
|
nav.popToRootViewController(animated: true)
|
|
@@ -555,11 +555,13 @@ enum CraftNativeMedia {
|
|
|
555
555
|
let staging = root.appendingPathComponent(UUID().uuidString + ".part")
|
|
556
556
|
defer { try? fm.removeItem(at: staging) }
|
|
557
557
|
do {
|
|
558
|
-
|
|
558
|
+
// URL resource values can retain a previous size when a retry
|
|
559
|
+
// reuses a temporary location. Validate the current file itself.
|
|
560
|
+
let size = (try fm.attributesOfItem(atPath: location.path)[.size] as? NSNumber)?.int64Value ?? 0
|
|
559
561
|
// URLSession normally rejects a truncated transfer; also guard this boundary.
|
|
560
562
|
let expected = response.expectedContentLength
|
|
561
563
|
let encoded = response.value(forHTTPHeaderField: "Content-Encoding")
|
|
562
|
-
if size <= 0 || (encoded == nil && expected >= 0 &&
|
|
564
|
+
if size <= 0 || (encoded == nil && expected >= 0 && size != expected) {
|
|
563
565
|
return .failure(CraftNativeActionError(code: "NETWORK_ERROR", message: "media.keep received an incomplete file"))
|
|
564
566
|
}
|
|
565
567
|
var folder = root
|
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import UIKit
|
|
2
2
|
|
|
3
3
|
private final class CraftNativeFlatListLayout: UICollectionViewFlowLayout {
|
|
4
|
+
private var preparedExtent: CGFloat?
|
|
5
|
+
|
|
6
|
+
override func prepare() {
|
|
7
|
+
// Content-size queries can prepare layout before layoutSubviews runs.
|
|
8
|
+
(collectionView as? CraftNativeFlatList)?.updateItemEstimate()
|
|
9
|
+
super.prepare()
|
|
10
|
+
preparedExtent = collectionView.map { extent(of: $0.bounds) }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
private func extent(of bounds: CGRect) -> CGFloat {
|
|
14
|
+
scrollDirection == .vertical ? bounds.width : bounds.height
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
|
|
5
|
-
guard let
|
|
6
|
-
return
|
|
7
|
-
|
|
8
|
-
|
|
18
|
+
guard let preparedExtent else { return true }
|
|
19
|
+
return abs(preparedExtent - extent(of: newBounds)) > .ulpOfOne
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext {
|
|
23
|
+
let context = super.invalidationContext(forBoundsChange: newBounds)
|
|
24
|
+
if shouldInvalidateLayout(forBoundsChange: newBounds), let flow = context as? UICollectionViewFlowLayoutInvalidationContext {
|
|
25
|
+
flow.invalidateFlowLayoutDelegateMetrics = true
|
|
26
|
+
flow.invalidateFlowLayoutAttributes = true
|
|
27
|
+
// Self-sized item dimensions are cached separately from flow
|
|
28
|
+
// metrics. Clear them before a narrower viewport validates old
|
|
29
|
+
// widths, while keeping the cells and their rendered state.
|
|
30
|
+
if let collectionView {
|
|
31
|
+
let paths = (0..<collectionView.numberOfSections).flatMap { section in
|
|
32
|
+
(0..<collectionView.numberOfItems(inSection: section)).map { IndexPath(item: $0, section: section) }
|
|
33
|
+
}
|
|
34
|
+
flow.invalidateItems(at: paths)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return context
|
|
9
38
|
}
|
|
10
39
|
}
|
|
11
40
|
|
|
@@ -190,9 +219,30 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
190
219
|
/// room again as they are.
|
|
191
220
|
func fittingHeight(width: CGFloat) -> CGFloat {
|
|
192
221
|
guard !isScrollEnabled else { return 0 }
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
222
|
+
// Flex measurement proposes several widths before committing a frame.
|
|
223
|
+
// Mutating the collection here lets self-sizing cache a provisional
|
|
224
|
+
// width and can reenter parent measurement through contentSize.didSet.
|
|
225
|
+
// Read the committed measurement, or estimate until layout measures
|
|
226
|
+
// rows at their actual width and onFittingHeightChanged asks for room.
|
|
227
|
+
let content: CGFloat
|
|
228
|
+
if abs(bounds.width - width) <= 0.5, contentSize.height > 0 {
|
|
229
|
+
content = contentSize.height
|
|
230
|
+
} else if flowLayout.scrollDirection == .horizontal {
|
|
231
|
+
content = items.isEmpty ? 0 : 44
|
|
232
|
+
} else {
|
|
233
|
+
var rows = 0
|
|
234
|
+
var pending = 0
|
|
235
|
+
for item in items {
|
|
236
|
+
if item.isChrome {
|
|
237
|
+
rows += (pending + columns - 1) / columns + 1
|
|
238
|
+
pending = 0
|
|
239
|
+
} else {
|
|
240
|
+
pending += 1
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
rows += (pending + columns - 1) / columns
|
|
244
|
+
content = CGFloat(rows) * 44 + CGFloat(max(0, rows - 1)) * flowLayout.minimumLineSpacing
|
|
245
|
+
}
|
|
196
246
|
return max(0, ceil(content + contentInset.top + contentInset.bottom))
|
|
197
247
|
}
|
|
198
248
|
|
|
@@ -208,13 +258,17 @@ final class CraftNativeFlatList: UICollectionView, UICollectionViewDelegateFlowL
|
|
|
208
258
|
return max(1, (width - spacing) / CGFloat(count))
|
|
209
259
|
}
|
|
210
260
|
|
|
211
|
-
|
|
261
|
+
fileprivate func updateItemEstimate() {
|
|
212
262
|
let estimate = flowLayout.scrollDirection == .horizontal
|
|
213
263
|
? CGSize(width: 44, height: itemExtent(isChrome: true))
|
|
214
264
|
: CGSize(width: itemExtent(isChrome: false), height: 44)
|
|
215
265
|
if flowLayout.estimatedItemSize != estimate, bounds.width > 0 {
|
|
216
266
|
flowLayout.estimatedItemSize = estimate
|
|
217
267
|
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
override func layoutSubviews() {
|
|
271
|
+
updateItemEstimate()
|
|
218
272
|
super.layoutSubviews()
|
|
219
273
|
craftApplyCornerRadius()
|
|
220
274
|
}
|
|
@@ -4650,16 +4650,22 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4650
4650
|
}
|
|
4651
4651
|
/// The scroll view the screen starts with, when nothing sits above it:
|
|
4652
4652
|
/// the root's first child, its first, and so on, none padded on top.
|
|
4653
|
-
func leading(_ node: RenderedNode) ->
|
|
4654
|
-
if let scroll = node.view as? CraftNativeScrollView { return scroll.isVertical && scroll.isScrollEnabled ?
|
|
4653
|
+
func leading(_ node: RenderedNode) -> RenderedNode? {
|
|
4654
|
+
if let scroll = node.view as? CraftNativeScrollView { return scroll.isVertical && scroll.isScrollEnabled ? node : nil }
|
|
4655
4655
|
if let list = node.view as? CraftNativeFlatList {
|
|
4656
|
-
return list.isScrollEnabled && (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal ?
|
|
4656
|
+
return list.isScrollEnabled && (list.collectionViewLayout as? UICollectionViewFlowLayout)?.scrollDirection != .horizontal ? node : nil
|
|
4657
4657
|
}
|
|
4658
4658
|
guard paddingInsets(node.style).top == 0,
|
|
4659
4659
|
let first = node.children.first(where: { $0.type != "Modal" && !$0.view.isHidden }) else { return nil }
|
|
4660
4660
|
return leading(first)
|
|
4661
4661
|
}
|
|
4662
4662
|
let scroll = find(renderedRoot)
|
|
4663
|
+
let leadingScroll = leading(renderedRoot)
|
|
4664
|
+
// Only the screen's leading scroller draws beneath floating chrome.
|
|
4665
|
+
// A bounded list inside a form must clip its rows to its viewport.
|
|
4666
|
+
if let leadingScroll, leadingScroll.style["overflow"] as? String != "hidden" {
|
|
4667
|
+
leadingScroll.view.clipsToBounds = false
|
|
4668
|
+
}
|
|
4663
4669
|
// Under a large title the screen runs edge to edge, as UIKit's own
|
|
4664
4670
|
// screens do: the scroll view starts under the bar and insets its
|
|
4665
4671
|
// content by it, so the title collapses and pull to refresh draws
|
|
@@ -4667,7 +4673,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
4667
4673
|
// view: its content scrolls up under the bar behind iOS's scroll edge
|
|
4668
4674
|
// effect, rather than over the bar's back button. Other screens stay
|
|
4669
4675
|
// inside the safe area.
|
|
4670
|
-
let edgeToEdge = scroll != nil && (prefersLargeTitle == true || scroll ===
|
|
4676
|
+
let edgeToEdge = scroll != nil && (prefersLargeTitle == true || scroll === leadingScroll?.view)
|
|
4671
4677
|
let fullEdgeToEdge = rootIsSafeAreaView
|
|
4672
4678
|
let topEdgeToEdge = fullEdgeToEdge || edgeToEdge
|
|
4673
4679
|
rootTopToSafeArea?.isActive = !topEdgeToEdge
|
|
@@ -5801,6 +5807,8 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5801
5807
|
case "sectionlist": return "SectionList"
|
|
5802
5808
|
case "flatlist": return "FlatList"
|
|
5803
5809
|
case "svg": return "Svg"
|
|
5810
|
+
case "g": return "G"
|
|
5811
|
+
case "svgtext", "svg-text": return "SvgText"
|
|
5804
5812
|
case "touchableopacity": return "TouchableOpacity"
|
|
5805
5813
|
case "touchablehighlight": return "TouchableHighlight"
|
|
5806
5814
|
case "pressable": return "Pressable"
|
|
@@ -5848,7 +5856,20 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
5848
5856
|
stack.removeArrangedSubview(spacer)
|
|
5849
5857
|
spacer.removeFromSuperview()
|
|
5850
5858
|
}
|
|
5851
|
-
|
|
5859
|
+
// STX permits text directly inside containers (including HTML links).
|
|
5860
|
+
// Give it a native label, inheriting typography without duplicating
|
|
5861
|
+
// the parent's padding, dimensions or flex layout.
|
|
5862
|
+
let typography = style.filter { key, _ in
|
|
5863
|
+
key == "color" || key == "letterSpacing" || key == "lineHeight"
|
|
5864
|
+
|| key.hasPrefix("font") || key.hasPrefix("text")
|
|
5865
|
+
}
|
|
5866
|
+
let nodes = children.compactMap { child -> [String: Any]? in
|
|
5867
|
+
if let node = child as? [String: Any] { return node }
|
|
5868
|
+
if let text = child as? String {
|
|
5869
|
+
return ["type": "Text", "style": typography, "children": [text]]
|
|
5870
|
+
}
|
|
5871
|
+
return nil
|
|
5872
|
+
}
|
|
5852
5873
|
let keys = nodes.compactMap { explicitKey($0, props: $0["props"] as? [String: Any] ?? [:]) }
|
|
5853
5874
|
let counts = Dictionary(keys.map { ($0, 1) }, uniquingKeysWith: +)
|
|
5854
5875
|
let old = Dictionary(uniqueKeysWithValues: parent.children.map { ($0.identity, $0) })
|
|
@@ -6157,10 +6178,10 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6157
6178
|
}
|
|
6158
6179
|
|
|
6159
6180
|
/// Views that take their own touches whatever handlers the screen gives
|
|
6160
|
-
/// them: a scroll view, a modal, and a video's
|
|
6181
|
+
/// them: native controls, a scroll view, a modal, and a video's controls (with
|
|
6161
6182
|
/// interaction off, its play, scrubber and full screen never answered).
|
|
6162
6183
|
private func ownsTouches(_ view: UIView) -> Bool {
|
|
6163
|
-
view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
6184
|
+
view is UIControl || view is UIScrollView || view is CraftNativeModalView || view is CraftNativeVideoView
|
|
6164
6185
|
}
|
|
6165
6186
|
|
|
6166
6187
|
private func updatePressHandler(_ handler: String?, for view: UIView) {
|
|
@@ -6172,7 +6193,7 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6172
6193
|
let recognizer = UITapGestureRecognizer(target: self, action: #selector(viewPressed(_:)))
|
|
6173
6194
|
tapRecognizers[id] = recognizer
|
|
6174
6195
|
view.addGestureRecognizer(recognizer)
|
|
6175
|
-
} else if
|
|
6196
|
+
} else if !actionable, let recognizer = tapRecognizers.removeValue(forKey: id) {
|
|
6176
6197
|
view.removeGestureRecognizer(recognizer)
|
|
6177
6198
|
}
|
|
6178
6199
|
// A container keeps receiving touches without a handler of its own,
|
|
@@ -6420,11 +6441,12 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6420
6441
|
)
|
|
6421
6442
|
// A label paints its background inside its own bounds, so a rounded
|
|
6422
6443
|
// chip of text clips to its corners.
|
|
6423
|
-
//
|
|
6424
|
-
//
|
|
6425
|
-
//
|
|
6426
|
-
|
|
6427
|
-
view.clipsToBounds =
|
|
6444
|
+
// Native scrollers clip their viewport unless explicitly made
|
|
6445
|
+
// visible. trackContentScrollView opens only the leading scroller
|
|
6446
|
+
// beneath the screen's floating chrome.
|
|
6447
|
+
let overflow = style["overflow"] as? String
|
|
6448
|
+
view.clipsToBounds = overflow == "hidden"
|
|
6449
|
+
|| (view is UIScrollView && overflow != "visible")
|
|
6428
6450
|
|| (view is UILabel && (view.craftRequestedCornerRadius ?? 0) > 0)
|
|
6429
6451
|
view.layer.masksToBounds = view.clipsToBounds
|
|
6430
6452
|
view.craftLayoutStyle = CraftNativeLayoutStyle(style)
|
|
@@ -6809,7 +6831,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
|
|
|
6809
6831
|
} else if type != "TextInput" {
|
|
6810
6832
|
view.accessibilityValue = nil
|
|
6811
6833
|
}
|
|
6812
|
-
let
|
|
6834
|
+
let hasLinkTarget = ["href", "to"].contains { !(props[$0] as? String ?? "").isEmpty }
|
|
6835
|
+
let linkedControl = ["Link", "TouchableOpacity", "TouchableHighlight", "Pressable"].contains(type) && hasLinkTarget
|
|
6836
|
+
let role = (props["accessibilityRole"] as? String) ?? (linkedControl ? "link" : nil)
|
|
6813
6837
|
// A scroll view, a list or a group of controls holds what VoiceOver
|
|
6814
6838
|
// reads: its label names the group, as a tab strip or a pager's
|
|
6815
6839
|
// name, and never stands in for its content. As an element of its
|