@webview-native-ui/engine 1.0.0 → 1.0.2-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 (238) hide show
  1. package/README.md +48 -35
  2. package/WebViewNativeUIControls.podspec +17 -0
  3. package/dist/controls/activation.d.ts +15 -0
  4. package/dist/controls/activation.js +370 -0
  5. package/dist/controls/components/index.d.ts +16 -0
  6. package/dist/controls/components/index.js +38 -0
  7. package/dist/controls/components/ion-back-button.d.ts +4 -0
  8. package/dist/controls/components/ion-back-button.js +16 -0
  9. package/dist/controls/components/ion-button.d.ts +4 -0
  10. package/dist/controls/components/ion-button.js +26 -0
  11. package/dist/controls/components/ion-buttons.d.ts +5 -0
  12. package/dist/controls/components/ion-buttons.js +34 -0
  13. package/dist/controls/components/ion-fab.d.ts +4 -0
  14. package/dist/controls/components/ion-fab.js +164 -0
  15. package/dist/controls/components/ion-menu-button.d.ts +7 -0
  16. package/dist/controls/components/ion-menu-button.js +41 -0
  17. package/dist/controls/components/ion-segment.d.ts +5 -0
  18. package/dist/controls/components/ion-segment.js +24 -0
  19. package/dist/controls/components/ion-tab-bar.d.ts +5 -0
  20. package/dist/controls/components/ion-tab-bar.js +49 -0
  21. package/dist/controls/components/searchable-tabs.d.ts +20 -0
  22. package/dist/controls/components/searchable-tabs.js +525 -0
  23. package/dist/controls/definitions.d.ts +360 -0
  24. package/dist/controls/definitions.js +1 -0
  25. package/dist/controls/extensions.d.ts +44 -0
  26. package/dist/controls/extensions.js +164 -0
  27. package/dist/controls/foldable/fold-state.d.ts +4 -0
  28. package/dist/controls/foldable/fold-state.js +8 -0
  29. package/dist/controls/foldable/types.d.ts +9 -0
  30. package/dist/controls/foldable/types.js +1 -0
  31. package/dist/controls/html.d.ts +8 -0
  32. package/dist/controls/html.js +260 -0
  33. package/dist/controls/index.d.ts +7 -0
  34. package/dist/controls/index.js +4 -0
  35. package/dist/controls/ionic.d.ts +1 -0
  36. package/dist/controls/ionic.js +1 -0
  37. package/dist/controls/legacy-layout.d.ts +7 -0
  38. package/dist/controls/legacy-layout.js +23 -0
  39. package/dist/controls/overlays/content.d.ts +2 -0
  40. package/dist/controls/overlays/content.js +55 -0
  41. package/dist/controls/overlays/controller.d.ts +7 -0
  42. package/dist/controls/overlays/controller.js +369 -0
  43. package/dist/controls/overlays/covered-overlay.d.ts +4 -0
  44. package/dist/controls/overlays/covered-overlay.js +9 -0
  45. package/dist/controls/overlays/dialog-animation.d.ts +7 -0
  46. package/dist/controls/overlays/dialog-animation.js +49 -0
  47. package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
  48. package/dist/controls/overlays/dismiss-animation.js +14 -0
  49. package/dist/controls/overlays/focus.d.ts +2 -0
  50. package/dist/controls/overlays/focus.js +67 -0
  51. package/dist/controls/overlays/popover.d.ts +10 -0
  52. package/dist/controls/overlays/popover.js +117 -0
  53. package/dist/controls/overlays/styles.d.ts +9 -0
  54. package/dist/controls/overlays/styles.js +194 -0
  55. package/dist/controls/prehide.d.ts +6 -0
  56. package/dist/controls/prehide.js +280 -0
  57. package/dist/controls/runtime.d.ts +5 -0
  58. package/dist/controls/runtime.js +1685 -0
  59. package/dist/controls/shared/bridge.d.ts +4 -0
  60. package/dist/controls/shared/bridge.js +15 -0
  61. package/dist/controls/shared/candidate.d.ts +23 -0
  62. package/dist/controls/shared/candidate.js +128 -0
  63. package/dist/controls/shared/clipping.d.ts +1 -0
  64. package/dist/controls/shared/clipping.js +1 -0
  65. package/dist/controls/shared/connection.d.ts +21 -0
  66. package/dist/controls/shared/connection.js +112 -0
  67. package/dist/controls/shared/control-options.d.ts +4 -0
  68. package/dist/controls/shared/control-options.js +14 -0
  69. package/dist/controls/shared/covered-snapshot.d.ts +35 -0
  70. package/dist/controls/shared/covered-snapshot.js +41 -0
  71. package/dist/controls/shared/covered-source.d.ts +22 -0
  72. package/dist/controls/shared/covered-source.js +72 -0
  73. package/dist/controls/shared/crossfade.d.ts +11 -0
  74. package/dist/controls/shared/crossfade.js +71 -0
  75. package/dist/controls/shared/diagnostics.d.ts +2 -0
  76. package/dist/controls/shared/diagnostics.js +16 -0
  77. package/dist/controls/shared/dom.d.ts +45 -0
  78. package/dist/controls/shared/dom.js +258 -0
  79. package/dist/controls/shared/events.d.ts +12 -0
  80. package/dist/controls/shared/events.js +13 -0
  81. package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
  82. package/dist/controls/shared/horizontal-navigation.js +90 -0
  83. package/dist/controls/shared/icons.d.ts +5 -0
  84. package/dist/controls/shared/icons.js +66 -0
  85. package/dist/controls/shared/modal-animation.d.ts +21 -0
  86. package/dist/controls/shared/modal-animation.js +97 -0
  87. package/dist/controls/shared/modal.d.ts +26 -0
  88. package/dist/controls/shared/modal.js +261 -0
  89. package/dist/controls/shared/projection-id.d.ts +2 -0
  90. package/dist/controls/shared/projection-id.js +2 -0
  91. package/dist/controls/shared/scroll-placement.d.ts +1 -0
  92. package/dist/controls/shared/scroll-placement.js +1 -0
  93. package/dist/controls/shared/tab-bar-width.d.ts +7 -0
  94. package/dist/controls/shared/tab-bar-width.js +72 -0
  95. package/dist/controls/shared/toolbar-options.d.ts +10 -0
  96. package/dist/controls/shared/toolbar-options.js +38 -0
  97. package/dist/controls/shared/transition-identity.d.ts +3 -0
  98. package/dist/controls/shared/transition-identity.js +64 -0
  99. package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
  100. package/dist/controls/shared/vertical-bars-geometry.js +82 -0
  101. package/dist/controls/shared/vertical-navigation.d.ts +5 -0
  102. package/dist/controls/shared/vertical-navigation.js +39 -0
  103. package/dist/controls/startup.d.ts +7 -0
  104. package/dist/controls/startup.js +72 -0
  105. package/dist/controls/transport.d.ts +10 -0
  106. package/dist/controls/transport.js +1 -0
  107. package/dist/controls/vertical-bars-web-search.d.ts +8 -0
  108. package/dist/controls/vertical-bars-web-search.js +83 -0
  109. package/dist/controls/vertical-bars-web.d.ts +2 -0
  110. package/dist/controls/vertical-bars-web.js +609 -0
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +1 -0
  113. package/dist/integration/index.d.ts +36 -0
  114. package/dist/integration/index.js +25 -0
  115. package/dist/native/bootstrap.js +1 -1
  116. package/dist/native/session.js +14 -8
  117. package/dist/projection/pause.d.ts +10 -0
  118. package/dist/projection/pause.js +58 -0
  119. package/dist/projection/styles.d.ts +1 -1
  120. package/dist/projection/styles.js +0 -1
  121. package/dist/projection/surfaces.js +11 -8
  122. package/dist/projection/web.js +48 -13
  123. package/dist/types.d.ts +5 -1
  124. package/docs/README.md +17 -10
  125. package/docs/concepts.md +39 -14
  126. package/docs/configuration/README.md +4 -2
  127. package/docs/configuration/ionic-overlays.md +36 -2
  128. package/docs/configuration/pause-resume.md +51 -0
  129. package/docs/controls.md +39 -0
  130. package/docs/getting-started/capacitor.md +3 -3
  131. package/docs/getting-started/cordova.md +3 -1
  132. package/docs/getting-started/initialization.md +18 -2
  133. package/docs/reference/platforms.md +2 -2
  134. package/docs/reference/projection.md +1 -1
  135. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
  136. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
  137. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
  138. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
  139. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
  140. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
  141. package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
  142. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
  143. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
  144. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
  145. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
  146. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
  147. package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
  148. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
  149. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
  150. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
  151. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
  152. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
  153. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
  154. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
  155. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
  156. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
  157. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
  158. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +6 -4
  159. package/ios/Sources/WebViewNativeUIEngine/ProjectionOrigins.swift +31 -0
  160. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
  161. package/package.json +30 -5
  162. package/src/controls/activation.ts +427 -0
  163. package/src/controls/components/index.ts +45 -0
  164. package/src/controls/components/ion-back-button.ts +22 -0
  165. package/src/controls/components/ion-button.ts +28 -0
  166. package/src/controls/components/ion-buttons.ts +37 -0
  167. package/src/controls/components/ion-fab.ts +151 -0
  168. package/src/controls/components/ion-menu-button.ts +37 -0
  169. package/src/controls/components/ion-segment.ts +27 -0
  170. package/src/controls/components/ion-tab-bar.ts +55 -0
  171. package/src/controls/components/searchable-tabs.ts +534 -0
  172. package/src/controls/definitions.ts +338 -0
  173. package/src/controls/extensions.ts +198 -0
  174. package/src/controls/foldable/fold-state.ts +10 -0
  175. package/src/controls/foldable/types.ts +4 -0
  176. package/src/controls/html.ts +239 -0
  177. package/src/controls/index.ts +7 -0
  178. package/src/controls/ionic.ts +1 -0
  179. package/src/controls/legacy-layout.ts +21 -0
  180. package/src/controls/overlays/content.ts +51 -0
  181. package/src/controls/overlays/controller.ts +394 -0
  182. package/src/controls/overlays/covered-overlay.ts +11 -0
  183. package/src/controls/overlays/dialog-animation.ts +53 -0
  184. package/src/controls/overlays/dismiss-animation.ts +15 -0
  185. package/src/controls/overlays/focus.ts +64 -0
  186. package/src/controls/overlays/popover.ts +116 -0
  187. package/src/controls/overlays/styles.ts +195 -0
  188. package/src/controls/prehide.ts +285 -0
  189. package/src/controls/runtime.ts +1731 -0
  190. package/src/controls/shared/bridge.ts +18 -0
  191. package/src/controls/shared/candidate.ts +163 -0
  192. package/src/controls/shared/clipping.ts +1 -0
  193. package/src/controls/shared/connection.ts +105 -0
  194. package/src/controls/shared/control-options.ts +20 -0
  195. package/src/controls/shared/covered-snapshot.ts +52 -0
  196. package/src/controls/shared/covered-source.ts +78 -0
  197. package/src/controls/shared/crossfade.ts +70 -0
  198. package/src/controls/shared/diagnostics.ts +13 -0
  199. package/src/controls/shared/dom.ts +310 -0
  200. package/src/controls/shared/events.ts +14 -0
  201. package/src/controls/shared/horizontal-navigation.ts +106 -0
  202. package/src/controls/shared/icons.ts +64 -0
  203. package/src/controls/shared/modal-animation.ts +109 -0
  204. package/src/controls/shared/modal.ts +286 -0
  205. package/src/controls/shared/projection-id.ts +2 -0
  206. package/src/controls/shared/scroll-placement.ts +1 -0
  207. package/src/controls/shared/tab-bar-width.ts +66 -0
  208. package/src/controls/shared/toolbar-options.ts +51 -0
  209. package/src/controls/shared/transition-identity.ts +67 -0
  210. package/src/controls/shared/vertical-bars-geometry.ts +92 -0
  211. package/src/controls/shared/vertical-navigation.ts +34 -0
  212. package/src/controls/startup.ts +74 -0
  213. package/src/controls/transport.ts +11 -0
  214. package/src/controls/vertical-bars-web-search.ts +86 -0
  215. package/src/controls/vertical-bars-web.ts +674 -0
  216. package/src/dom/geometry.ts +24 -0
  217. package/src/dom/overlays.ts +196 -0
  218. package/src/dom/scroll-bindings.ts +66 -0
  219. package/src/dom/scroll-placement.ts +92 -0
  220. package/src/dom/visibility.ts +29 -0
  221. package/src/index.ts +19 -0
  222. package/src/integration/index.ts +64 -0
  223. package/src/ionic.ts +34 -0
  224. package/src/native/bootstrap.ts +132 -0
  225. package/src/native/bridge.ts +18 -0
  226. package/src/native/resource-url.ts +11 -0
  227. package/src/native/session.ts +708 -0
  228. package/src/native/updates.ts +103 -0
  229. package/src/native/values.ts +29 -0
  230. package/src/projection/events.ts +58 -0
  231. package/src/projection/pause.ts +58 -0
  232. package/src/projection/placement.ts +34 -0
  233. package/src/projection/scope.ts +13 -0
  234. package/src/projection/styles.ts +26 -0
  235. package/src/projection/surface.ts +88 -0
  236. package/src/projection/surfaces.ts +119 -0
  237. package/src/projection/web.ts +1013 -0
  238. package/src/types.ts +58 -0
