@rdlabo/ionic-theme-ios27 1.1.0-beta.pr224.shac63de393e27b → 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.
Files changed (91) hide show
  1. package/README.md +8 -1
  2. package/dist/css/components/ion-menu.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +15 -8
  7. package/dist/native/components/index.d.ts +1 -0
  8. package/dist/native/components/index.d.ts.map +1 -1
  9. package/dist/native/components/index.js +8 -3
  10. package/dist/native/components/ion-back-button.d.ts.map +1 -1
  11. package/dist/native/components/ion-back-button.js +5 -2
  12. package/dist/native/components/ion-button.d.ts.map +1 -1
  13. package/dist/native/components/ion-button.js +10 -2
  14. package/dist/native/components/ion-buttons.d.ts.map +1 -1
  15. package/dist/native/components/ion-buttons.js +12 -4
  16. package/dist/native/components/ion-fab.js +4 -4
  17. package/dist/native/components/ion-menu-button.d.ts.map +1 -1
  18. package/dist/native/components/ion-menu-button.js +5 -4
  19. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  20. package/dist/native/components/searchable-tabs.js +4 -1
  21. package/dist/native/definitions.d.ts +29 -3
  22. package/dist/native/definitions.d.ts.map +1 -1
  23. package/dist/native/definitions.js +6 -1
  24. package/dist/native/index.d.ts +6 -2
  25. package/dist/native/index.d.ts.map +1 -1
  26. package/dist/native/index.js +154 -17
  27. package/dist/native/prehide.d.ts +5 -0
  28. package/dist/native/prehide.d.ts.map +1 -0
  29. package/dist/native/prehide.js +246 -0
  30. package/dist/native/runtime.d.ts +1 -1
  31. package/dist/native/runtime.d.ts.map +1 -1
  32. package/dist/native/runtime.js +162 -28
  33. package/dist/native/shared/candidate.d.ts.map +1 -1
  34. package/dist/native/shared/candidate.js +7 -3
  35. package/dist/native/shared/dom.d.ts +33 -2
  36. package/dist/native/shared/dom.d.ts.map +1 -1
  37. package/dist/native/shared/dom.js +171 -10
  38. package/dist/native/vertical-bars-web.d.ts +3 -0
  39. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  40. package/dist/native/vertical-bars-web.js +341 -0
  41. package/dist/native-integration/index.d.ts +3 -2
  42. package/dist/native-integration/index.d.ts.map +1 -1
  43. package/dist/native-integration/index.js +11 -5
  44. package/dist/transition/index.d.ts +2 -2
  45. package/dist/transition/index.d.ts.map +1 -1
  46. package/dist/transition/index.js +1 -1
  47. package/dist/transition/ios.transition.d.ts.map +1 -1
  48. package/dist/transition/ios.transition.js +1 -0
  49. package/dist/utils.d.ts +3 -3
  50. package/dist/utils.d.ts.map +1 -1
  51. package/dist/utils.js +1 -1
  52. package/dist/vertical-bars.d.ts +4 -0
  53. package/dist/vertical-bars.d.ts.map +1 -0
  54. package/dist/vertical-bars.js +2 -0
  55. package/docs/iphone-duo.md +255 -0
  56. package/docs/migration.md +2 -2
  57. package/docs/native-ui-shell.md +15 -3
  58. package/docs/special-markup.md +6 -0
  59. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +15 -1
  60. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +358 -0
  61. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +215 -35
  62. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +10 -1
  63. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +29 -0
  64. package/package.json +6 -2
  65. package/src/index.ts +15 -8
  66. package/src/native/components/index.ts +12 -3
  67. package/src/native/components/ion-back-button.ts +8 -2
  68. package/src/native/components/ion-button.ts +12 -2
  69. package/src/native/components/ion-buttons.ts +9 -4
  70. package/src/native/components/ion-fab.ts +4 -4
  71. package/src/native/components/ion-menu-button.ts +5 -4
  72. package/src/native/components/searchable-tabs.ts +9 -1
  73. package/src/native/definitions.ts +40 -3
  74. package/src/native/index.ts +188 -20
  75. package/src/native/prehide.ts +255 -0
  76. package/src/native/runtime.ts +173 -27
  77. package/src/native/shared/candidate.ts +11 -3
  78. package/src/native/shared/dom.ts +212 -11
  79. package/src/native/vertical-bars-web.ts +374 -0
  80. package/src/native-integration/index.ts +13 -8
  81. package/src/styles/components/ion-menu.scss +2 -2
  82. package/src/styles/ionic-theme-ios27.scss +1 -0
  83. package/src/styles/vertical-bars.scss +342 -0
  84. package/src/transition/index.ts +11 -6
  85. package/src/transition/ios.transition.ts +1 -0
  86. package/src/utils.ts +8 -8
  87. package/src/vertical-bars.ts +3 -0
  88. package/dist/native/lifecycle.d.ts +0 -4
  89. package/dist/native/lifecycle.d.ts.map +0 -1
  90. package/dist/native/lifecycle.js +0 -34
  91. package/src/native/lifecycle.ts +0 -38
