also looking at this
fix(mobile): preserve native editing, navigation, and durable media requests
#369
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.
| @@ -1915,6 +1915,42 @@ final class NativeRenderUnitTests: XCTestCase { | ||
| 1915 | 1915 | XCTAssertEqual(proof.font?.pointSize, 22) |
| 1916 | 1916 | } |
| 1917 | 1917 | |
| 1918 | func testRetainedRowButtonStillDispatchesAfterItsPreviousSiblingIsRemoved() throws { | |
| 1919 | let controller = CraftNativeScreenController(config: CraftConfig()) | |
| 1920 | controller.loadViewIfNeeded() | |
| 1921 | controller.evaluateScript("globalThis.__rowPress = ''; globalThis.__stxNativeCallback = function(json) { const message = JSON.parse(json); if (message.type === 'EVENT') globalThis.__rowPress = message.payload.handlerName; };") | |
| 1922 | func row(_ id: String) -> [String: Any] { | |
| 1923 | ["id": "row-\(id)", "type": "View", "props": ["key": id], "children": [ | |
| 1924 | ["id": "input-\(id)", "type": "TextInput", "props": ["key": "input-\(id)", "defaultValue": "Draft"]], | |
| 1925 | ["id": "button-\(id)", "type": "Button", "props": ["key": "button-\(id)", "title": "Select row"], "events": ["onPress": "select-\(id)"]], | |
| 1926 | ]] | |
| 1927 | } | |
| 1928 | controller.render(document([row("a"), row("b")])) | |
| 1929 | let button = try XCTUnwrap(find(UIButton.self, key: "button-b", below: controller.view)) | |
| 1930 | controller.render(document([row("b"), row("c")])) | |
| 1931 | XCTAssertTrue(button === find(UIButton.self, key: "button-b", below: controller.view)) | |
| 1932 | button.sendActions(for: .touchUpInside) | |
| 1933 | XCTAssertEqual(controller.evaluateScript("__rowPress")?.toString(), "select-b") | |
| 1934 | } | |
| 1935 | ||
| 1936 | func testScrollCanCancelButtonTrackingWithoutTakingSliderOrInputTracking() throws { | |
| 1937 | let controller = CraftNativeScreenController(config: CraftConfig()) | |
| 1938 | controller.loadViewIfNeeded() | |
| 1939 | controller.render(document([[ | |
| 1940 | "type": "ScrollView", "props": ["key": "form"], "style": ["height": 300], "children": [ | |
| 1941 | ["type": "Button", "props": ["key": "action", "title": "Save"]], | |
| 1942 | ["type": "Slider", "props": ["key": "position", "value": 0.5]], | |
| 1943 | ["type": "Switch", "props": ["key": "toggle", "value": false]], | |
| 1944 | ["type": "TextInput", "props": ["key": "draft", "value": "Draft"]], | |
| 1945 | ], | |
| 1946 | ]])) | |
| 1947 | let form = try XCTUnwrap(find(UIScrollView.self, key: "form", below: controller.view)) | |
| 1948 | XCTAssertTrue(form.touchesShouldCancel(in: try XCTUnwrap(find(UIButton.self, key: "action", below: controller.view)))) | |
| 1949 | XCTAssertFalse(form.touchesShouldCancel(in: try XCTUnwrap(find(UISlider.self, key: "position", below: controller.view)))) | |
| 1950 | XCTAssertFalse(form.touchesShouldCancel(in: try XCTUnwrap(find(UISwitch.self, key: "toggle", below: controller.view)))) | |
| 1951 | XCTAssertFalse(form.touchesShouldCancel(in: try XCTUnwrap(find(UITextField.self, key: "draft", below: controller.view)))) | |
| 1952 | } | |
| 1953 | ||
| 1918 | 1954 | func testScrollKeyboardTapPreservesTextAreaFocusAndSelectionButDismissesBackground() throws { |
| 1919 | 1955 | let controller = CraftNativeScreenController(config: CraftConfig()) |
| 1920 | 1956 | let window = try hostWindow() |
| @@ -4010,6 +4010,10 @@ private final class CraftNativeScrollView: UIScrollView, UIGestureRecognizerDele | ||
| 4010 | 4010 | required init?(coder: NSCoder) { nil } |
| 4011 | 4011 | |
| 4012 | 4012 | override func touchesShouldCancel(in view: UIView) -> Bool { |
| 4013 | // A button tracks its tap immediately, but a drag that starts there | |
| 4014 | // still belongs to the list. Keep direct manipulation controls (a | |
| 4015 | // slider, switch or text field) on their native tracking path. | |
| 4016 | if view is UIButton { return true } | |
| 4013 | 4017 | if view is UIControl { return false } |
| 4014 | 4018 | return super.touchesShouldCancel(in: view) |
| 4015 | 4019 | } |
| @@ -263,7 +263,7 @@ describe('native renderer component contract', () => { | ||
| 263 | 263 | expect(android).toContain('checkboxChecked is Boolean -> checkboxChecked') |
| 264 | 264 | expect(navigationHome).toContain('<Checkbox testID="checkbox-checked-precedence" :checked="false" :value="true" />') |
| 265 | 265 | expect(androidNavigationTest).toContain('checked should take precedence over value') |
| 266 | expect(iosNavigationTest).toContain('app.buttons["checkbox-checked-precedence"].value as? String, "0"') | |
| 266 | expect(iosNavigationTest).toContain('app.buttons["checkbox-checked-precedence"].value as? String, "unchecked"') | |
| 267 | 267 | expect(guide).toContain('controlled values are present,') |
| 268 | 268 | expect(guide).toContain('`checked` takes precedence. It delivers') |
| 269 | 269 | }) |