@@ -0,0 +1,674 @@
1
+ import { getUnprojectedVisibility } from '../index.js';
2
+
3
+ import { enableSupportIonicOverlays } from '../ionic.js';
4
+ import { getProjectionState } from './shared/covered-source.js';
5
+ import { controlSelected } from './shared/control-options.js';
6
+ import { createCrossfade, fadeMarker } from './shared/crossfade.js';
7
+ import { alignVerticalBarClone, verticalBarFrame } from './shared/vertical-bars-geometry.js';
8
+ import { createVerticalBarsWebSearch, searchProjectionClass } from './vertical-bars-web-search.js';
9
+ import { inVerticalBarsSurface, topModal, movingModal, modalPreparing, modalPresentation } from './shared/modal.js';
10
+ import type { NativeUIRuntimeHandle, NativeUIOptions, NativeUIStatus } from './definitions.js';
11
+ import { VERTICAL_BARS_TRANSITION_CANCELED } from '../integration/index.js';
12
+ import {
13
+ activateProjectedElement,
14
+ createVerticalBarsPageState,
15
+ verticalBarsActionCandidate,
16
+ verticalBarsBackCandidate,
17
+ verticalBarsEnteringPage,
18
+ verticalBarsToolbarActions,
19
+ isExcluded,
20
+ isNativeUIDisabled,
21
+ isVerticalBarsToolbarGroup,
22
+ preferredVerticalBarsBack,
23
+ verticalBarsOwned,
24
+ marker,
25
+ syncToolbarText,
26
+ toolbarTextMarker,
27
+ prehideOnlyMutation,
28
+ prehiddenClass,
29
+ unprojected,
30
+ withoutPrehide,
31
+ } from './shared/dom.js';
32
+
33
+ const backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';
34
+ const toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';
35
+ const readyClass = 'ios-theme-vertical-bars-toolbar-ready';
36
+ const toolbarControlSize = 48;
37
+ const toolbarItemGap = 4;
38
+ const toolbarControlGap = 12;
39
+ const toolbarGroupHeight = (count: number) => count * toolbarControlSize + Math.max(0, count - 1) * toolbarItemGap;
40
+
41
+ interface ToolbarProjection {
42
+ source: HTMLElement;
43
+ root: HTMLElement;
44
+ projection: HTMLElement;
45
+ actions: { source: HTMLElement; projection: HTMLElement; previousAriaHidden: string | null | undefined }[];
46
+ }
47
+
48
+ interface BackProjection {
49
+ source: HTMLIonBackButtonElement;
50
+ projection: HTMLIonBackButtonElement;
51
+ root: HTMLElement;
52
+ previousAriaHidden: string | null | undefined;
53
+ }
54
+
55
+ interface ToolbarSource {
56
+ group: HTMLElement;
57
+ actions: HTMLElement[];
58
+ }
59
+
60
+ export const createVerticalBarsWebProjection = (
61
+ doc: Document,
62
+ options: NativeUIOptions,
63
+ enabled: () => boolean = () => true,
64
+ ): NativeUIRuntimeHandle => {
65
+ const projectionState = getProjectionState(doc);
66
+ const { isProjectionMutation, sourcesChangedEvent, targetsChangedEvent } = projectionState;
67
+ const win = doc.defaultView!;
68
+ const crossfade = createCrossfade(win);
69
+ let incomingInstant: boolean | undefined;
70
+ const toolbarEnabled = options.ionic?.auto !== false;
71
+ let root: HTMLElement | undefined;
72
+ let backProjections: BackProjection[] = [];
73
+ let toolbarProjections: ToolbarProjection[] = [];
74
+ let suspended = 0;
75
+ let stopped = false;
76
+ let frame = 0;
77
+ let updates = 0;
78
+ let observingVerticalBars = false;
79
+ const verticalBarsPages = createVerticalBarsPageState();
80
+ let sourceObserver: MutationObserver | undefined;
81
+ let waiters: (() => void)[] = [];
82
+ const listeners = new AbortController();
83
+ const verticalBarsRoot = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
84
+ const surfaceOf = (element: Element) => element.closest<HTMLElement>('ion-modal') ?? verticalBarsRoot()!;
85
+ const hasNativeSources = () =>
86
+ projectionState.snapshotsEnabled && !!doc.querySelector('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]');
87
+ const isPrepared = (element: Element) => {
88
+ const modal = element.closest('ion-modal');
89
+ // Preparing hidden proxy DOM inside an animating Ionic sheet can rewind its
90
+ // compositor animation. Native owns entry; prepare the image target at DidPresent.
91
+ if (modal && (modalPreparing(modal) || modalPresentation(modal))) return false;
92
+ return (
93
+ (projectionState.snapshotsEnabled &&
94
+ (element.matches('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]') ||
95
+ !!element.querySelector('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]'))) ||
96
+ [...projectionState.preparedOwners].some((owner) => owner === element || owner.contains(element) || element.contains(owner))
97
+ );
98
+ };
99
+ // Covered toolbar controls keep a Web visual on their own surface, below the modal.
100
+ const isCovered = (element: Element) =>
101
+ (!!movingModal(doc) && element.closest('ion-modal') === movingModal(doc)) ||
102
+ projectionState.owns(element as HTMLElement) ||
103
+ projectionState.isVisible(element as HTMLElement) ||
104
+ isPrepared(element) ||
105
+ (!!topModal(doc) && surfaceOf(element) !== topModal(doc));
106
+ const projectedSources = () =>
107
+ [
108
+ ...Array.from(doc.querySelectorAll<HTMLElement>('[data-native-ui-vertical-bars], [data-native-ui-vertical-bars-member]')),
109
+ ...projectionState.owners,
110
+ ...projectionState.preparedOwners,
111
+ ...backProjections.map(({ source }) => source),
112
+ search.source,
113
+ ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source)),
114
+ ].filter((source): source is HTMLElement => !!source);
115
+ const isRendered = (element: HTMLElement) =>
116
+ withoutPrehide(element, () => {
117
+ const style = getComputedStyle(element);
118
+ const rect = element.getBoundingClientRect();
119
+ return (
120
+ element.isConnected &&
121
+ style.display !== 'none' &&
122
+ (style.visibility === 'visible' || getUnprojectedVisibility(element) === 'visible') &&
123
+ rect.width > 0 &&
124
+ rect.height > 0
125
+ );
126
+ });
127
+ const search = createVerticalBarsWebSearch(
128
+ doc,
129
+ (element) =>
130
+ options.ionic?.auto !== false &&
131
+ controlSelected(element, options, 'tabs', true) &&
132
+ !!verticalBarsRoot()?.contains(element) &&
133
+ (!options.root || options.root.contains(element)) &&
134
+ !element.closest('[data-native-ui-placement="element"]') &&
135
+ inVerticalBarsSurface(element) &&
136
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
137
+ !isNativeUIDisabled(element) &&
138
+ !verticalBarsPages.isDeparted(element) &&
139
+ isRendered(element),
140
+ () => {
141
+ void schedule();
142
+ },
143
+ );
144
+ const inEligibleToolbar = (element: HTMLElement) =>
145
+ !!verticalBarsRoot()?.contains(element) &&
146
+ (!options.root || options.root.contains(element)) &&
147
+ !element.closest('[data-native-ui-placement="element"]') &&
148
+ (inVerticalBarsSurface(element) || isCovered(element)) &&
149
+ verticalBarsActionCandidate(element) &&
150
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
151
+ (!verticalBarsPages.isDeparted(element) || projectionState.isVisible(element));
152
+ const isEligibleBack = (element: HTMLIonBackButtonElement) =>
153
+ !!verticalBarsRoot()?.contains(element) &&
154
+ (!options.root || options.root.contains(element)) &&
155
+ !element.closest('[data-native-ui-placement="element"]') &&
156
+ verticalBarsOwned(element) &&
157
+ verticalBarsBackCandidate(element) &&
158
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
159
+ (!verticalBarsPages.isDeparted(element) || projectionState.isVisible(element)) &&
160
+ unprojected(projectedSources(), () => isRendered(element));
161
+ const pageOrder = (element: Element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page')!);
162
+ const findBack = (surface = topModal(doc) ?? verticalBarsRoot()) => {
163
+ const candidates = Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).filter(
164
+ (element) => surfaceOf(element) === surface && isEligibleBack(element),
165
+ );
166
+ return preferredVerticalBarsBack(candidates, doc);
167
+ };
168
+ const findToolbarGroups = () => {
169
+ const candidates = Array.from(doc.querySelectorAll<HTMLIonButtonsElement>(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap(
170
+ (group): ToolbarSource[] => {
171
+ const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));
172
+ if (!actions.length) return [];
173
+ if (!isVerticalBarsToolbarGroup(group)) return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));
174
+ return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];
175
+ },
176
+ );
177
+ for (const action of Array.from(doc.querySelectorAll<HTMLElement>('ion-button, ion-menu-button'))) {
178
+ if (action.closest('ion-buttons') || !verticalBarsOwned(action) || !inEligibleToolbar(action)) continue;
179
+ if (unprojected(projectedSources(), () => isRendered(action))) candidates.push({ group: action, actions: [action] });
180
+ }
181
+ const highestPages = new Map<HTMLElement, number>();
182
+ for (const { group } of candidates) {
183
+ const surface = surfaceOf(group);
184
+ highestPages.set(surface, Math.max(highestPages.get(surface) ?? -1, pageOrder(group)));
185
+ }
186
+ return candidates.filter(({ group }) => (enabled() || isCovered(group)) && pageOrder(group) === highestPages.get(surfaceOf(group)));
187
+ };
188
+ const isCurrentToolbarAction = (source: HTMLElement) =>
189
+ inVerticalBarsSurface(source) && findToolbarGroups().some(({ actions }) => actions.includes(source));
190
+ const nativeOwned = projectionState.nativeOwns;
191
+ const restore = (retire?: (surface: HTMLElement) => void, retained = new Set<BackProjection | ToolbarProjection>()) => {
192
+ search.restore(retire);
193
+ for (const current of backProjections) {
194
+ if (retained.has(current)) continue;
195
+ const { source, projection, root: surface, previousAriaHidden } = current;
196
+ surface.classList.remove(readyClass);
197
+ if (retire) retire(projection);
198
+ else projection.remove();
199
+ if (!nativeOwned(source)) {
200
+ source.removeAttribute(marker);
201
+ if (previousAriaHidden === null) source.removeAttribute('aria-hidden');
202
+ else if (previousAriaHidden !== undefined) source.setAttribute('aria-hidden', previousAriaHidden);
203
+ }
204
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
205
+ }
206
+ backProjections = backProjections.filter((current) => retained.has(current));
207
+ for (const current of toolbarProjections) {
208
+ if (retained.has(current)) continue;
209
+ const { projection, actions, root: surface } = current;
210
+ surface.classList.remove(readyClass);
211
+ if (retire) retire(projection);
212
+ else projection.remove();
213
+ for (const { source, previousAriaHidden } of actions) {
214
+ if (!nativeOwned(source)) {
215
+ source.removeAttribute(marker);
216
+ if (previousAriaHidden === null) source.removeAttribute('aria-hidden');
217
+ else if (previousAriaHidden !== undefined) source.setAttribute('aria-hidden', previousAriaHidden);
218
+ }
219
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
220
+ }
221
+ }
222
+ toolbarProjections = toolbarProjections.filter((current) => retained.has(current));
223
+ sourceObserver?.disconnect();
224
+ sourceObserver = undefined;
225
+ root?.classList.remove(readyClass);
226
+ root = undefined;
227
+ };
228
+ const copyAttributes = (target: HTMLElement, original: HTMLElement) => {
229
+ for (const name of ['id', 'slot', marker, 'aria-hidden']) target.removeAttribute(name);
230
+ for (const attribute of Array.from(target.attributes))
231
+ if (
232
+ !['class', marker, fadeMarker, 'data-native-ui-proxy-prepared'].includes(attribute.name) &&
233
+ !attribute.name.startsWith('data-native-ui-snapshot') &&
234
+ !original.hasAttribute(attribute.name)
235
+ )
236
+ target.removeAttribute(attribute.name);
237
+ for (const attribute of Array.from(original.attributes))
238
+ if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name) && !attribute.name.startsWith('data-native-ui-snapshot'))
239
+ target.setAttribute(attribute.name, attribute.value);
240
+ target.classList.remove(prehiddenClass);
241
+ // Establish ownership before insertion/layout: glass can sample even a single Web paint.
242
+ target.toggleAttribute('data-native-ui-proxy-prepared', isPrepared(original));
243
+ };
244
+ const syncBack = (target: HTMLIonBackButtonElement, original: HTMLIonBackButtonElement, surface: HTMLElement) => {
245
+ copyAttributes(target, original);
246
+ target.classList.add(backProjectionClass, 'ion-cloned-element');
247
+ target.disabled = original.disabled;
248
+ target.defaultHref = original.defaultHref;
249
+ target.icon = original.icon;
250
+ target.color = original.color;
251
+ target.mode = original.mode;
252
+ target.text = '';
253
+ alignVerticalBarClone(target, original, surface);
254
+ };
255
+ const syncAction = (target: HTMLElement, original: HTMLElement) => {
256
+ copyAttributes(target, original);
257
+ target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');
258
+ const label =
259
+ original.getAttribute('aria-label') ??
260
+ original.shadowRoot?.querySelector('[part~=native]')?.getAttribute('aria-label') ??
261
+ original.textContent?.trim();
262
+ if (label) target.setAttribute('aria-label', label);
263
+ if ('disabled' in original) (target as HTMLIonButtonElement).disabled = (original as HTMLIonButtonElement).disabled;
264
+ if (original.matches('ion-button')) {
265
+ const icon = original.querySelector<HTMLElement>('ion-icon, svg');
266
+ const clonedIcon = icon?.cloneNode(true) as HTMLElement | undefined;
267
+ clonedIcon?.setAttribute('slot', 'icon-only');
268
+ target.replaceChildren(...(clonedIcon ? [clonedIcon] : [document.createTextNode(original.textContent?.trim() || label || '')]));
269
+ }
270
+ };
271
+ const sameToolbar = (current: ToolbarProjection, { group, actions }: ToolbarSource) =>
272
+ group === current.source &&
273
+ surfaceOf(group) === current.root &&
274
+ actions.length === current.actions.length &&
275
+ actions.every((action, index) => action === current.actions[index].source);
276
+ const sameSources = (backs: HTMLIonBackButtonElement[], groups: ToolbarSource[]) =>
277
+ backs.length === backProjections.length &&
278
+ backs.every((source, index) => source === backProjections[index].source && surfaceOf(source) === backProjections[index].root) &&
279
+ groups.length === toolbarProjections.length &&
280
+ groups.every((group, index) => sameToolbar(toolbarProjections[index], group));
281
+ const syncExisting = () => {
282
+ for (const { source, projection, root: surface } of backProjections) {
283
+ if (!surface.classList.contains(readyClass)) surface.classList.add(readyClass);
284
+ if (projectionState.isVisible(source)) {
285
+ projection.inert = true;
286
+ continue;
287
+ }
288
+ syncBack(projection, source, surface);
289
+ projection.inert = isCovered(source);
290
+ if (projection.inert) projection.setAttribute('aria-hidden', 'true');
291
+ }
292
+ const offsets = new Map<HTMLElement, number>();
293
+ for (const { projection, source, actions, root: surface } of toolbarProjections) {
294
+ if (!surface.classList.contains(readyClass)) surface.classList.add(readyClass);
295
+ const topOffset = offsets.get(surface) ?? (findBack(surface) ? toolbarControlSize + toolbarControlGap : 0);
296
+ if (projectionState.isVisible(source) || actions.some((action) => projectionState.isVisible(action.source))) {
297
+ projection.inert = true;
298
+ offsets.set(surface, topOffset + toolbarGroupHeight(actions.length) + toolbarControlGap);
299
+ continue;
300
+ }
301
+ if (actions.length === 1 && !projection.matches('ion-buttons')) syncAction(projection, actions[0].source);
302
+ else copyAttributes(projection, source);
303
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
304
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
305
+ // Preserve appearance without bypassing the modal's input/focus boundary.
306
+ projection.inert = isCovered(source);
307
+ if (projection.inert) projection.setAttribute('aria-hidden', 'true');
308
+ if (projection.matches('ion-buttons')) actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));
309
+ const frames = actions.map(({ source }) => verticalBarFrame(source));
310
+ if (frames.every((frame) => !!frame)) {
311
+ if (actions.length === 1) alignVerticalBarClone(projection, actions[0].source, surface);
312
+ else {
313
+ const left = Math.min(...frames.map((frame) => frame!.x));
314
+ const top = Math.min(...frames.map((frame) => frame!.y));
315
+ const right = Math.max(...frames.map((frame) => frame!.x + frame!.width));
316
+ const bottom = Math.max(...frames.map((frame) => frame!.y + frame!.height));
317
+ const origin = surface.getBoundingClientRect();
318
+ Object.assign(projection.style, {
319
+ left: `${left - origin.left}px`,
320
+ top: `${top - origin.top}px`,
321
+ right: 'auto',
322
+ bottom: 'auto',
323
+ width: `${right - left}px`,
324
+ height: `${bottom - top}px`,
325
+ padding: '0',
326
+ boxSizing: 'border-box',
327
+ });
328
+ actions.forEach(({ source, projection: clone }) => alignVerticalBarClone(clone, source, projection));
329
+ }
330
+ }
331
+ offsets.set(surface, topOffset + toolbarGroupHeight(actions.length) + toolbarControlGap);
332
+ }
333
+ };
334
+ const project = (nextBacks: HTMLIonBackButtonElement[], groups: ToolbarSource[]) => {
335
+ root = topModal(doc) ?? verticalBarsRoot()!;
336
+ let topOffset = 0;
337
+ for (const source of nextBacks) {
338
+ const surface = surfaceOf(source);
339
+ if (surface === root) topOffset = toolbarControlSize + toolbarControlGap;
340
+ if (backProjections.some((current) => current.source === source)) continue;
341
+ const previousAriaHidden = nativeOwned(source) && source.hasAttribute(marker) ? undefined : source.getAttribute('aria-hidden');
342
+ const projection = source.cloneNode(false) as HTMLIonBackButtonElement;
343
+ syncBack(projection, source, surface);
344
+ projection.addEventListener(
345
+ 'click',
346
+ (event) => {
347
+ event.preventDefault();
348
+ event.stopImmediatePropagation();
349
+ if (inVerticalBarsSurface(source) && source === findBack()) activateProjectedElement(source);
350
+ },
351
+ { capture: true },
352
+ );
353
+ projectionState.setTarget(source, projection);
354
+ surface.append(projection);
355
+ backProjections.push({ source, projection, root: surface, previousAriaHidden });
356
+ source.setAttribute(marker, '');
357
+ source.setAttribute('aria-hidden', 'true');
358
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
359
+ if (surface === root) topOffset = toolbarControlSize + toolbarControlGap;
360
+ }
361
+ for (const { group, actions: sources } of groups) {
362
+ if (toolbarProjections.some((current) => sameToolbar(current, { group, actions: sources }))) {
363
+ topOffset += toolbarGroupHeight(sources.length) + toolbarControlGap;
364
+ continue;
365
+ }
366
+ const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false) as HTMLElement;
367
+ if (sources.length === 1) syncAction(projection, sources[0]);
368
+ else copyAttributes(projection, group);
369
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
370
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
371
+ const actions = sources.map((source) => {
372
+ const clone = sources.length === 1 ? projection : (source.cloneNode(false) as HTMLElement);
373
+ if (sources.length > 1) syncAction(clone, source);
374
+ clone.addEventListener(
375
+ 'click',
376
+ (event) => {
377
+ event.preventDefault();
378
+ event.stopImmediatePropagation();
379
+ if (isCurrentToolbarAction(source)) activateProjectedElement(source);
380
+ },
381
+ { capture: true },
382
+ );
383
+ if (sources.length > 1) projection.append(clone);
384
+ projectionState.setTarget(source, clone);
385
+ const previousAriaHidden = nativeOwned(source) && source.hasAttribute(marker) ? undefined : source.getAttribute('aria-hidden');
386
+ source.setAttribute(marker, '');
387
+ source.setAttribute('aria-hidden', 'true');
388
+ source.dispatchEvent(new CustomEvent('nativeUIChange'));
389
+ return { source, projection: clone, previousAriaHidden };
390
+ });
391
+ const surface = surfaceOf(group);
392
+ projectionState.setTarget(group, projection);
393
+ surface.append(projection);
394
+ toolbarProjections.push({ source: group, root: surface, projection, actions });
395
+ topOffset += toolbarGroupHeight(actions.length) + toolbarControlGap;
396
+ }
397
+ backProjections.sort((a, b) => nextBacks.indexOf(a.source) - nextBacks.indexOf(b.source));
398
+ toolbarProjections.sort(
399
+ (a, b) => groups.findIndex((group) => sameToolbar(a, group)) - groups.findIndex((group) => sameToolbar(b, group)),
400
+ );
401
+ syncExisting();
402
+ if (nextBacks.length || toolbarProjections.length) root.classList.add(readyClass);
403
+ sourceObserver = new MutationObserver((records) => {
404
+ if (
405
+ records.some(
406
+ (record) =>
407
+ record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record)),
408
+ )
409
+ )
410
+ void schedule();
411
+ });
412
+ for (const back of nextBacks)
413
+ if (back.shadowRoot)
414
+ sourceObserver.observe(back.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
415
+ for (const { group } of groups)
416
+ sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
417
+ };
418
+ const reveal = () => {
419
+ if (incomingInstant === undefined) return;
420
+ for (const surface of [
421
+ ...backProjections.map(({ projection }) => projection),
422
+ ...toolbarProjections.map(({ projection }) => projection),
423
+ search.surface,
424
+ ])
425
+ if (surface) crossfade.play(surface, false, incomingInstant);
426
+ incomingInstant = undefined;
427
+ };
428
+ const performUpdate = () => {
429
+ frame = 0;
430
+ syncToolbarText(doc);
431
+ const currentRoot = verticalBarsRoot();
432
+ if (observingVerticalBars !== !!currentRoot) {
433
+ observingVerticalBars = !!currentRoot;
434
+ observer.disconnect();
435
+ observer.observe(doc.documentElement, {
436
+ subtree: true,
437
+ childList: true,
438
+ characterData: observingVerticalBars,
439
+ attributes: true,
440
+ attributeOldValue: true,
441
+ attributeFilter: observingVerticalBars ? undefined : ['class'],
442
+ });
443
+ }
444
+ if (
445
+ stopped ||
446
+ suspended ||
447
+ options.verticalBars?.enabled === false ||
448
+ !currentRoot ||
449
+ (!enabled() && !topModal(doc) && !projectionState.owners.length && !projectionState.preparedOwners.length && !hasNativeSources())
450
+ )
451
+ return restore();
452
+ const surfaces = new Set(
453
+ Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).map(surfaceOf),
454
+ );
455
+ const nextBacks = toolbarEnabled
456
+ ? [...surfaces]
457
+ .map((surface) => findBack(surface))
458
+ .filter((back): back is HTMLIonBackButtonElement => !!back && (enabled() || isCovered(back)))
459
+ : [];
460
+ const groups = toolbarEnabled ? findToolbarGroups() : [];
461
+ const projectionRoot = topModal(doc) ?? currentRoot;
462
+ if (enabled()) search.update(projectionRoot);
463
+ else search.restore();
464
+ if (!nextBacks.length && !groups.length && !search.source) return restore();
465
+ if (root && sameSources(nextBacks, groups)) {
466
+ syncExisting();
467
+ reveal();
468
+ return;
469
+ }
470
+ // Image scopes retain their DOM identity while another surface adds/removes controls.
471
+ const retained = new Set<BackProjection | ToolbarProjection>([
472
+ ...backProjections.filter((current) => nextBacks.includes(current.source) && current.root === surfaceOf(current.source)),
473
+ ...toolbarProjections.filter((current) => groups.some((group) => sameToolbar(current, group))),
474
+ ]);
475
+ restore(undefined, retained);
476
+ project(nextBacks, groups);
477
+ if (enabled()) search.update(projectionRoot);
478
+ else search.restore();
479
+ reveal();
480
+ updates++;
481
+ };
482
+ const update = () => {
483
+ try {
484
+ performUpdate();
485
+ } catch {
486
+ restore();
487
+ } finally {
488
+ win.dispatchEvent(new Event(targetsChangedEvent));
489
+ const pending = waiters;
490
+ waiters = [];
491
+ pending.forEach((resolve) => resolve());
492
+ }
493
+ };
494
+ const schedule = (): Promise<void> => {
495
+ if (stopped) return Promise.resolve();
496
+ // Release Web ownership before the native runtime can acknowledge its next frame.
497
+ if (!enabled() && !topModal(doc) && !projectionState.owners.length && !projectionState.preparedOwners.length && !hasNativeSources())
498
+ restore();
499
+ const done = new Promise<void>((resolve) => waiters.push(resolve));
500
+ if (!frame) frame = win.requestAnimationFrame(update);
501
+ return done;
502
+ };
503
+ const observer = new MutationObserver((records) => {
504
+ const insideProjection = (target: Node) => {
505
+ const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;
506
+ const rootNode = target.getRootNode();
507
+ const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;
508
+ return !![element, shadowHost].some((candidate) =>
509
+ candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}, .${searchProjectionClass}`),
510
+ );
511
+ };
512
+ const verticalBarsChanged = records.some(
513
+ (record) =>
514
+ (record.type === 'attributes' && (record.target as Element).matches('ion-app')) ||
515
+ (record.type === 'childList' &&
516
+ Array.from(record.addedNodes).some(
517
+ (node) =>
518
+ node instanceof Element &&
519
+ (node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')),
520
+ )),
521
+ );
522
+ if (
523
+ (observingVerticalBars &&
524
+ records.some(
525
+ (record) =>
526
+ !isProjectionMutation(record) &&
527
+ record.attributeName !== marker &&
528
+ record.attributeName !== toolbarTextMarker &&
529
+ !prehideOnlyMutation(record) &&
530
+ !insideProjection(record.target),
531
+ )) ||
532
+ (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record)))
533
+ )
534
+ void schedule();
535
+ });
536
+ observer.observe(doc.documentElement, {
537
+ subtree: true,
538
+ childList: true,
539
+ attributes: true,
540
+ attributeOldValue: true,
541
+ attributeFilter: ['class'],
542
+ });
543
+ const pageLifecycle = (event: Event) => {
544
+ const page = event.target;
545
+ if (!(page instanceof HTMLElement) || !page.matches('.ion-page')) return;
546
+ if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {
547
+ const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;
548
+ verticalBarsPages.cancel(entering, page);
549
+ } else verticalBarsPages.lifecycle(event);
550
+ if (verticalBarsRoot()) void schedule();
551
+ };
552
+ for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])
553
+ doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });
554
+ doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });
555
+ const stopOverlays = enableSupportIonicOverlays(undefined, doc)!.subscribe(({ element, phase }) => {
556
+ if (!element?.matches('ion-modal')) return;
557
+ if (phase === 'willPresent' || phase === 'didPresent') void schedule();
558
+ // Retire modal clones before they can paint at the background rail offset.
559
+ if (phase === 'didDismiss') update();
560
+ });
561
+ listeners.signal.addEventListener('abort', stopOverlays, { once: true });
562
+ win.addEventListener(
563
+ 'nativeUIVerticalBarsAcquire',
564
+ (event) => {
565
+ const instant = (event as CustomEvent<{ instant: boolean }>).detail?.instant ?? true;
566
+ incomingInstant = undefined;
567
+ // Acquisition is dispatched before Native records the source's original ARIA.
568
+ // Transfer Web-owned visibility without destroying the prepared image target.
569
+ for (const current of [...backProjections, ...toolbarProjections.flatMap(({ actions }) => actions)]) {
570
+ if (!nativeOwned(current.source) || current.previousAriaHidden === undefined) continue;
571
+ if (current.previousAriaHidden === null) current.source.removeAttribute('aria-hidden');
572
+ else current.source.setAttribute('aria-hidden', current.previousAriaHidden);
573
+ current.previousAriaHidden = undefined;
574
+ }
575
+ if ([...backProjections, ...toolbarProjections].some(({ source }) => isCovered(source))) {
576
+ // Conceal existing Web surfaces immediately; leave discovery and geometry
577
+ // to the scheduled update so acquisition does not create new controls.
578
+ const retained = [
579
+ ...backProjections,
580
+ ...toolbarProjections.filter(({ actions }) => actions.length > 1),
581
+ ...toolbarProjections.flatMap(({ actions }) => actions),
582
+ ];
583
+ for (const { source, projection } of retained)
584
+ for (const attribute of ['data-native-ui-vertical-bars', 'data-native-ui-vertical-bars-member'])
585
+ projection.toggleAttribute(attribute, source.hasAttribute(attribute));
586
+ void schedule();
587
+ return;
588
+ }
589
+ restore((surface) => {
590
+ // Keep only the outgoing visual at its last rectangle after Web ownership
591
+ // is released. The native acknowledgement transfers input exactly once.
592
+ const box = surface.getBoundingClientRect();
593
+ surface.style.setProperty('position', 'fixed', 'important');
594
+ surface.style.setProperty('display', 'block', 'important');
595
+ surface.style.setProperty('left', `${box.left}px`, 'important');
596
+ surface.style.setProperty('top', `${box.top}px`, 'important');
597
+ surface.style.setProperty('right', 'auto', 'important');
598
+ surface.style.setProperty('bottom', 'auto', 'important');
599
+ surface.style.setProperty('width', `${box.width}px`, 'important');
600
+ surface.style.setProperty('height', `${box.height}px`, 'important');
601
+ crossfade.retire(surface, instant);
602
+ });
603
+ },
604
+ { signal: listeners.signal },
605
+ );
606
+ win.addEventListener(
607
+ 'nativeUIVerticalBarsRelease',
608
+ (event) => {
609
+ incomingInstant = (event as CustomEvent<{ instant: boolean }>).detail.instant;
610
+ void schedule();
611
+ },
612
+ { signal: listeners.signal },
613
+ );
614
+ win.addEventListener(
615
+ 'resize',
616
+ () => {
617
+ void schedule();
618
+ },
619
+ { signal: listeners.signal },
620
+ );
621
+ win.addEventListener(
622
+ 'nativeUIRefresh',
623
+ () => {
624
+ void schedule();
625
+ },
626
+ { signal: listeners.signal },
627
+ );
628
+ win.addEventListener(
629
+ sourcesChangedEvent,
630
+ () => {
631
+ void schedule();
632
+ },
633
+ { signal: listeners.signal },
634
+ );
635
+ void schedule();
636
+
637
+ return {
638
+ markForCheck: () => {
639
+ void schedule();
640
+ },
641
+ getStatus: (): NativeUIStatus => ({
642
+ state: stopped ? 'stopped' : 'web',
643
+ projected:
644
+ backProjections.length + Number(!!search.source) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
645
+ updates,
646
+ }),
647
+ async suspend() {
648
+ suspended++;
649
+ restore();
650
+ let resumed = false;
651
+ return {
652
+ async resume() {
653
+ if (resumed) return;
654
+ resumed = true;
655
+ suspended = Math.max(0, suspended - 1);
656
+ if (!suspended) await void schedule();
657
+ },
658
+ };
659
+ },
660
+ async destroy() {
661
+ if (stopped) return;
662
+ stopped = true;
663
+ win.cancelAnimationFrame(frame);
664
+ frame = 0;
665
+ waiters.forEach((resolve) => resolve());
666
+ waiters = [];
667
+ observer.disconnect();
668
+ sourceObserver?.disconnect();
669
+ listeners.abort();
670
+ crossfade.destroy();
671
+ restore();
672
+ },
673
+ };
674
+ };