@webview-native-ui/engine 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/WebViewNativeUIControls.podspec +17 -0
- package/dist/controls/activation.d.ts +15 -0
- package/dist/controls/activation.js +370 -0
- package/dist/controls/components/index.d.ts +16 -0
- package/dist/controls/components/index.js +38 -0
- package/dist/controls/components/ion-back-button.d.ts +4 -0
- package/dist/controls/components/ion-back-button.js +16 -0
- package/dist/controls/components/ion-button.d.ts +4 -0
- package/dist/controls/components/ion-button.js +26 -0
- package/dist/controls/components/ion-buttons.d.ts +5 -0
- package/dist/controls/components/ion-buttons.js +34 -0
- package/dist/controls/components/ion-fab.d.ts +4 -0
- package/dist/controls/components/ion-fab.js +164 -0
- package/dist/controls/components/ion-menu-button.d.ts +7 -0
- package/dist/controls/components/ion-menu-button.js +41 -0
- package/dist/controls/components/ion-segment.d.ts +5 -0
- package/dist/controls/components/ion-segment.js +24 -0
- package/dist/controls/components/ion-tab-bar.d.ts +5 -0
- package/dist/controls/components/ion-tab-bar.js +49 -0
- package/dist/controls/components/searchable-tabs.d.ts +20 -0
- package/dist/controls/components/searchable-tabs.js +525 -0
- package/dist/controls/definitions.d.ts +360 -0
- package/dist/controls/definitions.js +1 -0
- package/dist/controls/extensions.d.ts +44 -0
- package/dist/controls/extensions.js +164 -0
- package/dist/controls/foldable/fold-state.d.ts +4 -0
- package/dist/controls/foldable/fold-state.js +8 -0
- package/dist/controls/foldable/types.d.ts +9 -0
- package/dist/controls/foldable/types.js +1 -0
- package/dist/controls/html.d.ts +8 -0
- package/dist/controls/html.js +260 -0
- package/dist/controls/index.d.ts +7 -0
- package/dist/controls/index.js +4 -0
- package/dist/controls/ionic.d.ts +1 -0
- package/dist/controls/ionic.js +1 -0
- package/dist/controls/legacy-layout.d.ts +7 -0
- package/dist/controls/legacy-layout.js +23 -0
- package/dist/controls/overlays/content.d.ts +2 -0
- package/dist/controls/overlays/content.js +55 -0
- package/dist/controls/overlays/controller.d.ts +7 -0
- package/dist/controls/overlays/controller.js +369 -0
- package/dist/controls/overlays/covered-overlay.d.ts +4 -0
- package/dist/controls/overlays/covered-overlay.js +9 -0
- package/dist/controls/overlays/dialog-animation.d.ts +7 -0
- package/dist/controls/overlays/dialog-animation.js +49 -0
- package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
- package/dist/controls/overlays/dismiss-animation.js +14 -0
- package/dist/controls/overlays/focus.d.ts +2 -0
- package/dist/controls/overlays/focus.js +67 -0
- package/dist/controls/overlays/popover.d.ts +10 -0
- package/dist/controls/overlays/popover.js +117 -0
- package/dist/controls/overlays/styles.d.ts +9 -0
- package/dist/controls/overlays/styles.js +194 -0
- package/dist/controls/prehide.d.ts +6 -0
- package/dist/controls/prehide.js +280 -0
- package/dist/controls/runtime.d.ts +5 -0
- package/dist/controls/runtime.js +1685 -0
- package/dist/controls/shared/bridge.d.ts +4 -0
- package/dist/controls/shared/bridge.js +15 -0
- package/dist/controls/shared/candidate.d.ts +23 -0
- package/dist/controls/shared/candidate.js +128 -0
- package/dist/controls/shared/clipping.d.ts +1 -0
- package/dist/controls/shared/clipping.js +1 -0
- package/dist/controls/shared/connection.d.ts +21 -0
- package/dist/controls/shared/connection.js +112 -0
- package/dist/controls/shared/control-options.d.ts +4 -0
- package/dist/controls/shared/control-options.js +14 -0
- package/dist/controls/shared/covered-snapshot.d.ts +35 -0
- package/dist/controls/shared/covered-snapshot.js +41 -0
- package/dist/controls/shared/covered-source.d.ts +22 -0
- package/dist/controls/shared/covered-source.js +72 -0
- package/dist/controls/shared/crossfade.d.ts +11 -0
- package/dist/controls/shared/crossfade.js +71 -0
- package/dist/controls/shared/diagnostics.d.ts +2 -0
- package/dist/controls/shared/diagnostics.js +16 -0
- package/dist/controls/shared/dom.d.ts +45 -0
- package/dist/controls/shared/dom.js +258 -0
- package/dist/controls/shared/events.d.ts +12 -0
- package/dist/controls/shared/events.js +13 -0
- package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
- package/dist/controls/shared/horizontal-navigation.js +90 -0
- package/dist/controls/shared/icons.d.ts +5 -0
- package/dist/controls/shared/icons.js +66 -0
- package/dist/controls/shared/modal-animation.d.ts +21 -0
- package/dist/controls/shared/modal-animation.js +97 -0
- package/dist/controls/shared/modal.d.ts +26 -0
- package/dist/controls/shared/modal.js +261 -0
- package/dist/controls/shared/projection-id.d.ts +2 -0
- package/dist/controls/shared/projection-id.js +2 -0
- package/dist/controls/shared/scroll-placement.d.ts +1 -0
- package/dist/controls/shared/scroll-placement.js +1 -0
- package/dist/controls/shared/tab-bar-width.d.ts +7 -0
- package/dist/controls/shared/tab-bar-width.js +72 -0
- package/dist/controls/shared/toolbar-options.d.ts +10 -0
- package/dist/controls/shared/toolbar-options.js +38 -0
- package/dist/controls/shared/transition-identity.d.ts +3 -0
- package/dist/controls/shared/transition-identity.js +64 -0
- package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
- package/dist/controls/shared/vertical-bars-geometry.js +82 -0
- package/dist/controls/shared/vertical-navigation.d.ts +5 -0
- package/dist/controls/shared/vertical-navigation.js +39 -0
- package/dist/controls/startup.d.ts +7 -0
- package/dist/controls/startup.js +72 -0
- package/dist/controls/transport.d.ts +10 -0
- package/dist/controls/transport.js +1 -0
- package/dist/controls/vertical-bars-web-search.d.ts +8 -0
- package/dist/controls/vertical-bars-web-search.js +83 -0
- package/dist/controls/vertical-bars-web.d.ts +2 -0
- package/dist/controls/vertical-bars-web.js +609 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration/index.d.ts +36 -0
- package/dist/integration/index.js +25 -0
- package/dist/native/session.js +14 -8
- package/dist/projection/pause.d.ts +10 -0
- package/dist/projection/pause.js +58 -0
- package/dist/projection/styles.d.ts +1 -1
- package/dist/projection/styles.js +0 -1
- package/dist/projection/surfaces.js +2 -1
- package/dist/projection/web.js +39 -5
- package/dist/types.d.ts +5 -1
- package/docs/README.md +1 -0
- package/docs/configuration/README.md +3 -1
- package/docs/configuration/ionic-overlays.md +36 -2
- package/docs/configuration/pause-resume.md +51 -0
- package/docs/controls.md +39 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
- package/package.json +30 -5
- package/src/controls/activation.ts +427 -0
- package/src/controls/components/index.ts +45 -0
- package/src/controls/components/ion-back-button.ts +22 -0
- package/src/controls/components/ion-button.ts +28 -0
- package/src/controls/components/ion-buttons.ts +37 -0
- package/src/controls/components/ion-fab.ts +151 -0
- package/src/controls/components/ion-menu-button.ts +37 -0
- package/src/controls/components/ion-segment.ts +27 -0
- package/src/controls/components/ion-tab-bar.ts +55 -0
- package/src/controls/components/searchable-tabs.ts +534 -0
- package/src/controls/definitions.ts +338 -0
- package/src/controls/extensions.ts +198 -0
- package/src/controls/foldable/fold-state.ts +10 -0
- package/src/controls/foldable/types.ts +4 -0
- package/src/controls/html.ts +239 -0
- package/src/controls/index.ts +7 -0
- package/src/controls/ionic.ts +1 -0
- package/src/controls/legacy-layout.ts +21 -0
- package/src/controls/overlays/content.ts +51 -0
- package/src/controls/overlays/controller.ts +394 -0
- package/src/controls/overlays/covered-overlay.ts +11 -0
- package/src/controls/overlays/dialog-animation.ts +53 -0
- package/src/controls/overlays/dismiss-animation.ts +15 -0
- package/src/controls/overlays/focus.ts +64 -0
- package/src/controls/overlays/popover.ts +116 -0
- package/src/controls/overlays/styles.ts +195 -0
- package/src/controls/prehide.ts +285 -0
- package/src/controls/runtime.ts +1731 -0
- package/src/controls/shared/bridge.ts +18 -0
- package/src/controls/shared/candidate.ts +163 -0
- package/src/controls/shared/clipping.ts +1 -0
- package/src/controls/shared/connection.ts +105 -0
- package/src/controls/shared/control-options.ts +20 -0
- package/src/controls/shared/covered-snapshot.ts +52 -0
- package/src/controls/shared/covered-source.ts +78 -0
- package/src/controls/shared/crossfade.ts +70 -0
- package/src/controls/shared/diagnostics.ts +13 -0
- package/src/controls/shared/dom.ts +310 -0
- package/src/controls/shared/events.ts +14 -0
- package/src/controls/shared/horizontal-navigation.ts +106 -0
- package/src/controls/shared/icons.ts +64 -0
- package/src/controls/shared/modal-animation.ts +109 -0
- package/src/controls/shared/modal.ts +286 -0
- package/src/controls/shared/projection-id.ts +2 -0
- package/src/controls/shared/scroll-placement.ts +1 -0
- package/src/controls/shared/tab-bar-width.ts +66 -0
- package/src/controls/shared/toolbar-options.ts +51 -0
- package/src/controls/shared/transition-identity.ts +67 -0
- package/src/controls/shared/vertical-bars-geometry.ts +92 -0
- package/src/controls/shared/vertical-navigation.ts +34 -0
- package/src/controls/startup.ts +74 -0
- package/src/controls/transport.ts +11 -0
- package/src/controls/vertical-bars-web-search.ts +86 -0
- package/src/controls/vertical-bars-web.ts +674 -0
- package/src/dom/geometry.ts +24 -0
- package/src/dom/overlays.ts +196 -0
- package/src/dom/scroll-bindings.ts +66 -0
- package/src/dom/scroll-placement.ts +92 -0
- package/src/dom/visibility.ts +29 -0
- package/src/index.ts +19 -0
- package/src/integration/index.ts +64 -0
- package/src/ionic.ts +34 -0
- package/src/native/bootstrap.ts +132 -0
- package/src/native/bridge.ts +18 -0
- package/src/native/resource-url.ts +11 -0
- package/src/native/session.ts +708 -0
- package/src/native/updates.ts +103 -0
- package/src/native/values.ts +29 -0
- package/src/projection/events.ts +58 -0
- package/src/projection/pause.ts +58 -0
- package/src/projection/placement.ts +34 -0
- package/src/projection/scope.ts +13 -0
- package/src/projection/styles.ts +26 -0
- package/src/projection/surface.ts +88 -0
- package/src/projection/surfaces.ts +119 -0
- package/src/projection/web.ts +1013 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/// <reference lib="es2021.weakref" />
|
|
2
|
+
const searches = new WeakMap();
|
|
3
|
+
export const registerNativeSearch = (tabBar, trigger, footer) => {
|
|
4
|
+
const binding = { tabBar, trigger, footer, active: false, focused: false };
|
|
5
|
+
const entries = (searches.get(tabBar.ownerDocument) ?? []).filter((entry) => entry.deref() && entry.deref()?.footer !== footer);
|
|
6
|
+
entries.push(new WeakRef(binding));
|
|
7
|
+
searches.set(tabBar.ownerDocument, entries);
|
|
8
|
+
tabBar.ownerDocument.defaultView?.dispatchEvent(new Event('nativeUIRefresh'));
|
|
9
|
+
return binding;
|
|
10
|
+
};
|
|
11
|
+
export const getNativeSearchBindings = (doc) => (searches.get(doc) ?? []).flatMap((entry) => entry.deref() ?? []);
|
|
12
|
+
export const requestNativeSearch = async (binding, active, focus) => (await runtimes.get(binding.tabBar.ownerDocument)?.search?.(binding, active, focus)) ?? false;
|
|
13
|
+
const runtimes = new WeakMap();
|
|
14
|
+
export const VERTICAL_BARS_TRANSITION_CANCELED = 'iosThemeVerticalBarsTransitionCanceled';
|
|
15
|
+
export const setNativeUIIntegration = (doc, runtime) => {
|
|
16
|
+
if (runtime)
|
|
17
|
+
runtimes.set(doc, runtime);
|
|
18
|
+
else
|
|
19
|
+
runtimes.delete(doc);
|
|
20
|
+
};
|
|
21
|
+
export const hasNativeUIIntegration = (doc) => runtimes.has(doc);
|
|
22
|
+
export const isNativeUI = (element) => element.hasAttribute('data-native-ui-projected');
|
|
23
|
+
export const suspendNativeUI = async (scopes, options) => (await runtimes.get(scopes[0]?.ownerDocument)?.suspend(scopes, options)) ?? (() => { });
|
|
24
|
+
/** Shared geometry used by the theme's transition adapter; no theme import in the plugin. */
|
|
25
|
+
export const nativeWebViewMetrics = { radius: 0 };
|
package/dist/native/session.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { applyNativeValues } from './values.js';
|
|
2
2
|
import { projectionPlacement } from '../projection/placement.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getProjectionPause } from '../projection/pause.js';
|
|
4
4
|
import { readScrollPlacement } from '../dom/scroll-placement.js';
|
|
5
5
|
import { projectionAncestors } from '../dom/geometry.js';
|
|
6
6
|
import { createWebProjection } from '../projection/web.js';
|
|
@@ -19,8 +19,8 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
19
19
|
let ready = false;
|
|
20
20
|
let requestedDestination = 'native';
|
|
21
21
|
let destination = 'native';
|
|
22
|
-
const
|
|
23
|
-
const effectiveDestination = () => requestedDestination === 'native' &&
|
|
22
|
+
const projectionRequest = getProjectionPause(document);
|
|
23
|
+
const effectiveDestination = () => requestedDestination === 'native' && projectionRequest.isPaused(target) ? 'web' : requestedDestination;
|
|
24
24
|
let automaticMotion = false;
|
|
25
25
|
let nativeDragging = false;
|
|
26
26
|
let placementPending = false;
|
|
@@ -575,7 +575,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
575
575
|
applyDestination(effectiveDestination());
|
|
576
576
|
});
|
|
577
577
|
observer.observe(document.documentElement, { childList: true, attributes: true, subtree: true });
|
|
578
|
-
const applyDestination = (next, force
|
|
578
|
+
const applyDestination = (next, force) => {
|
|
579
579
|
if (stopped || (!force && next === destination))
|
|
580
580
|
return;
|
|
581
581
|
automaticMotion = false;
|
|
@@ -602,6 +602,9 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
602
602
|
});
|
|
603
603
|
}
|
|
604
604
|
else if (destination === 'web') {
|
|
605
|
+
// A native show starts a new local request; the screen-wide Web request prevents restoration.
|
|
606
|
+
if (force === 'native' && projectionRequest.isPaused())
|
|
607
|
+
manager?.end();
|
|
605
608
|
beginProjection();
|
|
606
609
|
if (!manager?.token)
|
|
607
610
|
void bridge.send({ action: 'visibility', visible: false }).catch(() => { });
|
|
@@ -614,7 +617,10 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
614
617
|
}
|
|
615
618
|
};
|
|
616
619
|
// Let a renderer claim a native child window during the same Ionic event dispatch.
|
|
617
|
-
const
|
|
620
|
+
const stopPause = projectionRequest.subscribe(() => queueMicrotask(() => {
|
|
621
|
+
if (ready)
|
|
622
|
+
applyDestination(effectiveDestination());
|
|
623
|
+
}));
|
|
618
624
|
let events;
|
|
619
625
|
const destroy = async () => {
|
|
620
626
|
if (stopped)
|
|
@@ -624,7 +630,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
624
630
|
retireValueRead();
|
|
625
631
|
clearPlacement();
|
|
626
632
|
generation++;
|
|
627
|
-
|
|
633
|
+
stopPause();
|
|
628
634
|
observer.disconnect();
|
|
629
635
|
sizeObserver.disconnect();
|
|
630
636
|
window.removeEventListener('resize', resized);
|
|
@@ -656,7 +662,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
656
662
|
placementPending = false;
|
|
657
663
|
requestedDestination = next;
|
|
658
664
|
void bridge.send({ action: 'destination', destination: next }).catch(() => { });
|
|
659
|
-
applyDestination(effectiveDestination(),
|
|
665
|
+
applyDestination(effectiveDestination(), next);
|
|
660
666
|
}
|
|
661
667
|
if (updated) {
|
|
662
668
|
void refreshValues();
|
|
@@ -677,7 +683,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
677
683
|
show: () => {
|
|
678
684
|
placementPending = false;
|
|
679
685
|
requestedDestination = 'native';
|
|
680
|
-
applyDestination(effectiveDestination(),
|
|
686
|
+
applyDestination(effectiveDestination(), 'native');
|
|
681
687
|
},
|
|
682
688
|
hide: () => {
|
|
683
689
|
requestedDestination = destination = 'none';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const getProjectionPause: (doc: Document) => {
|
|
2
|
+
isPaused: (target?: HTMLElement) => boolean;
|
|
3
|
+
subscribe(listener: () => void): () => void;
|
|
4
|
+
pause: () => void;
|
|
5
|
+
resume: () => void;
|
|
6
|
+
};
|
|
7
|
+
/** Request temporary Web projection across this document. Pair each call with resume(). */
|
|
8
|
+
export declare const pause: () => void;
|
|
9
|
+
/** Release one shared pause, regardless of caller; resume exactly once per pause(). */
|
|
10
|
+
export declare const resume: () => void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getOverlayCoordinator } from '../dom/overlays.js';
|
|
2
|
+
const key = Symbol.for('@webview-native-ui/engine/projection-pause/v1');
|
|
3
|
+
const createPause = (doc) => {
|
|
4
|
+
const overlays = getOverlayCoordinator(doc);
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
let depth = 0;
|
|
7
|
+
let releaseFrame = 0;
|
|
8
|
+
const paused = () => depth > 0 || releaseFrame !== 0;
|
|
9
|
+
const notify = () => {
|
|
10
|
+
for (const listener of listeners)
|
|
11
|
+
listener();
|
|
12
|
+
};
|
|
13
|
+
const change = (delta) => {
|
|
14
|
+
if (delta < 0 && depth === 0)
|
|
15
|
+
return;
|
|
16
|
+
const wasPaused = paused();
|
|
17
|
+
depth += delta;
|
|
18
|
+
const win = doc.defaultView;
|
|
19
|
+
if (releaseFrame && win) {
|
|
20
|
+
win.cancelAnimationFrame(releaseFrame);
|
|
21
|
+
releaseFrame = 0;
|
|
22
|
+
}
|
|
23
|
+
if (depth === 0 && win) {
|
|
24
|
+
// Give DOM dismissal a paint opportunity before requesting Native return.
|
|
25
|
+
// Keep all consumers paused meanwhile, including Ionic change callbacks.
|
|
26
|
+
releaseFrame = win.requestAnimationFrame(() => {
|
|
27
|
+
releaseFrame = win.requestAnimationFrame(() => {
|
|
28
|
+
releaseFrame = 0;
|
|
29
|
+
notify();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else if (wasPaused !== paused())
|
|
34
|
+
notify();
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
// Without a target, only the document-wide request applies; renderers own local coverage.
|
|
38
|
+
isPaused: (target) => paused() || (!!target && overlays.isCovered(target)),
|
|
39
|
+
subscribe(listener) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
const stopOverlays = overlays.subscribe(listener);
|
|
42
|
+
return () => {
|
|
43
|
+
listeners.delete(listener);
|
|
44
|
+
stopOverlays();
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
pause: () => change(1),
|
|
48
|
+
resume: () => change(-1),
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export const getProjectionPause = (doc) => {
|
|
52
|
+
const shared = doc;
|
|
53
|
+
return (shared[key] ??= createPause(doc));
|
|
54
|
+
};
|
|
55
|
+
/** Request temporary Web projection across this document. Pair each call with resume(). */
|
|
56
|
+
export const pause = () => getProjectionPause(document).pause();
|
|
57
|
+
/** Release one shared pause, regardless of caller; resume exactly once per pause(). */
|
|
58
|
+
export const resume = () => getProjectionPause(document).resume();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const snapshotStyles = "\n@layer native-ui-projection {\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]), :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::part(native) { visibility: hidden !important; }\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::before, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::after,\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *::before, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *::after {\n visibility: hidden !important;\n}\n}\n[data-native-ui-snapshot-visible]
|
|
1
|
+
export declare const snapshotStyles = "\n@layer native-ui-projection {\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]), :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::part(native) { visibility: hidden !important; }\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::before, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content])::after,\n:is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *::before, :is([data-native-ui-snapshot-source], [data-native-ui-snapshot-prepared], [data-native-ui-snapshot-content]) *::after {\n visibility: hidden !important;\n}\n}\n[data-native-ui-snapshot-visible][data-native-ui-snapshot-positioned] { position:relative!important; }\n";
|
|
2
2
|
export declare const snapshotShadowStyles = "\n@layer native-ui-projection {\n:host([data-native-ui-snapshot-content]),\n:host([data-native-ui-snapshot-content])::before,\n:host([data-native-ui-snapshot-content])::after,\n:host([data-native-ui-snapshot-content]) *,\n:host([data-native-ui-snapshot-content]) *::before,\n:host([data-native-ui-snapshot-content]) *::after {\n visibility: hidden !important;\n}\n}\n";
|
|
@@ -8,7 +8,6 @@ ${hidden} *::before, ${hidden} *::after {
|
|
|
8
8
|
visibility: hidden !important;
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
[data-native-ui-snapshot-visible] { opacity:1!important;overflow:visible!important; }
|
|
12
11
|
[data-native-ui-snapshot-visible][data-native-ui-snapshot-positioned] { position:relative!important; }
|
|
13
12
|
`;
|
|
14
13
|
export const snapshotShadowStyles = `
|
|
@@ -45,7 +45,8 @@ export const createWebProjectionSurfaces = (options = {}) => {
|
|
|
45
45
|
if (motionFrame || stopped || !moving.size || !doc?.defaultView)
|
|
46
46
|
return;
|
|
47
47
|
// Ancestor transforms already carry the image. Only the host's own motion needs rebasing.
|
|
48
|
-
const surfaces = values().filter((surface) => (surface.token || surface.restoring) &&
|
|
48
|
+
const surfaces = values().filter((surface) => (surface.token || surface.restoring) &&
|
|
49
|
+
(surface.restoring ? surface.visibleOwners : surface.owners).some((owner) => moving.has(target(owner))));
|
|
49
50
|
if (!surfaces.length)
|
|
50
51
|
return;
|
|
51
52
|
motionFrame = doc.defaultView.requestAnimationFrame(() => {
|
package/dist/projection/web.js
CHANGED
|
@@ -2,6 +2,7 @@ import { projectionURL } from '../native/resource-url.js';
|
|
|
2
2
|
import { bounded, boundedListener } from '../native/bridge.js';
|
|
3
3
|
import { bindProjectionEvents } from './events.js';
|
|
4
4
|
import { snapshotStyles, snapshotShadowStyles } from './styles.js';
|
|
5
|
+
import { getProjectionPause } from './pause.js';
|
|
5
6
|
const layout = ({ control: c }, positionIndependent = false) => [
|
|
6
7
|
c.id,
|
|
7
8
|
c.kind,
|
|
@@ -20,6 +21,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
20
21
|
throw new RangeError('captureDelayMs must be a finite, nonnegative timer interval');
|
|
21
22
|
let destination = 'native';
|
|
22
23
|
const doc = scope.element.ownerDocument;
|
|
24
|
+
const projectionRequest = getProjectionPause(doc);
|
|
25
|
+
let locallyRequested = false;
|
|
23
26
|
const win = doc.defaultView;
|
|
24
27
|
// Native tokens are local to a transport; never use them as global CSS identities.
|
|
25
28
|
const imageIdentity = Array.from(win.crypto.getRandomValues(new Uint32Array(4)), (value) => value.toString(16)).join('-');
|
|
@@ -324,6 +327,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
324
327
|
preparedVisualRevision = visual;
|
|
325
328
|
adopted = true;
|
|
326
329
|
dispose(previous);
|
|
330
|
+
if (projectionRequest.isPaused())
|
|
331
|
+
present();
|
|
327
332
|
}
|
|
328
333
|
catch {
|
|
329
334
|
// Preparing a visual is optional. Preserve the previous image or the Web fallback.
|
|
@@ -618,11 +623,11 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
618
623
|
const rect = element.getBoundingClientRect();
|
|
619
624
|
return { ...image.data.parts[index], x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
620
625
|
});
|
|
621
|
-
const
|
|
626
|
+
const present = (force = false) => {
|
|
622
627
|
retireRestoration();
|
|
623
|
-
if (stopped || destination === 'none' || active)
|
|
628
|
+
if (stopped || destination === 'none' || (active && !force))
|
|
624
629
|
return;
|
|
625
|
-
const image = cached;
|
|
630
|
+
const image = active ?? cached;
|
|
626
631
|
if (!image ||
|
|
627
632
|
image.viewport !== viewport() ||
|
|
628
633
|
!image.sources.every(validSource) ||
|
|
@@ -685,7 +690,12 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
685
690
|
paintFrame = 0;
|
|
686
691
|
if (!isCurrent())
|
|
687
692
|
return;
|
|
688
|
-
void call({ action: 'painted', token: image.data.token })
|
|
693
|
+
void call({ action: 'painted', token: image.data.token })
|
|
694
|
+
.then((result) => {
|
|
695
|
+
if (isCurrent() && result?.shown === false)
|
|
696
|
+
abandon(image, false);
|
|
697
|
+
})
|
|
698
|
+
.catch(() => {
|
|
689
699
|
if (isCurrent())
|
|
690
700
|
abandon(image, false);
|
|
691
701
|
});
|
|
@@ -697,7 +707,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
697
707
|
abandon(image, false);
|
|
698
708
|
});
|
|
699
709
|
};
|
|
700
|
-
const
|
|
710
|
+
const restore = () => {
|
|
701
711
|
if (!active || stopped || ending)
|
|
702
712
|
return;
|
|
703
713
|
// A return belongs to the captured surface, never to the modal exposed beneath it.
|
|
@@ -796,6 +806,25 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
796
806
|
win.dispatchEvent(new Event(sourcesChangedEvent));
|
|
797
807
|
}
|
|
798
808
|
};
|
|
809
|
+
const begin = () => {
|
|
810
|
+
const starting = !locallyRequested;
|
|
811
|
+
locallyRequested = true;
|
|
812
|
+
present(starting && projectionRequest.isPaused());
|
|
813
|
+
};
|
|
814
|
+
const end = () => {
|
|
815
|
+
locallyRequested = false;
|
|
816
|
+
if (!projectionRequest.isPaused())
|
|
817
|
+
restore();
|
|
818
|
+
};
|
|
819
|
+
const projectionChanged = () => queueMicrotask(() => {
|
|
820
|
+
if (stopped)
|
|
821
|
+
return;
|
|
822
|
+
if (projectionRequest.isPaused())
|
|
823
|
+
present();
|
|
824
|
+
else if (!locallyRequested)
|
|
825
|
+
restore();
|
|
826
|
+
callbacks.wake();
|
|
827
|
+
});
|
|
799
828
|
let eventListener;
|
|
800
829
|
try {
|
|
801
830
|
if (callbacks.events)
|
|
@@ -807,6 +836,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
807
836
|
// Native hide has already happened. Retire images and invalidate pending captures,
|
|
808
837
|
// without calling the plugin's public hide method or disturbing its fallback DOM.
|
|
809
838
|
if (destination === 'none') {
|
|
839
|
+
locallyRequested = false;
|
|
810
840
|
invalidate();
|
|
811
841
|
return;
|
|
812
842
|
}
|
|
@@ -814,6 +844,8 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
814
844
|
begin();
|
|
815
845
|
if (next === 'native') {
|
|
816
846
|
end();
|
|
847
|
+
if (projectionRequest.isPaused())
|
|
848
|
+
present(true);
|
|
817
849
|
schedule();
|
|
818
850
|
}
|
|
819
851
|
if (updated)
|
|
@@ -826,6 +858,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
826
858
|
await bounded(Promise.resolve(listener.remove())).catch(() => { });
|
|
827
859
|
throw error;
|
|
828
860
|
}
|
|
861
|
+
const stopPause = projectionRequest.subscribe(projectionChanged);
|
|
829
862
|
return {
|
|
830
863
|
get owners() {
|
|
831
864
|
return [...owners];
|
|
@@ -855,6 +888,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
855
888
|
invalidate,
|
|
856
889
|
async destroy() {
|
|
857
890
|
stopped = true;
|
|
891
|
+
stopPause();
|
|
858
892
|
invalidate();
|
|
859
893
|
imagePool.forEach((pool) => pool.forEach((image) => image.remove()));
|
|
860
894
|
imagePool.clear();
|
package/dist/types.d.ts
CHANGED
|
@@ -79,7 +79,11 @@ export type WebProjectionResult<Request extends WebProjectionRequest> = Request
|
|
|
79
79
|
action: 'cover' | 'restore';
|
|
80
80
|
} ? {
|
|
81
81
|
shown: boolean;
|
|
82
|
-
} :
|
|
82
|
+
} : Request extends {
|
|
83
|
+
action: 'painted';
|
|
84
|
+
} ? {
|
|
85
|
+
shown: boolean;
|
|
86
|
+
} | void : void;
|
|
83
87
|
export interface ListenerHandle {
|
|
84
88
|
remove(): Promise<void> | void;
|
|
85
89
|
}
|
package/docs/README.md
CHANGED
|
@@ -49,6 +49,7 @@ that require each option. Read only the relevant detail:
|
|
|
49
49
|
- [Value bindings](configuration/values.md): keep the fallback host's component data aligned with native state.
|
|
50
50
|
- [Capture timing](configuration/capture.md): prepare pixels after native appearance settles.
|
|
51
51
|
- [Ionic overlays](configuration/ionic-overlays.md): connect Ionic coverage with one shared application initializer.
|
|
52
|
+
- [Custom Web overlays](configuration/pause-resume.md): re-export pause/resume from a plugin so applications can bracket their own overlays.
|
|
52
53
|
|
|
53
54
|
## 4. Use advanced APIs when you own a renderer
|
|
54
55
|
|
|
@@ -24,6 +24,8 @@ same position: include that successful selection notification in `receiveListene
|
|
|
24
24
|
The Engine exposes [presentation intent](../reference/state.md) separately from
|
|
25
25
|
component values. Neither requires an application-side Engine wrapper. Ionic
|
|
26
26
|
overlay detection is a separate, opt-in [shared initializer](ionic-overlays.md),
|
|
27
|
-
not a registration argument
|
|
27
|
+
not a registration argument. For application-owned overlays, plugins can
|
|
28
|
+
[re-export pause/resume](pause-resume.md) so applications explicitly bracket their
|
|
29
|
+
overlays.
|
|
28
30
|
|
|
29
31
|
[Documentation overview](../README.md)
|
|
@@ -5,10 +5,17 @@ above Engine-managed native content. Native registration handles capture and
|
|
|
5
5
|
restoration, but it cannot infer Ionic's presentation lifecycle from a native
|
|
6
6
|
view. This shared monitor supplies that coverage decision for all registrations.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
[
|
|
8
|
+
Applications use this adapter with an Engine-enabled plugin. Plugin authors first
|
|
9
|
+
integrate through the [Capacitor](../getting-started/capacitor.md) or
|
|
10
|
+
[Cordova](../getting-started/cordova.md) guide. Non-Ionic applications use their own
|
|
10
11
|
lifecycle notifications and do not install this adapter.
|
|
11
12
|
|
|
13
|
+
The `/ionic` entry point provides shared framework integration. It observes Ionic
|
|
14
|
+
overlays and determines which registered surfaces they cover. A plugin's explicit
|
|
15
|
+
presentation controls remain part of that plugin's API, connected through
|
|
16
|
+
[projection notifications](events.md) or its existing
|
|
17
|
+
[renderer API](../advanced/renderer.md).
|
|
18
|
+
|
|
12
19
|
For example, opening an Ionic settings modal should cover the background native
|
|
13
20
|
tab bar while leaving the modal's own native close button usable. A blanket hide
|
|
14
21
|
notification cannot express that relationship. The shared monitor evaluates each
|
|
@@ -32,6 +39,10 @@ One monitor is shared per document, including between the npm module and the run
|
|
|
32
39
|
|
|
33
40
|
The default covers Modal, Popover, Alert, Action Sheet, Loading, Picker, Toast and Menu lifecycle events. It compares each projection's DOM with the active overlay stack: content inside the front modal stays native, while covered content temporarily uses its prepared image. Toasts and inactive sheet backdrops do not cover a surface. Nested overlays keep it covered until the remaining stack permits restoration. Missing images retain the normal Web fallback. This temporary coverage never overwrites a plugin's explicit `web` or `none` destination. Projections registered later read the current state.
|
|
34
41
|
|
|
42
|
+
Here, Alert means Ionic's `ion-alert`. This adapter does not intercept
|
|
43
|
+
`window.alert()`, detect HTML `<dialog>` automatically, or observe native dialogs.
|
|
44
|
+
Custom Web overlays can supply lifecycle events through `additional`, below.
|
|
45
|
+
|
|
35
46
|
A popover displayed in a native window is different from a popover rendered in
|
|
36
47
|
the WebView: its DOM placeholder must not be treated as a covering Web overlay.
|
|
37
48
|
The `@rdlabo/capacitor-native-ui` plugin reports this native presentation ownership
|
|
@@ -42,6 +53,8 @@ explicit integration; it is not inferred from the popover's DOM alone.
|
|
|
42
53
|
## Customize unusual layouts
|
|
43
54
|
|
|
44
55
|
Optional settings are document-wide. Keep the defaults for ordinary Ionic overlays.
|
|
56
|
+
`IonicOverlayOptions`, exported from the same `/ionic` entry point, is the TypeScript
|
|
57
|
+
type for these settings; it is not another initialization function.
|
|
45
58
|
Use `exclude` for an application overlay that should not cover native content, and
|
|
46
59
|
`additional` when the same app also uses a custom dialog with its own lifecycle
|
|
47
60
|
events. Use `covers` only when DOM containment and backdrop state do not describe
|
|
@@ -63,3 +76,24 @@ enableSupportIonicOverlays({
|
|
|
63
76
|
```
|
|
64
77
|
|
|
65
78
|
Use `covers(overlay, target)` only when a registered DOM projection needs a custom coverage rule; return `undefined` for the default, or a boolean to override it. Exclusions and explicit native-window ownership take precedence. The standard relationship uses overlay order, DOM containment and backdrop state, rather than attempting to interpret arbitrary CSS stacking contexts. Configure unusual layouts explicitly.
|
|
79
|
+
|
|
80
|
+
## When a plugin needs to know which modal is behind another
|
|
81
|
+
|
|
82
|
+
For ordinary application setup, use `enableSupportIonicOverlays()` as shown above.
|
|
83
|
+
The Engine then handles Ionic overlay coverage without additional calls.
|
|
84
|
+
|
|
85
|
+
A plugin that places native controls inside Ionic modals may also need to know
|
|
86
|
+
which modal lies behind the one being opened. Two helpers from the same `/ionic`
|
|
87
|
+
entry point let the plugin read the modal order already tracked by the Engine.
|
|
88
|
+
|
|
89
|
+
For example, open modal A, then modal B above it:
|
|
90
|
+
|
|
91
|
+
- `ionicModalStack(document)` returns `[A, B]`, from back to front.
|
|
92
|
+
- `coveredIonicSurface(B)` returns A, the modal beneath B.
|
|
93
|
+
- If a popover opens above B, `coveredIonicSurface(popover)` returns B.
|
|
94
|
+
- `coveredIonicSurface(A)` returns `undefined`: the page behind A is not a modal.
|
|
95
|
+
|
|
96
|
+
A and B here are the actual `ion-modal` DOM elements. These helpers only read the
|
|
97
|
+
tracked modal order; they do not open or close overlays, switch projection, or
|
|
98
|
+
start monitoring. Call them after `enableSupportIonicOverlays()` has enabled the
|
|
99
|
+
monitor and the relevant overlay has entered its presentation lifecycle.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Cover native UI with a custom Web overlay
|
|
2
|
+
|
|
3
|
+
Use `pause()` before presenting a custom modal or `div` over native UI and
|
|
4
|
+
`resume()` when it closes. For Ionic overlays, use the automatic
|
|
5
|
+
[Ionic integration](ionic-overlays.md), which shares this coordination but only
|
|
6
|
+
covers surfaces behind the overlay.
|
|
7
|
+
|
|
8
|
+
## Plugin authors: re-export the Engine functions
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
export { pause, resume } from '@webview-native-ui/engine';
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The functions share one pause count across Engine-enabled plugins and bundles in
|
|
15
|
+
the same document. No wrapper or native plugin method is needed.
|
|
16
|
+
|
|
17
|
+
## Application authors: pair each pause with a resume
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { pause, resume } from 'your-plugin';
|
|
21
|
+
|
|
22
|
+
pause();
|
|
23
|
+
try {
|
|
24
|
+
await showCustomModal();
|
|
25
|
+
} finally {
|
|
26
|
+
resume();
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`showCustomModal()` must resolve when the modal closes. With event-driven modals,
|
|
31
|
+
call `resume()` on dismissal, cancellation or failure. Use one pair per overlay,
|
|
32
|
+
even with multiple plugins; nested overlays each need their own pair.
|
|
33
|
+
|
|
34
|
+
A pause replaces visible native surfaces with prepared Web images or their normal
|
|
35
|
+
Web fallback. Hidden surfaces stay hidden. The final `resume()` releases
|
|
36
|
+
the request, preserving each plugin's presentation intent and remaining Ionic
|
|
37
|
+
overlay coverage.
|
|
38
|
+
|
|
39
|
+
The counter does not track callers or pair individual calls. An extra `resume()`
|
|
40
|
+
while paused can release another overlay's request; only a `resume()` at zero has
|
|
41
|
+
no effect. Each owner must release exactly once for each pause it requested.
|
|
42
|
+
|
|
43
|
+
Both functions return immediately, without waiting for native acknowledgments or
|
|
44
|
+
painting. Returning from `pause()` does not mean native UI has already moved
|
|
45
|
+
behind the Web overlay; it may remain in front until the handoff completes.
|
|
46
|
+
The final `resume()` defers native restoration until the second animation frame,
|
|
47
|
+
allowing overlay removal to paint without guaranteeing compositor completion.
|
|
48
|
+
Call it after removing the overlay or completing
|
|
49
|
+
its dismissal animation. A new `pause()` cancels a pending restoration.
|
|
50
|
+
Calling `pause()` just before a blocking `window.alert()` therefore does
|
|
51
|
+
not guarantee a completed handoff. They do not detect custom overlays automatically.
|
package/docs/controls.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Shared controls for native adapters
|
|
2
|
+
|
|
3
|
+
The existing Engine package also owns the control implementation used by Capacitor Native UI.
|
|
4
|
+
No additional npm package or workspace is required.
|
|
5
|
+
|
|
6
|
+
- `@webview-native-ui/engine`: generic projection API; unchanged dependency boundary.
|
|
7
|
+
- `@webview-native-ui/engine/controls`: control runtime and transport contract.
|
|
8
|
+
- `@webview-native-ui/engine/controls/ionic`: optional Ionic overlay implementation.
|
|
9
|
+
- `@webview-native-ui/engine/integration`: the single shared transition/search integration state.
|
|
10
|
+
|
|
11
|
+
An adapter creates one `createNativeUI({ plugin, getPlatform, isAvailable })` instance and keeps it
|
|
12
|
+
for its lifetime. The plugin implements the `NativeUIPlugin` transport interface. The adapter owns
|
|
13
|
+
host registration, method dispatch, listener delivery, and lifecycle hooks; Engine owns rendering,
|
|
14
|
+
DOM ownership, extension acknowledgement, and teardown sequencing. Cordova can supply this same
|
|
15
|
+
transport without importing Capacitor. The Cordova Native UI plugin itself is a subsequent change.
|
|
16
|
+
|
|
17
|
+
SwiftPM exposes the optional `WebViewNativeUIControls` product, depending on `WebViewNativeUIEngine`.
|
|
18
|
+
CocoaPods exposes `WebViewNativeUIControls` with the same dependency. Android exposes the optional
|
|
19
|
+
`io.github.webview-native-ui:controls` artifact from `android/controls`; it depends on `engine` and
|
|
20
|
+
Compose. Generic Engine consumers keep their existing dependencies and Android minimum SDK 23.
|
|
21
|
+
Controls require Android API 24. Native adapters create `NativeUIController` and forward requests
|
|
22
|
+
and events. iOS receives JSON dictionaries and completion closures; Android receives JSONObjects.
|
|
23
|
+
|
|
24
|
+
The Ionic theme imports Engine integration directly. The Capacitor compatibility integration export
|
|
25
|
+
re-exports that module. Resolve a single Engine version in the application; do not bundle a private
|
|
26
|
+
copy of integration into the theme or adapter. The `/controls` entry does not load Ionic runtime code;
|
|
27
|
+
load `/controls/ionic` only when opting into Ionic overlays.
|
|
28
|
+
|
|
29
|
+
## Verification and release order
|
|
30
|
+
|
|
31
|
+
Run `npm run test:controls` for the migrated control tests and `npm test` for generic projection.
|
|
32
|
+
SwiftPM's `WebViewNativeUIEngine-Package` scheme covers both native targets. Android's
|
|
33
|
+
`:controls:testReleaseUnitTest` covers the migrated snapshot parser. Existing Capacitor transport
|
|
34
|
+
and hosted lifecycle tests remain in its repository, and the theme retains the real demo E2E suite.
|
|
35
|
+
|
|
36
|
+
Build and verify Engine first, including both signed Maven artifacts in the same Central bundle and
|
|
37
|
+
both npm-distributed podspecs. Release the Engine native artifacts and npm package at the same version
|
|
38
|
+
before refreshing the Capacitor and theme registry lockfiles. Candidate verification uses local npm
|
|
39
|
+
tarballs and a temporary SwiftPM path or local Maven repository; do not commit these temporary paths.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
enum NativeUIButton {
|
|
4
|
+
private final class BadgeLabel: UILabel {}
|
|
5
|
+
private static let activationID = UIAction.Identifier("native-ui-activate")
|
|
6
|
+
|
|
7
|
+
/// Reuse an unambiguous presentation slot while keeping action IDs source-specific.
|
|
8
|
+
@MainActor
|
|
9
|
+
static func reassociate(_ nodes: [NativeUIControl], controls: inout [String: UIView],
|
|
10
|
+
fingerprints: inout [String: NativeUIControl]) -> Set<String> {
|
|
11
|
+
func identity(_ node: NativeUIControl) -> String? {
|
|
12
|
+
guard node.placement == nil, kinds.contains(node.kind), node.items.count == 1 else { return nil }
|
|
13
|
+
return node.items[0].content.transitionId
|
|
14
|
+
}
|
|
15
|
+
// swiftlint:disable:next force_unwrapping
|
|
16
|
+
let old = Dictionary(grouping: fingerprints.values.filter { identity($0) != nil }, by: { identity($0)! })
|
|
17
|
+
// swiftlint:disable:next force_unwrapping
|
|
18
|
+
let incoming = Dictionary(grouping: nodes.filter { identity($0) != nil }, by: { identity($0)! })
|
|
19
|
+
let retained = Set(nodes.map(\.id))
|
|
20
|
+
var reused = Set<String>()
|
|
21
|
+
for (key, matches) in incoming {
|
|
22
|
+
guard matches.count == 1, let node = matches.first, controls[node.id] == nil,
|
|
23
|
+
let previous = old[key], previous.count == 1, let source = previous.first,
|
|
24
|
+
!retained.contains(source.id), let button = controls[source.id] as? UIButton else { continue }
|
|
25
|
+
controls.removeValue(forKey: source.id)
|
|
26
|
+
fingerprints.removeValue(forKey: source.id)
|
|
27
|
+
controls[node.id] = button
|
|
28
|
+
reused.insert(node.id)
|
|
29
|
+
}
|
|
30
|
+
return reused
|
|
31
|
+
}
|
|
32
|
+
// These Ionic components share the same native UIButton presentation.
|
|
33
|
+
static let kinds: [NativeUIComponent] = [.button, .backButton, .menuButton]
|
|
34
|
+
|
|
35
|
+
@available(iOS 26.0, *)
|
|
36
|
+
static func make(_ node: NativeUIControl, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton? {
|
|
37
|
+
guard node.items.count == 1, let item = node.items.first else { return nil }
|
|
38
|
+
let control = render(item.content, glass: item.content.buttonFill != .clear, existing: existing, rendering: rendering, activate: activate)
|
|
39
|
+
control.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
40
|
+
return control
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@available(iOS 26.0, *)
|
|
44
|
+
static func render(_ item: NativeUIItemContent, glass: Bool, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton {
|
|
45
|
+
var configuration: UIButton.Configuration = glass ? .glass() : .plain()
|
|
46
|
+
configuration.title = item.label
|
|
47
|
+
configuration.image = rendering.image(item)
|
|
48
|
+
configuration.imagePadding = item.imagePadding ?? 4
|
|
49
|
+
configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: item.contentInsetLeading ?? 4,
|
|
50
|
+
bottom: 0, trailing: item.contentInsetTrailing ?? 4)
|
|
51
|
+
configuration.imagePlacement = item.iconPosition == .trailing ? .trailing : .leading
|
|
52
|
+
configuration.baseForegroundColor = rendering.color(item.color)
|
|
53
|
+
configuration.titleLineBreakMode = .byTruncatingTail
|
|
54
|
+
let size = item.fontSize
|
|
55
|
+
let weight = item.fontWeight
|
|
56
|
+
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
|
57
|
+
var outgoing = incoming
|
|
58
|
+
outgoing.font = UIFont.systemFont(ofSize: size, weight: weight >= 600 ? .semibold : weight >= 500 ? .medium : .regular)
|
|
59
|
+
return outgoing
|
|
60
|
+
}
|
|
61
|
+
let button = existing ?? UIButton(configuration: configuration)
|
|
62
|
+
if existing != nil { button.configuration = configuration }
|
|
63
|
+
button.titleLabel?.numberOfLines = 1
|
|
64
|
+
// Web and UIKit can differ by a fraction of a point in text measurement.
|
|
65
|
+
// Absorb that rounding without widening the projected button.
|
|
66
|
+
button.titleLabel?.adjustsFontSizeToFitWidth = true
|
|
67
|
+
button.titleLabel?.minimumScaleFactor = 0.99
|
|
68
|
+
button.isEnabled = !item.disabled
|
|
69
|
+
button.accessibilityLabel = item.accessibilityLabel
|
|
70
|
+
button.accessibilityIdentifier = item.id
|
|
71
|
+
if item.selected { button.accessibilityTraits.insert(.selected) } else { button.accessibilityTraits.remove(.selected) }
|
|
72
|
+
if item.disabled { button.accessibilityTraits.insert(.notEnabled) } else { button.accessibilityTraits.remove(.notEnabled) }
|
|
73
|
+
button.subviews.filter { $0 is BadgeLabel }.forEach { $0.removeFromSuperview() }
|
|
74
|
+
button.accessibilityValue = item.badge?.value
|
|
75
|
+
if let badge = item.badge {
|
|
76
|
+
let label = BadgeLabel()
|
|
77
|
+
label.text = badge.value
|
|
78
|
+
label.font = .systemFont(ofSize: 11, weight: .semibold)
|
|
79
|
+
label.textColor = rendering.color(badge.textColor)
|
|
80
|
+
label.backgroundColor = rendering.color(badge.color)
|
|
81
|
+
label.textAlignment = .center
|
|
82
|
+
label.layer.cornerRadius = 8
|
|
83
|
+
label.clipsToBounds = true
|
|
84
|
+
label.isAccessibilityElement = false
|
|
85
|
+
label.translatesAutoresizingMaskIntoConstraints = false
|
|
86
|
+
button.addSubview(label)
|
|
87
|
+
NSLayoutConstraint.activate([
|
|
88
|
+
label.centerXAnchor.constraint(equalTo: button.centerXAnchor, constant: 14),
|
|
89
|
+
label.topAnchor.constraint(equalTo: button.topAnchor, constant: 1),
|
|
90
|
+
label.widthAnchor.constraint(greaterThanOrEqualToConstant: max(16, label.intrinsicContentSize.width + 6)),
|
|
91
|
+
label.heightAnchor.constraint(equalToConstant: 16)
|
|
92
|
+
])
|
|
93
|
+
}
|
|
94
|
+
button.removeAction(identifiedBy: activationID, for: .touchUpInside)
|
|
95
|
+
button.addAction(UIAction(identifier: activationID) { _ in activate(item.id) }, for: .touchUpInside)
|
|
96
|
+
return button
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
enum NativeUIButtons {
|
|
4
|
+
static let kind = NativeUIComponent.buttons
|
|
5
|
+
|
|
6
|
+
@available(iOS 26.0, *)
|
|
7
|
+
static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIView {
|
|
8
|
+
let items = node.items
|
|
9
|
+
let rtl = node.rtl
|
|
10
|
+
let effect = UIGlassEffect(style: .regular)
|
|
11
|
+
let control = UIVisualEffectView(effect: effect)
|
|
12
|
+
control.layer.cornerRadius = node.frame.height * scale / 2
|
|
13
|
+
control.clipsToBounds = true
|
|
14
|
+
control.accessibilityIdentifier = node.id
|
|
15
|
+
for item in items {
|
|
16
|
+
let button = NativeUIButton.render(item.content, glass: false, rendering: rendering, activate: activate)
|
|
17
|
+
button.semanticContentAttribute = rtl ? .forceRightToLeft : .forceLeftToRight
|
|
18
|
+
let rect = item.frame.rect
|
|
19
|
+
button.frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
|
|
20
|
+
control.contentView.addSubview(button)
|
|
21
|
+
}
|
|
22
|
+
return control
|
|
23
|
+
}
|
|
24
|
+
}
|