@rdlabo/capacitor-native-ui 0.0.0-stage → 2.0.0-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 (261) hide show
  1. package/LICENSE +21 -0
  2. package/Package.swift +23 -0
  3. package/README.md +66 -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 +237 -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 +197 -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 +19 -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 +776 -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/control-options.d.ts +6 -0
  129. package/dist/native/shared/control-options.d.ts.map +1 -0
  130. package/dist/native/shared/control-options.js +17 -0
  131. package/dist/native/shared/control-options.js.map +1 -0
  132. package/dist/native/shared/crossfade.d.ts +12 -0
  133. package/dist/native/shared/crossfade.d.ts.map +1 -0
  134. package/dist/native/shared/crossfade.js +67 -0
  135. package/dist/native/shared/crossfade.js.map +1 -0
  136. package/dist/native/shared/diagnostics.d.ts +4 -0
  137. package/dist/native/shared/diagnostics.d.ts.map +1 -0
  138. package/dist/native/shared/diagnostics.js +18 -0
  139. package/dist/native/shared/diagnostics.js.map +1 -0
  140. package/dist/native/shared/dom.d.ts +46 -0
  141. package/dist/native/shared/dom.d.ts.map +1 -0
  142. package/dist/native/shared/dom.js +180 -0
  143. package/dist/native/shared/dom.js.map +1 -0
  144. package/dist/native/shared/events.d.ts +15 -0
  145. package/dist/native/shared/events.d.ts.map +1 -0
  146. package/dist/native/shared/events.js +17 -0
  147. package/dist/native/shared/events.js.map +1 -0
  148. package/dist/native/shared/icons.d.ts +8 -0
  149. package/dist/native/shared/icons.d.ts.map +1 -0
  150. package/dist/native/shared/icons.js +61 -0
  151. package/dist/native/shared/icons.js.map +1 -0
  152. package/dist/native/shared/modal.d.ts +10 -0
  153. package/dist/native/shared/modal.d.ts.map +1 -0
  154. package/dist/native/shared/modal.js +160 -0
  155. package/dist/native/shared/modal.js.map +1 -0
  156. package/dist/native/shared/projection-id.d.ts +4 -0
  157. package/dist/native/shared/projection-id.d.ts.map +1 -0
  158. package/dist/native/shared/projection-id.js +7 -0
  159. package/dist/native/shared/projection-id.js.map +1 -0
  160. package/dist/native/shared/toolbar-options.d.ts +13 -0
  161. package/dist/native/shared/toolbar-options.d.ts.map +1 -0
  162. package/dist/native/shared/toolbar-options.js +28 -0
  163. package/dist/native/shared/toolbar-options.js.map +1 -0
  164. package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
  165. package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
  166. package/dist/native/shared/vertical-bars-geometry.js +85 -0
  167. package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
  168. package/dist/native/startup.d.ts +8 -0
  169. package/dist/native/startup.d.ts.map +1 -0
  170. package/dist/native/startup.js +70 -0
  171. package/dist/native/startup.js.map +1 -0
  172. package/dist/native/vertical-bars-web-search.d.ts +10 -0
  173. package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
  174. package/dist/native/vertical-bars-web-search.js +90 -0
  175. package/dist/native/vertical-bars-web-search.js.map +1 -0
  176. package/dist/native/vertical-bars-web.d.ts +5 -0
  177. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  178. package/dist/native/vertical-bars-web.js +422 -0
  179. package/dist/native/vertical-bars-web.js.map +1 -0
  180. package/dist/native-integration/index.d.ts +25 -0
  181. package/dist/native-integration/index.d.ts.map +1 -0
  182. package/dist/native-integration/index.js +33 -0
  183. package/dist/native-integration/index.js.map +1 -0
  184. package/dist/vertical-bars.d.ts +6 -0
  185. package/dist/vertical-bars.js +5 -0
  186. package/docs/appearance.md +45 -0
  187. package/docs/configuration.md +219 -0
  188. package/docs/framework-independent.md +107 -0
  189. package/docs/ionic-vertical-bars.md +173 -0
  190. package/docs/migration.md +184 -0
  191. package/docs/native-ui-extensions.md +394 -0
  192. package/docs/native-ui.md +528 -0
  193. package/docs/setup.md +224 -0
  194. package/docs/vertical-bars.md +431 -0
  195. package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
  196. package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
  197. package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
  198. package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
  199. package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
  200. package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
  201. package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
  202. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
  203. package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
  204. package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
  205. package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
  206. package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
  207. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
  208. package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
  209. package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
  210. package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
  211. package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
  212. package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
  213. package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
  214. package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
  215. package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
  216. package/package.json +89 -4
  217. package/src/core.ts +14 -0
  218. package/src/native/activation.ts +385 -0
  219. package/src/native/components/index.ts +45 -0
  220. package/src/native/components/ion-back-button.ts +22 -0
  221. package/src/native/components/ion-button.ts +25 -0
  222. package/src/native/components/ion-buttons.ts +37 -0
  223. package/src/native/components/ion-fab.ts +151 -0
  224. package/src/native/components/ion-menu-button.ts +37 -0
  225. package/src/native/components/ion-segment.ts +27 -0
  226. package/src/native/components/ion-tab-bar.ts +50 -0
  227. package/src/native/components/searchable-tabs.ts +499 -0
  228. package/src/native/definitions.ts +274 -0
  229. package/src/native/extensions.ts +176 -0
  230. package/src/native/foldable/fold-state.ts +10 -0
  231. package/src/native/foldable/types.ts +4 -0
  232. package/src/native/html.ts +199 -0
  233. package/src/native/index.ts +32 -0
  234. package/src/native/legacy-layout.ts +21 -0
  235. package/src/native/overlays/content.ts +51 -0
  236. package/src/native/overlays/controller.ts +349 -0
  237. package/src/native/overlays/dialog-animation.ts +53 -0
  238. package/src/native/overlays/dismiss-animation.ts +15 -0
  239. package/src/native/overlays/focus.ts +64 -0
  240. package/src/native/overlays/popover.ts +120 -0
  241. package/src/native/overlays/styles.ts +181 -0
  242. package/src/native/prehide.ts +285 -0
  243. package/src/native/runtime.ts +979 -0
  244. package/src/native/shared/bridge.ts +18 -0
  245. package/src/native/shared/candidate.ts +162 -0
  246. package/src/native/shared/clipping.ts +24 -0
  247. package/src/native/shared/control-options.ts +20 -0
  248. package/src/native/shared/crossfade.ts +67 -0
  249. package/src/native/shared/diagnostics.ts +13 -0
  250. package/src/native/shared/dom.ts +304 -0
  251. package/src/native/shared/events.ts +14 -0
  252. package/src/native/shared/icons.ts +57 -0
  253. package/src/native/shared/modal.ts +157 -0
  254. package/src/native/shared/projection-id.ts +2 -0
  255. package/src/native/shared/toolbar-options.ts +49 -0
  256. package/src/native/shared/vertical-bars-geometry.ts +80 -0
  257. package/src/native/startup.ts +68 -0
  258. package/src/native/vertical-bars-web-search.ts +86 -0
  259. package/src/native/vertical-bars-web.ts +500 -0
  260. package/src/native-integration/index.ts +49 -0
  261. package/src/vertical-bars.ts +12 -0