@@ -0,0 +1,5 @@
1
+ export declare const prehideVerticalBarsToolbarSources: (doc: Document) => {
2
+ suspend: () => () => void;
3
+ stop: () => void;
4
+ };
5
+ //# sourceMappingURL=prehide.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prehide.d.ts","sourceRoot":"","sources":["../../src/native/prehide.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,iCAAiC,GAAI,KAAK,QAAQ,KAAG;IAAE,OAAO,EAAE,MAAM,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CA6N9G,CAAC"}
@@ -0,0 +1,246 @@
1
+ import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core';
2
+ import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
3
+ import { childElements, clearVerticalBarsPlacement, createVerticalBarsPageState, verticalBarsActionCandidate, verticalBarsBackCandidate, verticalBarsBackWebClass, verticalBarsOwned, isVerticalBarsToolbarActionShape, isPermanentlyExcluded, isShellDisabled, prehiddenClass, prehideRootClass, rejectedClass, setVerticalBarsPlacement, } from './shared/dom';
4
+ const overlays = 'ion-menu, ion-modal, ion-popover';
5
+ const sourceSelector = 'ion-back-button, ion-menu-button, ion-buttons, ion-button';
6
+ const isHtmlElement = (node) => !!node && node.nodeType === 1;
7
+ const asElement = (node) => (isHtmlElement(node) ? node : undefined);
8
+ const backSupported = (element) => {
9
+ const back = element;
10
+ return back.icon === undefined && back.color === undefined && !!back.shadowRoot;
11
+ };
12
+ const eligibleBack = verticalBarsBackCandidate;
13
+ const eligible = verticalBarsActionCandidate;
14
+ export const prehideVerticalBarsToolbarSources = (doc) => {
15
+ doc.documentElement.classList.add(prehideRootClass);
16
+ const scopes = new Map();
17
+ const owner = new WeakMap();
18
+ const pendingBacks = new Map();
19
+ const verticalBarsPages = createVerticalBarsPageState();
20
+ const listeners = new AbortController();
21
+ const root = () => doc.querySelector('ion-app.ios-theme-vertical-bars');
22
+ let suspended = 0;
23
+ let stopped = false;
24
+ const routedPage = (element) => element.closest('.ion-page:not(ion-app, body)');
25
+ const release = (scope) => {
26
+ for (const [element, pending] of pendingBacks) {
27
+ if (pending.scope !== scope)
28
+ continue;
29
+ clearTimeout(pending.timer);
30
+ pendingBacks.delete(element);
31
+ }
32
+ const owned = scopes.get(scope);
33
+ if (!owned)
34
+ return;
35
+ owned.forEach((element) => {
36
+ if (owner.get(element) !== scope)
37
+ return;
38
+ owner.delete(element);
39
+ element.classList.remove(prehiddenClass);
40
+ element.classList.remove(verticalBarsBackWebClass);
41
+ clearVerticalBarsPlacement(element);
42
+ });
43
+ scopes.delete(scope);
44
+ };
45
+ const capture = (scope) => {
46
+ if (!root()?.contains(scope) || scope.closest(overlays) || verticalBarsPages.isDeparted(scope))
47
+ return;
48
+ const owned = scopes.get(scope) ?? new Set();
49
+ const place = (element, rail) => {
50
+ if (owned.has(element))
51
+ return;
52
+ const previousScope = owner.get(element);
53
+ if (previousScope)
54
+ scopes.get(previousScope)?.delete(element);
55
+ owner.set(element, scope);
56
+ setVerticalBarsPlacement(element, rail);
57
+ if (element.matches('ion-back-button'))
58
+ element.classList.toggle(verticalBarsBackWebClass, !rail);
59
+ owned.add(element);
60
+ };
61
+ const sources = scope.matches('ion-back-button') ? [scope] : Array.from(scope.querySelectorAll(sourceSelector));
62
+ sources.forEach((element) => {
63
+ if (element.closest(overlays) || (scope.matches('.ion-page') && routedPage(element) !== scope))
64
+ return;
65
+ if (element.matches('ion-buttons')) {
66
+ const children = childElements(element);
67
+ const actions = children.filter((child) => eligible(child) && isVerticalBarsToolbarActionShape(child));
68
+ const group = eligible(element) &&
69
+ actions.length === children.length &&
70
+ (actions.length > 1 || (actions.length === 1 && actions[0].matches('ion-menu-button')));
71
+ const freshGroup = !owned.has(element);
72
+ place(element, group);
73
+ children.forEach((child) => {
74
+ if (!child.matches('ion-back-button'))
75
+ place(child, freshGroup && actions.includes(child));
76
+ });
77
+ }
78
+ else if (element.matches('ion-back-button') || !element.parentElement?.matches('ion-buttons')) {
79
+ if (owned.has(element))
80
+ return;
81
+ if (element.matches('ion-back-button') &&
82
+ eligibleBack(element) &&
83
+ element.icon === undefined &&
84
+ element.color === undefined &&
85
+ !element.shadowRoot &&
86
+ !element.classList.contains('hydrated')) {
87
+ const previousPending = pendingBacks.get(element);
88
+ if (previousPending && previousPending.scope !== scope) {
89
+ clearTimeout(previousPending.timer);
90
+ pendingBacks.delete(element);
91
+ }
92
+ if (!pendingBacks.has(element)) {
93
+ const timer = setTimeout(() => {
94
+ if (pendingBacks.get(element)?.scope !== scope)
95
+ return;
96
+ pendingBacks.delete(element);
97
+ place(element, false);
98
+ reconcile();
99
+ }, 1500);
100
+ pendingBacks.set(element, { scope, timer });
101
+ }
102
+ return;
103
+ }
104
+ const pending = pendingBacks.get(element);
105
+ if (pending) {
106
+ clearTimeout(pending.timer);
107
+ pendingBacks.delete(element);
108
+ }
109
+ place(element, element.matches('ion-back-button')
110
+ ? eligibleBack(element) && backSupported(element)
111
+ : eligible(element) && isVerticalBarsToolbarActionShape(element));
112
+ }
113
+ });
114
+ scopes.set(scope, owned);
115
+ };
116
+ const reconcile = () => {
117
+ const verticalBars = root();
118
+ if (!verticalBars) {
119
+ Array.from(scopes.keys()).forEach(release);
120
+ return;
121
+ }
122
+ for (const scope of scopes.keys())
123
+ if (!scope.isConnected)
124
+ release(scope);
125
+ for (const [element, pending] of pendingBacks)
126
+ if (element.shadowRoot || element.classList.contains('hydrated'))
127
+ capture(pending.scope);
128
+ verticalBars.querySelectorAll('.ion-page:not(ion-app, body, .ion-page-hidden)').forEach(capture);
129
+ verticalBars.querySelectorAll(sourceSelector).forEach((element) => {
130
+ if (routedPage(element) || element.closest(overlays))
131
+ return;
132
+ const scope = element.closest('ion-toolbar') ?? (element.matches('ion-back-button') ? element : null);
133
+ if (scope)
134
+ capture(scope);
135
+ });
136
+ for (const owned of scopes.values()) {
137
+ for (const element of owned) {
138
+ const groupOwnsChildren = element.parentElement?.matches('ion-buttons') &&
139
+ verticalBarsOwned(element.parentElement) &&
140
+ childElements(element.parentElement).every((child) => verticalBarsOwned(child));
141
+ const hide = !suspended &&
142
+ verticalBarsOwned(element) &&
143
+ !groupOwnsChildren &&
144
+ element.isConnected &&
145
+ !element.closest(`.${rejectedClass}`) &&
146
+ !isPermanentlyExcluded(element) &&
147
+ !isShellDisabled(element) &&
148
+ (!element.matches('ion-back-button') || backSupported(element) || !element.classList.contains('hydrated')) &&
149
+ (!element.matches('ion-buttons') || childElements(element).every((child) => verticalBarsOwned(child)));
150
+ element.classList.toggle(prehiddenClass, hide);
151
+ if (element.matches('ion-back-button'))
152
+ element.classList.toggle(verticalBarsBackWebClass, !hide);
153
+ }
154
+ }
155
+ };
156
+ const onWillEnter = (event) => {
157
+ const page = asElement(event.target);
158
+ if (page?.matches('.ion-page')) {
159
+ verticalBarsPages.lifecycle(event);
160
+ release(page);
161
+ capture(page);
162
+ reconcile();
163
+ }
164
+ };
165
+ const onPageLifecycle = (event) => {
166
+ verticalBarsPages.lifecycle(event);
167
+ if (event.type !== LIFECYCLE_DID_LEAVE)
168
+ return;
169
+ const page = asElement(event.target);
170
+ if (page)
171
+ release(page);
172
+ };
173
+ const onTransitionCanceled = (event) => {
174
+ const leaving = asElement(event.target);
175
+ if (!leaving)
176
+ return;
177
+ const entering = event.detail?.entering;
178
+ verticalBarsPages.cancel(entering, leaving);
179
+ reconcile();
180
+ };
181
+ doc.addEventListener(LIFECYCLE_WILL_ENTER, onWillEnter, { capture: true, signal: listeners.signal });
182
+ for (const name of [LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
183
+ doc.addEventListener(name, onPageLifecycle, { capture: true, signal: listeners.signal });
184
+ doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, onTransitionCanceled, { capture: true, signal: listeners.signal });
185
+ reconcile();
186
+ const mutationRelevant = (record) => {
187
+ if (record.type === 'childList')
188
+ return [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some((node) => isHtmlElement(node) &&
189
+ (node.matches(`${sourceSelector}, .ion-page, ion-toolbar`) || !!node.querySelector(`${sourceSelector}, .ion-page, ion-toolbar`)));
190
+ if (record.attributeName !== 'class')
191
+ return true;
192
+ const current = record.target.className;
193
+ const previous = record.oldValue ?? '';
194
+ const withoutOwned = (value) => value
195
+ .split(/\s+/)
196
+ .filter((name) => name && ![prehiddenClass, prehideRootClass, verticalBarsBackWebClass].includes(name))
197
+ .join(' ');
198
+ return typeof current !== 'string' || withoutOwned(previous) !== withoutOwned(current);
199
+ };
200
+ let scheduled = false;
201
+ const observer = new (doc.defaultView?.MutationObserver ?? MutationObserver)((records) => {
202
+ if (scheduled || stopped || !records.some(mutationRelevant))
203
+ return;
204
+ scheduled = true;
205
+ queueMicrotask(() => {
206
+ scheduled = false;
207
+ if (!stopped)
208
+ reconcile();
209
+ });
210
+ });
211
+ observer.observe(doc.documentElement, {
212
+ subtree: true,
213
+ childList: true,
214
+ attributes: true,
215
+ attributeOldValue: true,
216
+ attributeFilter: ['class', 'hidden', 'inert', 'icon', 'color'],
217
+ });
218
+ return {
219
+ suspend: () => {
220
+ if (stopped)
221
+ return () => { };
222
+ suspended++;
223
+ doc.documentElement.classList.remove(prehideRootClass);
224
+ reconcile();
225
+ let resumed = false;
226
+ return () => {
227
+ if (resumed || stopped)
228
+ return;
229
+ resumed = true;
230
+ if (--suspended === 0) {
231
+ doc.documentElement.classList.add(prehideRootClass);
232
+ reconcile();
233
+ }
234
+ };
235
+ },
236
+ stop: () => {
237
+ if (stopped)
238
+ return;
239
+ stopped = true;
240
+ observer.disconnect();
241
+ listeners.abort();
242
+ doc.documentElement.classList.remove(prehideRootClass);
243
+ Array.from(scopes.keys()).forEach(release);
244
+ },
245
+ };
246
+ };
@@ -1,3 +1,3 @@
1
1
  import type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellPlugin } from './definitions';
2
- export declare const createRuntime: (doc: Document, plugin: NativeUIShellPlugin, options?: NativeUIShellOptions) => Promise<NativeUIShellHandle>;
2
+ export declare const createRuntime: (doc: Document, plugin: NativeUIShellPlugin, options?: NativeUIShellOptions, nativeVerticalBars?: () => boolean, verticalBarsOnly?: boolean) => Promise<NativeUIShellHandle>;
3
3
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,eAAe,CAAC;AAkBvB,eAAO,MAAM,aAAa,GACxB,KAAK,QAAQ,EACb,QAAQ,mBAAmB,EAC3B,UAAS,oBAAyB,KACjC,OAAO,CAAC,mBAAmB,CA+jB7B,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,eAAe,CAAC;AA6BvB,eAAO,MAAM,aAAa,GACxB,KAAK,QAAQ,EACb,QAAQ,mBAAmB,EAC3B,UAAS,oBAAyB,EAClC,qBAAoB,MAAM,OAAoB,EAC9C,0BAAwB,KACvB,OAAO,CAAC,mBAAmB,CAosB7B,CAAC"}
@@ -1,26 +1,31 @@
1
1
  import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
2
- import { getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
2
+ import { VERTICAL_BARS_TRANSITION_CANCELED, getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
3
3
  import { createSearchSupport } from './components/searchable-tabs';
4
- import { readCandidate, selector, shadowSelector, motionSelector } from './components';
5
- import { marker, unprojected } from './shared/dom';
4
+ import { readCandidate, selector, shadowSelector, motionSelector, isVerticalBarsCandidate } from './components';
5
+ import { activateProjectedElement, createVerticalBarsPageState, isVerticalBarsSource, preferredVerticalBarsBack, marker, prehideOnlyMutation, rejectedClass, unprojected, } from './shared/dom';
6
6
  import { createIconRenderer } from './shared/icons';
7
7
  import { CSS_MOTION_EVENTS } from './shared/events';
8
8
  import { createCrossfade, fadeMarker } from './shared/crossfade';
9
9
  const overlays = 'ion-modal, ion-popover, ion-alert, ion-action-sheet, ion-loading, ion-picker, ion-toast, ion-menu';
10
10
  const overlayNames = ['Modal', 'Popover', 'Alert', 'ActionSheet', 'Loading', 'Picker', 'Toast'];
11
+ const verticalBarsMarker = 'data-native-ui-shell-vertical-bars';
12
+ const verticalBarsMemberMarker = 'data-native-ui-shell-vertical-bars-member';
11
13
  const bounded = (promise) => new Promise((resolve, reject) => {
12
14
  const timer = setTimeout(() => reject(new Error('Native UI Shell bridge timed out')), 5000);
13
15
  promise.then(resolve, reject).finally(() => clearTimeout(timer));
14
16
  });
15
- export const createRuntime = async (doc, plugin, options = {}) => {
17
+ export const createRuntime = async (doc, plugin, options = {}, nativeVerticalBars = () => true, verticalBarsOnly = false) => {
16
18
  const win = doc.defaultView;
17
19
  const icons = createIconRenderer();
18
20
  const crossfade = createCrossfade(win);
19
21
  const ids = new WeakMap();
20
22
  let rejected = new WeakMap();
21
23
  const sources = new Map();
24
+ const verticalBarsOwners = new Set();
25
+ const verticalBarsMembers = new Set();
22
26
  const suspended = new Set();
23
27
  const pages = new Set();
28
+ const verticalBarsPages = createVerticalBarsPageState();
24
29
  const presented = new Set();
25
30
  const manualSuspensions = new Set();
26
31
  const moving = new Map();
@@ -74,13 +79,22 @@ export const createRuntime = async (doc, plugin, options = {}) => {
74
79
  const style = doc.createElement('style');
75
80
  const hidden = `[${marker}]:not([${fadeMarker}])`;
76
81
  style.textContent = `${hidden}, ${hidden} *, ${hidden}::before, ${hidden}::after, ${hidden}::part(native) { visibility: hidden !important; }
77
- [${marker}], [${marker}] * { pointer-events: none !important; }`;
82
+ [${marker}], [${marker}] * { pointer-events: none !important; }
83
+ /* Ionic disables the covered page while a menu is open. VerticalBars rail
84
+ controls remain outside that page; zero specificity preserves any
85
+ pointer-events rule supplied by the application itself. */
86
+ :where(.menu-content-open) :where([${verticalBarsMarker}]) {
87
+ pointer-events: auto;
88
+ }
89
+ :where(.menu-content-open) :where([${verticalBarsMarker}]) > :where(:not([${verticalBarsMemberMarker}])) {
90
+ pointer-events: none;
91
+ }`;
78
92
  const restore = (element) => {
79
93
  lastSnapshot = '';
80
94
  search.release(element);
81
95
  element.removeAttribute(marker);
82
96
  if (!stopped)
83
- crossfade.play(element, false, handoffInstant);
97
+ crossfade.play(element, false, handoffInstant || isVerticalBarsSource(element));
84
98
  if (element.getAttribute('aria-hidden') === 'true') {
85
99
  const previous = sources.get(element);
86
100
  if (previous == null)
@@ -91,7 +105,13 @@ export const createRuntime = async (doc, plugin, options = {}) => {
91
105
  sources.delete(element);
92
106
  element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
93
107
  };
94
- const restoreAll = () => Array.from(sources.keys()).forEach(restore);
108
+ const restoreAll = () => {
109
+ Array.from(sources.keys()).forEach(restore);
110
+ verticalBarsOwners.forEach((element) => element.removeAttribute(verticalBarsMarker));
111
+ verticalBarsOwners.clear();
112
+ verticalBarsMembers.forEach((element) => element.removeAttribute(verticalBarsMemberMarker));
113
+ verticalBarsMembers.clear();
114
+ };
95
115
  const finishWaiters = () => {
96
116
  const current = waiters;
97
117
  waiters = [];
@@ -106,8 +126,48 @@ export const createRuntime = async (doc, plugin, options = {}) => {
106
126
  };
107
127
  const search = createSearchSupport(doc, id, schedule);
108
128
  const candidateSources = (candidate) => candidate.sources ?? [candidate.element];
129
+ const setRejected = (element, value) => {
130
+ element.classList.toggle(rejectedClass, value);
131
+ };
132
+ const measuringPointerPages = new WeakSet();
109
133
  const readEnabledCandidate = (element) => {
110
- const candidate = readCandidate(element, id);
134
+ if (element.matches('ion-back-button') && element.closest('ion-app.ios-theme-vertical-bars') && !isVerticalBarsCandidate(element))
135
+ return;
136
+ if (verticalBarsOnly && !isVerticalBarsCandidate(element))
137
+ return;
138
+ const pointerPage = isVerticalBarsCandidate(element) ? element.closest('.ion-page') : undefined;
139
+ let candidate;
140
+ if (pointerPage && getComputedStyle(pointerPage).pointerEvents === 'none') {
141
+ const previous = pointerPage.style.getPropertyValue('pointer-events');
142
+ const priority = pointerPage.style.getPropertyPriority('pointer-events');
143
+ const hadStyle = pointerPage.hasAttribute('style');
144
+ measuringPointerPages.add(pointerPage);
145
+ pointerPage.style.setProperty('pointer-events', 'auto', 'important');
146
+ try {
147
+ candidate = readCandidate(element, id);
148
+ }
149
+ finally {
150
+ if (previous)
151
+ pointerPage.style.setProperty('pointer-events', previous, priority);
152
+ else
153
+ pointerPage.style.removeProperty('pointer-events');
154
+ if (!hadStyle && !pointerPage.style.length)
155
+ pointerPage.removeAttribute('style');
156
+ win.setTimeout(() => measuringPointerPages.delete(pointerPage), 0);
157
+ }
158
+ }
159
+ else
160
+ candidate = readCandidate(element, id);
161
+ if (candidate && isVerticalBarsCandidate(element)) {
162
+ if (!nativeVerticalBars())
163
+ return undefined;
164
+ candidate.control.placement = 'vertical-bars';
165
+ if (['ion-button', 'ion-buttons', 'ion-menu-button'].includes(candidate.control.kind)) {
166
+ const slot = (element.matches('ion-buttons') ? element : (element.closest('ion-buttons') ?? element)).getAttribute('slot');
167
+ if (slot === 'start' || slot === 'end')
168
+ candidate.control.toolbarSlot = slot;
169
+ }
170
+ }
111
171
  return candidate && controlEnabled(candidate) ? candidate : undefined;
112
172
  };
113
173
  const flush = async () => {
@@ -121,17 +181,21 @@ export const createRuntime = async (doc, plugin, options = {}) => {
121
181
  await fail(error);
122
182
  }
123
183
  };
124
- const blocked = (element) => Array.from(suspended).some((scopes) => scopes.some((scope) => scope.contains(element))) ||
125
- Array.from(pages).some((page) => page.contains(element)) ||
126
- Array.from(moving.keys()).some((surface) => surface.contains(element));
184
+ const blocked = (element) => verticalBarsPages.isDeparted(element) ||
185
+ (!isVerticalBarsSource(element) &&
186
+ (Array.from(suspended).some((scopes) => scopes.some((scope) => scope.contains(element))) ||
187
+ Array.from(pages).some((scope) => scope.contains(element)) ||
188
+ Array.from(moving.keys()).some((surface) => surface.contains(element))));
127
189
  const painted = () => new Promise((resolve) => win.requestAnimationFrame(() => win.requestAnimationFrame(() => resolve())));
128
- const overlayOpen = () => {
190
+ const overlayOpen = (includeMenu = true) => {
129
191
  for (const element of presented)
130
192
  if (!element.isConnected)
131
193
  presented.delete(element);
194
+ const presentedOverlayOpen = Array.from(presented).some((element) => includeMenu || !element.matches('ion-menu'));
132
195
  return (manualSuspensions.size > 0 ||
133
- presented.size > 0 ||
134
- Array.from(doc.querySelectorAll(overlays)).some((element) => element.presented || element.classList.contains('show-menu')));
196
+ presentedOverlayOpen ||
197
+ Array.from(doc.querySelectorAll(overlays)).some((element) => (includeMenu || !element.matches('ion-menu')) &&
198
+ (element.presented || element.classList.contains('show-menu'))));
135
199
  };
136
200
  const signature = (candidate) => JSON.stringify({ control: candidate.control, icons: candidate.icons.map(({ source }) => source) });
137
201
  const read = () => {
@@ -142,16 +206,22 @@ export const createRuntime = async (doc, plugin, options = {}) => {
142
206
  for (const surface of moving.keys())
143
207
  if (!surface.isConnected)
144
208
  moving.delete(surface);
145
- if (doc.hidden || overlayOpen())
209
+ if (doc.hidden || overlayOpen(false))
146
210
  return [];
147
211
  if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0) && !search.hasActive())
148
212
  return [];
149
- return unprojected(sources.keys(), () => search
213
+ const menuOpen = overlayOpen();
214
+ const candidates = unprojected(sources.keys(), () => search
150
215
  .decorate(Array.from(doc.querySelectorAll(selector))
151
- .filter((element) => !blocked(element))
216
+ .filter((element) => !verticalBarsPages.isDeparted(element) && (!blocked(element) || (menuOpen && isVerticalBarsSource(element))))
152
217
  .map(readEnabledCandidate)
153
218
  .filter((candidate) => !!candidate), blocked)
154
219
  .filter((candidate) => !rejected.has(candidate.element) || rejected.get(candidate.element) !== signature(candidate)));
220
+ const back = preferredVerticalBarsBack(candidates
221
+ .filter((candidate) => candidate.control.kind === 'ion-back-button' && isVerticalBarsCandidate(candidate.element))
222
+ .map((candidate) => candidate.element), doc);
223
+ const selected = candidates.filter((candidate) => candidate.control.kind !== 'ion-back-button' || !isVerticalBarsCandidate(candidate.element) || candidate.element === back);
224
+ return menuOpen ? selected.filter((candidate) => isVerticalBarsCandidate(candidate.element)) : selected;
155
225
  };
156
226
  const observe = () => {
157
227
  const wanted = new Set();
@@ -186,6 +256,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
186
256
  reason = error instanceof Error ? error.message : String(error);
187
257
  console.warn('[Native UI Shell] Returning to Web:', reason);
188
258
  await handle.destroy();
259
+ win.dispatchEvent(new Event('nativeUIShellRefresh'));
189
260
  };
190
261
  const sync = async () => {
191
262
  frame = 0;
@@ -215,13 +286,19 @@ export const createRuntime = async (doc, plugin, options = {}) => {
215
286
  const removed = Array.from(sources.keys()).filter((element) => !retained.has(element));
216
287
  if (removed.length) {
217
288
  removed.forEach(restore);
218
- if (!handoffInstant) {
289
+ if (!handoffInstant && removed.some((element) => !isVerticalBarsSource(element))) {
219
290
  await painted();
220
291
  if (stopped || dirty)
221
292
  return;
222
293
  }
223
294
  }
224
- const data = { viewportWidth: win.innerWidth, controls: candidates.map((candidate) => candidate.control) };
295
+ const root = doc.querySelector('ion-app.ios-theme-vertical-bars');
296
+ const verticalBarEdge = root
297
+ ? root.classList.contains('ios-theme-vertical-bars-left')
298
+ ? 'left'
299
+ : 'right'
300
+ : undefined;
301
+ const data = { viewportWidth: win.innerWidth, verticalBarEdge, controls: candidates.map((candidate) => candidate.control) };
225
302
  const serialized = JSON.stringify(data);
226
303
  if (serialized === lastSnapshot && !forceRefresh)
227
304
  return;
@@ -247,6 +324,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
247
324
  for (const candidate of candidates) {
248
325
  if (result.rejectedControls?.includes(candidate.control.id)) {
249
326
  rejected.set(candidate.element, signatures.get(candidate.element));
327
+ setRejected(candidate.element, true);
250
328
  dirty = true;
251
329
  }
252
330
  }
@@ -254,6 +332,11 @@ export const createRuntime = async (doc, plugin, options = {}) => {
254
332
  const current = dirty ? new Map(currentCandidates.map((candidate) => [candidate.element, signature(candidate)])) : signatures;
255
333
  const currentSources = new Set(currentCandidates.flatMap(candidateSources));
256
334
  const accepted = candidates.filter((candidate) => current.get(candidate.element) === signatures.get(candidate.element));
335
+ accepted.forEach((candidate) => setRejected(candidate.element, false));
336
+ const acceptedVerticalBarsOwners = new Set(accepted.filter((candidate) => candidate.control.placement === 'vertical-bars').map((candidate) => candidate.element));
337
+ const acceptedVerticalBarsMembers = new Set(accepted
338
+ .filter((candidate) => candidate.control.placement === 'vertical-bars')
339
+ .flatMap((candidate) => Array.from(candidate.actions.values())));
257
340
  const invalidated = candidates.length !== accepted.length;
258
341
  acceptedRevision = result.revision;
259
342
  lastSnapshot = invalidated ? '' : serialized;
@@ -261,10 +344,30 @@ export const createRuntime = async (doc, plugin, options = {}) => {
261
344
  for (const element of sources.keys())
262
345
  if (!currentSources.has(element))
263
346
  restore(element);
347
+ for (const element of verticalBarsOwners) {
348
+ if (acceptedVerticalBarsOwners.has(element))
349
+ continue;
350
+ element.removeAttribute(verticalBarsMarker);
351
+ verticalBarsOwners.delete(element);
352
+ }
353
+ for (const element of acceptedVerticalBarsOwners) {
354
+ element.setAttribute(verticalBarsMarker, '');
355
+ verticalBarsOwners.add(element);
356
+ }
357
+ for (const element of verticalBarsMembers) {
358
+ if (acceptedVerticalBarsMembers.has(element))
359
+ continue;
360
+ element.removeAttribute(verticalBarsMemberMarker);
361
+ verticalBarsMembers.delete(element);
362
+ }
363
+ for (const element of acceptedVerticalBarsMembers) {
364
+ element.setAttribute(verticalBarsMemberMarker, '');
365
+ verticalBarsMembers.add(element);
366
+ }
264
367
  for (const element of accepted.flatMap(candidateSources)) {
265
368
  if (!sources.has(element)) {
266
369
  sources.set(element, element.getAttribute('aria-hidden'));
267
- crossfade.play(element, true, handoffInstant);
370
+ crossfade.play(element, true, handoffInstant || isVerticalBarsSource(element));
268
371
  element.setAttribute(marker, '');
269
372
  element.setAttribute('aria-hidden', 'true');
270
373
  element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
@@ -298,10 +401,18 @@ export const createRuntime = async (doc, plugin, options = {}) => {
298
401
  finishWaiters();
299
402
  }
300
403
  };
301
- const observation = { subtree: true, childList: true, characterData: true, attributes: true };
404
+ const observation = {
405
+ subtree: true,
406
+ childList: true,
407
+ characterData: true,
408
+ attributes: true,
409
+ attributeOldValue: true,
410
+ };
302
411
  const observer = new MutationObserver((records) => {
303
412
  if (records.some((record) => record.attributeName !== marker &&
304
413
  record.attributeName !== fadeMarker &&
414
+ !prehideOnlyMutation(record) &&
415
+ !(record.attributeName === 'style' && measuringPointerPages.has(record.target)) &&
305
416
  !(record.attributeName === 'aria-hidden' &&
306
417
  sources.has(record.target) &&
307
418
  record.target.getAttribute('aria-hidden') === 'true')))
@@ -311,6 +422,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
311
422
  const on = (target, name, callback = schedule) => target.addEventListener(name, callback, { capture: true, signal: listeners.signal });
312
423
  const pageWill = (event) => {
313
424
  const page = event.target;
425
+ verticalBarsPages.lifecycle(event);
314
426
  getNativeSearchBindings(doc)
315
427
  .filter((binding) => page.contains(binding.footer))
316
428
  .forEach((binding) => search.retire(binding));
@@ -320,7 +432,9 @@ export const createRuntime = async (doc, plugin, options = {}) => {
320
432
  schedule();
321
433
  };
322
434
  const pageDid = (event) => {
323
- pages.delete(event.target);
435
+ const page = event.target;
436
+ verticalBarsPages.lifecycle(event);
437
+ pages.delete(page);
324
438
  schedule();
325
439
  if (tabSwitchHandoff && pages.size === 0)
326
440
  endTabSwitchHandoff();
@@ -388,6 +502,15 @@ export const createRuntime = async (doc, plugin, options = {}) => {
388
502
  on(doc, name, pageWill);
389
503
  for (const name of [LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
390
504
  on(doc, name, pageDid);
505
+ on(doc, VERTICAL_BARS_TRANSITION_CANCELED, (event) => {
506
+ const leaving = event.target;
507
+ const entering = event.detail?.entering;
508
+ verticalBarsPages.cancel(entering, leaving);
509
+ pages.delete(leaving);
510
+ if (entering)
511
+ pages.delete(entering);
512
+ schedule();
513
+ });
391
514
  on(doc, 'ionTabsWillChange', () => {
392
515
  armTabSwitchHandoff();
393
516
  schedule();
@@ -408,12 +531,17 @@ export const createRuntime = async (doc, plugin, options = {}) => {
408
531
  schedule();
409
532
  });
410
533
  }
534
+ const eventMenu = (event) => event.composedPath().find((target) => target instanceof HTMLElement && target.matches('ion-menu'));
411
535
  on(doc, 'ionWillOpen', (event) => {
412
- presented.add(event.target);
536
+ const menu = eventMenu(event);
537
+ if (menu)
538
+ presented.add(menu);
413
539
  schedule();
414
540
  });
415
541
  on(doc, 'ionDidClose', (event) => {
416
- presented.delete(event.target);
542
+ const menu = eventMenu(event);
543
+ if (menu)
544
+ presented.delete(menu);
417
545
  schedule();
418
546
  });
419
547
  for (const name of [
@@ -496,6 +624,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
496
624
  catch {
497
625
  }
498
626
  style.remove();
627
+ doc.querySelectorAll(`.${rejectedClass}`).forEach((element) => setRejected(element, false));
499
628
  icons.clear();
500
629
  pages.clear();
501
630
  presented.clear();
@@ -513,7 +642,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
513
642
  event.revision < acceptedRevision ||
514
643
  event.revision > revision ||
515
644
  event.sequence <= lastSequence ||
516
- overlayOpen())
645
+ overlayOpen(false))
517
646
  return;
518
647
  if (event.revision > acceptedRevision && event.revision === updatingRevision) {
519
648
  pendingActivations.push(event);
@@ -532,7 +661,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
532
661
  const searchAction = candidate?.control.search && [candidate.control.search.trigger.id, candidate.control.search.closeId].includes(event.id);
533
662
  if (!searchAction && (!item || item.disabled || item.visible === false))
534
663
  return;
535
- element.click();
664
+ activateProjectedElement(element);
536
665
  lastSnapshot = '';
537
666
  schedule();
538
667
  };
@@ -569,9 +698,14 @@ export const createRuntime = async (doc, plugin, options = {}) => {
569
698
  suspended.add(scopes);
570
699
  await flush();
571
700
  await new Promise((resolve) => win.requestAnimationFrame(() => resolve()));
572
- return () => {
701
+ return (canceled = false) => {
573
702
  suspended.delete(scopes);
574
- scopes.forEach((scope) => pages.delete(scope));
703
+ if (canceled || !scopes.some((scope) => scope.closest('ion-app.ios-theme-vertical-bars'))) {
704
+ scopes.forEach((scope) => pages.delete(scope));
705
+ if (canceled) {
706
+ verticalBarsPages.cancel(scopes[0], scopes[1]);
707
+ }
708
+ }
575
709
  schedule();
576
710
  };
577
711
  },
@@ -1 +1 @@
1
- {"version":3,"file":"candidate.d.ts","sourceRoot":"","sources":["../../../src/native/shared/candidate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAItF,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAClC,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,WAAW,CAAA;KAAE,EAAE,CAAC;CACnE;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;AAGxD,eAAO,MAAM,eAAe,GAAI,SAAS,WAAW,EAAE,MAAM,sBAAsB,EAAE,IAAI,QAAQ,KAAG,SAgBlG,CAAC;AAGF,eAAO,MAAM,UAAU,GACrB,WAAW,SAAS,EACpB,OAAO,WAAW,EAClB,IAAI,QAAQ,EACZ,UAAS,UAAkB,EAC3B,cAAmB,KAClB,SAAS,GAAG,SA8Dd,CAAC"}
1
+ {"version":3,"file":"candidate.d.ts","sourceRoot":"","sources":["../../../src/native/shared/candidate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAItF,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAClC,KAAK,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,WAAW,CAAA;KAAE,EAAE,CAAC;CACnE;AAED,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;AAGxD,eAAO,MAAM,eAAe,GAAI,SAAS,WAAW,EAAE,MAAM,sBAAsB,EAAE,IAAI,QAAQ,KAAG,SAgBlG,CAAC;AAGF,eAAO,MAAM,UAAU,GACrB,WAAW,SAAS,EACpB,OAAO,WAAW,EAClB,IAAI,QAAQ,EACZ,UAAS,UAAkB,EAC3B,cAAmB,KAClB,SAAS,GAAG,SAsEd,CAAC"}