Handle requests for new windows by proposing a new visit#225
Merged
Conversation
Changes default Hotwire Native behaviour to automatically start a new visit to the requested url when the WKUIDelegate receives a request to create a new web view for a navigation. The motivation here was to handle links requesting new windows that are embedded in an iframe seamlessly through HotwireNative navigation, rather than discarding them. Previously iOS clients would have to subclass WKUIController and implement their own handling of the WKUIDelegate method func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? The new behaviour proposed here matches the implementation in hotwire-native-android in HotwireWebChromeClient https://github.com/hotwired/hotwire-native-android/blob/588d69c124f2baf7fe605c91fd96d67896e6b534/core/src/main/kotlin/dev/hotwire/core/turbo/webview/HotwireWebChromeClient.kt#L74 We attempt to filter out common blank urls before calling the delegate method. The Android implementation avoids this step by using requestFocusNodeHref. If Navigator is not used, the default implementation of the new delegate method does nothing, so this is a non-breaking change.
jayohms
reviewed
Feb 4, 2026
jayohms
left a comment
Contributor
There was a problem hiding this comment.
Thank you @zoejessica! Left one question...
This reverts commit 4f4af7a.
Implement the WKUIDelegate createWebViewWith method so that a new-window request originating from a non-main frame (e.g. a link inside an iframe embed) is loaded into the web view instead of being discarded. This lets taps inside an embed route out of the iframe — for example launching the YouTube app from an embedded video. Requests with no URL, or those targeting the main frame, are ignored.
Add a configurable set of invalid schemes (about, data, file, javascript) to ExternalNavigationWebViewPolicyDecisionHandler. When a matching navigation carries one of these schemes, cancel it without routing rather than proposing an external visit. This guards against routing blank or unsafe URLs — such as those produced when a new-window request from an iframe is loaded into the web view.
Add tests covering the new external-navigation behaviour: about: and data: schemes cancel without routing, and a form submission is not treated as external navigation.
* main: (37 commits) Add tests for always-on visit retry handler Fix inaccurate log messages in Navigator session inspection Set `lazyLoadTabs` to `true` by default. Split HotwireLogger into focused files under Logging/ Move and expand custom logger tests Remove obsolete logger. Enable debug logging automatically for debug builds in the Demo app. Remove obsolete `Logging` in favor of `HotwireLogger`. Allow injection of a custom logger Make redirect-resolution timeout configurable Add ScriptMessage.isReady accessor Consolidate Turbo.js status codes into a TurboError type. Use `HotwireNativeError` instead of `Error`. Fix refresh_historical_location restoring from snapshot cache Log HTTP status code on cold boot visit failures Add logging to JSFetch recovery handler and error paths Retry failed visit directly when no topmost visitable Remove isRetryable logic and always provide clients with a retry handler Update tests Rename to JSFetchRecoveryHandler ...
The port to WebViewPolicyNavigationSimulator dropped changes that main had made to WebViewNavigationSimulator, because the file was renamed. Reinstate them: - Cancel the captured navigation (decisionHandler(.cancel)) once the continuation resumes, rather than allowing it. Allowing it lets the web view navigate to external URLs, blocking the shared WKProcessPool and stalling subsequent tests. Intermediate loads are still allowed so the page can finish loading and trigger the simulated interaction. - Add stopLoading() to stop the web view and detach its delegate, now called from test tearDown. The initial about:blank document load is allowed (not cancelled) so page-initiated navigations can still be captured.
svara
approved these changes
Jul 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes default Hotwire Native behaviour to automatically start a new visit to the requested url when the
WKUIDelegatereceives a request to create a new web view for a navigation.The motivation here was to handle links requesting new windows that are embedded in an iframe seamlessly through HotwireNative navigation, rather than discarding them. For example, routing the links in a Youtube embed out to the native Youtube app. Previously, iOS clients would have to subclass
WKUIControllerand implement their own handling of thisWKUIDelegatemethod:The new behaviour proposed here matches the implementation in hotwire-native-android in
HotwireWebChromeClient.We attempt to filter out common blank urls before calling the delegate method. The Android implementation avoids this step by using
requestFocusNodeHref, which isn't available to us.If
Navigatoris not used, the default implementation of the new delegate method does nothing, so this is a non-breaking change.