@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.1

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 (255) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +23 -0
  3. package/README.md +53 -2
  4. package/RdlaboCapacitorNativeUi.podspec +17 -0
  5. package/android/build.gradle +69 -0
  6. package/android/proguard-rules.pro +0 -0
  7. package/android/src/main/AndroidManifest.xml +1 -0
  8. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
  9. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
  10. package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
  11. package/dist/_virtual/_rolldown/runtime.js +13 -0
  12. package/dist/core.d.ts +4 -0
  13. package/dist/core.js +3 -0
  14. package/dist/native/activation.d.ts +14 -0
  15. package/dist/native/activation.d.ts.map +1 -0
  16. package/dist/native/activation.js +305 -0
  17. package/dist/native/activation.js.map +1 -0
  18. package/dist/native/components/index.d.ts +19 -0
  19. package/dist/native/components/index.d.ts.map +1 -0
  20. package/dist/native/components/index.js +38 -0
  21. package/dist/native/components/index.js.map +1 -0
  22. package/dist/native/components/ion-back-button.d.ts +10 -0
  23. package/dist/native/components/ion-back-button.d.ts.map +1 -0
  24. package/dist/native/components/ion-back-button.js +21 -0
  25. package/dist/native/components/ion-back-button.js.map +1 -0
  26. package/dist/native/components/ion-button.d.ts +10 -0
  27. package/dist/native/components/ion-button.d.ts.map +1 -0
  28. package/dist/native/components/ion-button.js +25 -0
  29. package/dist/native/components/ion-button.js.map +1 -0
  30. package/dist/native/components/ion-buttons.d.ts +11 -0
  31. package/dist/native/components/ion-buttons.d.ts.map +1 -0
  32. package/dist/native/components/ion-buttons.js +30 -0
  33. package/dist/native/components/ion-buttons.js.map +1 -0
  34. package/dist/native/components/ion-fab.d.ts +10 -0
  35. package/dist/native/components/ion-fab.d.ts.map +1 -0
  36. package/dist/native/components/ion-fab.js +132 -0
  37. package/dist/native/components/ion-fab.js.map +1 -0
  38. package/dist/native/components/ion-menu-button.d.ts +13 -0
  39. package/dist/native/components/ion-menu-button.d.ts.map +1 -0
  40. package/dist/native/components/ion-menu-button.js +44 -0
  41. package/dist/native/components/ion-menu-button.js.map +1 -0
  42. package/dist/native/components/ion-segment.d.ts +11 -0
  43. package/dist/native/components/ion-segment.d.ts.map +1 -0
  44. package/dist/native/components/ion-segment.js +30 -0
  45. package/dist/native/components/ion-segment.js.map +1 -0
  46. package/dist/native/components/ion-tab-bar.d.ts +11 -0
  47. package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
  48. package/dist/native/components/ion-tab-bar.js +50 -0
  49. package/dist/native/components/ion-tab-bar.js.map +1 -0
  50. package/dist/native/components/searchable-tabs.d.ts +20 -0
  51. package/dist/native/components/searchable-tabs.d.ts.map +1 -0
  52. package/dist/native/components/searchable-tabs.js +413 -0
  53. package/dist/native/components/searchable-tabs.js.map +1 -0
  54. package/dist/native/definitions.d.ts +255 -0
  55. package/dist/native/definitions.d.ts.map +1 -0
  56. package/dist/native/definitions.js +0 -0
  57. package/dist/native/extensions.d.ts +31 -0
  58. package/dist/native/extensions.d.ts.map +1 -0
  59. package/dist/native/extensions.js +122 -0
  60. package/dist/native/extensions.js.map +1 -0
  61. package/dist/native/foldable/fold-state.d.ts +5 -0
  62. package/dist/native/foldable/fold-state.d.ts.map +1 -0
  63. package/dist/native/foldable/fold-state.js +15 -0
  64. package/dist/native/foldable/fold-state.js.map +1 -0
  65. package/dist/native/foldable/types.d.ts +12 -0
  66. package/dist/native/foldable/types.d.ts.map +1 -0
  67. package/dist/native/foldable/types.js +0 -0
  68. package/dist/native/html.d.ts +9 -0
  69. package/dist/native/html.d.ts.map +1 -0
  70. package/dist/native/html.js +188 -0
  71. package/dist/native/html.js.map +1 -0
  72. package/dist/native/index.d.ts +10 -0
  73. package/dist/native/index.d.ts.map +1 -0
  74. package/dist/native/index.js +28 -0
  75. package/dist/native/index.js.map +1 -0
  76. package/dist/native/legacy-layout.d.ts +5 -0
  77. package/dist/native/legacy-layout.d.ts.map +1 -0
  78. package/dist/native/legacy-layout.js +27 -0
  79. package/dist/native/legacy-layout.js.map +1 -0
  80. package/dist/native/overlays/content.d.ts +4 -0
  81. package/dist/native/overlays/content.d.ts.map +1 -0
  82. package/dist/native/overlays/content.js +51 -0
  83. package/dist/native/overlays/content.js.map +1 -0
  84. package/dist/native/overlays/controller.d.ts +8 -0
  85. package/dist/native/overlays/controller.d.ts.map +1 -0
  86. package/dist/native/overlays/controller.js +285 -0
  87. package/dist/native/overlays/controller.js.map +1 -0
  88. package/dist/native/overlays/dialog-animation.d.ts +9 -0
  89. package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
  90. package/dist/native/overlays/dialog-animation.js +53 -0
  91. package/dist/native/overlays/dialog-animation.js.map +1 -0
  92. package/dist/native/overlays/dismiss-animation.d.ts +4 -0
  93. package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
  94. package/dist/native/overlays/dismiss-animation.js +18 -0
  95. package/dist/native/overlays/dismiss-animation.js.map +1 -0
  96. package/dist/native/overlays/focus.d.ts +5 -0
  97. package/dist/native/overlays/focus.d.ts.map +1 -0
  98. package/dist/native/overlays/focus.js +59 -0
  99. package/dist/native/overlays/focus.js.map +1 -0
  100. package/dist/native/overlays/popover.d.ts +12 -0
  101. package/dist/native/overlays/popover.d.ts.map +1 -0
  102. package/dist/native/overlays/popover.js +110 -0
  103. package/dist/native/overlays/popover.js.map +1 -0
  104. package/dist/native/overlays/styles.d.ts +11 -0
  105. package/dist/native/overlays/styles.d.ts.map +1 -0
  106. package/dist/native/overlays/styles.js +167 -0
  107. package/dist/native/overlays/styles.js.map +1 -0
  108. package/dist/native/prehide.d.ts +8 -0
  109. package/dist/native/prehide.d.ts.map +1 -0
  110. package/dist/native/prehide.js +238 -0
  111. package/dist/native/prehide.js.map +1 -0
  112. package/dist/native/runtime.d.ts +7 -0
  113. package/dist/native/runtime.d.ts.map +1 -0
  114. package/dist/native/runtime.js +788 -0
  115. package/dist/native/runtime.js.map +1 -0
  116. package/dist/native/shared/bridge.d.ts +6 -0
  117. package/dist/native/shared/bridge.d.ts.map +1 -0
  118. package/dist/native/shared/bridge.js +18 -0
  119. package/dist/native/shared/bridge.js.map +1 -0
  120. package/dist/native/shared/candidate.d.ts +25 -0
  121. package/dist/native/shared/candidate.d.ts.map +1 -0
  122. package/dist/native/shared/candidate.js +106 -0
  123. package/dist/native/shared/candidate.js.map +1 -0
  124. package/dist/native/shared/clipping.d.ts +5 -0
  125. package/dist/native/shared/clipping.d.ts.map +1 -0
  126. package/dist/native/shared/clipping.js +26 -0
  127. package/dist/native/shared/clipping.js.map +1 -0
  128. package/dist/native/shared/crossfade.d.ts +12 -0
  129. package/dist/native/shared/crossfade.d.ts.map +1 -0
  130. package/dist/native/shared/crossfade.js +67 -0
  131. package/dist/native/shared/crossfade.js.map +1 -0
  132. package/dist/native/shared/diagnostics.d.ts +4 -0
  133. package/dist/native/shared/diagnostics.d.ts.map +1 -0
  134. package/dist/native/shared/diagnostics.js +18 -0
  135. package/dist/native/shared/diagnostics.js.map +1 -0
  136. package/dist/native/shared/dom.d.ts +46 -0
  137. package/dist/native/shared/dom.d.ts.map +1 -0
  138. package/dist/native/shared/dom.js +180 -0
  139. package/dist/native/shared/dom.js.map +1 -0
  140. package/dist/native/shared/events.d.ts +15 -0
  141. package/dist/native/shared/events.d.ts.map +1 -0
  142. package/dist/native/shared/events.js +17 -0
  143. package/dist/native/shared/events.js.map +1 -0
  144. package/dist/native/shared/icons.d.ts +8 -0
  145. package/dist/native/shared/icons.d.ts.map +1 -0
  146. package/dist/native/shared/icons.js +61 -0
  147. package/dist/native/shared/icons.js.map +1 -0
  148. package/dist/native/shared/modal.d.ts +10 -0
  149. package/dist/native/shared/modal.d.ts.map +1 -0
  150. package/dist/native/shared/modal.js +160 -0
  151. package/dist/native/shared/modal.js.map +1 -0
  152. package/dist/native/shared/projection-id.d.ts +4 -0
  153. package/dist/native/shared/projection-id.d.ts.map +1 -0
  154. package/dist/native/shared/projection-id.js +7 -0
  155. package/dist/native/shared/projection-id.js.map +1 -0
  156. package/dist/native/shared/toolbar-options.d.ts +9 -0
  157. package/dist/native/shared/toolbar-options.d.ts.map +1 -0
  158. package/dist/native/shared/toolbar-options.js +36 -0
  159. package/dist/native/shared/toolbar-options.js.map +1 -0
  160. package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
  161. package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
  162. package/dist/native/shared/vertical-bars-geometry.js +85 -0
  163. package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
  164. package/dist/native/startup.d.ts +8 -0
  165. package/dist/native/startup.d.ts.map +1 -0
  166. package/dist/native/startup.js +86 -0
  167. package/dist/native/startup.js.map +1 -0
  168. package/dist/native/vertical-bars-web-search.d.ts +10 -0
  169. package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
  170. package/dist/native/vertical-bars-web-search.js +90 -0
  171. package/dist/native/vertical-bars-web-search.js.map +1 -0
  172. package/dist/native/vertical-bars-web.d.ts +5 -0
  173. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  174. package/dist/native/vertical-bars-web.js +421 -0
  175. package/dist/native/vertical-bars-web.js.map +1 -0
  176. package/dist/native-integration/index.d.ts +25 -0
  177. package/dist/native-integration/index.d.ts.map +1 -0
  178. package/dist/native-integration/index.js +33 -0
  179. package/dist/native-integration/index.js.map +1 -0
  180. package/dist/vertical-bars.d.ts +6 -0
  181. package/dist/vertical-bars.js +5 -0
  182. package/docs/configuration.md +187 -0
  183. package/docs/framework-independent.md +82 -0
  184. package/docs/migration.md +206 -0
  185. package/docs/native-mini-player.md +47 -0
  186. package/docs/native-ui-extensions.md +365 -0
  187. package/docs/native-ui.md +409 -0
  188. package/docs/setup.md +205 -0
  189. package/docs/vertical-bars.md +379 -0
  190. package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
  191. package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
  192. package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
  193. package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
  194. package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
  195. package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
  196. package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
  197. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
  198. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
  199. package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
  200. package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
  201. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
  202. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
  203. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
  204. package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
  205. package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
  206. package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
  207. package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
  208. package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
  209. package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
  210. package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
  211. package/package.json +89 -4
  212. package/src/core.ts +15 -0
  213. package/src/native/activation.ts +394 -0
  214. package/src/native/components/index.ts +45 -0
  215. package/src/native/components/ion-back-button.ts +22 -0
  216. package/src/native/components/ion-button.ts +25 -0
  217. package/src/native/components/ion-buttons.ts +37 -0
  218. package/src/native/components/ion-fab.ts +151 -0
  219. package/src/native/components/ion-menu-button.ts +37 -0
  220. package/src/native/components/ion-segment.ts +27 -0
  221. package/src/native/components/ion-tab-bar.ts +50 -0
  222. package/src/native/components/searchable-tabs.ts +499 -0
  223. package/src/native/definitions.ts +324 -0
  224. package/src/native/extensions.ts +176 -0
  225. package/src/native/foldable/fold-state.ts +10 -0
  226. package/src/native/foldable/types.ts +4 -0
  227. package/src/native/html.ts +193 -0
  228. package/src/native/index.ts +41 -0
  229. package/src/native/legacy-layout.ts +21 -0
  230. package/src/native/overlays/content.ts +51 -0
  231. package/src/native/overlays/controller.ts +349 -0
  232. package/src/native/overlays/dialog-animation.ts +53 -0
  233. package/src/native/overlays/dismiss-animation.ts +15 -0
  234. package/src/native/overlays/focus.ts +64 -0
  235. package/src/native/overlays/popover.ts +120 -0
  236. package/src/native/overlays/styles.ts +181 -0
  237. package/src/native/prehide.ts +285 -0
  238. package/src/native/runtime.ts +988 -0
  239. package/src/native/shared/bridge.ts +18 -0
  240. package/src/native/shared/candidate.ts +162 -0
  241. package/src/native/shared/clipping.ts +24 -0
  242. package/src/native/shared/crossfade.ts +67 -0
  243. package/src/native/shared/diagnostics.ts +13 -0
  244. package/src/native/shared/dom.ts +304 -0
  245. package/src/native/shared/events.ts +14 -0
  246. package/src/native/shared/icons.ts +57 -0
  247. package/src/native/shared/modal.ts +157 -0
  248. package/src/native/shared/projection-id.ts +2 -0
  249. package/src/native/shared/toolbar-options.ts +53 -0
  250. package/src/native/shared/vertical-bars-geometry.ts +80 -0
  251. package/src/native/startup.ts +84 -0
  252. package/src/native/vertical-bars-web-search.ts +86 -0
  253. package/src/native/vertical-bars-web.ts +498 -0
  254. package/src/native-integration/index.ts +49 -0
  255. package/src/vertical-bars.ts +11 -0
