ReviewOS

also looking at this

craft-native/craft

feat(maps): add key-free native Android maps and shared map controls

#363
Merged chris wants to merge codex/wildloop-native-map-providers into main
28 files +1857 -481

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-screen/NativeRenderUnitTests.swiftmodified+37-0
Changes to packages/ios/fixtures/native-screen/NativeRenderUnitTests.swift
@@ -1613,6 +1613,43 @@ final class NativeRenderUnitTests: XCTestCase {
16131613 XCTAssertTrue(disabled.accessibilityTraits.contains(.notEnabled))
16141614 }
16151615
1616 func testHidingAModalDismissesItsKeyboardWithoutRestoringHiddenInputFocus() throws {
1617 let controller = CraftNativeScreenController(config: CraftConfig())
1618 let window = try hostWindow()
1619 window.rootViewController = controller
1620 window.makeKeyAndVisible()
1621 defer { window.isHidden = true }
1622 controller.render(document([
1623 ["type": "TextInput", "props": ["key": "outside"]],
1624 ["id": "review-modal", "type": "Modal", "props": ["key": "modal", "visible": true, "animationType": "none"],
1625 "children": [["type": "TextInput", "props": ["key": "review", "multiline": true], "events": ["onChangeText": "reviewChanged"]]]],
1626 ]))
1627 controller.view.layoutIfNeeded()
1628 let input = try XCTUnwrap(find(UITextView.self, key: "review", below: controller.view))
1629 XCTAssertTrue(input.becomeFirstResponder())
1630 input.text = "A trail review"
1631 controller.textViewDidChange(input)
1632 _ = try controller.applyMutation(batch(1, [
1633 ["op": "updateNode", "id": "review-modal", "patch": ["props": ["visible": false]]],
1634 ]))
1635 XCTAssertFalse(input.isFirstResponder, "Saving or cancelling a modal must dismiss its keyboard")
1636 let settled = expectation(description: "hidden modal input acknowledgement settled")
1637 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { settled.fulfill() }
1638 wait(for: [settled], timeout: 2)
1639 XCTAssertFalse(input.isFirstResponder, "A delayed input acknowledgement cannot reopen the keyboard")
1640 _ = try controller.applyMutation(batch(2, [
1641 ["op": "updateNode", "id": "review-modal", "patch": ["props": ["visible": true]]],
1642 ]))
1643 XCTAssertFalse(input.isFirstResponder, "Reopening the modal waits for an explicit focus")
1644 XCTAssertTrue(input.becomeFirstResponder())
1645 let outside = try XCTUnwrap(find(UITextField.self, key: "outside", below: controller.view))
1646 XCTAssertTrue(outside.becomeFirstResponder())
1647 _ = try controller.applyMutation(batch(3, [
1648 ["op": "updateNode", "id": "review-modal", "patch": ["props": ["visible": false]]],
1649 ]))
1650 XCTAssertTrue(outside.isFirstResponder, "Hiding a modal must not dismiss another field's keyboard")
1651 }
1652
16161653 func testAShownModalInAScrollViewCoversTheScreenAndTakesItsTouches() throws {
16171654 let controller = CraftNativeScreenController(config: CraftConfig())
16181655 let window = try hostWindow()
packages/ios/templates/CraftNativeScreen.swiftmodified+14-0
Changes to packages/ios/templates/CraftNativeScreen.swift
@@ -3373,6 +3373,8 @@ private final class CraftNativeModalView: UIView {
33733373 let generation = animationGeneration
33743374 layer.removeAllAnimations()
33753375 guard visible else {
3376 craftScreenController?.forgetFocusedInput(in: self)
3377 endEditing(true)
33763378 if animationType == "slide" && wasVisible {
33773379 let distance = max(bounds.height, superview?.bounds.height ?? UIScreen.main.bounds.height)
33783380 UIView.animate(withDuration: 0.24, animations: {
@@ -3412,6 +3414,8 @@ private final class CraftNativeModalView: UIView {
34123414 }
34133415
34143416 func resetForTeardown() {
3417 craftScreenController?.forgetFocusedInput(in: self)
3418 endEditing(true)
34153419 animationGeneration += 1
34163420 layer.removeAllAnimations()
34173421 isHidden = true
@@ -4329,6 +4333,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
43294333 func forgetFocusedInput(_ input: UIView? = nil) {
43304334 if input == nil || lastFocusedInput === input { lastFocusedInput = nil }
43314335 }
4336 func forgetFocusedInput(in container: UIView) {
4337 if let input = lastFocusedInput, input === container || input.isDescendant(of: container) {
4338 lastFocusedInput = nil
4339 }
4340 }
43324341 private var flatListRows: [ObjectIdentifier: [String: RenderedNode]] = [:]
43334342 private var flatListOwners: [String: String] = [:]
43344343 /// A drawing's shapes are not views: a change to one redraws its `Svg`.
@@ -6051,6 +6060,11 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
60516060 private func restoreFocus(_ focused: UIView?) {
60526061 guard let focused, focused.window != nil, !focused.isFirstResponder,
60536062 lastFocusedInput === focused, firstResponder(in: rootStack) == nil else { return }
6063 var ancestor: UIView? = focused
6064 while let view = ancestor {
6065 if view.isHidden { return }
6066 ancestor = view.superview
6067 }
60546068 // A delayed acknowledgement belongs to the input that scheduled it.
60556069 // A newer field or an explicit dismissal owns focus now.
60566070 _ = focused.becomeFirstResponder()