@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.1
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/LICENSE +21 -0
- package/Package.swift +23 -0
- package/README.md +53 -2
- package/RdlaboCapacitorNativeUi.podspec +17 -0
- package/android/build.gradle +69 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +3 -0
- package/dist/native/activation.d.ts +14 -0
- package/dist/native/activation.d.ts.map +1 -0
- package/dist/native/activation.js +305 -0
- package/dist/native/activation.js.map +1 -0
- package/dist/native/components/index.d.ts +19 -0
- package/dist/native/components/index.d.ts.map +1 -0
- package/dist/native/components/index.js +38 -0
- package/dist/native/components/index.js.map +1 -0
- package/dist/native/components/ion-back-button.d.ts +10 -0
- package/dist/native/components/ion-back-button.d.ts.map +1 -0
- package/dist/native/components/ion-back-button.js +21 -0
- package/dist/native/components/ion-back-button.js.map +1 -0
- package/dist/native/components/ion-button.d.ts +10 -0
- package/dist/native/components/ion-button.d.ts.map +1 -0
- package/dist/native/components/ion-button.js +25 -0
- package/dist/native/components/ion-button.js.map +1 -0
- package/dist/native/components/ion-buttons.d.ts +11 -0
- package/dist/native/components/ion-buttons.d.ts.map +1 -0
- package/dist/native/components/ion-buttons.js +30 -0
- package/dist/native/components/ion-buttons.js.map +1 -0
- package/dist/native/components/ion-fab.d.ts +10 -0
- package/dist/native/components/ion-fab.d.ts.map +1 -0
- package/dist/native/components/ion-fab.js +132 -0
- package/dist/native/components/ion-fab.js.map +1 -0
- package/dist/native/components/ion-menu-button.d.ts +13 -0
- package/dist/native/components/ion-menu-button.d.ts.map +1 -0
- package/dist/native/components/ion-menu-button.js +44 -0
- package/dist/native/components/ion-menu-button.js.map +1 -0
- package/dist/native/components/ion-segment.d.ts +11 -0
- package/dist/native/components/ion-segment.d.ts.map +1 -0
- package/dist/native/components/ion-segment.js +30 -0
- package/dist/native/components/ion-segment.js.map +1 -0
- package/dist/native/components/ion-tab-bar.d.ts +11 -0
- package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
- package/dist/native/components/ion-tab-bar.js +50 -0
- package/dist/native/components/ion-tab-bar.js.map +1 -0
- package/dist/native/components/searchable-tabs.d.ts +20 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -0
- package/dist/native/components/searchable-tabs.js +413 -0
- package/dist/native/components/searchable-tabs.js.map +1 -0
- package/dist/native/definitions.d.ts +255 -0
- package/dist/native/definitions.d.ts.map +1 -0
- package/dist/native/definitions.js +0 -0
- package/dist/native/extensions.d.ts +31 -0
- package/dist/native/extensions.d.ts.map +1 -0
- package/dist/native/extensions.js +122 -0
- package/dist/native/extensions.js.map +1 -0
- package/dist/native/foldable/fold-state.d.ts +5 -0
- package/dist/native/foldable/fold-state.d.ts.map +1 -0
- package/dist/native/foldable/fold-state.js +15 -0
- package/dist/native/foldable/fold-state.js.map +1 -0
- package/dist/native/foldable/types.d.ts +12 -0
- package/dist/native/foldable/types.d.ts.map +1 -0
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/html.d.ts +9 -0
- package/dist/native/html.d.ts.map +1 -0
- package/dist/native/html.js +188 -0
- package/dist/native/html.js.map +1 -0
- package/dist/native/index.d.ts +10 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +28 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/legacy-layout.d.ts +5 -0
- package/dist/native/legacy-layout.d.ts.map +1 -0
- package/dist/native/legacy-layout.js +27 -0
- package/dist/native/legacy-layout.js.map +1 -0
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +285 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +9 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +53 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +12 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +110 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +11 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +167 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts +8 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +238 -0
- package/dist/native/prehide.js.map +1 -0
- package/dist/native/runtime.d.ts +7 -0
- package/dist/native/runtime.d.ts.map +1 -0
- package/dist/native/runtime.js +788 -0
- package/dist/native/runtime.js.map +1 -0
- package/dist/native/shared/bridge.d.ts +6 -0
- package/dist/native/shared/bridge.d.ts.map +1 -0
- package/dist/native/shared/bridge.js +18 -0
- package/dist/native/shared/bridge.js.map +1 -0
- package/dist/native/shared/candidate.d.ts +25 -0
- package/dist/native/shared/candidate.d.ts.map +1 -0
- package/dist/native/shared/candidate.js +106 -0
- package/dist/native/shared/candidate.js.map +1 -0
- package/dist/native/shared/clipping.d.ts +5 -0
- package/dist/native/shared/clipping.d.ts.map +1 -0
- package/dist/native/shared/clipping.js +26 -0
- package/dist/native/shared/clipping.js.map +1 -0
- package/dist/native/shared/crossfade.d.ts +12 -0
- package/dist/native/shared/crossfade.d.ts.map +1 -0
- package/dist/native/shared/crossfade.js +67 -0
- package/dist/native/shared/crossfade.js.map +1 -0
- package/dist/native/shared/diagnostics.d.ts +4 -0
- package/dist/native/shared/diagnostics.d.ts.map +1 -0
- package/dist/native/shared/diagnostics.js +18 -0
- package/dist/native/shared/diagnostics.js.map +1 -0
- package/dist/native/shared/dom.d.ts +46 -0
- package/dist/native/shared/dom.d.ts.map +1 -0
- package/dist/native/shared/dom.js +180 -0
- package/dist/native/shared/dom.js.map +1 -0
- package/dist/native/shared/events.d.ts +15 -0
- package/dist/native/shared/events.d.ts.map +1 -0
- package/dist/native/shared/events.js +17 -0
- package/dist/native/shared/events.js.map +1 -0
- package/dist/native/shared/icons.d.ts +8 -0
- package/dist/native/shared/icons.d.ts.map +1 -0
- package/dist/native/shared/icons.js +61 -0
- package/dist/native/shared/icons.js.map +1 -0
- package/dist/native/shared/modal.d.ts +10 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +160 -0
- package/dist/native/shared/modal.js.map +1 -0
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/shared/toolbar-options.d.ts +9 -0
- package/dist/native/shared/toolbar-options.d.ts.map +1 -0
- package/dist/native/shared/toolbar-options.js +36 -0
- package/dist/native/shared/toolbar-options.js.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.js +85 -0
- package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
- package/dist/native/startup.d.ts +8 -0
- package/dist/native/startup.d.ts.map +1 -0
- package/dist/native/startup.js +86 -0
- package/dist/native/startup.js.map +1 -0
- package/dist/native/vertical-bars-web-search.d.ts +10 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +90 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts +5 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +421 -0
- package/dist/native/vertical-bars-web.js.map +1 -0
- package/dist/native-integration/index.d.ts +25 -0
- package/dist/native-integration/index.d.ts.map +1 -0
- package/dist/native-integration/index.js +33 -0
- package/dist/native-integration/index.js.map +1 -0
- package/dist/vertical-bars.d.ts +6 -0
- package/dist/vertical-bars.js +5 -0
- package/docs/configuration.md +187 -0
- package/docs/framework-independent.md +82 -0
- package/docs/migration.md +206 -0
- package/docs/native-mini-player.md +47 -0
- package/docs/native-ui-extensions.md +365 -0
- package/docs/native-ui.md +409 -0
- package/docs/setup.md +205 -0
- package/docs/vertical-bars.md +379 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
- package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
- package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
- package/package.json +89 -4
- package/src/core.ts +15 -0
- package/src/native/activation.ts +394 -0
- package/src/native/components/index.ts +45 -0
- package/src/native/components/ion-back-button.ts +22 -0
- package/src/native/components/ion-button.ts +25 -0
- package/src/native/components/ion-buttons.ts +37 -0
- package/src/native/components/ion-fab.ts +151 -0
- package/src/native/components/ion-menu-button.ts +37 -0
- package/src/native/components/ion-segment.ts +27 -0
- package/src/native/components/ion-tab-bar.ts +50 -0
- package/src/native/components/searchable-tabs.ts +499 -0
- package/src/native/definitions.ts +324 -0
- package/src/native/extensions.ts +176 -0
- package/src/native/foldable/fold-state.ts +10 -0
- package/src/native/foldable/types.ts +4 -0
- package/src/native/html.ts +193 -0
- package/src/native/index.ts +41 -0
- package/src/native/legacy-layout.ts +21 -0
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +349 -0
- package/src/native/overlays/dialog-animation.ts +53 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +120 -0
- package/src/native/overlays/styles.ts +181 -0
- package/src/native/prehide.ts +285 -0
- package/src/native/runtime.ts +988 -0
- package/src/native/shared/bridge.ts +18 -0
- package/src/native/shared/candidate.ts +162 -0
- package/src/native/shared/clipping.ts +24 -0
- package/src/native/shared/crossfade.ts +67 -0
- package/src/native/shared/diagnostics.ts +13 -0
- package/src/native/shared/dom.ts +304 -0
- package/src/native/shared/events.ts +14 -0
- package/src/native/shared/icons.ts +57 -0
- package/src/native/shared/modal.ts +157 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/shared/toolbar-options.ts +53 -0
- package/src/native/shared/vertical-bars-geometry.ts +80 -0
- package/src/native/startup.ts +84 -0
- package/src/native/vertical-bars-web-search.ts +86 -0
- package/src/native/vertical-bars-web.ts +498 -0
- package/src/native-integration/index.ts +49 -0
- package/src/vertical-bars.ts +11 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Animation } from '@ionic/core';
|
|
2
|
+
import { createAnimation } from '@ionic/core/components/index.js';
|
|
3
|
+
import { iosPopoverEnterAnimation } from '@rdlabo/ionic-theme-utils';
|
|
4
|
+
import type { Frame } from '../definitions';
|
|
5
|
+
import { dismissAnimation } from './dismiss-animation';
|
|
6
|
+
|
|
7
|
+
export type Dialog = HTMLIonPopoverElement;
|
|
8
|
+
|
|
9
|
+
/** Prepare the source's final layout while SwiftUI owns visible motion. */
|
|
10
|
+
export const nativeDialogAnimation = (overlay: Dialog, close: () => Promise<void>, anchor: Promise<Frame | undefined>) => {
|
|
11
|
+
const enterAnimation = overlay.enterAnimation;
|
|
12
|
+
const builder = enterAnimation ?? iosPopoverEnterAnimation;
|
|
13
|
+
overlay.enterAnimation = (base, options) => {
|
|
14
|
+
const animation = createAnimation();
|
|
15
|
+
const play = animation.play;
|
|
16
|
+
animation.play = async () => {
|
|
17
|
+
const frame = await anchor;
|
|
18
|
+
const proxy = overlay.ownerDocument.createElement('ion-button');
|
|
19
|
+
if (frame) proxy.getBoundingClientRect = () => new DOMRect(frame.x, frame.y, frame.width, frame.height);
|
|
20
|
+
// The real trigger is display:none while projected. Use only a detached
|
|
21
|
+
// measurement proxy; never replace or mutate the application's button.
|
|
22
|
+
const positioned = frame ? { ...options, trigger: proxy } : options;
|
|
23
|
+
animation.addAnimation(builder(base, positioned));
|
|
24
|
+
const finishWithoutMotion = (item: Animation) => {
|
|
25
|
+
item.duration(0).delay(0);
|
|
26
|
+
item.childAnimations.forEach(finishWithoutMotion);
|
|
27
|
+
};
|
|
28
|
+
finishWithoutMotion(animation);
|
|
29
|
+
// Ionic owns this wrapper and destroys its children on dismissal.
|
|
30
|
+
await play({ sync: true });
|
|
31
|
+
};
|
|
32
|
+
return animation;
|
|
33
|
+
};
|
|
34
|
+
const leaveAnimation = overlay.leaveAnimation;
|
|
35
|
+
const opacity = overlay.style.opacity;
|
|
36
|
+
const event = 'ionPopoverWillDismiss';
|
|
37
|
+
const dismiss = () => {
|
|
38
|
+
const closing = close();
|
|
39
|
+
overlay.leaveAnimation = () => dismissAnimation(closing);
|
|
40
|
+
};
|
|
41
|
+
overlay.addEventListener(event, dismiss);
|
|
42
|
+
return {
|
|
43
|
+
hide() {
|
|
44
|
+
overlay.style.opacity = '0';
|
|
45
|
+
},
|
|
46
|
+
stop() {
|
|
47
|
+
overlay.removeEventListener(event, dismiss);
|
|
48
|
+
overlay.enterAnimation = enterAnimation;
|
|
49
|
+
overlay.leaveAnimation = leaveAnimation;
|
|
50
|
+
overlay.style.opacity = opacity;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createAnimation } from '@ionic/core/components/index.js';
|
|
2
|
+
|
|
3
|
+
/** Let Ionic finish dismissal after the native presentation has closed. */
|
|
4
|
+
export const dismissAnimation = (closing: Promise<void>) => {
|
|
5
|
+
const animation = createAnimation();
|
|
6
|
+
const play = animation.play;
|
|
7
|
+
animation.play = async (options) => {
|
|
8
|
+
// A native failure must not suppress Ionic's didDismiss event/data.
|
|
9
|
+
await closing.catch((error) => {
|
|
10
|
+
console.error('Native popover dismissal failed; completing the Ionic lifecycle.', error);
|
|
11
|
+
});
|
|
12
|
+
await play(options);
|
|
13
|
+
};
|
|
14
|
+
return animation;
|
|
15
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export const activeElement = (doc: Document): Element | null => {
|
|
2
|
+
let element = doc.activeElement;
|
|
3
|
+
while (element?.shadowRoot?.activeElement) element = element.shadowRoot.activeElement;
|
|
4
|
+
return element;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const focusableElements = (root: HTMLElement): HTMLElement[] => {
|
|
8
|
+
const elements: HTMLElement[] = [];
|
|
9
|
+
const win = root.ownerDocument.defaultView!;
|
|
10
|
+
const collect = (scope: HTMLElement | ShadowRoot) => {
|
|
11
|
+
for (const node of Array.from(scope.querySelectorAll<HTMLElement>('*'))) {
|
|
12
|
+
if (node.inert || node.closest('[inert]') || (node as HTMLButtonElement).disabled) continue;
|
|
13
|
+
const before = elements.length;
|
|
14
|
+
if (node.shadowRoot) collect(node.shadowRoot);
|
|
15
|
+
const css = win.getComputedStyle(node);
|
|
16
|
+
if (
|
|
17
|
+
elements.length === before &&
|
|
18
|
+
node.tabIndex >= 0 &&
|
|
19
|
+
!node.hidden &&
|
|
20
|
+
css.display !== 'none' &&
|
|
21
|
+
css.visibility !== 'hidden' &&
|
|
22
|
+
node.getClientRects().length
|
|
23
|
+
)
|
|
24
|
+
elements.push(node);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
collect(root);
|
|
28
|
+
return elements
|
|
29
|
+
.map((element, index) => ({ element, index }))
|
|
30
|
+
.sort((a, b) => (a.element.tabIndex || Infinity) - (b.element.tabIndex || Infinity) || a.index - b.index)
|
|
31
|
+
.map(({ element }) => element);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const trapFocus = (root: HTMLElement, enabled: () => boolean): (() => void) => {
|
|
35
|
+
const doc = root.ownerDocument;
|
|
36
|
+
const previous = root.getAttribute('tabindex');
|
|
37
|
+
root.tabIndex = -1;
|
|
38
|
+
let last: Element | null;
|
|
39
|
+
const onFocus = (event: FocusEvent) => {
|
|
40
|
+
if (!enabled()) return;
|
|
41
|
+
if (event.composedPath().includes(root)) {
|
|
42
|
+
last = activeElement(doc);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const elements = focusableElements(root);
|
|
46
|
+
(elements.find((element) => element === last) ?? elements[0] ?? root).focus({ preventScroll: true });
|
|
47
|
+
};
|
|
48
|
+
const onKey = (event: KeyboardEvent) => {
|
|
49
|
+
if (event.key !== 'Tab' || event.altKey || event.ctrlKey || event.metaKey || !enabled()) return;
|
|
50
|
+
const elements = focusableElements(root);
|
|
51
|
+
const index = elements.findIndex((element) => element === activeElement(doc));
|
|
52
|
+
const next = event.shiftKey ? (index <= 0 ? elements.length - 1 : index - 1) : (index + 1) % elements.length;
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
(elements[next] ?? root).focus();
|
|
55
|
+
};
|
|
56
|
+
doc.addEventListener('focusin', onFocus, true);
|
|
57
|
+
doc.addEventListener('keydown', onKey, true);
|
|
58
|
+
return () => {
|
|
59
|
+
doc.removeEventListener('focusin', onFocus, true);
|
|
60
|
+
doc.removeEventListener('keydown', onKey, true);
|
|
61
|
+
if (previous === null) root.removeAttribute('tabindex');
|
|
62
|
+
else root.setAttribute('tabindex', previous);
|
|
63
|
+
};
|
|
64
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { NativeUIPopoverPresentation } from '../definitions';
|
|
2
|
+
import { projectionIds } from '../shared/projection-id';
|
|
3
|
+
import { isDark } from '../shared/dom';
|
|
4
|
+
import { moveContent } from './content';
|
|
5
|
+
import { captureRelayAppearance, sheetText } from './styles';
|
|
6
|
+
|
|
7
|
+
export const popoverTrigger = (overlay: HTMLIonPopoverElement): Element | undefined => {
|
|
8
|
+
const event = overlay.event as MouseEvent | undefined;
|
|
9
|
+
const target = (overlay.trigger ? overlay.ownerDocument.getElementById(overlay.trigger) : event?.target) as Element | undefined;
|
|
10
|
+
return target?.closest?.('ion-button,ion-fab-button,ion-item') ?? target;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Only accepted native rail buttons can retain the page projection. */
|
|
14
|
+
export const verticalBarsPopoverAnchor = (overlay: HTMLIonPopoverElement): string | undefined => {
|
|
15
|
+
const trigger = popoverTrigger(overlay);
|
|
16
|
+
if (
|
|
17
|
+
!trigger?.matches('ion-button[data-native-ui-vertical-bars-member]') ||
|
|
18
|
+
!trigger.closest('[data-native-ui-projected]') ||
|
|
19
|
+
(trigger as HTMLIonButtonElement).disabled
|
|
20
|
+
)
|
|
21
|
+
return undefined;
|
|
22
|
+
return projectionIds.get(trigger);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const popoverPresentation = (overlay: HTMLIonPopoverElement): NativeUIPopoverPresentation => {
|
|
26
|
+
const content = overlay.shadowRoot!.querySelector<HTMLElement>('.popover-content')!;
|
|
27
|
+
const rect = content.getBoundingClientRect();
|
|
28
|
+
const anchorId = verticalBarsPopoverAnchor(overlay);
|
|
29
|
+
if (!anchorId) throw new Error('Native Vertical Bars anchor unavailable');
|
|
30
|
+
return {
|
|
31
|
+
kind: 'popover',
|
|
32
|
+
animated: overlay.animated,
|
|
33
|
+
dark: isDark(overlay.ownerDocument.defaultView!.getComputedStyle(overlay)),
|
|
34
|
+
anchorId,
|
|
35
|
+
width: rect.width,
|
|
36
|
+
height: rect.height,
|
|
37
|
+
backgroundColor: overlay.ownerDocument.defaultView!.getComputedStyle(content).backgroundColor,
|
|
38
|
+
backdropDismiss: overlay.backdropDismiss,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const relayPopover = (overlay: HTMLIonPopoverElement, destination: HTMLElement) => {
|
|
43
|
+
const doc = destination.ownerDocument;
|
|
44
|
+
const source = overlay.shadowRoot!.querySelector<HTMLElement>('.popover-content')!;
|
|
45
|
+
const content = Array.from(source.querySelector('slot')!.assignedElements()) as HTMLElement[];
|
|
46
|
+
const matchesSourceAppearance = captureRelayAppearance(content);
|
|
47
|
+
const size = source.getBoundingClientRect();
|
|
48
|
+
const root = doc.createElement('ion-popover');
|
|
49
|
+
for (const attribute of Array.from(overlay.attributes)) root.setAttribute(attribute.name, attribute.value);
|
|
50
|
+
Object.assign(root.style, {
|
|
51
|
+
position: 'absolute',
|
|
52
|
+
inset: '0',
|
|
53
|
+
display: 'block',
|
|
54
|
+
visibility: 'visible',
|
|
55
|
+
opacity: '1',
|
|
56
|
+
pointerEvents: 'auto',
|
|
57
|
+
});
|
|
58
|
+
root.setAttribute('role', 'dialog');
|
|
59
|
+
root.setAttribute('aria-modal', 'true');
|
|
60
|
+
const shadow = root.attachShadow({ mode: 'open' });
|
|
61
|
+
const inheritedStyles = doc.createElement('style');
|
|
62
|
+
inheritedStyles.textContent =
|
|
63
|
+
sheetText(overlay.shadowRoot!.adoptedStyleSheets) +
|
|
64
|
+
Array.from(overlay.shadowRoot!.querySelectorAll('style'), (style) => style.textContent).join('\n');
|
|
65
|
+
shadow.append(inheritedStyles);
|
|
66
|
+
const shell = doc.createElement('div');
|
|
67
|
+
shell.className = 'popover-content';
|
|
68
|
+
const css = overlay.ownerDocument.defaultView!.getComputedStyle(source);
|
|
69
|
+
Object.assign(shell.style, {
|
|
70
|
+
position: 'relative',
|
|
71
|
+
// Ionic hides this shell until its Web enter animation runs. SwiftUI owns
|
|
72
|
+
// the relay animation, so its content must already be opaque.
|
|
73
|
+
opacity: '1',
|
|
74
|
+
boxSizing: 'border-box',
|
|
75
|
+
inset: 'auto',
|
|
76
|
+
width: '100%',
|
|
77
|
+
maxWidth: 'none',
|
|
78
|
+
height: '100%',
|
|
79
|
+
maxHeight: 'none',
|
|
80
|
+
transform: 'none',
|
|
81
|
+
boxShadow: 'none',
|
|
82
|
+
border: css.border,
|
|
83
|
+
borderColor: 'transparent',
|
|
84
|
+
borderRadius: '0',
|
|
85
|
+
overflow: css.overflow,
|
|
86
|
+
background: css.backgroundColor,
|
|
87
|
+
});
|
|
88
|
+
shell.append(doc.createElement('slot'));
|
|
89
|
+
shadow.append(shell);
|
|
90
|
+
destination.append(root);
|
|
91
|
+
// Components such as ion-select-popover dismiss their closest ion-popover.
|
|
92
|
+
root.dismiss = (...args) => overlay.dismiss(...args);
|
|
93
|
+
const restore = moveContent(content, root);
|
|
94
|
+
const select = (event: MouseEvent) => {
|
|
95
|
+
if (!event.composedPath().includes(shell)) {
|
|
96
|
+
if (overlay.backdropDismiss) void overlay.dismiss(undefined, 'backdrop');
|
|
97
|
+
} else if (overlay.dismissOnSelect) void overlay.dismiss();
|
|
98
|
+
};
|
|
99
|
+
root.addEventListener('click', select);
|
|
100
|
+
return {
|
|
101
|
+
root,
|
|
102
|
+
matchesSourceAppearance() {
|
|
103
|
+
const previousWidth = root.style.width;
|
|
104
|
+
const previousHeight = root.style.height;
|
|
105
|
+
root.style.width = `${size.width}px`;
|
|
106
|
+
root.style.height = `${size.height}px`;
|
|
107
|
+
try {
|
|
108
|
+
return matchesSourceAppearance();
|
|
109
|
+
} finally {
|
|
110
|
+
root.style.width = previousWidth;
|
|
111
|
+
root.style.height = previousHeight;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
stop() {
|
|
115
|
+
root.removeEventListener('click', select);
|
|
116
|
+
restore();
|
|
117
|
+
root.remove();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
export const sheetText = (sheets: readonly CSSStyleSheet[]): string =>
|
|
2
|
+
sheets.flatMap((sheet) => Array.from(sheet.cssRules, (rule) => rule.cssText)).join('\n');
|
|
3
|
+
|
|
4
|
+
export const relayStyles = (
|
|
5
|
+
source: Document,
|
|
6
|
+
destination: Document,
|
|
7
|
+
overlay: HTMLElement,
|
|
8
|
+
changed?: () => void,
|
|
9
|
+
): { destination: HTMLElement; ready: Promise<void>; stop: () => void } => {
|
|
10
|
+
const base = destination.createElement('base');
|
|
11
|
+
base.href = source.baseURI;
|
|
12
|
+
destination.head.append(base);
|
|
13
|
+
const copies = new Map<Element, Element>();
|
|
14
|
+
const sheets = destination.createElement('style');
|
|
15
|
+
destination.head.append(sheets);
|
|
16
|
+
const sync = () => {
|
|
17
|
+
sheets.textContent = sheetText(source.adoptedStyleSheets);
|
|
18
|
+
const sources = Array.from(source.querySelectorAll('meta[name=viewport],link[rel=stylesheet],style:not([data-overlay-relay-local])'));
|
|
19
|
+
for (const [node, copy] of copies)
|
|
20
|
+
if (!sources.includes(node)) {
|
|
21
|
+
copy.remove();
|
|
22
|
+
copies.delete(node);
|
|
23
|
+
}
|
|
24
|
+
for (const node of sources) {
|
|
25
|
+
let copy = copies.get(node);
|
|
26
|
+
if (!copy) {
|
|
27
|
+
copy = node.cloneNode(true) as Element;
|
|
28
|
+
copies.set(node, copy);
|
|
29
|
+
} else {
|
|
30
|
+
for (const attribute of Array.from(copy.attributes)) copy.removeAttribute(attribute.name);
|
|
31
|
+
for (const attribute of Array.from(node.attributes)) copy.setAttribute(attribute.name, attribute.value);
|
|
32
|
+
copy.textContent = node.textContent;
|
|
33
|
+
}
|
|
34
|
+
destination.head.append(copy);
|
|
35
|
+
}
|
|
36
|
+
destination.head.append(surface);
|
|
37
|
+
};
|
|
38
|
+
const surface = destination.createElement('style');
|
|
39
|
+
surface.textContent = 'html,body{background:transparent!important}';
|
|
40
|
+
const observers: MutationObserver[] = [];
|
|
41
|
+
const mirror = (node: Element, target: HTMLElement, flatten = false) => {
|
|
42
|
+
const update = () => {
|
|
43
|
+
for (const attribute of Array.from(target.attributes)) if (!node.hasAttribute(attribute.name)) target.removeAttribute(attribute.name);
|
|
44
|
+
for (const attribute of Array.from(node.attributes)) target.setAttribute(attribute.name, attribute.value);
|
|
45
|
+
// The popover content must not inherit the source page's rail layout.
|
|
46
|
+
if (target.localName === 'ion-app') target.classList.remove('ios-theme-vertical-bars');
|
|
47
|
+
if (flatten) target.style.setProperty('display', 'contents', 'important');
|
|
48
|
+
};
|
|
49
|
+
update();
|
|
50
|
+
const observer = new MutationObserver(() => {
|
|
51
|
+
update();
|
|
52
|
+
changed?.();
|
|
53
|
+
});
|
|
54
|
+
observer.observe(node, { attributes: true });
|
|
55
|
+
observers.push(observer);
|
|
56
|
+
};
|
|
57
|
+
mirror(source.documentElement, destination.documentElement);
|
|
58
|
+
mirror(source.body, destination.body);
|
|
59
|
+
const ancestors: Element[] = [];
|
|
60
|
+
for (let node = overlay.parentElement; node && node !== source.body; node = node.parentElement) ancestors.unshift(node);
|
|
61
|
+
let parent = destination.body;
|
|
62
|
+
for (const node of ancestors) {
|
|
63
|
+
const shell = destination.createElement(node.localName);
|
|
64
|
+
mirror(node, shell, true);
|
|
65
|
+
parent.append(shell);
|
|
66
|
+
parent = shell;
|
|
67
|
+
}
|
|
68
|
+
sync();
|
|
69
|
+
// A copied external stylesheet can still be loading when Ionic's content is
|
|
70
|
+
// ready. Do not expose an unstyled first native frame (notably button sizes).
|
|
71
|
+
const ready = Promise.all(
|
|
72
|
+
Array.from(destination.querySelectorAll<HTMLLinkElement>('link[rel=stylesheet]'), (link) => {
|
|
73
|
+
if (link.sheet) return Promise.resolve();
|
|
74
|
+
return new Promise<void>((resolve, reject) => {
|
|
75
|
+
link.addEventListener('load', () => resolve(), { once: true });
|
|
76
|
+
link.addEventListener('error', () => reject(new Error('Popover stylesheet unavailable')), { once: true });
|
|
77
|
+
});
|
|
78
|
+
}),
|
|
79
|
+
).then(() => undefined);
|
|
80
|
+
const observer = new MutationObserver(() => {
|
|
81
|
+
sync();
|
|
82
|
+
changed?.();
|
|
83
|
+
});
|
|
84
|
+
observer.observe(source.head, { subtree: true, childList: true, characterData: true, attributes: true });
|
|
85
|
+
observers.push(observer);
|
|
86
|
+
return { destination: parent, ready, stop: () => observers.forEach((item) => item.disconnect()) };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Compare the live content at the same proposed size before exposing a native
|
|
90
|
+
// surface. Ancestor/sibling selectors and container queries can change when
|
|
91
|
+
// ancestor shells become display:contents; unsupported contexts stay on Web.
|
|
92
|
+
export const captureRelayAppearance = (roots: Element[]): (() => boolean) => {
|
|
93
|
+
const properties = [
|
|
94
|
+
'display',
|
|
95
|
+
'position',
|
|
96
|
+
'box-sizing',
|
|
97
|
+
'color',
|
|
98
|
+
'background-color',
|
|
99
|
+
'background-image',
|
|
100
|
+
'background-size',
|
|
101
|
+
'background-position',
|
|
102
|
+
'box-shadow',
|
|
103
|
+
'visibility',
|
|
104
|
+
'opacity',
|
|
105
|
+
'transform',
|
|
106
|
+
'filter',
|
|
107
|
+
'overflow',
|
|
108
|
+
'text-decoration',
|
|
109
|
+
'font-family',
|
|
110
|
+
'font-size',
|
|
111
|
+
'font-weight',
|
|
112
|
+
'font-style',
|
|
113
|
+
'line-height',
|
|
114
|
+
'letter-spacing',
|
|
115
|
+
'text-align',
|
|
116
|
+
'text-transform',
|
|
117
|
+
'white-space',
|
|
118
|
+
'direction',
|
|
119
|
+
'padding-top',
|
|
120
|
+
'padding-right',
|
|
121
|
+
'padding-bottom',
|
|
122
|
+
'padding-left',
|
|
123
|
+
'margin-top',
|
|
124
|
+
'margin-right',
|
|
125
|
+
'margin-bottom',
|
|
126
|
+
'margin-left',
|
|
127
|
+
'border-top-width',
|
|
128
|
+
'border-right-width',
|
|
129
|
+
'border-bottom-width',
|
|
130
|
+
'border-left-width',
|
|
131
|
+
'border-top-color',
|
|
132
|
+
'border-radius',
|
|
133
|
+
'flex-direction',
|
|
134
|
+
'flex-wrap',
|
|
135
|
+
'justify-content',
|
|
136
|
+
'align-items',
|
|
137
|
+
'grid-template-columns',
|
|
138
|
+
'grid-template-rows',
|
|
139
|
+
'gap',
|
|
140
|
+
'width',
|
|
141
|
+
'height',
|
|
142
|
+
];
|
|
143
|
+
const snapshots: { node: Element; values: string[] }[] = [];
|
|
144
|
+
const values = (node: Element) => {
|
|
145
|
+
const css = node.ownerDocument.defaultView!.getComputedStyle(node);
|
|
146
|
+
return properties.map((property) => css.getPropertyValue(property));
|
|
147
|
+
};
|
|
148
|
+
const visit = (node: Element) => {
|
|
149
|
+
if (snapshots.length >= 1000) throw new Error('Popover content too complex to verify native styling');
|
|
150
|
+
snapshots.push({ node, values: values(node) });
|
|
151
|
+
Array.from(node.children).forEach(visit);
|
|
152
|
+
if (node.shadowRoot)
|
|
153
|
+
Array.from(node.shadowRoot.children)
|
|
154
|
+
.filter((child) => !child.matches('style, link'))
|
|
155
|
+
.forEach(visit);
|
|
156
|
+
};
|
|
157
|
+
roots.forEach(visit);
|
|
158
|
+
return () =>
|
|
159
|
+
snapshots.every(({ node, values: before }) =>
|
|
160
|
+
values(node).every((value, index) => {
|
|
161
|
+
return value === before[index];
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** Flattening an ancestor must not remove a containing block or visual effect. */
|
|
167
|
+
export const relayLayoutSupported = (overlay: HTMLElement): boolean => {
|
|
168
|
+
const win = overlay.ownerDocument.defaultView!;
|
|
169
|
+
for (let node = overlay.parentElement; node; node = node.parentElement) {
|
|
170
|
+
const css = win.getComputedStyle(node);
|
|
171
|
+
if (
|
|
172
|
+
css.containerType !== 'normal' ||
|
|
173
|
+
(css.transform !== 'none' && !new DOMMatrixReadOnly(css.transform).isIdentity) ||
|
|
174
|
+
css.perspective !== 'none' ||
|
|
175
|
+
css.filter !== 'none' ||
|
|
176
|
+
css.opacity !== '1'
|
|
177
|
+
)
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
};
|