@rdlabo/ionic-theme-ios27 1.2.0-1 → 1.2.0-3
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 +9 -0
- 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.map +1 -1
- package/dist/index.js +31 -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/components/searchable-tabs.d.ts.map +1 -1
- package/dist/native/components/searchable-tabs.js +6 -7
- 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 +40 -19
- 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/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/tab-bar-searchable/index.d.ts.map +1 -1
- package/dist/tab-bar-searchable/index.js +39 -11
- package/dist/vertical-bars.d.ts +1 -2
- package/dist/vertical-bars.d.ts.map +1 -1
- package/dist/vertical-bars.js +0 -1
- package/docs/iphone-duo-with-original-theme.md +37 -15
- package/docs/iphone-duo.md +90 -47
- package/docs/migration.md +3 -3
- package/docs/native-ui-shell.md +12 -8
- package/docs/special-markup.md +0 -13
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +1 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +50 -6
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +118 -0
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +32 -179
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +5 -0
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +67 -0
- package/package.json +1 -1
- package/src/index.ts +40 -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/components/searchable-tabs.ts +8 -11
- 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 +40 -18
- 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/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 +110 -35
- package/src/tab-bar-searchable/index.ts +40 -9
- package/src/vertical-bars.ts +1 -2
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 = [];
|
|
@@ -591,24 +608,29 @@ export const createRuntime = async (
|
|
|
591
608
|
'focusout',
|
|
592
609
|
])
|
|
593
610
|
on(doc, name);
|
|
594
|
-
|
|
595
|
-
|
|
611
|
+
const refreshLayout = () => {
|
|
612
|
+
rejected = new WeakMap();
|
|
613
|
+
forceRefresh = true;
|
|
596
614
|
schedule();
|
|
615
|
+
};
|
|
616
|
+
on(doc, 'visibilitychange', () => {
|
|
617
|
+
if (doc.hidden) {
|
|
618
|
+
getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
|
|
619
|
+
schedule();
|
|
620
|
+
} else refreshLayout();
|
|
597
621
|
});
|
|
598
|
-
on(win, 'resize');
|
|
622
|
+
on(win, 'resize', refreshLayout);
|
|
599
623
|
on(win, 'keyboardWillShow', () => search.keyboard(true));
|
|
600
624
|
on(win, 'keyboardWillHide', () => search.keyboard(false));
|
|
601
625
|
on(win, 'scroll');
|
|
602
626
|
on(win.matchMedia('(prefers-color-scheme: dark)'), 'change');
|
|
603
627
|
on(win, 'nativeUIShellRefresh', (event) => {
|
|
604
|
-
rejected = new WeakMap();
|
|
605
628
|
if ((event as Event & { retireSearch?: boolean }).retireSearch)
|
|
606
629
|
getNativeSearchBindings(doc).forEach((binding) => search.retire(binding));
|
|
607
|
-
|
|
608
|
-
schedule();
|
|
630
|
+
refreshLayout();
|
|
609
631
|
});
|
|
610
632
|
if (win.visualViewport) {
|
|
611
|
-
on(win.visualViewport, 'resize');
|
|
633
|
+
on(win.visualViewport, 'resize', refreshLayout);
|
|
612
634
|
on(win.visualViewport, 'scroll');
|
|
613
635
|
}
|
|
614
636
|
const handle: NativeUIShellHandle = {
|
|
@@ -675,7 +697,7 @@ export const createRuntime = async (
|
|
|
675
697
|
event.revision < acceptedRevision ||
|
|
676
698
|
event.revision > revision ||
|
|
677
699
|
event.sequence <= lastSequence ||
|
|
678
|
-
overlayOpen(false)
|
|
700
|
+
overlayOpen(false, true)
|
|
679
701
|
)
|
|
680
702
|
return;
|
|
681
703
|
// Native may send input before update() resolves on the JS bridge.
|
|
@@ -688,7 +710,7 @@ export const createRuntime = async (
|
|
|
688
710
|
const element = actions.get(event.id);
|
|
689
711
|
if (!element) return;
|
|
690
712
|
const owner = Array.from(sources.keys()).find((source) => source === element || source.contains(element));
|
|
691
|
-
if (!owner) return;
|
|
713
|
+
if (!owner || !inVerticalBarsSurface(owner)) return;
|
|
692
714
|
const direct = !blocked(owner) && unprojected(sources.keys(), () => readEnabledCandidate(owner));
|
|
693
715
|
const candidate = direct || read().find((candidate) => candidate.actions.has(event.id));
|
|
694
716
|
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
|
|