Conversation
| for (const child of Array.from(node.children)) collect(child); | ||
| }; | ||
| content.forEach(collect); | ||
| for (const element of content) destination.append(destination.ownerDocument.adoptNode(element)); |
There was a problem hiding this comment.
🟡 モーダル内の外部フォームが送信できない
モーダル内の入力や送信ボタンが外側のフォームを form 属性で参照すると、別 Document への移動で関連が切れます。送信しても元のフォームのハンドラは動きません。
Learn more
HTML の form="id" は同じ Document 内のフォームとの関連を作ります。リレーはモーダルのコンテンツだけを子 WebView の Document に移し、元の Document にある外部フォームは移しません。したがって、form 属性付きボタンや入力はフォームの構成要素でなくなります。既存の フォーム送信の契約 と異なり、submit ハンドラも呼ばれません。
Example: 元の画面の <form id="edit" onsubmit="..."> をモーダル内の <ion-button type="submit" form="edit">保存</ion-button> から送信する場合、リレー後はボタンとフォームが異なる Document にあるため送信されません。
Recommended fix: 同一 Document の外部フォームに依存するコンテンツはリレー対象から除外するか、フォームとの関連および送信を元の Document に明示的に中継してください。
Was this helpful? React with 👍 or 👎 to provide feedback.
| const autoHeight = | ||
| overlay.localName === 'ion-modal' && doc.defaultView!.getComputedStyle(overlay).getPropertyValue('--height').trim() === 'auto'; | ||
| if (overlay.localName !== 'ion-modal' || overlay.breakpoints?.length || autoHeight) { | ||
| opening = opening.then(restoreWeb).catch(console.error); |
There was a problem hiding this comment.
🟡 別のオーバーレイが開いてもモーダル画面がすぐに退かない
モーダルのネイティブ表示を準備中に別のオーバーレイを開くと、復元は準備完了まで待機します。その間、後から開いたオーバーレイはネイティブ画面に覆われます。
Learn more
opening はネイティブブリッジの prepareOverlay と presentOverlay を待つ直列キューです。未対応オーバーレイの WillPresent で restoreWeb をその末尾に追加しても、進行中の connect は中断されません。進行中のオープンが完了するまではネイティブ画面が上に残り、後続の Ionic オーバーレイを隠します。
Example: モーダルの表示準備中に ionAlertWillPresent が発火すると、Alert が開き始めても presentOverlay の応答が来るまでモーダルのネイティブ画面は閉じません。
Recommended fix: 未対応オーバーレイの発生を同期的な世代番号やキャンセルトークンで記録し、connect の各ブリッジ応答後に確認して表示を取り消してください。必要なら既に開いた画面を直ちに閉じる処理も行ってください。
Was this helpful? React with 👍 or 👎 to provide feedback.
| original = source.uiDelegate | ||
| automaticallyOpensWindows = source.configuration.preferences.javaScriptCanOpenWindowsAutomatically | ||
| super.init() | ||
| source.configuration.preferences.javaScriptCanOpenWindowsAutomatically = true |
| guard let id = prepared, navigationAction.targetFrame == nil, | ||
| navigationAction.request.url?.absoluteString == "about:blank" else { | ||
| return original?.webView?(webView, createWebViewWith: configuration, | ||
| for: navigationAction, windowFeatures: windowFeatures) | ||
| } | ||
| prepared = nil |
Playwright test resultsDetails
Failed testschromium › native-ui-shell.spec.ts › native modal preparation retains the covered projection and releases it on fallback |
|
📊 Ionic 9 Playwright Test Report View the detailed Ionic 9 report: https://rdlabo-dev.github.io/ionic-theme-ios27/pr-260/ Ionic 8 runs against the same screenshots in a separate matrix job. View both results in the workflow run. |
WebKit percent-encodes the '#' marker in the about:blank popup URL, so the window interception never matched and every relay silently fell back to Web rendering. Compare the decoded URL on both sides of the bridge. - Release the relay when an Ionic menu opens so the menu is never hidden beneath the native overlay. - Bound bridge, lifecycle, layout and child-load waits so a stalled presentation restores Web ownership instead of hanging the opening queue; a timed-out early wait now also releases the projection retention through the shared release path. - Consume the modal handoff flag only when a sync actually runs, so a retained sync cannot eat the instant transition needed by the deferred retirement after the overlay releases. - Mark the overlay host view as modal for accessibility so covered page projections stay out of VoiceOver while they remain projected. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- While a relayed overlay is up, covered page controls stay projected but obscured; the XCTest now asserts that state and taps the relayed button through a screen coordinate (the hosted WebView's content is not hit-testable by XCTest). The Web fallback path keeps its original assertions. - A disabled ion-buttons group keeps its direct children projected individually, so the stale nonexistence assertions are corrected. - Add an e2e guardrail: opening an Ionic menu restores a relayed modal to the source WebView. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The relay previously suppressed Ionic's animation and then replayed a second UIKit presentation, leaving a dead gap of a second or more between the tap and any visible response. Keep the Web enter animation visible while the child window stages underneath it, freeze the rendered overlay at didPresent, and present the native host instantly over identical pixels. The placeholder lifts once the hosted document paints, so the swap is imperceptible and perceived startup matches ordinary Ionic. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
On iOS 26/27 the anchor itself becomes the popover surface, so a separate bubble beside the pill read as a z-order bug: the glass capsule bled the popover edge through and SwiftUI's .popover(item:) drew rail-anchored popovers below the toolbar layer. Grow a window-level surface out of the projected control instead, collapse it back on dismissal, and drop the SwiftUI popover path that could no longer be reached. When the projected view is gone the page's measured anchor rect still drives a plain UIKit popover. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The relay kept every projection alive for the overlay's lifetime, so a modal's enter animation ran under the still-visible native pills. Only popovers need that retention — the page stays visible and the morph needs its anchor — while modals and alerts should hide covered controls through the same willPresent retirement the Web path always used. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A card modal was presented with a .large() detent, which fills the iPhone screen and made it indistinguishable from a normal modal. The card now uses a custom detent below the maximum height, sized from a topInset the page measures with the Ionic card formula (max(30, safe-area-top) + 10), so the shrunken presenting page peeks above the sheet again. Every relayed popover also played two animations at once: the Ionic Web enter ran first and the native surface swapped in afterwards, which flickered and re-laid out mid-flight. The Web enter is now hidden for all relayed popovers - anchored ones morph out of the projected control and the rest present with UIKit's own popover animation - and no source snapshot is taken because nothing is ever revealed. The anchored morph itself reads more like the button unfolding now: the content is revealed by the clip instead of fading in separately, and on dismissal the surface stays opaque while it shrinks back into the capsule rather than vanishing mid-collapse. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
UIKit keeps the popover chrome unresolved when the surface presents before its transition view completes a first render pass — most visible on the very first popover after launch (flat pill, no shadow). Walk to the popover container during host layout and pin the same shadow the anchored morph surface uses so every presentation renders identically. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Ionic's disconnectedCallback does not dismiss a presented overlay, so a popover removed or page-hidden without ionPopoverDidDismiss kept its retain() lease forever. sync() then returned early on every later update and the last control snapshot stayed frozen on screen — stale glass pills stacking over subsequent pages and their modals. Watch overlay ancestors for detach and exclusion (ion-page-hidden / ion-page-invisible) and release the connection. Also run releaseProjection before child-window teardown so a throwing close() cannot skip it, and cover the dismiss-then-navigate path with a UI test. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
A dismissed UIKit popover could leave retentions stuck at 1 and freeze every later control sync: moveContent restore threw NotAllowedError when reassigning adoptedStyleSheets (WebKit re-associates the sheets with the new document across adoptNode), which aborted cleanup before the finally block that releases the projection lease and closes the native host — so the next overlay prepare call also rejected. Run every teardown step through an isolated, bounded stage so a throwing or hanging step cannot skip the release, and fall back to an inline style copy when a shadow root adopted sheets no longer match its document. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
UIKit's default popover chrome draws a ~15pt corner while the theme spec and the anchored morph surface are 34pt — the fallback read visibly squarer than both the SwiftUI idiom and our own morph. ShellPopoverBackgroundView (a public UIPopoverBackgroundView subclass registered via popoverBackgroundViewClass) now draws the silhouette: 34pt rounded body + arrow wedge, a UIGlassEffect surface masked by the shape path, and the same 0.18/24/(0,8) shadow the morph pins. The host clips the relayed document to the same radius so its square corners stay inside the shape. First-launch shadow stability now comes from the background view's own shadowPath, replacing the private-hierarchy pinning entirely. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…utton A grouped toolbar button's visible pill is the ion-buttons backdrop-filter capsule, one padding ring wider than the projected UIButton. Growing the morph surface from the inner button left the Web capsule's rim peeking out of the surface's rounded corner. Send the capsule rect as the morph's start frame and hide the capsule while the surface is up, restoring it after native dismissal. Fall back to the projected view's frame when no anchor rect is provided. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
Add opt-in overlay content relay above Native UI Shell.
controls.modal,controls.popover, andcontrols.alerteach default tofalse; applications enable only the components they need.Ionic keeps its overlay host, controller registry, lifecycle, application state, and dismissal handlers. Existing live content moves into a native-hosted WebView through a shared relay layer for styles, focus, restoration, and cleanup. Covered page controls remain natively projected while the relay is active.
sourceViewandsourceRect. Vertical Bars uses SwiftUI's standard toolbar popover. Ionic supplies initial content size/background; the native presentation owns its arrow, corners, outline, and transition. Popover chrome follows the Web theme's dark/light appearance.UIAlertController.The relay waits for the child WebView's blank navigation before adopting content, preserving handlers across initialization and repeated presentation. Component-specific helpers share the controller and dismissal primitives. Demo examples cover inline/compact/scrollable Popovers and an Alert input callback.
Preview limitations
--height: automodals stay on the Web. Native presentation follows Ionic'sdidPresent; custom Web animations do not replace native transitions.Validation
No package version change. Toolbar geometry investigation is separate from this PR.