@@ -0,0 +1,379 @@
1
+ ---
2
+ title: Vertical Bars (preview)
3
+ ---
4
+
5
+ # Vertical Bars (preview)
6
+
7
+ Vertical Bars moves eligible Ionic navigation and actions into a side rail while keeping the original components as the source of labels, icons and behavior. It works with the iOS theme or an existing Ionic theme, independently of hinge posture and the full Native UI.
8
+
9
+ Use this page for layout, control eligibility, native button appearance and the runtime API. For device events and split panes, see [iPhone Duo support](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo.md). For a step-by-step browser preview and native setup, start with [iPhone Duo with your existing theme](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo-with-original-theme.md).
10
+
11
+ Available in `1.2.0` as a **preview** feature. APIs and supported behavior may change.
12
+
13
+ See [shared configuration](configuration.md) for all defaults and live `rail.enableVerticalControlArea()` / `rail.disableVerticalControlArea()` methods. [Plain HTML controls](framework-independent.md) use the same native rail renderers.
14
+
15
+ ## Enable Vertical Bars
16
+
17
+ Install the native package as a direct dependency:
18
+
19
+ ```sh
20
+ npm install @rdlabo/capacitor-native-ui
21
+ ```
22
+
23
+ For Capacitor iOS, build your Web assets and run `npx cap sync ios`. Supply the Web layout through your app or theme as described below.
24
+
25
+ After `ion-app` is mounted, start one runtime:
26
+
27
+ ```ts
28
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
29
+
30
+ const rail = await enableNativeUI({ verticalBars: { only: true } });
31
+ ```
32
+
33
+ Configure your Web rail layout and [apply device placement](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo.md#project-controls-into-the-rail) for an iPhone Duo. The application owns placement and cleanup: call `await rail.destroy()` when its owner is disposed. If you already use `enableNativeUI()`, keep that runtime; it includes Vertical Bars. Do not start both.
34
+
35
+ The `/vertical-bars` entry point requires `@capacitor/core`, including in browser builds. Using only the theme’s rail CSS needs no native runtime. For native setup and navigation transitions, follow the [existing-theme guide](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/iphone-duo-with-original-theme.md).
36
+
37
+ On supported Capacitor iOS, eligible controls use native SwiftUI rendering. Web, Android and unavailable native projection use Web clones. A browser preview can verify layout and actions, but cannot compare native button appearance.
38
+
39
+ ## Placement and Web layout
40
+
41
+ The application or theme owns Web spacing, rail positioning, and fallback styles. The plugin reads the supplied Ionic layout and placement; it does not reserve Web content space on its own. With the iOS27 theme, use its `setVerticalBarsLayout({ edge, inset })` helper. Native-only applications use their own CSS; no theme helper is required. Native availability comes from device metrics, independently of Web layout.
42
+
43
+ The [theme's optional rail stylesheet](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/vertical-bars-layout.md) supplies one ready-made layout. Its classes and CSS variables are not a requirement to install the theme package: applications may provide equivalent layout themselves. The runtime currently uses the `ios-theme-vertical-bars` class on `ion-app` to identify the rail layout, regardless of who supplies its styles.
44
+
45
+ ## Native rendering
46
+
47
+ On supported iOS versions, adding `.ios-theme-vertical-bars` changes only controls that the system relocates into the physical side rail. Native UI presents eligible tabs, back navigation, menu buttons, and toolbar actions through a SwiftUI `TabView` and toolbar once the class is applied. When the OS reports a rail edge — on iPhone Duo linked against iOS 27.1 or later — it must agree with the applied placement; a disagreeing report keeps the rail on the Web. Without a device-reported edge, projection retains its Web fallback. A placement preference cannot enable an unsupported native rail. SwiftUI owns their adaptive placement and Liquid Glass appearance; Ionic remains the source of labels, icons, selected/disabled state, routing, form submission, and click handlers.
48
+
49
+ The SwiftUI surface is clipped and hit-tested to the system rail. Web content remains visible and interactive outside that physical region. The runtime optimistically updates tab selection before forwarding the action to the original `ion-tab-button`, using the same event and stale-revision protection as the other native controls. Overlay layout follows the [optional theme rail layout](https://github.com/rdlabo-dev/ionic-theme-ios27/blob/main/packages/theme-ios27/docs/vertical-bars-layout.md#reserve-the-vertical-rail) when that stylesheet is used; custom layouts remain the application’s responsibility.
50
+
51
+
52
+ ### Native popovers (Vertical Bars only)
53
+
54
+ Native popover presentation is limited to popovers anchored to accepted native Vertical Bars `ion-button` controls. It is available with the standalone Vertical Bars runtime; the full Native UI is not required. Enable it explicitly when starting the runtime (`popover` defaults to `false`):
55
+
56
+ ```ts
57
+ import { enableNativeUI } from '@rdlabo/capacitor-native-ui';
58
+
59
+ const rail = await enableNativeUI({
60
+ verticalBars: { only: true, popover: true },
61
+ });
62
+ ```
63
+
64
+ Use an `ion-popover` whose `trigger` identifies the projected button, or pass that button's click event to `PopoverController.create({ event, ... })`. Ordinary toolbar and body popovers remain on the Web. If a rail-anchored popover falls back to Web rendering, the rail also returns to Web projection until dismissal. See [native popover behavior and limitations](native-ui.md#popovers-anchored-to-vertical-bars).
65
+
66
+
67
+ ## Toolbar actions
68
+
69
+ A standard `ion-back-button` can project from outside a fixed toolbar, including routed content or a persistent app shell. `ion-menu-button` and other toolbar actions require a fixed toolbar.
70
+
71
+ By default, an `ion-button` moves into the rail when it contains an `ion-icon` or SVG with `slot="icon-only"`. The button must be in a fixed `ion-toolbar` directly inside `ion-header` or `ion-footer`, outside scrolling `ion-content`.
72
+
73
+ | Icon markup | Placement |
74
+ | --- | --- |
75
+ | `slot="icon-only"` | Vertical rail |
76
+ | `slot="start"`, `slot="end"`, or no slot | Original horizontal toolbar |
77
+ | No icon | Original horizontal toolbar |
78
+
79
+ ```html
80
+ <ion-header>
81
+ <ion-toolbar>
82
+ <ion-buttons slot="end">
83
+ <ion-button aria-label="Done">
84
+ <ion-icon name="checkmark-outline" slot="icon-only"></ion-icon>
85
+ </ion-button>
86
+ </ion-buttons>
87
+ </ion-toolbar>
88
+ </ion-header>
89
+ ```
90
+
91
+ All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors follow this placement rule. Keep accessible names and the original click or form-submit handlers on the source buttons. `type="submit"` and `.button-submit` do not select a different placement.
92
+
93
+ The rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
94
+
95
+ A toolbar whose content has all moved into the rail collapses while projection is active. Toolbars containing a title, direct text, other content or a horizontal-only control remain visible. Removing projection restores the original toolbar.
96
+
97
+ ### Select actions and grouping independently
98
+
99
+ Use `vertical: { buttons, button, backButton }` to configure toolbar actions.
100
+ All fields default to `true`. `vertical: false` disables automatic toolbar
101
+ selection and grouping; local attributes may still opt in.
102
+ For vertical-layout-only automatic toolbar projection, use
103
+ `enableNativeUI({ horizontal: false, vertical: true })`.
104
+ This selects by layout, not device model, and leaves tabs, segments and FABs unchanged.
105
+ See [toolbar configuration](native-ui.md#toolbar-selection-and-grouping)
106
+ for horizontal settings, boolean shorthands and defaults.
107
+
108
+ | Option | Responsibility | What it does not do |
109
+ | --- | --- | --- |
110
+ | `button` | Automatically select eligible `ion-button` and `ion-menu-button` actions, whether inside or outside `ion-buttons`. | Does not select back buttons. |
111
+ | `buttons` | Allow selected actions in `ion-buttons` to project as a group when the existing grouping rules allow it. | Does not select, enable or disable any child action. |
112
+ | `backButton` | Automatically select eligible `ion-back-button` elements, independently of their parent group. | Does not group back buttons with other actions. |
113
+
114
+ For ordinary icon-only buttons in a group, with no local attributes:
115
+
116
+ | `buttons` | `button` | Result |
117
+ | --- | --- | --- |
118
+ | `true` | `true` | Eligible buttons project together when the group qualifies. |
119
+ | `false` | `true` | Eligible buttons project individually. |
120
+ | `true` | `false` | No ordinary buttons project; grouping alone selects nothing. |
121
+ | `false` | `false` | No ordinary buttons project. |
122
+
123
+ These results concern ordinary actions only. Back navigation, tabs and native
124
+ search are independent. For example, to select only back navigation among toolbar
125
+ actions:
126
+
127
+ ```ts
128
+ const rail = await enableNativeUI({
129
+ verticalBars: { only: true },
130
+ vertical: { buttons: false, button: false, backButton: true },
131
+ });
132
+ ```
133
+
134
+ This does not disable tab projection or tab rail CSS.
135
+
136
+ ### Explicit action selection and local grouping
137
+
138
+ Use `data-vertical-bars="true|false"` on the element whose behavior you want to
139
+ override. The attribute is **not inherited**.
140
+
141
+ | Element | Meaning of its attribute | Fallback when absent or invalid |
142
+ | --- | --- | --- |
143
+ | `ion-button` / `ion-menu-button` | Select or exclude this action. Explicit `true` bypasses the icon-only selection rule. | `button` and the automatic shape rules |
144
+ | `ion-buttons` | Enable or disable grouping of already-selected actions. Never selects children. | `buttons` and the existing grouping rules |
145
+ | `ion-back-button` | Select or exclude this back button. | `backButton` and the supported back-button rules |
146
+
147
+ Empty or unknown attribute values are ignored. To disable automatic action
148
+ selection and explicitly select a text button:
149
+
150
+ ```ts
151
+ const rail = await enableNativeUI({
152
+ verticalBars: { only: true },
153
+ vertical: { buttons: true, button: false, backButton: false },
154
+ });
155
+ ```
156
+
157
+ ```html
158
+ <ion-buttons>
159
+ <ion-button data-vertical-bars="true">Save</ion-button>
160
+ <ion-button>Cancel</ion-button>
161
+ </ion-buttons>
162
+ ```
163
+
164
+ Only Save moves; Cancel stays in the horizontal toolbar. Adding
165
+ `data-vertical-bars="true"` to the group would not select Cancel.
166
+
167
+ To group two explicitly selected actions even when `buttons: false`:
168
+
169
+ ```html
170
+ <ion-buttons data-vertical-bars="true">
171
+ <ion-button data-vertical-bars="true">Save</ion-button>
172
+ <ion-button data-vertical-bars="true">Done</ion-button>
173
+ </ion-buttons>
174
+ ```
175
+
176
+ Set the group's attribute to `false` to project the same actions individually.
177
+ Grouping keeps the existing eligibility rules: the entire group must qualify,
178
+ and a single ordinary button projects on its own. A group with horizontal
179
+ content projects its selected actions individually. The source group and its
180
+ unselected children remain in their original toolbar.
181
+
182
+ Explicit action selection still requires a fixed header/footer toolbar.
183
+ Shell opt-outs, horizontal-only classes and overlay restrictions still apply.
184
+ Placement and grouping are captured for the routed-page visit: attribute changes
185
+ take effect after leaving and re-entering the page. Restart the runtime to change
186
+ startup options.
187
+
188
+ ### Back buttons inside groups
189
+
190
+ An `ion-back-button` remains a navigation action even inside `ion-buttons`.
191
+ It ignores `buttons`, `button`, and the group's `data-vertical-bars` attribute.
192
+ Its own attribute overrides `backButton`. It uses the dedicated back position,
193
+ never an ordinary action group, and is not duplicated as a toolbar action.
194
+
195
+ ```html
196
+ <ion-buttons data-vertical-bars="false">
197
+ <!-- Still projects when backButton is true. -->
198
+ <ion-back-button></ion-back-button>
199
+ </ion-buttons>
200
+ ```
201
+
202
+ The Ionic DOM container does not redefine the navigation role. The theme forwards
203
+ the projected back action to Ionic; it does not replace Ionic routing with a
204
+ native navigation stack. Native layout remains system-managed.
205
+
206
+ Existing back-button eligibility still applies, including the supported default
207
+ icon/color and page-position rules. Ancestor `data-native-ui="disabled"`,
208
+ `.ios-theme-horizontal-only` on `ion-buttons`, and other structural exclusions
209
+ remain opt-outs; those are distinct from the grouping setting.
210
+
211
+ ### Placement versus appearance
212
+
213
+ `data-vertical-bars` controls selection or grouping as described above.
214
+ `data-projection="source|system"` controls native appearance and follows its own
215
+ button-then-group inheritance rules. Neither appearance value selects an action.
216
+
217
+ ### Choose button appearance
218
+
219
+ `buttonProjection` and the local projection settings below are available in `1.2.0`.
220
+
221
+ For native vertical `ion-button` and `ion-menu-button` actions, choose who controls appearance:
222
+
223
+ | `buttonProjection` | Appearance |
224
+ | --- | --- |
225
+ | `'system'` (default) | SwiftUI styles the buttons and tints their icons. Ionic fill, colors and borders are not applied. |
226
+ | `'source'` | Projects the supported Ionic fill and computed colors described in [Source fill rules](#source-fill-rules). |
227
+
228
+ ```ts
229
+ const rail = await enableNativeUI({ verticalBars: { only: true, buttonProjection: 'source' } });
230
+ ```
231
+
232
+ `enableNativeUI()` accepts these settings inside `verticalBars`; the legacy global `enableVerticalControlArea()` still accepts its original top-level settings. Use one runtime, and destroy it before restarting with different options. Either mode preserves actions, disabled state and grouping. Disabled icons use the native disabled appearance. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
233
+
234
+ **Migration from the experimental releases:** the default changes from source styling to `system`. Set `buttonProjection: 'source'` to retain the previous projection behavior.
235
+
236
+ ### Override individual buttons or groups
237
+
238
+ Use `data-projection` for local exceptions. Existing classes remain supported:
239
+
240
+ | Attribute | Equivalent class |
241
+ | --- | --- |
242
+ | `data-projection="source"` | `ios-theme-projection-source` |
243
+ | `data-projection="system"` | `ios-theme-projection-system` |
244
+
245
+ ```html
246
+ <ion-buttons data-projection="source">
247
+ <ion-button fill="solid" aria-label="Add">
248
+ <ion-icon name="add-outline" slot="icon-only"></ion-icon>
249
+ </ion-button>
250
+ <ion-button data-projection="system" aria-label="Search">
251
+ <ion-icon name="search-outline" slot="icon-only"></ion-icon>
252
+ </ion-button>
253
+ </ion-buttons>
254
+ ```
255
+
256
+ The first matching setting wins:
257
+
258
+ 1. The button's local setting.
259
+ 2. Its nearest `ion-buttons` local setting.
260
+ 3. The startup `buttonProjection` option, or `system` if omitted.
261
+
262
+ On the same element, a valid `data-projection` value takes precedence over the classes. Empty or unknown values are ignored. Without a valid attribute, `system` wins if both classes are present. Removing an attribute falls back to the element's classes, then the next level above. Attribute and class changes apply without restarting the runtime; grouping and placement stay unchanged.
263
+
264
+ Only native vertical `ion-button` and `ion-menu-button` actions interpret these settings. Other ancestors, back buttons, tabs and FABs do not. They do not change Web styling. To keep a control or subtree on the Web entirely, use [`data-native-ui="disabled"`](native-ui.md#supported-markup).
265
+
266
+ ### Source fill rules
267
+
268
+ For an `ion-button` resolved to `source`, fill is chosen separately from the projection mode:
269
+
270
+ | Button | Effective fill |
271
+ | --- | --- |
272
+ | Explicit `fill="clear"`, `"solid"` or `"outline"` | The explicit value |
273
+ | Omitted fill or `fill="default"` inside `ion-buttons` | `clear` |
274
+ | Omitted fill or `fill="default"` outside `ion-buttons` | `buttonDefaultFill` |
275
+
276
+ `verticalBars.buttonDefaultFill` accepts `'solid'` or `'glass'`; omission is equivalent to `'glass'`. Use `'solid'` for Ionic's default button design, or `'glass'` for a glass design. It applies to every button resolved to `source`, including a local exception under a global `system` setting.
277
+
278
+ ```ts
279
+ // Keep system styling globally; local source buttons use Ionic's default fill.
280
+ const rail = await enableNativeUI({ verticalBars: { only: true, buttonDefaultFill: 'solid' } });
281
+ ```
282
+
283
+ ```html
284
+ <!-- In a fixed toolbar, outside ion-buttons: omitted fill resolves to solid. -->
285
+ <ion-button data-projection="source" aria-label="Add">
286
+ <ion-icon name="add-outline" slot="icon-only"></ion-icon>
287
+ </ion-button>
288
+ ```
289
+
290
+ | Effective fill | Native appearance in `source` mode |
291
+ | --- | --- |
292
+ | `clear` | Icon color, without a glass background; CSS backgrounds are ignored |
293
+ | `solid` | Computed background color tints a prominent glass button |
294
+ | `outline` | Computed border color and width, with native glass |
295
+ | `null` | Native glass with source icon colors |
296
+
297
+ Inside `ion-buttons`, explicitly set `fill="solid"` to project a background; `buttonDefaultFill: 'solid'` does not override the group's clear default. Native Liquid Glass tinting can differ from the CSS color, especially for translucent backgrounds. With the iOS theme, ordinary `ion-buttons` retain group projection; `ion-buttons.ios-theme-disabled` projects eligible buttons individually. Local projection settings do not change that grouping rule.
298
+
299
+ ## Tab bar
300
+
301
+ When the app contains `ion-tabs`, its tab bar moves into the reserved region and uses the native Duo edge spacing; the Ionic `slot` value does not select a different position. Without native projection, the stable Web rail is icon-only, matching the native resting presentation. While the user presses and drags across that rail, every icon-and-label tab reveals its label so the pending destination stays identifiable. The Web tab bar receives pointer input in the simulated system region. Native tabs and a restored Web tab bar fade in over 180ms; disappearance remains immediate. Reduced motion disables this fade. Use `ion-menu` when navigation should become a sidebar; this mode does not convert tabs into a menu. Web clones also work when no `ion-tabs` exists. Disabling the mode or leaving the page removes the native ownership or Web clones and restores their sources. Override `--ios-theme-vertical-bars-toolbar-top` when the simulated system controls use a different vertical layout.
302
+
303
+ ## Vertical Control Area API
304
+
305
+ The generated reference below documents the handle returned by `enableVerticalControlArea()`.
306
+
307
+ <docgen-index>
308
+
309
+ * [`setPlacement(...)`](#setplacement)
310
+ * [`enableVerticalControlArea()`](#enableverticalcontrolarea)
311
+ * [`disableVerticalControlArea()`](#disableverticalcontrolarea)
312
+ * [Interfaces](#interfaces)
313
+ * [Type Aliases](#type-aliases)
314
+
315
+ </docgen-index>
316
+
317
+ <docgen-api>
318
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
319
+
320
+ ### setPlacement(...)
321
+
322
+ ```typescript
323
+ setPlacement(placement: VerticalBarEdge | VerticalBarPlacement, rtl?: boolean | undefined) => void
324
+ ```
325
+
326
+ Legacy Web CSS adapter. Use the theme setVerticalBarsLayout helper instead.
327
+
328
+ | Param | Type |
329
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------- |
330
+ | **`placement`** | <code><a href="#verticalbaredge">VerticalBarEdge</a> \| <a href="#verticalbarplacement">VerticalBarPlacement</a></code> |
331
+ | **`rtl`** | <code>boolean</code> |
332
+
333
+ --------------------
334
+
335
+
336
+ ### enableVerticalControlArea()
337
+
338
+ ```typescript
339
+ enableVerticalControlArea() => Promise<void>
340
+ ```
341
+
342
+ Enable rail projection when available, preserving other settings. Rejects after destroy.
343
+
344
+ --------------------
345
+
346
+
347
+ ### disableVerticalControlArea()
348
+
349
+ ```typescript
350
+ disableVerticalControlArea() => Promise<void>
351
+ ```
352
+
353
+ Restore rail sources to the Web without stopping ordinary native controls. Rejects after destroy.
354
+
355
+ --------------------
356
+
357
+
358
+ ### Interfaces
359
+
360
+
361
+ #### VerticalBarPlacement
362
+
363
+ | Prop | Type | Description |
364
+ | ---------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------- |
365
+ | **`edge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | |
366
+ | **`inset`** | <code>number</code> | Explicit rail width in CSS pixels; omitted to use the stylesheet's safe-area rules. |
367
+ | **`nativeEdge`** | <code><a href="#verticalbaredge">VerticalBarEdge</a></code> | Ignored. Native availability is read from the plugin’s own device metrics. |
368
+
369
+
370
+ ### Type Aliases
371
+
372
+
373
+ #### VerticalBarEdge
374
+
375
+ Logical edge in the reading direction, matching UIVerticalBarEdge and capacitor-foldable.
376
+
377
+ <code>'leading' | 'trailing' | null</code>
378
+
379
+ </docgen-api>
@@ -0,0 +1,71 @@
1
+ import UIKit
2
+
3
+ enum NativeUIButton {
4
+ private final class BadgeLabel: UILabel {}
5
+ // These Ionic components share the same native UIButton presentation.
6
+ static let kinds: [NativeUIComponent] = [.button, .backButton, .menuButton]
7
+
8
+ @available(iOS 26.0, *)
9
+ static func make(_ node: NativeUIControl, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton? {
10
+ guard node.items.count == 1, let item = node.items.first else { return nil }
11
+ let control = render(item.content, glass: true, rendering: rendering, activate: activate)
12
+ control.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
13
+ return control
14
+ }
15
+
16
+ @available(iOS 26.0, *)
17
+ static func render(_ item: NativeUIItemContent, glass: Bool, existing: UIButton? = nil, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIButton {
18
+ var configuration: UIButton.Configuration = glass ? .glass() : .plain()
19
+ configuration.title = item.label
20
+ configuration.image = rendering.image(item)
21
+ configuration.imagePadding = item.imagePadding ?? 4
22
+ configuration.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: item.contentInsetLeading ?? 4,
23
+ bottom: 0, trailing: item.contentInsetTrailing ?? 4)
24
+ configuration.imagePlacement = item.iconPosition == .trailing ? .trailing : .leading
25
+ configuration.baseForegroundColor = rendering.color(item.color)
26
+ configuration.titleLineBreakMode = .byTruncatingTail
27
+ let size = item.fontSize
28
+ let weight = item.fontWeight
29
+ configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
30
+ var outgoing = incoming
31
+ outgoing.font = UIFont.systemFont(ofSize: size, weight: weight >= 600 ? .semibold : weight >= 500 ? .medium : .regular)
32
+ return outgoing
33
+ }
34
+ let button = existing ?? UIButton(configuration: configuration)
35
+ if existing != nil { button.configuration = configuration }
36
+ button.titleLabel?.numberOfLines = 1
37
+ // Web and UIKit can differ by a fraction of a point in text measurement.
38
+ // Absorb that rounding without widening the projected button.
39
+ button.titleLabel?.adjustsFontSizeToFitWidth = true
40
+ button.titleLabel?.minimumScaleFactor = 0.99
41
+ button.isEnabled = !item.disabled
42
+ button.accessibilityLabel = item.accessibilityLabel
43
+ button.accessibilityIdentifier = item.id
44
+ if item.selected { button.accessibilityTraits.insert(.selected) }
45
+ button.subviews.filter { $0 is BadgeLabel }.forEach { $0.removeFromSuperview() }
46
+ button.accessibilityValue = item.badge?.value
47
+ if let badge = item.badge {
48
+ let label = BadgeLabel()
49
+ label.text = badge.value
50
+ label.font = .systemFont(ofSize: 11, weight: .semibold)
51
+ label.textColor = rendering.color(badge.textColor)
52
+ label.backgroundColor = rendering.color(badge.color)
53
+ label.textAlignment = .center
54
+ label.layer.cornerRadius = 8
55
+ label.clipsToBounds = true
56
+ label.isAccessibilityElement = false
57
+ label.translatesAutoresizingMaskIntoConstraints = false
58
+ button.addSubview(label)
59
+ NSLayoutConstraint.activate([
60
+ label.centerXAnchor.constraint(equalTo: button.centerXAnchor, constant: 14),
61
+ label.topAnchor.constraint(equalTo: button.topAnchor, constant: 1),
62
+ label.widthAnchor.constraint(greaterThanOrEqualToConstant: max(16, label.intrinsicContentSize.width + 6)),
63
+ label.heightAnchor.constraint(equalToConstant: 16)
64
+ ])
65
+ }
66
+ if existing == nil {
67
+ button.addAction(UIAction { _ in activate(item.id) }, for: .touchUpInside)
68
+ }
69
+ return button
70
+ }
71
+ }
@@ -0,0 +1,24 @@
1
+ import UIKit
2
+
3
+ enum NativeUIButtons {
4
+ static let kind = NativeUIComponent.buttons
5
+
6
+ @available(iOS 26.0, *)
7
+ static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) -> UIView {
8
+ let items = node.items
9
+ let rtl = node.rtl
10
+ let effect = UIGlassEffect(style: .regular)
11
+ let control = UIVisualEffectView(effect: effect)
12
+ control.layer.cornerRadius = node.frame.height * scale / 2
13
+ control.clipsToBounds = true
14
+ control.accessibilityIdentifier = node.id
15
+ for item in items {
16
+ let button = NativeUIButton.render(item.content, glass: false, rendering: rendering, activate: activate)
17
+ button.semanticContentAttribute = rtl ? .forceRightToLeft : .forceLeftToRight
18
+ let rect = item.frame.rect
19
+ button.frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
20
+ control.contentView.addSubview(button)
21
+ }
22
+ return control
23
+ }
24
+ }
@@ -0,0 +1,19 @@
1
+ import UIKit
2
+
3
+ // Static composition of supported wire kinds. Component files own their names and rendering.
4
+ enum NativeUIComponents {
5
+ static let supported = Set(NativeUIButton.kinds + [NativeUIButtons.kind, NativeUITabBar.kind, NativeUISegment.kind, NativeUIFab.kind])
6
+
7
+ @available(iOS 26.0, *)
8
+ static func make(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering,
9
+ tabDelegate: UITabBarDelegate, activate: @escaping (String) -> Void) -> UIView? {
10
+ switch node.kind {
11
+ case .button, .backButton, .menuButton: return NativeUIButton.make(node, rendering: rendering, activate: activate)
12
+ case .buttons: return NativeUIButtons.make(node, scale: scale, rendering: rendering, activate: activate)
13
+ case .tabBar: return NativeUITabBar.make(node, rendering: rendering, delegate: tabDelegate)
14
+ case .segment: return NativeUISegment.make(node, scale: scale, rendering: rendering, activate: activate)
15
+ case .fab: return NativeUIFab()
16
+ case .extension: return nil // App-local SwiftUI content is hosted by the shell.
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,80 @@
1
+ import UIKit
2
+
3
+ final class NativeUIFab: UIView {
4
+ static let kind = NativeUIComponent.fab
5
+ var buttons: [String: UIButton] = [:]
6
+ var content: [String: NativeUIItemContent] = [:]
7
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
8
+ let hit = super.hitTest(point, with: event)
9
+ return hit === self ? nil : hit
10
+ }
11
+
12
+ @available(iOS 26.0, *)
13
+ func apply(_ node: NativeUIControl, scale: CGFloat, rendering: NativeUIControlRendering, activate: @escaping (String) -> Void) {
14
+ let items = node.items
15
+ let retained = Set(items.map(\.id))
16
+ var lostFocus = false
17
+ for id in Array(self.buttons.keys) where !retained.contains(id) {
18
+ lostFocus = lostFocus || self.buttons[id]?.accessibilityElementIsFocused() == true
19
+ self.buttons.removeValue(forKey: id)?.removeFromSuperview()
20
+ self.content.removeValue(forKey: id)
21
+ }
22
+ self.accessibilityIdentifier = node.id
23
+ for item in items {
24
+ let id = item.id
25
+ let artwork = item.content.fabArtwork
26
+ let previous = self.buttons[id]
27
+ let button = previous ?? NativeUIButton.render(artwork, glass: true, rendering: rendering, activate: activate)
28
+ if previous == nil {
29
+ self.buttons[id] = button
30
+ self.addSubview(button)
31
+ } else if self.content[id] != artwork {
32
+ let changedIcon = self.content[id]?.selected != item.content.selected
33
+ let duration = UIAccessibility.isReduceMotionEnabled ? 0 : min(item.content.iconTransition ?? 0, 1)
34
+ if changedIcon, let imageView = button.imageView, duration > 0 {
35
+ UIView.transition(with: imageView, duration: duration,
36
+ options: [.transitionCrossDissolve, .beginFromCurrentState, .allowUserInteraction]) {
37
+ _ = NativeUIButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate)
38
+ }
39
+ } else {
40
+ UIView.performWithoutAnimation { _ = NativeUIButton.render(artwork, glass: true, existing: button, rendering: rendering, activate: activate) }
41
+ }
42
+ }
43
+ self.content[id] = artwork
44
+ button.accessibilityTraits.remove(.selected)
45
+ if id == items.first?.id && items.count > 1 {
46
+ button.accessibilityValue = item.content.selected
47
+ ? NSLocalizedString("Expanded", comment: "FAB accessibility state")
48
+ : NSLocalizedString("Collapsed", comment: "FAB accessibility state")
49
+ } else {
50
+ button.accessibilityValue = nil
51
+ }
52
+ let hidden = item.visible == false
53
+ if hidden && !button.isHidden && button.accessibilityElementIsFocused() { lostFocus = true }
54
+ button.isHidden = hidden
55
+ button.isUserInteractionEnabled = button.isEnabled && !hidden
56
+ button.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
57
+ let rect = item.frame.rect
58
+ let frame = CGRect(x: rect.minX * scale, y: rect.minY * scale, width: rect.width * scale, height: rect.height * scale)
59
+ if button.frame != frame { button.frame = frame }
60
+ }
61
+ let ordered = items.compactMap { self.buttons[$0.id] }.filter { !$0.isHidden }
62
+ self.accessibilityElements = ordered
63
+ if lostFocus, let main = ordered.first, main.isEnabled, self.window != nil {
64
+ UIAccessibility.post(notification: .layoutChanged, argument: main)
65
+ }
66
+ }
67
+ }
68
+
69
+ extension NativeUIItemContent {
70
+ var fabArtwork: Self {
71
+ var artwork = self
72
+ if selected {
73
+ artwork.icon = closeIcon
74
+ artwork.iconWidth = closeIconWidth
75
+ artwork.iconHeight = closeIconHeight
76
+ artwork.label = ""
77
+ }
78
+ return artwork
79
+ }
80
+ }