@rdlabo/ionic-theme-ios27 1.2.0-0 → 1.2.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 +12 -2
- package/dist/css/components/ion-tabs.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -12
- package/dist/native/components/index.d.ts.map +1 -1
- package/dist/native/components/index.js +4 -1
- package/dist/native/components/ion-button.d.ts.map +1 -1
- package/dist/native/components/ion-button.js +3 -3
- package/dist/native/components/ion-buttons.d.ts.map +1 -1
- package/dist/native/components/ion-buttons.js +3 -2
- package/dist/native/definitions.d.ts +8 -15
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/definitions.js +1 -6
- package/dist/native/index.d.ts +1 -2
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +64 -28
- package/dist/native/prehide.d.ts.map +1 -1
- package/dist/native/prehide.js +6 -4
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +27 -12
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +8 -0
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +11 -10
- package/dist/native/shared/modal.d.ts +8 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +143 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -1
- package/dist/native/vertical-bars-web.js +11 -3
- package/dist/native-integration/index.d.ts.map +1 -1
- package/dist/native-integration/index.js +7 -2
- package/dist/native-integration/transition.d.ts +3 -0
- package/dist/native-integration/transition.d.ts.map +1 -0
- package/dist/native-integration/transition.js +7 -0
- package/dist/sheets-of-glass/animations.d.ts +13 -4
- package/dist/sheets-of-glass/animations.d.ts.map +1 -1
- package/dist/sheets-of-glass/animations.js +56 -40
- package/dist/sheets-of-glass/index.d.ts +1 -1
- package/dist/sheets-of-glass/index.d.ts.map +1 -1
- package/dist/sheets-of-glass/index.js +64 -20
- package/dist/transition/ios.transition.d.ts.map +1 -1
- package/dist/transition/ios.transition.js +2 -3
- package/dist/vertical-bars.d.ts +2 -2
- package/dist/vertical-bars.d.ts.map +1 -1
- package/dist/vertical-bars.js +1 -1
- package/docs/iphone-duo-with-original-theme.md +171 -0
- package/docs/iphone-duo.md +105 -50
- package/docs/migration.md +23 -0
- package/docs/native-ui-shell.md +10 -6
- package/docs/special-markup.md +3 -14
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +23 -4
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +15 -171
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +5 -0
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +30 -0
- package/package.json +1 -1
- package/src/index.ts +41 -9
- package/src/native/components/index.ts +6 -1
- package/src/native/components/ion-button.ts +4 -3
- package/src/native/components/ion-buttons.ts +4 -2
- package/src/native/definitions.ts +11 -19
- package/src/native/index.ts +67 -30
- package/src/native/prehide.ts +7 -4
- package/src/native/runtime.ts +28 -11
- package/src/native/shared/candidate.ts +10 -0
- package/src/native/shared/dom.ts +11 -10
- package/src/native/shared/modal.ts +157 -0
- package/src/native/vertical-bars-web.ts +12 -3
- package/src/native-integration/index.ts +8 -2
- package/src/native-integration/transition.ts +15 -0
- package/src/sheets-of-glass/animations.ts +66 -41
- package/src/sheets-of-glass/index.ts +82 -16
- package/src/styles/components/ion-tabs.scss +7 -4
- package/src/styles/vertical-bars.scss +102 -35
- package/src/transition/ios.transition.ts +4 -4
- package/src/vertical-bars.ts +2 -2
package/src/native/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import { createRuntime } from './runtime';
|
|
13
13
|
import { createVerticalBarsWebProjection } from './vertical-bars-web';
|
|
14
14
|
import { prehideVerticalBarsToolbarSources } from './prehide';
|
|
15
|
+
import { observeVerticalBarsModals } from './shared/modal';
|
|
15
16
|
export type {
|
|
16
17
|
NativeUIShellComponent,
|
|
17
18
|
NativeUIShellControls,
|
|
@@ -19,13 +20,11 @@ export type {
|
|
|
19
20
|
NativeUIShellOptions,
|
|
20
21
|
NativeUIShellStatus,
|
|
21
22
|
NativeUIShellSuspension,
|
|
22
|
-
DeviceLayout,
|
|
23
23
|
VerticalBarEdge,
|
|
24
24
|
VerticalBarPlacement,
|
|
25
25
|
VerticalControlAreaHandle,
|
|
26
26
|
WebViewMetrics,
|
|
27
27
|
} from './definitions';
|
|
28
|
-
export { HingeStatus } from './definitions';
|
|
29
28
|
|
|
30
29
|
const plugin = registerPlugin<NativeUIShellPlugin>('IonicNativeUIShell');
|
|
31
30
|
export const IonicNativeUIShell = plugin;
|
|
@@ -97,8 +96,7 @@ const manage = (
|
|
|
97
96
|
|
|
98
97
|
/** Reads the current native WebView geometry and applies it to page transitions. */
|
|
99
98
|
export const configureNativeTransition = async (): Promise<WebViewMetrics> => {
|
|
100
|
-
const metrics =
|
|
101
|
-
typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? (await plugin.getDeviceLayout()).webViewMetrics : { radius: 0 };
|
|
99
|
+
const metrics = typeof document !== 'undefined' && Capacitor.getPlatform() === 'ios' ? await plugin.getWebViewMetrics() : { radius: 0 };
|
|
102
100
|
setConfig({ radius: metrics.radius });
|
|
103
101
|
return metrics;
|
|
104
102
|
};
|
|
@@ -109,6 +107,39 @@ const physicalVerticalBarEdge = (edge: Exclude<VerticalBarEdge, null>, rtl: bool
|
|
|
109
107
|
|
|
110
108
|
const elementRtl = (element: Element): boolean => element.closest('[dir]')?.getAttribute('dir') === 'rtl';
|
|
111
109
|
|
|
110
|
+
// Device facts are supplied by the application; the theme only compares placement.
|
|
111
|
+
const nativePlacements = new WeakMap<HTMLElement, { edge: VerticalBarEdge; rtl?: boolean }>();
|
|
112
|
+
const horizontalFallbackAttribute = 'data-native-ui-shell-vertical-bars-suspended';
|
|
113
|
+
|
|
114
|
+
// Keep the requested rail while ordinary Native UI Shell temporarily uses its
|
|
115
|
+
// horizontal layout. Removing the effective class restores normal measurements,
|
|
116
|
+
// toolbar ownership and safe areas throughout the existing rendering pipeline.
|
|
117
|
+
const observeNativeVerticalBarsLayout = (doc: Document): (() => void) => {
|
|
118
|
+
const reconcile = () => {
|
|
119
|
+
const app = doc.querySelector<HTMLElement>('ion-app');
|
|
120
|
+
if (!app) return;
|
|
121
|
+
const requested = app.classList.contains('ios-theme-vertical-bars') || app.hasAttribute(horizontalFallbackAttribute);
|
|
122
|
+
const suspended = requested && nativePlacements.get(app)?.edge == null;
|
|
123
|
+
if (app.hasAttribute(horizontalFallbackAttribute) !== suspended) app.toggleAttribute(horizontalFallbackAttribute, suspended);
|
|
124
|
+
if (app.classList.contains('ios-theme-vertical-bars') !== (requested && !suspended)) {
|
|
125
|
+
app.classList.toggle('ios-theme-vertical-bars', requested && !suspended);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const observer = new MutationObserver(reconcile);
|
|
129
|
+
observer.observe(doc.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['class'] });
|
|
130
|
+
doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile);
|
|
131
|
+
reconcile();
|
|
132
|
+
return () => {
|
|
133
|
+
observer.disconnect();
|
|
134
|
+
doc.defaultView?.removeEventListener('nativeUIShellRefresh', reconcile);
|
|
135
|
+
const app = doc.querySelector<HTMLElement>(`ion-app[${horizontalFallbackAttribute}]`);
|
|
136
|
+
if (app) {
|
|
137
|
+
app.removeAttribute(horizontalFallbackAttribute);
|
|
138
|
+
app.classList.add('ios-theme-vertical-bars');
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
112
143
|
/**
|
|
113
144
|
* Applies one placement to the CSS layout and both Web/native projections.
|
|
114
145
|
* Pass `rtl` when the document direction is known; otherwise the nearest `dir` attribute is used.
|
|
@@ -117,11 +148,16 @@ export const setVerticalControlAreaPlacement = (placement: VerticalBarEdge | Ver
|
|
|
117
148
|
if (typeof document === 'undefined') return;
|
|
118
149
|
const app = document.querySelector<HTMLElement>('ion-app');
|
|
119
150
|
if (!app) throw new Error('Vertical Control Area requires ion-app');
|
|
120
|
-
const { edge, inset } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };
|
|
151
|
+
const { edge, inset = 0 } = placement && typeof placement === 'object' ? placement : { edge: placement, inset: 0 };
|
|
152
|
+
if (placement && typeof placement === 'object' && placement.nativeEdge !== undefined) {
|
|
153
|
+
nativePlacements.set(app, { edge: placement.nativeEdge, rtl });
|
|
154
|
+
}
|
|
155
|
+
app.removeAttribute(horizontalFallbackAttribute);
|
|
121
156
|
app.classList.toggle('ios-theme-vertical-bars', edge !== null);
|
|
122
157
|
app.classList.toggle('ios-theme-vertical-bars-left', edge !== null && physicalVerticalBarEdge(edge, rtl ?? elementRtl(app)) === 'left');
|
|
123
158
|
if (edge && Number.isFinite(inset) && inset > 0) app.style.setProperty('--ios-theme-vertical-bars-native-inset', `${inset}px`);
|
|
124
159
|
else app.style.removeProperty('--ios-theme-vertical-bars-native-inset');
|
|
160
|
+
document.defaultView?.dispatchEvent(new Event('nativeUIShellRefresh'));
|
|
125
161
|
};
|
|
126
162
|
|
|
127
163
|
/** Call once at application startup. Ionic markup remains the source of truth. */
|
|
@@ -160,6 +196,7 @@ export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise
|
|
|
160
196
|
);
|
|
161
197
|
if (active) return active;
|
|
162
198
|
activeConfiguration = configuration;
|
|
199
|
+
const stopModals = observeVerticalBarsModals(document);
|
|
163
200
|
const prehide =
|
|
164
201
|
options.controls === undefined || options.controls.toolbar === true ? prehideVerticalBarsToolbarSources(document) : undefined;
|
|
165
202
|
// A slow destroy of a superseded activation must not release a newer one.
|
|
@@ -174,38 +211,37 @@ export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise
|
|
|
174
211
|
manage(createVerticalBarsWebProjection(document, options), {
|
|
175
212
|
reason,
|
|
176
213
|
suspend: () => prehide?.suspend(),
|
|
177
|
-
destroy: () =>
|
|
214
|
+
destroy: () => {
|
|
215
|
+
prehide?.stop();
|
|
216
|
+
stopModals();
|
|
217
|
+
},
|
|
178
218
|
release,
|
|
179
219
|
});
|
|
180
220
|
return (start = active =
|
|
181
221
|
(async () => {
|
|
182
222
|
if (Capacitor.getPlatform() !== 'ios') return fallback('Requires Capacitor iOS');
|
|
183
223
|
let runtime: NativeUIShellHandle | undefined;
|
|
184
|
-
let
|
|
185
|
-
let
|
|
224
|
+
let metricsListener: Awaited<ReturnType<typeof plugin.addListener>> | undefined;
|
|
225
|
+
let stopVerticalBarsLayout: (() => void) | undefined;
|
|
186
226
|
try {
|
|
187
|
-
if (!options.verticalBarsOnly) await configureNativeTransition().catch(() => undefined);
|
|
188
227
|
const capabilities = await plugin.configure({ verticalBarsOnly: options.verticalBarsOnly === true });
|
|
189
228
|
if (!capabilities.supported) return fallback('Requires iOS 26 or later');
|
|
190
|
-
|
|
229
|
+
if (!options.verticalBarsOnly) stopVerticalBarsLayout = observeNativeVerticalBarsLayout(document);
|
|
230
|
+
// The application owns device state and selects the rail through placement classes.
|
|
191
231
|
const nativeVerticalBars = () => {
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const domEdge = root.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';
|
|
199
|
-
return nativeEdge === null || physicalVerticalBarEdge(nativeEdge, elementRtl(root)) === domEdge;
|
|
232
|
+
const app = document.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
|
|
233
|
+
if (!app) return false;
|
|
234
|
+
const reported = nativePlacements.get(app);
|
|
235
|
+
if (!reported?.edge) return false;
|
|
236
|
+
const physicalEdge = app.classList.contains('ios-theme-vertical-bars-left') ? 'left' : 'right';
|
|
237
|
+
return physicalEdge === physicalVerticalBarEdge(reported.edge, reported.rtl ?? elementRtl(app));
|
|
200
238
|
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
nativeEdge = (await plugin.getDeviceLayout()).placement.edge;
|
|
239
|
+
if (!options.verticalBarsOnly) {
|
|
240
|
+
metricsListener = await plugin.addListener('webViewMetricsChange', (metrics) => {
|
|
241
|
+
setConfig({ radius: metrics.radius });
|
|
242
|
+
});
|
|
243
|
+
await configureNativeTransition().catch(() => undefined);
|
|
244
|
+
}
|
|
209
245
|
const native = await createRuntime(document, plugin, options, nativeVerticalBars, options.verticalBarsOnly === true);
|
|
210
246
|
runtime = combine(
|
|
211
247
|
native,
|
|
@@ -215,15 +251,16 @@ export const enableNativeUIShell = (options: NativeUIShellOptions = {}): Promise
|
|
|
215
251
|
suspend: () => prehide?.suspend(),
|
|
216
252
|
release,
|
|
217
253
|
destroy: async () => {
|
|
218
|
-
await
|
|
219
|
-
|
|
254
|
+
await metricsListener?.remove().catch(() => {});
|
|
255
|
+
stopVerticalBarsLayout?.();
|
|
220
256
|
prehide?.stop();
|
|
257
|
+
stopModals();
|
|
221
258
|
},
|
|
222
259
|
});
|
|
223
260
|
} catch (error) {
|
|
224
261
|
await runtime?.destroy();
|
|
225
|
-
await
|
|
226
|
-
|
|
262
|
+
await metricsListener?.remove().catch(() => {});
|
|
263
|
+
stopVerticalBarsLayout?.();
|
|
227
264
|
return fallback(error instanceof Error ? error.message : String(error));
|
|
228
265
|
}
|
|
229
266
|
})());
|
package/src/native/prehide.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modalUsesVerticalBars } from './shared/modal';
|
|
1
2
|
import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core';
|
|
2
3
|
import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
|
|
3
4
|
import {
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
setVerticalBarsPlacement,
|
|
18
19
|
} from './shared/dom';
|
|
19
20
|
|
|
20
|
-
const overlays = 'ion-menu, ion-
|
|
21
|
+
const overlays = 'ion-menu, ion-popover';
|
|
21
22
|
const sourceSelector = 'ion-back-button, ion-menu-button, ion-buttons, ion-button';
|
|
22
23
|
// Realm- and teardown-safe: globals such as Element/HTMLElement may be gone
|
|
23
24
|
// when a queued mutation microtask or a stale listener still runs.
|
|
@@ -60,7 +61,7 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
|
|
|
60
61
|
scopes.delete(scope);
|
|
61
62
|
};
|
|
62
63
|
const capture = (scope: HTMLElement) => {
|
|
63
|
-
if (!root()?.contains(scope) || scope.closest(overlays) || verticalBarsPages.isDeparted(scope)) return;
|
|
64
|
+
if (!root()?.contains(scope) || scope.closest(overlays) || !modalUsesVerticalBars(scope) || verticalBarsPages.isDeparted(scope)) return;
|
|
64
65
|
const owned = scopes.get(scope) ?? new Set<HTMLElement>();
|
|
65
66
|
const place = (element: HTMLElement, rail: boolean) => {
|
|
66
67
|
if (owned.has(element)) return;
|
|
@@ -73,7 +74,8 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
|
|
|
73
74
|
};
|
|
74
75
|
const sources = scope.matches('ion-back-button') ? [scope] : Array.from(scope.querySelectorAll<HTMLElement>(sourceSelector));
|
|
75
76
|
sources.forEach((element) => {
|
|
76
|
-
if (element.closest(overlays) || (scope.matches('.ion-page') && routedPage(element) !== scope))
|
|
77
|
+
if (!modalUsesVerticalBars(element) || element.closest(overlays) || (scope.matches('.ion-page') && routedPage(element) !== scope))
|
|
78
|
+
return;
|
|
77
79
|
if (element.matches('ion-buttons')) {
|
|
78
80
|
const children = childElements(element);
|
|
79
81
|
const actions = children.filter((child) => eligible(child) && isVerticalBarsToolbarActionShape(child));
|
|
@@ -134,7 +136,7 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
|
|
|
134
136
|
Array.from(scopes.keys()).forEach(release);
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
137
|
-
for (const scope of scopes.keys()) if (!scope.isConnected) release(scope);
|
|
139
|
+
for (const scope of scopes.keys()) if (!scope.isConnected || !modalUsesVerticalBars(scope)) release(scope);
|
|
138
140
|
for (const [element, pending] of pendingBacks) if (element.shadowRoot || element.classList.contains('hydrated')) capture(pending.scope);
|
|
139
141
|
// Ionic inserts the destination as invisible before WillEnter. Hide its
|
|
140
142
|
// sources in that same mutation microtask; capture new DOM identities on active pages too.
|
|
@@ -193,6 +195,7 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
|
|
|
193
195
|
for (const name of [LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
|
|
194
196
|
doc.addEventListener(name, onPageLifecycle, { capture: true, signal: listeners.signal });
|
|
195
197
|
doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, onTransitionCanceled, { capture: true, signal: listeners.signal });
|
|
198
|
+
doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile, { signal: listeners.signal });
|
|
196
199
|
reconcile();
|
|
197
200
|
const mutationRelevant = (record: MutationRecord) => {
|
|
198
201
|
if (record.type === 'childList')
|
package/src/native/runtime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inVerticalBarsSurface, topModal, modalUsesVerticalBars, modalVerticalBarFrame } from './shared/modal';
|
|
1
2
|
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
3
|
import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
|
|
3
4
|
import { VERTICAL_BARS_TRANSITION_CANCELED, getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
|
|
@@ -126,7 +127,7 @@ export const createRuntime = async (
|
|
|
126
127
|
lastSnapshot = '';
|
|
127
128
|
search.release(element);
|
|
128
129
|
element.removeAttribute(marker);
|
|
129
|
-
if (!stopped) crossfade.play(element, false,
|
|
130
|
+
if (!stopped) crossfade.play(element, false, isVerticalBarsSource(element) ? !element.matches('ion-tab-bar') : handoffInstant);
|
|
130
131
|
if (element.getAttribute('aria-hidden') === 'true') {
|
|
131
132
|
const previous = sources.get(element);
|
|
132
133
|
if (previous == null) element.removeAttribute('aria-hidden');
|
|
@@ -205,14 +206,19 @@ export const createRuntime = async (
|
|
|
205
206
|
Array.from(pages).some((scope) => scope.contains(element)) ||
|
|
206
207
|
Array.from(moving.keys()).some((surface) => surface.contains(element))));
|
|
207
208
|
const painted = () => new Promise<void>((resolve) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => resolve())));
|
|
208
|
-
const overlayOpen = (includeMenu = true) => {
|
|
209
|
+
const overlayOpen = (includeMenu = true, allowModal = false) => {
|
|
210
|
+
const modal = topModal(doc);
|
|
211
|
+
const ignored = (element: Element) => allowModal && !!modal && modalUsesVerticalBars(modal) && element.matches('ion-modal');
|
|
209
212
|
for (const element of presented) if (!element.isConnected) presented.delete(element);
|
|
210
|
-
const presentedOverlayOpen = Array.from(presented).some(
|
|
213
|
+
const presentedOverlayOpen = Array.from(presented).some(
|
|
214
|
+
(element) => !ignored(element) && (includeMenu || !element.matches('ion-menu')),
|
|
215
|
+
);
|
|
211
216
|
return (
|
|
212
217
|
manualSuspensions.size > 0 ||
|
|
213
218
|
presentedOverlayOpen ||
|
|
214
219
|
Array.from(doc.querySelectorAll(overlays)).some(
|
|
215
220
|
(element) =>
|
|
221
|
+
!ignored(element) &&
|
|
216
222
|
(includeMenu || !element.matches('ion-menu')) &&
|
|
217
223
|
((element as Element & { presented?: boolean }).presented || element.classList.contains('show-menu')),
|
|
218
224
|
)
|
|
@@ -225,7 +231,7 @@ export const createRuntime = async (
|
|
|
225
231
|
search.keepSearchTabsVisible();
|
|
226
232
|
for (const page of pages) if (!page.isConnected) pages.delete(page);
|
|
227
233
|
for (const surface of moving.keys()) if (!surface.isConnected) moving.delete(surface);
|
|
228
|
-
if (doc.hidden || overlayOpen(false)) return [];
|
|
234
|
+
if (doc.hidden || overlayOpen(false, true)) return [];
|
|
229
235
|
if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0) && !search.hasActive()) return [];
|
|
230
236
|
const menuOpen = overlayOpen();
|
|
231
237
|
const candidates = unprojected(sources.keys(), () =>
|
|
@@ -233,7 +239,10 @@ export const createRuntime = async (
|
|
|
233
239
|
.decorate(
|
|
234
240
|
Array.from(doc.querySelectorAll<HTMLElement>(selector))
|
|
235
241
|
.filter(
|
|
236
|
-
(element) =>
|
|
242
|
+
(element) =>
|
|
243
|
+
inVerticalBarsSurface(element) &&
|
|
244
|
+
!verticalBarsPages.isDeparted(element) &&
|
|
245
|
+
(!blocked(element) || (menuOpen && isVerticalBarsSource(element))),
|
|
237
246
|
)
|
|
238
247
|
.map(readEnabledCandidate)
|
|
239
248
|
.filter((candidate): candidate is Candidate => !!candidate),
|
|
@@ -333,7 +342,14 @@ export const createRuntime = async (
|
|
|
333
342
|
? 'left'
|
|
334
343
|
: 'right'
|
|
335
344
|
: undefined;
|
|
336
|
-
const
|
|
345
|
+
const modal = topModal(doc);
|
|
346
|
+
const verticalBarFrame = modal && modalUsesVerticalBars(modal) ? modalVerticalBarFrame(modal) : undefined;
|
|
347
|
+
const data = {
|
|
348
|
+
verticalBarFrame,
|
|
349
|
+
viewportWidth: win.innerWidth,
|
|
350
|
+
verticalBarEdge,
|
|
351
|
+
controls: candidates.map((candidate) => candidate.control),
|
|
352
|
+
};
|
|
337
353
|
const serialized = JSON.stringify(data);
|
|
338
354
|
if (serialized === lastSnapshot && !forceRefresh) return;
|
|
339
355
|
const snapshot: ShellSnapshot = { ...data, revision: ++revision, transitionDuration: crossfade.duration(handoffInstant) };
|
|
@@ -403,13 +419,14 @@ export const createRuntime = async (
|
|
|
403
419
|
verticalBarsMembers.add(element);
|
|
404
420
|
}
|
|
405
421
|
for (const element of accepted.flatMap(candidateSources)) {
|
|
422
|
+
const newlyProjected = !sources.has(element) || !element.hasAttribute(marker);
|
|
406
423
|
if (!sources.has(element)) {
|
|
407
424
|
sources.set(element, element.getAttribute('aria-hidden'));
|
|
408
425
|
crossfade.play(element, true, handoffInstant || isVerticalBarsSource(element));
|
|
409
|
-
element.setAttribute(marker, '');
|
|
410
|
-
element.setAttribute('aria-hidden', 'true');
|
|
411
|
-
element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
412
426
|
}
|
|
427
|
+
element.setAttribute(marker, '');
|
|
428
|
+
element.setAttribute('aria-hidden', 'true');
|
|
429
|
+
if (newlyProjected) element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
413
430
|
}
|
|
414
431
|
const received = pendingActivations;
|
|
415
432
|
pendingActivations = [];
|
|
@@ -675,7 +692,7 @@ export const createRuntime = async (
|
|
|
675
692
|
event.revision < acceptedRevision ||
|
|
676
693
|
event.revision > revision ||
|
|
677
694
|
event.sequence <= lastSequence ||
|
|
678
|
-
overlayOpen(false)
|
|
695
|
+
overlayOpen(false, true)
|
|
679
696
|
)
|
|
680
697
|
return;
|
|
681
698
|
// Native may send input before update() resolves on the JS bridge.
|
|
@@ -688,7 +705,7 @@ export const createRuntime = async (
|
|
|
688
705
|
const element = actions.get(event.id);
|
|
689
706
|
if (!element) return;
|
|
690
707
|
const owner = Array.from(sources.keys()).find((source) => source === element || source.contains(element));
|
|
691
|
-
if (!owner) return;
|
|
708
|
+
if (!owner || !inVerticalBarsSurface(owner)) return;
|
|
692
709
|
const direct = !blocked(owner) && unprojected(sources.keys(), () => readEnabledCandidate(owner));
|
|
693
710
|
const candidate = direct || read().find((candidate) => candidate.actions.has(event.id));
|
|
694
711
|
const item = candidate?.control.items.find((item) => item.id === event.id);
|
|
@@ -54,6 +54,10 @@ export const appendItem = (
|
|
|
54
54
|
const labelStyle = getComputedStyle(labelElement);
|
|
55
55
|
const badge = child.querySelector<HTMLElement>('ion-badge');
|
|
56
56
|
const badgeStyle = badge && visible(badge) ? getComputedStyle(badge) : undefined;
|
|
57
|
+
const outline =
|
|
58
|
+
isVerticalBarsSource(child) && child.matches('ion-button') && (child as HTMLIonButtonElement).fill === 'outline'
|
|
59
|
+
? getComputedStyle(native ?? child)
|
|
60
|
+
: undefined;
|
|
57
61
|
const item: ShellItem = {
|
|
58
62
|
id: id(child),
|
|
59
63
|
...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
|
|
@@ -71,6 +75,12 @@ export const appendItem = (
|
|
|
71
75
|
fontSize: parseFloat(labelStyle.fontSize),
|
|
72
76
|
fontWeight: parseInt(labelStyle.fontWeight, 10) || 400,
|
|
73
77
|
color: getComputedStyle(native ?? child).color,
|
|
78
|
+
backgroundColor:
|
|
79
|
+
isVerticalBarsSource(child) && child.matches('ion-button') && (child as HTMLIonButtonElement).fill === 'solid'
|
|
80
|
+
? getComputedStyle(native ?? child).backgroundColor
|
|
81
|
+
: undefined,
|
|
82
|
+
borderColor: outline?.borderTopColor,
|
|
83
|
+
borderWidth: outline ? parseFloat(outline.borderTopWidth) : undefined,
|
|
74
84
|
badge: badgeStyle
|
|
75
85
|
? { value: badge!.textContent?.trim() ?? '', color: badgeStyle.backgroundColor, textColor: badgeStyle.color }
|
|
76
86
|
: undefined,
|
package/src/native/shared/dom.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fadeMarker } from './crossfade';
|
|
2
|
+
import { modalUsesVerticalBars } from './modal';
|
|
2
3
|
|
|
3
4
|
import type { Frame } from '../definitions';
|
|
4
5
|
|
|
@@ -98,7 +99,8 @@ const verticalBarsTags = new Set(['ion-button', 'ion-back-button', 'ion-buttons'
|
|
|
98
99
|
export const isVerticalBarsSource = (element: HTMLElement): boolean =>
|
|
99
100
|
verticalBarsTags.has(element.localName) &&
|
|
100
101
|
(element.matches('ion-tab-bar') || verticalBarsOwned(element)) &&
|
|
101
|
-
!element.closest('ion-menu, ion-
|
|
102
|
+
!element.closest('ion-menu, ion-popover') &&
|
|
103
|
+
modalUsesVerticalBars(element) &&
|
|
102
104
|
!!element.closest('ion-app.ios-theme-vertical-bars');
|
|
103
105
|
|
|
104
106
|
export const isVerticalBarsBackPosition = (element: HTMLElement): boolean => {
|
|
@@ -109,7 +111,8 @@ export const isVerticalBarsBackPosition = (element: HTMLElement): boolean => {
|
|
|
109
111
|
(back) =>
|
|
110
112
|
back.closest('.ion-page') === page &&
|
|
111
113
|
!back.matches('.ion-cloned-element') &&
|
|
112
|
-
!back.closest('ion-header[collapse], ion-footer[collapse], ion-menu, ion-
|
|
114
|
+
!back.closest('ion-header[collapse], ion-footer[collapse], ion-menu, ion-popover') &&
|
|
115
|
+
modalUsesVerticalBars(back),
|
|
113
116
|
);
|
|
114
117
|
const rank = (back: HTMLElement) =>
|
|
115
118
|
inFixedToolbar(back) ? 0 : back.closest('ion-header ion-toolbar, ion-footer ion-toolbar') ? 1 : back.closest('ion-toolbar') ? 3 : 2;
|
|
@@ -141,11 +144,7 @@ export const isShellDisabled = (element: Element): boolean =>
|
|
|
141
144
|
|
|
142
145
|
export const isVerticalBarsToolbarActionShape = (element: HTMLElement): boolean =>
|
|
143
146
|
element.matches('ion-menu-button') ||
|
|
144
|
-
(element.matches('ion-button') &&
|
|
145
|
-
!!element.querySelector('ion-icon, svg') &&
|
|
146
|
-
!childNodesOf(element).some((node) => node.nodeType === 3 && !!node.textContent?.trim()) &&
|
|
147
|
-
!element.matches('.ion-color, [color]') &&
|
|
148
|
-
['default', 'clear'].includes((element as HTMLIonButtonElement).fill ?? element.getAttribute('fill') ?? 'default'));
|
|
147
|
+
(element.matches('ion-button') && !!element.querySelector('ion-icon[slot="icon-only"], svg[slot="icon-only"]'));
|
|
149
148
|
|
|
150
149
|
// Placement belongs to the DOM identity for one routed-page epoch. Changes to
|
|
151
150
|
// content/disabled state affect rendering, never its chosen surface.
|
|
@@ -161,7 +160,7 @@ export const verticalBarsOwned = (element: HTMLElement): boolean => verticalBars
|
|
|
161
160
|
export const isVerticalBarsToolbarAction = (element: HTMLElement): boolean =>
|
|
162
161
|
verticalBarsOwned(element) && !isExcluded(element, verticalBarsEnteringPage(element)) && !isShellDisabled(element);
|
|
163
162
|
|
|
164
|
-
const overlays = 'ion-menu, ion-
|
|
163
|
+
const overlays = 'ion-menu, ion-popover';
|
|
165
164
|
|
|
166
165
|
/** Shared eligibility for a toolbar action candidate, before placement is decided (prehide) or consumed (projection). */
|
|
167
166
|
export const verticalBarsActionCandidate = (element: HTMLElement): boolean =>
|
|
@@ -169,7 +168,8 @@ export const verticalBarsActionCandidate = (element: HTMLElement): boolean =>
|
|
|
169
168
|
!element.closest('ion-buttons.ios-theme-horizontal-only, ion-button.ios-theme-horizontal-only') &&
|
|
170
169
|
!isPermanentlyExcluded(element) &&
|
|
171
170
|
!isShellDisabled(element) &&
|
|
172
|
-
!element.closest(overlays)
|
|
171
|
+
!element.closest(overlays) &&
|
|
172
|
+
modalUsesVerticalBars(element);
|
|
173
173
|
|
|
174
174
|
/** Shared eligibility for a back-button candidate; the back button may sit outside a fixed toolbar. */
|
|
175
175
|
export const verticalBarsBackCandidate = (element: HTMLElement): boolean =>
|
|
@@ -177,7 +177,8 @@ export const verticalBarsBackCandidate = (element: HTMLElement): boolean =>
|
|
|
177
177
|
!element.closest('ion-buttons.ios-theme-horizontal-only') &&
|
|
178
178
|
!isPermanentlyExcluded(element) &&
|
|
179
179
|
!isShellDisabled(element) &&
|
|
180
|
-
!element.closest(overlays)
|
|
180
|
+
!element.closest(overlays) &&
|
|
181
|
+
modalUsesVerticalBars(element);
|
|
181
182
|
|
|
182
183
|
export const verticalBarsToolbarActions = (element: HTMLElement): HTMLElement[] =>
|
|
183
184
|
// Back buttons use the dedicated rail slot; they are never toolbar actions.
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Frame } from '../definitions';
|
|
2
|
+
|
|
3
|
+
/** Modal hosts cover the viewport even when their visible dialog is centered. */
|
|
4
|
+
export const modalRailClass = 'ios-theme-vertical-bars-modal';
|
|
5
|
+
const states = new WeakMap<Document, { top?: HTMLIonModalElement }>();
|
|
6
|
+
|
|
7
|
+
export const topModal = (doc: Document): HTMLIonModalElement | undefined => states.get(doc)?.top;
|
|
8
|
+
export const modalUsesVerticalBars = (element: Element): boolean => {
|
|
9
|
+
const modal = element.closest('ion-modal');
|
|
10
|
+
return !modal || modal.classList.contains(modalRailClass);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Only the foreground modal may contribute controls; never the covered page. */
|
|
14
|
+
export const inVerticalBarsSurface = (element: Element): boolean => {
|
|
15
|
+
const modal = topModal(element.ownerDocument);
|
|
16
|
+
return modal ? element.closest('ion-modal') === modal && modalUsesVerticalBars(element) : !element.closest('ion-modal');
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const observeVerticalBarsModals = (doc: Document): (() => void) => {
|
|
20
|
+
const win = doc.defaultView!;
|
|
21
|
+
const state: { top?: HTMLIonModalElement } = {};
|
|
22
|
+
states.set(doc, state);
|
|
23
|
+
const presenting = new Set<HTMLIonModalElement>();
|
|
24
|
+
const dismissed = new WeakSet<HTMLIonModalElement>();
|
|
25
|
+
const observed = new Set<Element>();
|
|
26
|
+
const listeners = new AbortController();
|
|
27
|
+
let stopped = false;
|
|
28
|
+
let frame = 0;
|
|
29
|
+
let pointerActive = false;
|
|
30
|
+
let bounds = '';
|
|
31
|
+
const toolbarTop = '--ios-theme-vertical-bars-modal-offset-top';
|
|
32
|
+
const refresh = () => {
|
|
33
|
+
win.cancelAnimationFrame(frame);
|
|
34
|
+
frame = 0;
|
|
35
|
+
if (stopped) return;
|
|
36
|
+
const app = doc.querySelector('ion-app.ios-theme-vertical-bars');
|
|
37
|
+
const modals = Array.from(doc.querySelectorAll<HTMLIonModalElement>('ion-modal'));
|
|
38
|
+
const active = modals.filter(
|
|
39
|
+
(modal) => !dismissed.has(modal) && ((modal as HTMLIonModalElement & { presented?: boolean }).presented || presenting.has(modal)),
|
|
40
|
+
);
|
|
41
|
+
for (const modal of presenting) if (!modal.isConnected) presenting.delete(modal);
|
|
42
|
+
const top = active.sort((a, b) => a.overlayIndex - b.overlayIndex).slice(-1)[0];
|
|
43
|
+
let changed = state.top !== top;
|
|
44
|
+
state.top = top;
|
|
45
|
+
const nextBounds = top ? JSON.stringify(modalVerticalBarFrame(top)) : '';
|
|
46
|
+
changed ||= bounds !== nextBounds;
|
|
47
|
+
bounds = nextBounds;
|
|
48
|
+
const wanted = new Set<Element>();
|
|
49
|
+
for (const modal of modals) {
|
|
50
|
+
const content = modal.shadowRoot?.querySelector<HTMLElement>('[part~="content"]');
|
|
51
|
+
if (content) wanted.add(content);
|
|
52
|
+
// offsetWidth is unaffected by Ionic's presentation transforms.
|
|
53
|
+
const fullWidth = !!content && Math.abs(content.offsetWidth - win.innerWidth) <= 1 && Math.abs(content.offsetLeft) <= 1;
|
|
54
|
+
const enabled = !!app?.contains(modal) && active.includes(modal) && fullWidth;
|
|
55
|
+
if (enabled) {
|
|
56
|
+
const top = modalVerticalBarFrame(modal)?.y ?? 0;
|
|
57
|
+
const offset = `${top}px`;
|
|
58
|
+
if (modal.style.getPropertyValue(toolbarTop) !== offset) {
|
|
59
|
+
modal.style.setProperty(toolbarTop, offset);
|
|
60
|
+
changed = true;
|
|
61
|
+
}
|
|
62
|
+
} else modal.style.removeProperty(toolbarTop);
|
|
63
|
+
if (modal.classList.contains(modalRailClass) !== enabled) {
|
|
64
|
+
modal.classList.toggle(modalRailClass, enabled);
|
|
65
|
+
changed = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (wanted.size !== observed.size || [...wanted].some((element) => !observed.has(element))) {
|
|
69
|
+
resize?.disconnect();
|
|
70
|
+
observer.disconnect();
|
|
71
|
+
observer.observe(doc.documentElement, observation);
|
|
72
|
+
observed.clear();
|
|
73
|
+
for (const element of wanted) {
|
|
74
|
+
resize?.observe(element);
|
|
75
|
+
observer.observe(element, { attributes: true, attributeFilter: ['style', 'class'] });
|
|
76
|
+
observed.add(element);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (changed) win.dispatchEvent(new Event('nativeUIShellRefresh'));
|
|
80
|
+
if (
|
|
81
|
+
top &&
|
|
82
|
+
(pointerActive ||
|
|
83
|
+
Array.from(wanted).some((element) => element.getAnimations().some((animation) => animation.playState === 'running')))
|
|
84
|
+
)
|
|
85
|
+
schedule();
|
|
86
|
+
};
|
|
87
|
+
const schedule = () => {
|
|
88
|
+
if (!stopped && !frame) frame = win.requestAnimationFrame(refresh);
|
|
89
|
+
};
|
|
90
|
+
const resize = win.ResizeObserver ? new win.ResizeObserver(schedule) : undefined;
|
|
91
|
+
const observer = new MutationObserver(schedule);
|
|
92
|
+
const observation = { subtree: true, childList: true, attributes: true, attributeFilter: ['class', 'style'] };
|
|
93
|
+
observer.observe(doc.documentElement, observation);
|
|
94
|
+
for (const name of ['ionModalWillPresent', 'ionModalDidPresent', 'ionModalWillDismiss', 'ionModalDidDismiss', 'ionBreakpointDidChange']) {
|
|
95
|
+
doc.addEventListener(
|
|
96
|
+
name,
|
|
97
|
+
(event) => {
|
|
98
|
+
const modal = event.target as HTMLIonModalElement;
|
|
99
|
+
if (modal.localName !== 'ion-modal') return;
|
|
100
|
+
if (name === 'ionModalDidDismiss') {
|
|
101
|
+
presenting.delete(modal);
|
|
102
|
+
dismissed.add(modal);
|
|
103
|
+
} else {
|
|
104
|
+
presenting.add(modal);
|
|
105
|
+
dismissed.delete(modal);
|
|
106
|
+
}
|
|
107
|
+
refresh();
|
|
108
|
+
},
|
|
109
|
+
{ capture: true, signal: listeners.signal },
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
doc.addEventListener(
|
|
113
|
+
'pointerdown',
|
|
114
|
+
(event) => {
|
|
115
|
+
if ((event.target as Element).closest('ion-modal')) {
|
|
116
|
+
pointerActive = true;
|
|
117
|
+
schedule();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{ capture: true, signal: listeners.signal },
|
|
121
|
+
);
|
|
122
|
+
for (const name of ['pointerup', 'pointercancel'])
|
|
123
|
+
doc.addEventListener(
|
|
124
|
+
name,
|
|
125
|
+
() => {
|
|
126
|
+
pointerActive = false;
|
|
127
|
+
schedule();
|
|
128
|
+
},
|
|
129
|
+
{ capture: true, signal: listeners.signal },
|
|
130
|
+
);
|
|
131
|
+
win.addEventListener('resize', schedule, { signal: listeners.signal });
|
|
132
|
+
win.addEventListener('nativeUIShellRefresh', schedule, { signal: listeners.signal });
|
|
133
|
+
refresh();
|
|
134
|
+
return () => {
|
|
135
|
+
stopped = true;
|
|
136
|
+
win.cancelAnimationFrame(frame);
|
|
137
|
+
observer.disconnect();
|
|
138
|
+
resize?.disconnect();
|
|
139
|
+
listeners.abort();
|
|
140
|
+
if (states.get(doc) !== state) return;
|
|
141
|
+
states.delete(doc);
|
|
142
|
+
doc.querySelectorAll<HTMLElement>(`.${modalRailClass}`).forEach((modal) => {
|
|
143
|
+
modal.classList.remove(modalRailClass);
|
|
144
|
+
modal.style.removeProperty(toolbarTop);
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/** Visible modal bounds, clipped at the viewport for Ionic's translated sheets. */
|
|
150
|
+
export const modalVerticalBarFrame = (modal: Element): Frame | undefined => {
|
|
151
|
+
const rect = modal.shadowRoot?.querySelector('[part~="content"]')?.getBoundingClientRect();
|
|
152
|
+
const win = modal.ownerDocument.defaultView!;
|
|
153
|
+
if (!rect) return;
|
|
154
|
+
const y = Math.max(0, rect.top);
|
|
155
|
+
const height = Math.min(win.innerHeight, rect.bottom) - y;
|
|
156
|
+
return height > 0 ? { x: 0, y, width: win.innerWidth, height } : undefined;
|
|
157
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inVerticalBarsSurface, topModal } from './shared/modal';
|
|
1
2
|
import type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellStatus } from './definitions';
|
|
2
3
|
import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
|
|
3
4
|
import {
|
|
@@ -74,11 +75,13 @@ export const createVerticalBarsWebProjection = (
|
|
|
74
75
|
});
|
|
75
76
|
const inEligibleToolbar = (element: HTMLElement) =>
|
|
76
77
|
!!verticalBarsRoot()?.contains(element) &&
|
|
78
|
+
inVerticalBarsSurface(element) &&
|
|
77
79
|
verticalBarsActionCandidate(element) &&
|
|
78
80
|
!isExcluded(element, verticalBarsEnteringPage(element)) &&
|
|
79
81
|
!verticalBarsPages.isDeparted(element);
|
|
80
82
|
const isEligibleBack = (element: HTMLIonBackButtonElement) =>
|
|
81
83
|
!!verticalBarsRoot()?.contains(element) &&
|
|
84
|
+
inVerticalBarsSurface(element) &&
|
|
82
85
|
verticalBarsOwned(element) &&
|
|
83
86
|
verticalBarsBackCandidate(element) &&
|
|
84
87
|
!isExcluded(element, verticalBarsEnteringPage(element)) &&
|
|
@@ -151,7 +154,10 @@ export const createVerticalBarsWebProjection = (
|
|
|
151
154
|
const syncAction = (target: HTMLElement, original: HTMLElement) => {
|
|
152
155
|
copyAttributes(target, original);
|
|
153
156
|
target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');
|
|
154
|
-
const label =
|
|
157
|
+
const label =
|
|
158
|
+
original.getAttribute('aria-label') ??
|
|
159
|
+
original.shadowRoot?.querySelector('[part~=native]')?.getAttribute('aria-label') ??
|
|
160
|
+
original.textContent?.trim();
|
|
155
161
|
if (label) target.setAttribute('aria-label', label);
|
|
156
162
|
if ('disabled' in original) (target as HTMLIonButtonElement).disabled = (original as HTMLIonButtonElement).disabled;
|
|
157
163
|
if (original.matches('ion-button')) {
|
|
@@ -185,7 +191,7 @@ export const createVerticalBarsWebProjection = (
|
|
|
185
191
|
}
|
|
186
192
|
};
|
|
187
193
|
const project = (nextBack: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) => {
|
|
188
|
-
root = verticalBarsRoot()!;
|
|
194
|
+
root = topModal(doc) ?? verticalBarsRoot()!;
|
|
189
195
|
let topOffset = 0;
|
|
190
196
|
if (nextBack) {
|
|
191
197
|
backSource = nextBack;
|
|
@@ -269,7 +275,7 @@ export const createVerticalBarsWebProjection = (
|
|
|
269
275
|
const nextBack = findBack();
|
|
270
276
|
const groups = findToolbarGroups();
|
|
271
277
|
if (!nextBack && !groups.length) return restore();
|
|
272
|
-
if (sameSources(nextBack, groups)) return syncExisting();
|
|
278
|
+
if (root === (topModal(doc) ?? currentRoot) && sameSources(nextBack, groups)) return syncExisting();
|
|
273
279
|
restore();
|
|
274
280
|
project(nextBack, groups);
|
|
275
281
|
updates++;
|
|
@@ -287,6 +293,8 @@ export const createVerticalBarsWebProjection = (
|
|
|
287
293
|
};
|
|
288
294
|
const schedule = (): Promise<void> => {
|
|
289
295
|
if (stopped) return Promise.resolve();
|
|
296
|
+
// Release Web ownership before the native runtime can acknowledge its next frame.
|
|
297
|
+
if (!enabled()) restore();
|
|
290
298
|
const done = new Promise<void>((resolve) => waiters.push(resolve));
|
|
291
299
|
if (!frame) frame = win.requestAnimationFrame(update);
|
|
292
300
|
return done;
|
|
@@ -336,6 +344,7 @@ export const createVerticalBarsWebProjection = (
|
|
|
336
344
|
doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });
|
|
337
345
|
for (const name of ['ionModalWillPresent', 'ionModalDidDismiss'])
|
|
338
346
|
doc.addEventListener(name, schedule, { capture: true, signal: listeners.signal });
|
|
347
|
+
win.addEventListener('resize', schedule, { signal: listeners.signal });
|
|
339
348
|
win.addEventListener('nativeUIShellRefresh', schedule, { signal: listeners.signal });
|
|
340
349
|
schedule();
|
|
341
350
|
|