@@ -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
+ }
@@ -0,0 +1,439 @@
1
+ import UIKit
2
+
3
+ struct NativeUISearch: Decodable, Equatable {
4
+ let id: String
5
+ let field: NativeUIItemContent
6
+ let trigger: NativeUIItem
7
+ let closeId: String
8
+ let active: Bool
9
+ let available: Bool
10
+ let focused: Bool
11
+ let value: String
12
+ let placeholder: String
13
+ let disabled: Bool
14
+ let editSequence: Int
15
+ let valueVersion: Int
16
+
17
+ var isValid: Bool {
18
+ !id.isEmpty && !closeId.isEmpty && field.isValid && trigger.isValid && editSequence >= 0 && valueVersion >= 0
19
+ }
20
+ }
21
+
22
+ enum NativeUISearchPhase: String { case input, focus, blur, clear, commit }
23
+
24
+ // A controller's empty content must not intercept the existing WebView.
25
+ final class NativeUISearchHost: UIView {
26
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
27
+ guard let hit = super.hitTest(point, with: event) else { return nil }
28
+ var ancestor: UIView? = hit
29
+ while let current = ancestor, current !== self {
30
+ if current is UIControl || current is UISearchBar || current is UITabBar { return hit }
31
+ ancestor = current.superview
32
+ }
33
+ return nil
34
+ }
35
+ }
36
+
37
+ // Preserve UISearchBar's delegate while returning its clear action to Ionic.
38
+ final class NativeUISearchInputDelegate: NSObject, UITextFieldDelegate {
39
+ weak var original: UITextFieldDelegate?
40
+ var clear: (() -> Void)?
41
+ override func responds(to selector: Selector!) -> Bool {
42
+ super.responds(to: selector) || original?.responds(to: selector) == true
43
+ }
44
+ override func forwardingTarget(for selector: Selector!) -> Any? { original }
45
+ func textFieldShouldClear(_ textField: UITextField) -> Bool { clear?(); return false }
46
+ }
47
+
48
+ // Lets the plugin registry retain iOS 26-gated search controllers without
49
+ // availability-gated stored properties, mirroring NativeUIVerticalBarsControlling.
50
+ protocol NativeUISearchControlling: AnyObject {
51
+ var surface: NativeUISearchHost { get }
52
+ var ownsKeyboard: Bool { get }
53
+ var ownsKeyboardChrome: Bool { get }
54
+ var transitionCoordinator: UIViewControllerTransitionCoordinator? { get }
55
+ var activate: ((String) -> Void)? { get set }
56
+ var changed: ((String, NativeUISearchPhase, String, Bool, Int) -> Int)? { get set }
57
+ func attach(to parent: UIViewController, in container: UIView)
58
+ func detach()
59
+ func apply(_ snapshot: NativeUIControl, webFrame: CGRect, barFrame: CGRect, triggerFrame: CGRect, rendering: NativeUIControlRendering) -> Bool
60
+ }
61
+
62
+ @available(iOS 26.0, *)
63
+ final class NativeUISearchController: UITabBarController, UITabBarControllerDelegate, UISearchBarDelegate, NativeUISearchControlling {
64
+ // Wire stays active+focused; local session drives chrome (idle / presented / focused).
65
+ private enum Session: Equatable { case idle, presented, focused }
66
+
67
+ let surface = NativeUISearchHost()
68
+ private var restingBarFrame: CGRect?
69
+ private var restingBarAnchor: NativeUITabBar.Anchor?
70
+ private var fittingTabBar = false
71
+ private let search = UISearchController(searchResultsController: nil)
72
+ private let inputDelegate = NativeUISearchInputDelegate()
73
+ private var searchTab: UISearchTab!
74
+ private var ordinary: [String: UITab] = [:]
75
+ private var configuration: NativeUISearch?
76
+ private var selectedID = ""
77
+ private var closing = false
78
+ private var editingSequence = 0
79
+ private var valueVersion = -1
80
+ private var lockedWebFrame: CGRect? // frozen while search is active (width changes re-lock)
81
+ private var session: Session = .idle
82
+ private var focusWork: DispatchWorkItem?
83
+ private var pendingSelection: NativeUITabBar.PendingSelection? // optimistic ordinary tab
84
+ private var pendingExpiryWork: DispatchWorkItem?
85
+ var ownsKeyboard: Bool { search.searchBar.searchTextField.isFirstResponder }
86
+ var ownsKeyboardChrome: Bool { session == .focused || ownsKeyboard }
87
+ var activate: ((String) -> Void)?
88
+ var changed: ((String, NativeUISearchPhase, String, Bool, Int) -> Int)?
89
+
90
+ override func viewDidLoad() {
91
+ super.viewDidLoad()
92
+ view.backgroundColor = .clear
93
+ view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
94
+ surface.backgroundColor = .clear
95
+ surface.autoresizingMask = [.flexibleWidth, .flexibleHeight]
96
+ delegate = self
97
+ mode = .tabBar
98
+ NativeUITabBar.configureLayout(tabBar)
99
+ search.obscuresBackgroundDuringPresentation = false
100
+ search.hidesNavigationBarDuringPresentation = false
101
+ search.searchBar.delegate = self
102
+ search.searchBar.autocapitalizationType = .none
103
+ search.searchBar.autocorrectionType = .no
104
+ search.searchBar.spellCheckingType = .no
105
+ search.searchBar.searchTextField.clearButtonMode = .always
106
+ searchTab = UISearchTab { [weak self] _ in
107
+ let child = UIViewController()
108
+ child.view.backgroundColor = .clear
109
+ child.definesPresentationContext = true
110
+ child.navigationItem.searchController = self?.search
111
+ child.navigationItem.hidesSearchBarWhenScrolling = false
112
+ child.navigationItem.preferredSearchBarPlacement = .integrated
113
+ let navigation = UINavigationController(rootViewController: child)
114
+ navigation.view.backgroundColor = .clear
115
+ return navigation
116
+ }
117
+ searchTab.automaticallyActivatesSearch = false
118
+ inputDelegate.clear = { [weak self] in self?.emit(.clear) }
119
+ }
120
+
121
+ deinit {
122
+ focusWork?.cancel()
123
+ pendingExpiryWork?.cancel()
124
+ }
125
+
126
+ private func wantedSession(active: Bool, focused: Bool) -> Session {
127
+ if !active { return .idle }
128
+ return focused ? .focused : .presented
129
+ }
130
+
131
+ private func endEditing() {
132
+ focusWork?.cancel()
133
+ focusWork = nil
134
+ if search.searchBar.searchTextField.isFirstResponder {
135
+ search.searchBar.searchTextField.resignFirstResponder()
136
+ }
137
+ if search.isActive { search.isActive = false }
138
+ }
139
+
140
+ private func applySession(_ wanted: Session, selectingSearchTab: Bool) {
141
+ if wanted == session && !selectingSearchTab { return }
142
+ if wanted == .idle {
143
+ endEditing()
144
+ session = .idle
145
+ return
146
+ }
147
+ if selectingSearchTab || selectedTab !== searchTab { selectedTab = searchTab }
148
+ if wanted == .presented {
149
+ if session == .focused { endEditing() }
150
+ session = .presented
151
+ return
152
+ }
153
+ session = .focused
154
+ if !search.isActive { search.isActive = true }
155
+ guard !search.searchBar.searchTextField.isFirstResponder, focusWork == nil else { return }
156
+ let work = DispatchWorkItem { [weak self] in
157
+ guard let self else { return }
158
+ self.focusWork = nil
159
+ guard self.session == .focused, !self.search.searchBar.searchTextField.isFirstResponder else { return }
160
+ if !self.search.isActive { self.search.isActive = true }
161
+ _ = self.search.searchBar.searchTextField.becomeFirstResponder()
162
+ }
163
+ focusWork = work
164
+ DispatchQueue.main.async(execute: work)
165
+ }
166
+
167
+ private func armPendingSelection(_ id: String) {
168
+ pendingSelection = .start(id)
169
+ schedulePendingExpiry()
170
+ }
171
+
172
+ private func clearPendingSelection() {
173
+ pendingExpiryWork?.cancel()
174
+ pendingExpiryWork = nil
175
+ pendingSelection = nil
176
+ }
177
+
178
+ private func schedulePendingExpiry() {
179
+ pendingExpiryWork?.cancel()
180
+ pendingExpiryWork = nil
181
+ guard let pending = pendingSelection else { return }
182
+ let delay = max(0, pending.until - CFAbsoluteTimeGetCurrent()) + 0.02
183
+ let work = DispatchWorkItem { [weak self] in
184
+ guard let self else { return }
185
+ self.pendingExpiryWork = nil
186
+ guard let pending = self.pendingSelection, CFAbsoluteTimeGetCurrent() >= pending.until else { return }
187
+ self.pendingSelection = nil
188
+ // Revert to the last DOM selection if Ionic did not accept the action.
189
+ if let selected = self.ordinary[self.selectedID], self.selectedTab !== selected {
190
+ self.selectedTab = selected
191
+ }
192
+ }
193
+ pendingExpiryWork = work
194
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
195
+ }
196
+
197
+ private func resolveOrdinarySelection(_ items: [NativeUIItem], fallback: UITab?) {
198
+ let domSelected = ordinary[selectedID] ?? fallback
199
+ guard let pending = pendingSelection else {
200
+ pendingExpiryWork?.cancel()
201
+ pendingExpiryWork = nil
202
+ if selectedTab !== domSelected { selectedTab = domSelected }
203
+ return
204
+ }
205
+ let pendingTab = ordinary[pending.id]
206
+ let expired = CFAbsoluteTimeGetCurrent() >= pending.until
207
+ let unavailable = pendingTab == nil || (items.first { $0.id == pending.id }?.content.disabled ?? true)
208
+ if unavailable || expired {
209
+ clearPendingSelection()
210
+ selectedTab = domSelected
211
+ } else if selectedID == pending.id {
212
+ clearPendingSelection()
213
+ if selectedTab !== domSelected { selectedTab = domSelected }
214
+ } else if selectedTab !== pendingTab {
215
+ selectedTab = pendingTab
216
+ }
217
+ }
218
+
219
+ func attach(to parent: UIViewController, in container: UIView) {
220
+ parent.addChild(self)
221
+ container.addSubview(surface)
222
+ surface.addSubview(view)
223
+ didMove(toParent: parent)
224
+ }
225
+
226
+ func detach() {
227
+ closing = true
228
+ lockedWebFrame = nil
229
+ clearPendingSelection()
230
+ applySession(.idle, selectingSearchTab: false)
231
+ willMove(toParent: nil)
232
+ restingBarFrame = nil
233
+ surface.removeFromSuperview()
234
+ view.removeFromSuperview()
235
+ removeFromParent()
236
+ }
237
+
238
+ func apply(_ snapshot: NativeUIControl, webFrame: CGRect, barFrame: CGRect, triggerFrame: CGRect,
239
+ rendering: NativeUIControlRendering) -> Bool {
240
+ loadViewIfNeeded()
241
+ guard let configuration = snapshot.search else { return false }
242
+ if self.configuration?.id != configuration.id {
243
+ editingSequence = 0
244
+ valueVersion = -1
245
+ }
246
+ let wasActive = self.configuration.map { $0.available && $0.active } ?? false
247
+ self.configuration = configuration
248
+ let available = configuration.available
249
+ let active = available && configuration.active
250
+ let wanted = wantedSession(active: active, focused: configuration.focused)
251
+ let items = snapshot.items
252
+ let ids = items.map(\.id)
253
+ for id in Array(ordinary.keys) where !ids.contains(id) { ordinary.removeValue(forKey: id) }
254
+ for item in items {
255
+ let id = item.id
256
+ let tab = ordinary[id] ?? UITab(title: "", image: nil, identifier: id) { _ in
257
+ let child = UIViewController()
258
+ child.view.backgroundColor = .clear
259
+ return child
260
+ }
261
+ tab.accessibilityIdentifier = id
262
+ tab.title = item.content.label
263
+ tab.image = rendering.image(item.content)
264
+ tab.badgeValue = item.content.badge?.value
265
+ tab.isEnabled = !item.content.disabled
266
+ ordinary[id] = tab
267
+ if item.content.selected { selectedID = id }
268
+ }
269
+ let trigger = configuration.trigger
270
+ searchTab.accessibilityIdentifier = trigger.id
271
+ searchTab.image = rendering.image(trigger.content)
272
+ searchTab.isEnabled = !configuration.disabled
273
+ let requested = ids.compactMap { ordinary[$0] } + (available ? [searchTab!] : [])
274
+ if !active, tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
275
+ tabs = requested
276
+ }
277
+ for item in items {
278
+ if let nativeItem = ordinary[item.id]?.viewController?.tabBarItem {
279
+ nativeItem.accessibilityLabel = item.content.accessibilityLabel
280
+ NativeUITabBar.applyTypography(item.content, to: nativeItem)
281
+ NativeUITabBar.applyBadge(item.content.badge, to: nativeItem, rendering: rendering)
282
+ }
283
+ }
284
+ searchTab.viewController?.tabBarItem.accessibilityLabel = trigger.content.accessibilityLabel
285
+ let prominent = NSSelectorFromString("setProminentTabIdentifier:")
286
+ if responds(to: prominent) { setValue(available ? searchTab.identifier : nil, forKey: "prominentTabIdentifier") }
287
+ let field = configuration.field
288
+ search.searchBar.searchTextField.accessibilityIdentifier = configuration.id
289
+ search.searchBar.searchTextField.accessibilityLabel = field.accessibilityLabel
290
+ search.searchBar.setImage(rendering.image(field), for: .search, state: .normal)
291
+ search.searchBar.placeholder = configuration.placeholder
292
+ search.searchBar.searchTextField.isEnabled = !configuration.disabled
293
+ let nextValueVersion = configuration.valueVersion
294
+ if nextValueVersion != valueVersion || configuration.editSequence >= editingSequence {
295
+ valueVersion = nextValueVersion
296
+ let value = configuration.value
297
+ if search.searchBar.text != value { search.searchBar.text = value }
298
+ }
299
+ if search.searchBar.searchTextField.delegate !== inputDelegate {
300
+ inputDelegate.original = search.searchBar.searchTextField.delegate
301
+ search.searchBar.searchTextField.delegate = inputDelegate
302
+ }
303
+ // Resting keeps closing=true so hidden UISearchBar noise cannot emit; clear it while active.
304
+ closing = !active
305
+ surface.isHidden = false
306
+ surface.overrideUserInterfaceStyle = snapshot.dark ? .dark : .light
307
+ if active {
308
+ // UISearchTab session owns the bar + keyboard; freeze Web layout for the session.
309
+ if tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
310
+ tabs = requested
311
+ }
312
+ restingBarFrame = nil
313
+ view.frame = surface.bounds
314
+ let nextLock = surface.bounds.isEmpty ? webFrame : surface.frame
315
+ if let locked = lockedWebFrame, abs(locked.width - webFrame.width) > 0.5 {
316
+ // Rotation / size-class change: adopt the new width while still ignoring keyboard shrink.
317
+ lockedWebFrame = webFrame
318
+ } else if lockedWebFrame == nil {
319
+ lockedWebFrame = nextLock
320
+ }
321
+ if let lockedWebFrame, surface.frame != lockedWebFrame { surface.frame = lockedWebFrame }
322
+ clearPendingSelection()
323
+ applySession(wanted, selectingSearchTab: !wasActive)
324
+ return true
325
+ }
326
+
327
+ surface.frame = webFrame
328
+ view.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
329
+ tabBar.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
330
+ tabBar.accessibilityIdentifier = snapshot.id
331
+ if lockedWebFrame != nil {
332
+ lockedWebFrame = nil
333
+ }
334
+ applySession(.idle, selectingSearchTab: false)
335
+
336
+ view.frame = surface.bounds
337
+ resolveOrdinarySelection(items, fallback: requested.first)
338
+ restingBarFrame = surface.convert(barFrame, from: surface.superview)
339
+ restingBarAnchor = snapshot.tabBarAnchor
340
+ view.setNeedsLayout()
341
+ view.layoutIfNeeded()
342
+ return fitRestingTabBar()
343
+ }
344
+
345
+ override func viewDidLayoutSubviews() {
346
+ super.viewDidLayoutSubviews()
347
+ if session == .idle { _ = fitRestingTabBar() }
348
+ }
349
+
350
+ /// Match the ordinary tab platter to the source ion-tab-bar by resizing
351
+ /// the controller's view, so UIKit can keep managing its tab bar through
352
+ /// search expansion and dismissal.
353
+ private func fitRestingTabBar() -> Bool {
354
+ guard let bounds = restingBarFrame, !fittingTabBar else { return true }
355
+ fittingTabBar = true
356
+ defer { fittingTabBar = false }
357
+ tabBar.layoutIfNeeded()
358
+ for _ in 0..<2 {
359
+ let regions = NativeUITabBar.contentViews(tabBar)
360
+ func controlCount(_ view: UIView) -> Int {
361
+ (view is UIControl ? 1 : 0) + view.subviews.reduce(0) { $0 + controlCount($1) }
362
+ }
363
+ // Ordinary tabs contain their item controls; Search contains one.
364
+ // For a single tab, UIKit places ordinary tabs at the leading edge.
365
+ let rtl = tabBar.effectiveUserInterfaceLayoutDirection == .rightToLeft
366
+ guard let region = regions.max(by: {
367
+ let lhs = controlCount($0), rhs = controlCount($1)
368
+ if lhs != rhs { return lhs < rhs }
369
+ return rtl ? $0.frame.midX < $1.frame.midX : $0.frame.midX > $1.frame.midX
370
+ }) else { return false }
371
+ let content = region.frame
372
+ let contents = regions.map { $0.frame }
373
+ let measured = tabBar.convert(content, to: surface)
374
+ let width = bounds.width - measured.width
375
+ let x = bounds.minX + (bounds.width - measured.width) * (restingBarAnchor?.x ?? 0) - measured.minX
376
+ let y = bounds.minY + (bounds.height - measured.height) * (restingBarAnchor?.y ?? 0) - measured.minY
377
+ // UIKit rounds the platter frame; a subpoint difference must not
378
+ // keep invalidating its layout during search transitions.
379
+ if abs(width) <= 0.5 && abs(x) <= 0.5 && abs(y) <= 0.5 { break }
380
+ var frame = view.frame
381
+ // The controller also contains the separate search button. Include
382
+ // that space in its width limit without enlarging capped tab groups.
383
+ let searchWidth = contents.filter { $0 != content }.reduce(CGFloat(0)) { $0 + $1.width }
384
+ frame.size.width = min(frame.width + width, surface.bounds.width + searchWidth)
385
+ frame.origin.x += x
386
+ frame.origin.y += y
387
+ if abs(frame.width - view.frame.width) <= 0.5 && abs(x) <= 0.5 && abs(y) <= 0.5 { break }
388
+ view.frame = frame
389
+ view.setNeedsLayout()
390
+ view.layoutIfNeeded()
391
+ }
392
+ // Fitting the ordinary platter can move UIKit's separate Search control
393
+ // outside the WebView, especially for centered or trailing source bars.
394
+ // Reject that layout so Search keeps its usable Web fallback.
395
+ let visibleBounds = surface.bounds.insetBy(dx: -0.5, dy: -0.5)
396
+ let regions = NativeUITabBar.contentViews(tabBar)
397
+ return !regions.isEmpty && regions.allSatisfy {
398
+ visibleBounds.contains(tabBar.convert($0.frame, to: surface))
399
+ }
400
+ }
401
+
402
+ func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
403
+ guard let configuration else { return false }
404
+ if tab === searchTab {
405
+ restingBarFrame = nil
406
+ clearPendingSelection()
407
+ activate?(configuration.trigger.id)
408
+ return true
409
+ }
410
+ if configuration.active {
411
+ if tab.identifier == selectedID { activate?(configuration.closeId) }
412
+ else { activate?(tab.identifier) }
413
+ return false
414
+ }
415
+ armPendingSelection(tab.identifier)
416
+ if selectedTab !== tab { selectedTab = tab }
417
+ activate?(tab.identifier)
418
+ return true
419
+ }
420
+
421
+ private func emit(_ phase: NativeUISearchPhase) {
422
+ guard !closing, let configuration else { return }
423
+ editingSequence = changed?(configuration.id, phase, search.searchBar.text ?? "", search.searchBar.searchTextField.markedTextRange != nil, valueVersion) ?? editingSequence
424
+ }
425
+ func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { emit(.input) }
426
+ func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
427
+ if session != .idle { session = .focused }
428
+ emit(.focus)
429
+ }
430
+ func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
431
+ if session == .focused { session = .presented }
432
+ emit(.blur)
433
+ }
434
+ func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { emit(.commit) }
435
+ func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
436
+ closing = true
437
+ if let configuration { activate?(configuration.closeId) }
438
+ }
439
+ }