Repository navigation
feat: support app-local Native UI Shell extensions - #276
Conversation
| matches.length !== 1 || | ||
| !visible(element) || | ||
| element.closest('ion-modal, ion-popover, ion-menu') || | ||
| element.contains(element.ownerDocument.activeElement) |
There was a problem hiding this comment.
🟡 スクロールすると固定タブアクセサリが消える
タブ内のソースを画面外へスクロールすると、可視判定で候補から外れます。タブバーが残っていても固定アクセサリが消えます。
Learn more
タブアクセサリは元の DOM 要素の座標ではなく、所属するネイティブタブホストに描画されます。現在の visible(element) は画面内に完全に収まることを要求します(visible)。スクロールでソースが画面外になると候補が消え、read が次のスナップショットからアクセサリを除外します。
Example: ion-tabs 内のスクロール可能なページ上部にソースを置きます。下へスクロールしてソースを画面外にすると、固定タブバーは残るのにアクセサリが消えます。
Recommended fix: tab-accessory ではソースの画面内判定を適用せず、接続状態・非表示状態・除外設定とタブホストの有効性を個別に確認してください。通常の element 配置には現在の厳密なクリッピング判定を維持してください。
Was this helpful? React with 👍 or 👎 to provide feedback.
| candidate.control.kind !== 'ion-back-button' || !isVerticalBarsCandidate(candidate.element) || candidate.element === back, | ||
| ); | ||
| return overlayVisible ? selected.filter((candidate) => isVerticalBarsCandidate(candidate.element)) : selected; | ||
| }; |
There was a problem hiding this comment.
🟡 トースト表示でタブアクセサリだけ消える
トースト表示中は既存のフィルタが縦バー候補だけを残します。アクセサリは縦バー候補ではないため、タブバーが残っても退避します。
(Refers to this code)
Learn more
overlayOpen はトーストやピッカーを縦バー保持オーバーレイとして扱い、overlayVisible が true の状態でもネイティブタブバーの候補を残します。アクセサリは通常の HTML 要素なので isVerticalBarsCandidate に該当せず、この末尾のフィルタにより除外されます。除外されたソースは次の同期で Web に戻り、ネイティブアクセサリは解除されます。
Example: 縦バーのタブに固定アクセサリを表示してトーストを開きます。タブバーは残りますがアクセサリだけ消え、閉じると再表示されます。
Recommended fix: 縦バー保持オーバーレイ時には、保持されるネイティブタブバーに所属する tab-accessory 候補も最終フィルタで残してください。モーダルなど、縦バーも退避させるオーバーレイの挙動は変えないでください。
Was this helpful? React with 👍 or 👎 to provide feedback.
Playwright test resultsDetails
Flaky testschromium › native-ui-shell.spec.ts › Vertical Bars popover retains native controls and background geometry (full shell) |
|
📊 Ionic 9 Playwright Test Report View the detailed Ionic 9 report: https://rdlabo-dev.github.io/ionic-theme-ios27/pr-276/ Ionic 8 runs against the same screenshots in a separate matrix job. View both results in the workflow run. |
|
/update-screenshots |
|
✅ Screenshots have been updated successfully! The new screenshots have been committed to this PR. |
|
Thank you for taking the extension path in #276 / #277, and for the Now Playing sample. Sorry we were slow to come back after your comment on #248 — by then you had already landed this, which was the right call. We moved the mini-player into the app and it works really well. Default We pin export const miniPlayer = defineNativeUI({
name: 'app.mini-player',
selector: '[data-mini-player]',
read(element) {
if (element.closest('ion-modal, ion-popover, ion-menu')) return null;
return {
props: {
title: element.getAttribute('data-title') ?? '',
subtitle: element.getAttribute('data-subtitle') ?? '',
playing: element.getAttribute('data-playing') === 'true',
progress: finiteProgress(element.getAttribute('data-progress')),
artwork: artworkOf(element),
},
actions: {
open: element.querySelector('[data-mini-player-open]'),
toggle: element.querySelector('[data-mini-player-toggle]'),
},
};
},
});
await enableNativeUIShell({
controls: { tabs: true, segment: true, toolbar: true, fab: false },
extensions: [miniPlayer],
});try NativeUIShell.register("app.mini-player") { (props: MiniPlayerProps, actions: NativeUIActions) in
MiniPlayerView(props: props, actions: actions)
}A few things we handled in the app, in case they help other hosts — no change needed from you:
If you ever want library follow-ups, the two that would have saved us work are: treat transform inside the extension source as visible for its actions, and keep extensions up until I am very grateful for the plugins, and for turning this into something apps can own. Thank you very much!! |
Summary
Applications can now define Native UI Shell projections inside their own project, using a local
defineNativeUI()DOM reader and a named, typed SwiftUI renderer registered withNativeUIShell.register(). No separate npm package, library, or Capacitor plugin is needed.The Web element remains the source of truth. The shell synchronizes JSON props, dispatches native actions to the original Web targets, and owns projection acknowledgement, suspension, fallback, and teardown.
NativeUIActions.activate()is async and completes after Web click dispatch; rejection, cancellation, retirement, and bridge timeouts settle pending actions.elementplacement follows the source frame. Sources clipped by overflow ancestors, including Ionic shadow scroll containers, remain on the Web until fully visible.tab-accessoryplacement uses the system SwiftUI tab host. It currently requires Vertical Bars and iOS 26.1+; unsupported or ambiguous hosts keep the Web fallback.Review
Supervisor and independent acceptance review cycles passed after fixing the independent review's clipping-boundary finding. Tests cover API contracts, ownership, fallback, disabled-action revalidation, async settlement, controller containment, and scroll recovery.
Validation