ReviewOS

also looking at this

craft-native/craft

fix(mobile): preserve native editing, navigation, and durable media requests

#369
Merged chris wants to merge codex/native-vertical-scroll-tab-swipes into main
36 files +864 -185

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/ios/fixtures/native-navigation/NativeNavigationUITests.swiftmodified+15-5
Changes to packages/ios/fixtures/native-navigation/NativeNavigationUITests.swift
@@ -74,16 +74,26 @@ final class NativeNavigationUITests: XCTestCase {
7474 for _ in 0..<attempts {
7575 if inside(element, scroll) && element.isHittable { return }
7676 let viewport = scroll.frame
77 let nested: [CGRect] = scroll.descendants(matching: .scrollView).allElementsBoundByIndex.map { $0.frame }
78 var start: CGFloat = 0.5
79 for step in 0...8 {
80 let offset = CGFloat(step) * 0.05
77 // A picker wheel, slider or text selection owns its drag just as
78 // a nested scroll view does. Start on the list's own surface;
79 // dragging a wheel must not stand in for scrolling the page.
80 let trackingTypes: [XCUIElement.ElementType] = [.scrollView, .picker, .pickerWheel, .datePicker, .slider, .switch, .stepper, .textField, .secureTextField, .textView, .segmentedControl]
81 let blocked = scroll.descendants(matching: .any)
82 .matching(NSPredicate(format: "elementType IN %@", trackingTypes.map { NSNumber(value: $0.rawValue) }))
83 .allElementsBoundByIndex.map { $0.frame.insetBy(dx: -4, dy: -4) }
84 var start: CGFloat?
85 for step in 0...40 {
86 let offset = CGFloat(step) * 0.01
8187 let free = [0.5 + offset, 0.5 - offset].first { (fraction: CGFloat) -> Bool in
8288 let y = viewport.minY + viewport.height * fraction
83 return !nested.contains { (frame: CGRect) -> Bool in frame.minY - 4 <= y && y <= frame.maxY + 4 }
89 return !blocked.contains { $0.contains(CGPoint(x: viewport.midX, y: y)) }
8490 }
8591 if let free { start = free; break }
8692 }
93 guard let start else {
94 XCTFail("no list-owned drag surface outside native controls", file: file, line: line)
95 return
96 }
8797 // Content moves the opposite way to the finger, which may leave
8898 // a short scroll view: the drag stays its own until it lifts.
8999 let distance = max(-240, min(240, element.frame.midY - viewport.midY))
packages/ios/fixtures/native-navigation/NativeNavigationUnitTests.swiftmodified+31-0
Changes to packages/ios/fixtures/native-navigation/NativeNavigationUnitTests.swift
@@ -245,6 +245,37 @@ final class NativeNavigationUnitTests: XCTestCase {
245245 XCTAssertFalse(text.isScrollEnabled)
246246 }
247247
248 func testKeyedRowButtonKeepsItsNativeHitTargetAndDispatchAfterInputReordersRows() async throws {
249 let home = try XCTUnwrap(stack("/"))
250 let screen = try XCTUnwrap(home.topViewController as? CraftNativeScreenController)
251 func find(_ id: String, in view: UIView) -> UIView? {
252 if view.accessibilityIdentifier == id { return view }
253 return view.subviews.lazy.compactMap { find(id, in: $0) }.first
254 }
255 await displayWait("keyed input mounted") { find("navigation-input-b", in: screen.view) != nil }
256 let input = try XCTUnwrap(find("navigation-input-b", in: screen.view) as? UITextField)
257 XCTAssertTrue(input.becomeFirstResponder())
258 input.text = "draft"
259 input.sendActions(for: .editingChanged)
260 await displayWait("input-driven keyed reorder") {
261 find("navigation-input-c", in: screen.view) != nil
262 }
263 XCTAssertTrue(input === find("navigation-input-b", in: screen.view))
264 let button = try XCTUnwrap(find("navigation-select-b", in: screen.view) as? UIButton)
265 let scroll = try XCTUnwrap(find("native-root-scroll", in: screen.view) as? UIScrollView)
266 screen.view.layoutIfNeeded()
267 scroll.scrollRectToVisible(button.convert(button.bounds, to: scroll), animated: false)
268 window.layoutIfNeeded()
269 let point = button.convert(CGPoint(x: button.bounds.midX, y: button.bounds.midY), to: window)
270 let hit = window.hitTest(point, with: nil)
271 XCTAssertTrue(hit === button || hit?.isDescendant(of: button) == true,
272 "The retained button's visible center hit \(String(describing: hit)) at \(point), button frame \(button.frame)")
273 button.sendActions(for: .touchUpInside)
274 await displayWait("retained row button callback") {
275 (find("keyed-list-status", in: screen.view) as? UILabel)?.text == "Selected: b"
276 }
277 }
278
248279 func testRepeatedSelectorsRetainDistinctProtocolViewsAcrossReorders() throws {
249280 let controller = CraftNativeScreenController(config: CraftConfig())
250281 window.rootViewController = controller