@rdlabo/ionic-theme-ios27 1.1.0 → 1.2.0-0
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 +8 -1
- package/dist/css/components/ion-list.css +1 -1
- package/dist/css/components/ion-menu.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -8
- package/dist/native/components/index.d.ts +1 -0
- package/dist/native/components/index.d.ts.map +1 -1
- package/dist/native/components/index.js +8 -3
- package/dist/native/components/ion-back-button.d.ts.map +1 -1
- package/dist/native/components/ion-back-button.js +5 -2
- package/dist/native/components/ion-button.d.ts.map +1 -1
- package/dist/native/components/ion-button.js +10 -2
- package/dist/native/components/ion-buttons.d.ts.map +1 -1
- package/dist/native/components/ion-buttons.js +12 -4
- package/dist/native/components/ion-fab.js +4 -4
- package/dist/native/components/ion-menu-button.d.ts.map +1 -1
- package/dist/native/components/ion-menu-button.js +5 -4
- package/dist/native/components/searchable-tabs.d.ts.map +1 -1
- package/dist/native/components/searchable-tabs.js +4 -1
- package/dist/native/definitions.d.ts +29 -3
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/definitions.js +6 -1
- package/dist/native/index.d.ts +6 -2
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +154 -17
- package/dist/native/prehide.d.ts +5 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +246 -0
- package/dist/native/runtime.d.ts +1 -1
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +162 -28
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +7 -3
- package/dist/native/shared/dom.d.ts +33 -2
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +171 -10
- package/dist/native/vertical-bars-web.d.ts +3 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +341 -0
- package/dist/native-integration/index.d.ts +3 -2
- package/dist/native-integration/index.d.ts.map +1 -1
- package/dist/native-integration/index.js +11 -5
- package/dist/transition/index.d.ts +2 -2
- package/dist/transition/index.d.ts.map +1 -1
- package/dist/transition/index.js +1 -1
- package/dist/transition/ios.transition.d.ts.map +1 -1
- package/dist/transition/ios.transition.js +1 -0
- package/dist/utils.d.ts +3 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/vertical-bars.d.ts +4 -0
- package/dist/vertical-bars.d.ts.map +1 -0
- package/dist/vertical-bars.js +2 -0
- package/docs/iphone-duo.md +255 -0
- package/docs/migration.md +2 -2
- package/docs/native-ui-shell.md +15 -3
- package/docs/special-markup.md +6 -0
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +15 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +358 -0
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +215 -35
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +10 -1
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +29 -0
- package/package.json +6 -2
- package/src/index.ts +15 -8
- package/src/native/components/index.ts +12 -3
- package/src/native/components/ion-back-button.ts +8 -2
- package/src/native/components/ion-button.ts +12 -2
- package/src/native/components/ion-buttons.ts +9 -4
- package/src/native/components/ion-fab.ts +4 -4
- package/src/native/components/ion-menu-button.ts +5 -4
- package/src/native/components/searchable-tabs.ts +9 -1
- package/src/native/definitions.ts +40 -3
- package/src/native/index.ts +188 -20
- package/src/native/prehide.ts +255 -0
- package/src/native/runtime.ts +173 -27
- package/src/native/shared/candidate.ts +11 -3
- package/src/native/shared/dom.ts +212 -11
- package/src/native/vertical-bars-web.ts +374 -0
- package/src/native-integration/index.ts +13 -8
- package/src/styles/components/ion-list.scss +2 -2
- package/src/styles/components/ion-menu.scss +2 -2
- package/src/styles/ionic-theme-ios27.scss +1 -0
- package/src/styles/vertical-bars.scss +342 -0
- package/src/transition/index.ts +11 -6
- package/src/transition/ios.transition.ts +1 -0
- package/src/utils.ts +8 -8
- package/src/vertical-bars.ts +3 -0
- package/dist/native/lifecycle.d.ts +0 -4
- package/dist/native/lifecycle.d.ts.map +0 -1
- package/dist/native/lifecycle.js +0 -34
- package/src/native/lifecycle.ts +0 -38
package/src/native/runtime.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PluginListenerHandle } from '@capacitor/core';
|
|
2
2
|
import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
|
|
3
|
-
import { getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
|
|
3
|
+
import { VERTICAL_BARS_TRANSITION_CANCELED, getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
|
|
4
4
|
import { createSearchSupport } from './components/searchable-tabs';
|
|
5
5
|
import type {
|
|
6
6
|
ShellActivation,
|
|
@@ -10,8 +10,17 @@ import type {
|
|
|
10
10
|
NativeUIShellPlugin,
|
|
11
11
|
NativeUIShellStatus,
|
|
12
12
|
} from './definitions';
|
|
13
|
-
import { readCandidate, selector, shadowSelector, motionSelector } from './components';
|
|
14
|
-
import {
|
|
13
|
+
import { readCandidate, selector, shadowSelector, motionSelector, isVerticalBarsCandidate } from './components';
|
|
14
|
+
import {
|
|
15
|
+
activateProjectedElement,
|
|
16
|
+
createVerticalBarsPageState,
|
|
17
|
+
isVerticalBarsSource,
|
|
18
|
+
preferredVerticalBarsBack,
|
|
19
|
+
marker,
|
|
20
|
+
prehideOnlyMutation,
|
|
21
|
+
rejectedClass,
|
|
22
|
+
unprojected,
|
|
23
|
+
} from './shared/dom';
|
|
15
24
|
import { createIconRenderer } from './shared/icons';
|
|
16
25
|
import type { Candidate } from './shared/candidate';
|
|
17
26
|
import { CSS_MOTION_EVENTS } from './shared/events';
|
|
@@ -19,6 +28,8 @@ import { createCrossfade, fadeMarker } from './shared/crossfade';
|
|
|
19
28
|
|
|
20
29
|
const overlays = 'ion-modal, ion-popover, ion-alert, ion-action-sheet, ion-loading, ion-picker, ion-toast, ion-menu';
|
|
21
30
|
const overlayNames = ['Modal', 'Popover', 'Alert', 'ActionSheet', 'Loading', 'Picker', 'Toast'];
|
|
31
|
+
const verticalBarsMarker = 'data-native-ui-shell-vertical-bars';
|
|
32
|
+
const verticalBarsMemberMarker = 'data-native-ui-shell-vertical-bars-member';
|
|
22
33
|
|
|
23
34
|
// A failed bridge must not leave the source inaccessible indefinitely.
|
|
24
35
|
const bounded = <T>(promise: Promise<T>): Promise<T> =>
|
|
@@ -31,6 +42,8 @@ export const createRuntime = async (
|
|
|
31
42
|
doc: Document,
|
|
32
43
|
plugin: NativeUIShellPlugin,
|
|
33
44
|
options: NativeUIShellOptions = {},
|
|
45
|
+
nativeVerticalBars: () => boolean = () => true,
|
|
46
|
+
verticalBarsOnly = false,
|
|
34
47
|
): Promise<NativeUIShellHandle> => {
|
|
35
48
|
const win = doc.defaultView!;
|
|
36
49
|
const icons = createIconRenderer();
|
|
@@ -38,8 +51,11 @@ export const createRuntime = async (
|
|
|
38
51
|
const ids = new WeakMap<Element, string>();
|
|
39
52
|
let rejected = new WeakMap<HTMLElement, string>();
|
|
40
53
|
const sources = new Map<HTMLElement, string | null>();
|
|
54
|
+
const verticalBarsOwners = new Set<HTMLElement>();
|
|
55
|
+
const verticalBarsMembers = new Set<HTMLElement>();
|
|
41
56
|
const suspended = new Set<HTMLElement[]>();
|
|
42
57
|
const pages = new Set<HTMLElement>();
|
|
58
|
+
const verticalBarsPages = createVerticalBarsPageState();
|
|
43
59
|
const presented = new Set<HTMLElement>();
|
|
44
60
|
const manualSuspensions = new Set<symbol>();
|
|
45
61
|
const moving = new Map<HTMLElement, Set<string>>();
|
|
@@ -95,13 +111,22 @@ export const createRuntime = async (
|
|
|
95
111
|
const style = doc.createElement('style');
|
|
96
112
|
const hidden = `[${marker}]:not([${fadeMarker}])`;
|
|
97
113
|
style.textContent = `${hidden}, ${hidden} *, ${hidden}::before, ${hidden}::after, ${hidden}::part(native) { visibility: hidden !important; }
|
|
98
|
-
[${marker}], [${marker}] * { pointer-events: none !important; }
|
|
114
|
+
[${marker}], [${marker}] * { pointer-events: none !important; }
|
|
115
|
+
/* Ionic disables the covered page while a menu is open. VerticalBars rail
|
|
116
|
+
controls remain outside that page; zero specificity preserves any
|
|
117
|
+
pointer-events rule supplied by the application itself. */
|
|
118
|
+
:where(.menu-content-open) :where([${verticalBarsMarker}]) {
|
|
119
|
+
pointer-events: auto;
|
|
120
|
+
}
|
|
121
|
+
:where(.menu-content-open) :where([${verticalBarsMarker}]) > :where(:not([${verticalBarsMemberMarker}])) {
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
}`;
|
|
99
124
|
|
|
100
125
|
const restore = (element: HTMLElement) => {
|
|
101
126
|
lastSnapshot = '';
|
|
102
127
|
search.release(element);
|
|
103
128
|
element.removeAttribute(marker);
|
|
104
|
-
if (!stopped) crossfade.play(element, false, handoffInstant);
|
|
129
|
+
if (!stopped) crossfade.play(element, false, handoffInstant || isVerticalBarsSource(element));
|
|
105
130
|
if (element.getAttribute('aria-hidden') === 'true') {
|
|
106
131
|
const previous = sources.get(element);
|
|
107
132
|
if (previous == null) element.removeAttribute('aria-hidden');
|
|
@@ -110,7 +135,13 @@ export const createRuntime = async (
|
|
|
110
135
|
sources.delete(element);
|
|
111
136
|
element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
112
137
|
};
|
|
113
|
-
const restoreAll = () =>
|
|
138
|
+
const restoreAll = () => {
|
|
139
|
+
Array.from(sources.keys()).forEach(restore);
|
|
140
|
+
verticalBarsOwners.forEach((element) => element.removeAttribute(verticalBarsMarker));
|
|
141
|
+
verticalBarsOwners.clear();
|
|
142
|
+
verticalBarsMembers.forEach((element) => element.removeAttribute(verticalBarsMemberMarker));
|
|
143
|
+
verticalBarsMembers.clear();
|
|
144
|
+
};
|
|
114
145
|
const finishWaiters = () => {
|
|
115
146
|
const current = waiters;
|
|
116
147
|
waiters = [];
|
|
@@ -123,8 +154,39 @@ export const createRuntime = async (
|
|
|
123
154
|
};
|
|
124
155
|
const search = createSearchSupport(doc, id, schedule);
|
|
125
156
|
const candidateSources = (candidate: Candidate) => candidate.sources ?? [candidate.element];
|
|
157
|
+
const setRejected = (element: HTMLElement, value: boolean) => {
|
|
158
|
+
element.classList.toggle(rejectedClass, value);
|
|
159
|
+
};
|
|
160
|
+
const measuringPointerPages = new WeakSet<HTMLElement>();
|
|
126
161
|
const readEnabledCandidate = (element: HTMLElement): Candidate | undefined => {
|
|
127
|
-
|
|
162
|
+
if (element.matches('ion-back-button') && element.closest('ion-app.ios-theme-vertical-bars') && !isVerticalBarsCandidate(element))
|
|
163
|
+
return;
|
|
164
|
+
if (verticalBarsOnly && !isVerticalBarsCandidate(element)) return;
|
|
165
|
+
const pointerPage = isVerticalBarsCandidate(element) ? element.closest<HTMLElement>('.ion-page') : undefined;
|
|
166
|
+
let candidate: Candidate | undefined;
|
|
167
|
+
if (pointerPage && getComputedStyle(pointerPage).pointerEvents === 'none') {
|
|
168
|
+
const previous = pointerPage.style.getPropertyValue('pointer-events');
|
|
169
|
+
const priority = pointerPage.style.getPropertyPriority('pointer-events');
|
|
170
|
+
const hadStyle = pointerPage.hasAttribute('style');
|
|
171
|
+
measuringPointerPages.add(pointerPage);
|
|
172
|
+
pointerPage.style.setProperty('pointer-events', 'auto', 'important');
|
|
173
|
+
try {
|
|
174
|
+
candidate = readCandidate(element, id);
|
|
175
|
+
} finally {
|
|
176
|
+
if (previous) pointerPage.style.setProperty('pointer-events', previous, priority);
|
|
177
|
+
else pointerPage.style.removeProperty('pointer-events');
|
|
178
|
+
if (!hadStyle && !pointerPage.style.length) pointerPage.removeAttribute('style');
|
|
179
|
+
win.setTimeout(() => measuringPointerPages.delete(pointerPage), 0);
|
|
180
|
+
}
|
|
181
|
+
} else candidate = readCandidate(element, id);
|
|
182
|
+
if (candidate && isVerticalBarsCandidate(element)) {
|
|
183
|
+
if (!nativeVerticalBars()) return undefined;
|
|
184
|
+
candidate.control.placement = 'vertical-bars';
|
|
185
|
+
if (['ion-button', 'ion-buttons', 'ion-menu-button'].includes(candidate.control.kind)) {
|
|
186
|
+
const slot = (element.matches('ion-buttons') ? element : (element.closest('ion-buttons') ?? element)).getAttribute('slot');
|
|
187
|
+
if (slot === 'start' || slot === 'end') candidate.control.toolbarSlot = slot;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
128
190
|
return candidate && controlEnabled(candidate) ? candidate : undefined;
|
|
129
191
|
};
|
|
130
192
|
const flush = async () => {
|
|
@@ -137,17 +199,22 @@ export const createRuntime = async (
|
|
|
137
199
|
}
|
|
138
200
|
};
|
|
139
201
|
const blocked = (element: HTMLElement) =>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
202
|
+
verticalBarsPages.isDeparted(element) ||
|
|
203
|
+
(!isVerticalBarsSource(element) &&
|
|
204
|
+
(Array.from(suspended).some((scopes) => scopes.some((scope) => scope.contains(element))) ||
|
|
205
|
+
Array.from(pages).some((scope) => scope.contains(element)) ||
|
|
206
|
+
Array.from(moving.keys()).some((surface) => surface.contains(element))));
|
|
143
207
|
const painted = () => new Promise<void>((resolve) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => resolve())));
|
|
144
|
-
const overlayOpen = () => {
|
|
208
|
+
const overlayOpen = (includeMenu = true) => {
|
|
145
209
|
for (const element of presented) if (!element.isConnected) presented.delete(element);
|
|
210
|
+
const presentedOverlayOpen = Array.from(presented).some((element) => includeMenu || !element.matches('ion-menu'));
|
|
146
211
|
return (
|
|
147
212
|
manualSuspensions.size > 0 ||
|
|
148
|
-
|
|
213
|
+
presentedOverlayOpen ||
|
|
149
214
|
Array.from(doc.querySelectorAll(overlays)).some(
|
|
150
|
-
(element) =>
|
|
215
|
+
(element) =>
|
|
216
|
+
(includeMenu || !element.matches('ion-menu')) &&
|
|
217
|
+
((element as Element & { presented?: boolean }).presented || element.classList.contains('show-menu')),
|
|
151
218
|
)
|
|
152
219
|
);
|
|
153
220
|
};
|
|
@@ -158,19 +225,33 @@ export const createRuntime = async (
|
|
|
158
225
|
search.keepSearchTabsVisible();
|
|
159
226
|
for (const page of pages) if (!page.isConnected) pages.delete(page);
|
|
160
227
|
for (const surface of moving.keys()) if (!surface.isConnected) moving.delete(surface);
|
|
161
|
-
if (doc.hidden || overlayOpen()) return [];
|
|
228
|
+
if (doc.hidden || overlayOpen(false)) return [];
|
|
162
229
|
if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0) && !search.hasActive()) return [];
|
|
163
|
-
|
|
230
|
+
const menuOpen = overlayOpen();
|
|
231
|
+
const candidates = unprojected(sources.keys(), () =>
|
|
164
232
|
search
|
|
165
233
|
.decorate(
|
|
166
234
|
Array.from(doc.querySelectorAll<HTMLElement>(selector))
|
|
167
|
-
.filter(
|
|
235
|
+
.filter(
|
|
236
|
+
(element) => !verticalBarsPages.isDeparted(element) && (!blocked(element) || (menuOpen && isVerticalBarsSource(element))),
|
|
237
|
+
)
|
|
168
238
|
.map(readEnabledCandidate)
|
|
169
239
|
.filter((candidate): candidate is Candidate => !!candidate),
|
|
170
240
|
blocked,
|
|
171
241
|
)
|
|
172
242
|
.filter((candidate) => !rejected.has(candidate.element) || rejected.get(candidate.element) !== signature(candidate)),
|
|
173
243
|
);
|
|
244
|
+
const back = preferredVerticalBarsBack(
|
|
245
|
+
candidates
|
|
246
|
+
.filter((candidate) => candidate.control.kind === 'ion-back-button' && isVerticalBarsCandidate(candidate.element))
|
|
247
|
+
.map((candidate) => candidate.element),
|
|
248
|
+
doc,
|
|
249
|
+
);
|
|
250
|
+
const selected = candidates.filter(
|
|
251
|
+
(candidate) =>
|
|
252
|
+
candidate.control.kind !== 'ion-back-button' || !isVerticalBarsCandidate(candidate.element) || candidate.element === back,
|
|
253
|
+
);
|
|
254
|
+
return menuOpen ? selected.filter((candidate) => isVerticalBarsCandidate(candidate.element)) : selected;
|
|
174
255
|
};
|
|
175
256
|
const observe = () => {
|
|
176
257
|
const wanted = new Set<Element | ShadowRoot>();
|
|
@@ -202,6 +283,8 @@ export const createRuntime = async (
|
|
|
202
283
|
reason = error instanceof Error ? error.message : String(error);
|
|
203
284
|
console.warn('[Native UI Shell] Returning to Web:', reason);
|
|
204
285
|
await handle.destroy();
|
|
286
|
+
// The Web fallback re-evaluates eligibility once native projection is stopped.
|
|
287
|
+
win.dispatchEvent(new Event('nativeUIShellRefresh'));
|
|
205
288
|
};
|
|
206
289
|
const sync = async () => {
|
|
207
290
|
frame = 0;
|
|
@@ -239,12 +322,18 @@ export const createRuntime = async (
|
|
|
239
322
|
removed.forEach(restore);
|
|
240
323
|
// The outgoing tab is no longer visible, so waiting two frames only leaves its
|
|
241
324
|
// native snapshot over the destination. Stack transitions still need the paint.
|
|
242
|
-
if (!handoffInstant) {
|
|
325
|
+
if (!handoffInstant && removed.some((element) => !isVerticalBarsSource(element))) {
|
|
243
326
|
await painted();
|
|
244
327
|
if (stopped || dirty) return;
|
|
245
328
|
}
|
|
246
329
|
}
|
|
247
|
-
const
|
|
330
|
+
const root = doc.querySelector('ion-app.ios-theme-vertical-bars');
|
|
331
|
+
const verticalBarEdge: 'left' | 'right' | undefined = root
|
|
332
|
+
? root.classList.contains('ios-theme-vertical-bars-left')
|
|
333
|
+
? 'left'
|
|
334
|
+
: 'right'
|
|
335
|
+
: undefined;
|
|
336
|
+
const data = { viewportWidth: win.innerWidth, verticalBarEdge, controls: candidates.map((candidate) => candidate.control) };
|
|
248
337
|
const serialized = JSON.stringify(data);
|
|
249
338
|
if (serialized === lastSnapshot && !forceRefresh) return;
|
|
250
339
|
const snapshot: ShellSnapshot = { ...data, revision: ++revision, transitionDuration: crossfade.duration(handoffInstant) };
|
|
@@ -268,6 +357,7 @@ export const createRuntime = async (
|
|
|
268
357
|
for (const candidate of candidates) {
|
|
269
358
|
if (result.rejectedControls?.includes(candidate.control.id)) {
|
|
270
359
|
rejected.set(candidate.element, signatures.get(candidate.element)!);
|
|
360
|
+
setRejected(candidate.element, true);
|
|
271
361
|
dirty = true;
|
|
272
362
|
}
|
|
273
363
|
}
|
|
@@ -276,6 +366,15 @@ export const createRuntime = async (
|
|
|
276
366
|
const current = dirty ? new Map(currentCandidates.map((candidate) => [candidate.element, signature(candidate)])) : signatures;
|
|
277
367
|
const currentSources = new Set(currentCandidates.flatMap(candidateSources));
|
|
278
368
|
const accepted = candidates.filter((candidate) => current.get(candidate.element) === signatures.get(candidate.element));
|
|
369
|
+
accepted.forEach((candidate) => setRejected(candidate.element, false));
|
|
370
|
+
const acceptedVerticalBarsOwners = new Set(
|
|
371
|
+
accepted.filter((candidate) => candidate.control.placement === 'vertical-bars').map((candidate) => candidate.element),
|
|
372
|
+
);
|
|
373
|
+
const acceptedVerticalBarsMembers = new Set(
|
|
374
|
+
accepted
|
|
375
|
+
.filter((candidate) => candidate.control.placement === 'vertical-bars')
|
|
376
|
+
.flatMap((candidate) => Array.from(candidate.actions.values())),
|
|
377
|
+
);
|
|
279
378
|
const invalidated = candidates.length !== accepted.length;
|
|
280
379
|
acceptedRevision = result.revision;
|
|
281
380
|
lastSnapshot = invalidated ? '' : serialized;
|
|
@@ -285,10 +384,28 @@ export const createRuntime = async (
|
|
|
285
384
|
// Keep an existing cover while its content catches up. Only an ineligible
|
|
286
385
|
// source needs to return to Web; new sources still require an exact ack.
|
|
287
386
|
for (const element of sources.keys()) if (!currentSources.has(element)) restore(element);
|
|
387
|
+
for (const element of verticalBarsOwners) {
|
|
388
|
+
if (acceptedVerticalBarsOwners.has(element)) continue;
|
|
389
|
+
element.removeAttribute(verticalBarsMarker);
|
|
390
|
+
verticalBarsOwners.delete(element);
|
|
391
|
+
}
|
|
392
|
+
for (const element of acceptedVerticalBarsOwners) {
|
|
393
|
+
element.setAttribute(verticalBarsMarker, '');
|
|
394
|
+
verticalBarsOwners.add(element);
|
|
395
|
+
}
|
|
396
|
+
for (const element of verticalBarsMembers) {
|
|
397
|
+
if (acceptedVerticalBarsMembers.has(element)) continue;
|
|
398
|
+
element.removeAttribute(verticalBarsMemberMarker);
|
|
399
|
+
verticalBarsMembers.delete(element);
|
|
400
|
+
}
|
|
401
|
+
for (const element of acceptedVerticalBarsMembers) {
|
|
402
|
+
element.setAttribute(verticalBarsMemberMarker, '');
|
|
403
|
+
verticalBarsMembers.add(element);
|
|
404
|
+
}
|
|
288
405
|
for (const element of accepted.flatMap(candidateSources)) {
|
|
289
406
|
if (!sources.has(element)) {
|
|
290
407
|
sources.set(element, element.getAttribute('aria-hidden'));
|
|
291
|
-
crossfade.play(element, true, handoffInstant);
|
|
408
|
+
crossfade.play(element, true, handoffInstant || isVerticalBarsSource(element));
|
|
292
409
|
element.setAttribute(marker, '');
|
|
293
410
|
element.setAttribute('aria-hidden', 'true');
|
|
294
411
|
element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
|
|
@@ -320,13 +437,21 @@ export const createRuntime = async (
|
|
|
320
437
|
else finishWaiters();
|
|
321
438
|
}
|
|
322
439
|
};
|
|
323
|
-
const observation: MutationObserverInit = {
|
|
440
|
+
const observation: MutationObserverInit = {
|
|
441
|
+
subtree: true,
|
|
442
|
+
childList: true,
|
|
443
|
+
characterData: true,
|
|
444
|
+
attributes: true,
|
|
445
|
+
attributeOldValue: true,
|
|
446
|
+
};
|
|
324
447
|
const observer = new MutationObserver((records) => {
|
|
325
448
|
if (
|
|
326
449
|
records.some(
|
|
327
450
|
(record) =>
|
|
328
451
|
record.attributeName !== marker &&
|
|
329
452
|
record.attributeName !== fadeMarker &&
|
|
453
|
+
!prehideOnlyMutation(record) &&
|
|
454
|
+
!(record.attributeName === 'style' && measuringPointerPages.has(record.target as HTMLElement)) &&
|
|
330
455
|
!(
|
|
331
456
|
record.attributeName === 'aria-hidden' &&
|
|
332
457
|
sources.has(record.target as HTMLElement) &&
|
|
@@ -341,6 +466,7 @@ export const createRuntime = async (
|
|
|
341
466
|
target.addEventListener(name, callback, { capture: true, signal: listeners.signal });
|
|
342
467
|
const pageWill: EventListener = (event) => {
|
|
343
468
|
const page = event.target as HTMLElement;
|
|
469
|
+
verticalBarsPages.lifecycle(event);
|
|
344
470
|
getNativeSearchBindings(doc)
|
|
345
471
|
.filter((binding) => page.contains(binding.footer))
|
|
346
472
|
.forEach((binding) => search.retire(binding));
|
|
@@ -351,7 +477,9 @@ export const createRuntime = async (
|
|
|
351
477
|
schedule();
|
|
352
478
|
};
|
|
353
479
|
const pageDid: EventListener = (event) => {
|
|
354
|
-
|
|
480
|
+
const page = event.target as HTMLElement;
|
|
481
|
+
verticalBarsPages.lifecycle(event);
|
|
482
|
+
pages.delete(page);
|
|
355
483
|
schedule();
|
|
356
484
|
if (tabSwitchHandoff && pages.size === 0) endTabSwitchHandoff();
|
|
357
485
|
};
|
|
@@ -411,6 +539,14 @@ export const createRuntime = async (
|
|
|
411
539
|
for (const name of Object.values(CSS_MOTION_EVENTS)) on(doc, name, motion);
|
|
412
540
|
for (const name of [LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE]) on(doc, name, pageWill);
|
|
413
541
|
for (const name of [LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE]) on(doc, name, pageDid);
|
|
542
|
+
on(doc, VERTICAL_BARS_TRANSITION_CANCELED, (event) => {
|
|
543
|
+
const leaving = event.target as HTMLElement;
|
|
544
|
+
const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
|
|
545
|
+
verticalBarsPages.cancel(entering, leaving);
|
|
546
|
+
pages.delete(leaving);
|
|
547
|
+
if (entering) pages.delete(entering);
|
|
548
|
+
schedule();
|
|
549
|
+
});
|
|
414
550
|
on(doc, 'ionTabsWillChange', () => {
|
|
415
551
|
// Vanilla ion-tabs dispatches DOM events; @ionic/angular uses EventEmitters instead.
|
|
416
552
|
armTabSwitchHandoff();
|
|
@@ -433,12 +569,16 @@ export const createRuntime = async (
|
|
|
433
569
|
schedule();
|
|
434
570
|
});
|
|
435
571
|
}
|
|
572
|
+
const eventMenu = (event: Event) =>
|
|
573
|
+
event.composedPath().find((target): target is HTMLElement => target instanceof HTMLElement && target.matches('ion-menu'));
|
|
436
574
|
on(doc, 'ionWillOpen', (event) => {
|
|
437
|
-
|
|
575
|
+
const menu = eventMenu(event);
|
|
576
|
+
if (menu) presented.add(menu);
|
|
438
577
|
schedule();
|
|
439
578
|
});
|
|
440
579
|
on(doc, 'ionDidClose', (event) => {
|
|
441
|
-
|
|
580
|
+
const menu = eventMenu(event);
|
|
581
|
+
if (menu) presented.delete(menu);
|
|
442
582
|
schedule();
|
|
443
583
|
});
|
|
444
584
|
for (const name of [
|
|
@@ -516,6 +656,7 @@ export const createRuntime = async (
|
|
|
516
656
|
/* Always restore the Web, even after bridge loss. */
|
|
517
657
|
}
|
|
518
658
|
style.remove();
|
|
659
|
+
doc.querySelectorAll<HTMLElement>(`.${rejectedClass}`).forEach((element) => setRejected(element, false));
|
|
519
660
|
icons.clear();
|
|
520
661
|
pages.clear();
|
|
521
662
|
presented.clear();
|
|
@@ -534,7 +675,7 @@ export const createRuntime = async (
|
|
|
534
675
|
event.revision < acceptedRevision ||
|
|
535
676
|
event.revision > revision ||
|
|
536
677
|
event.sequence <= lastSequence ||
|
|
537
|
-
overlayOpen()
|
|
678
|
+
overlayOpen(false)
|
|
538
679
|
)
|
|
539
680
|
return;
|
|
540
681
|
// Native may send input before update() resolves on the JS bridge.
|
|
@@ -555,7 +696,7 @@ export const createRuntime = async (
|
|
|
555
696
|
candidate?.control.search && [candidate.control.search.trigger.id, candidate.control.search.closeId].includes(event.id);
|
|
556
697
|
if (!searchAction && (!item || item.disabled || item.visible === false)) return;
|
|
557
698
|
// The original Ionic host owns form submission, routerLink and selection events.
|
|
558
|
-
element
|
|
699
|
+
activateProjectedElement(element);
|
|
559
700
|
lastSnapshot = ''; // Reconcile even if Ionic rejects the proposed native selection.
|
|
560
701
|
schedule();
|
|
561
702
|
};
|
|
@@ -591,9 +732,14 @@ export const createRuntime = async (
|
|
|
591
732
|
await flush();
|
|
592
733
|
// Source DOM has been restored before Ionic starts moving it.
|
|
593
734
|
await new Promise<void>((resolve) => win.requestAnimationFrame(() => resolve()));
|
|
594
|
-
return () => {
|
|
735
|
+
return (canceled = false) => {
|
|
595
736
|
suspended.delete(scopes);
|
|
596
|
-
scopes.
|
|
737
|
+
if (canceled || !scopes.some((scope) => scope.closest('ion-app.ios-theme-vertical-bars'))) {
|
|
738
|
+
scopes.forEach((scope) => pages.delete(scope)); // Preserve ordinary iPhone handoff; cancellation has no DidLeave.
|
|
739
|
+
if (canceled) {
|
|
740
|
+
verticalBarsPages.cancel(scopes[0], scopes[1]); // The entering page is abandoned; the leaving page stays active.
|
|
741
|
+
}
|
|
742
|
+
}
|
|
597
743
|
schedule();
|
|
598
744
|
};
|
|
599
745
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ShellControl, ShellItem, NativeUIShellComponent } from '../definitions';
|
|
2
|
-
import { frame, isDark, text, visible } from './dom';
|
|
2
|
+
import { frame, isDark, isVerticalBarsSource, text, visible } from './dom';
|
|
3
3
|
import { iconSource } from './icons';
|
|
4
4
|
|
|
5
5
|
export interface Candidate {
|
|
@@ -39,7 +39,11 @@ export const appendItem = (
|
|
|
39
39
|
content: ParentNode = child,
|
|
40
40
|
label = text(child),
|
|
41
41
|
): ShellItem | undefined => {
|
|
42
|
-
if (
|
|
42
|
+
if (
|
|
43
|
+
!visible(child, isVerticalBarsSource(candidate.element)) ||
|
|
44
|
+
child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar')
|
|
45
|
+
)
|
|
46
|
+
return;
|
|
43
47
|
const icons = Array.from(content.querySelectorAll<HTMLElement>('ion-icon'));
|
|
44
48
|
const directSVGs = Array.from(content.querySelectorAll<SVGElement>('svg'));
|
|
45
49
|
if (icons.length + directSVGs.length > 1) return undefined;
|
|
@@ -54,7 +58,11 @@ export const appendItem = (
|
|
|
54
58
|
id: id(child),
|
|
55
59
|
...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
|
|
56
60
|
label,
|
|
57
|
-
accessibilityLabel:
|
|
61
|
+
accessibilityLabel:
|
|
62
|
+
child.getAttribute('aria-label') ??
|
|
63
|
+
native?.getAttribute('aria-label') ??
|
|
64
|
+
(icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??
|
|
65
|
+
label,
|
|
58
66
|
disabled:
|
|
59
67
|
!!(child as ItemElement).disabled ||
|
|
60
68
|
!!(candidate.element as ItemElement).disabled ||
|