@rdlabo/ionic-theme-ios27 0.1.0-beta.pr177.shaea4b7eeec994 → 0.2.1-beta.pr185.sha0793981f6cf4

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 (90) hide show
  1. package/README.md +26 -8
  2. package/dist/native/components/searchable-tabs.d.ts +2 -0
  3. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  4. package/dist/native/components/searchable-tabs.js +131 -21
  5. package/dist/native/runtime.d.ts.map +1 -1
  6. package/dist/native/runtime.js +14 -7
  7. package/docs/ios-adaptive.md +20 -21
  8. package/docs/native-ui-shell.md +4 -3
  9. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +243 -64
  10. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +77 -6
  11. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +79 -12
  12. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +258 -2
  13. package/node_modules/@rdlabo/ionic-theme-utils/LICENSE +21 -0
  14. package/node_modules/@rdlabo/ionic-theme-utils/README.md +28 -0
  15. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts +3 -0
  16. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts.map +1 -0
  17. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js +13 -0
  18. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js.map +1 -0
  19. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts +6 -0
  20. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts.map +1 -0
  21. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js +37 -0
  22. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js.map +1 -0
  23. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts +13 -0
  24. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts.map +1 -0
  25. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js +11 -0
  26. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js.map +1 -0
  27. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts +3 -0
  28. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts.map +1 -0
  29. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js +196 -0
  30. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js.map +1 -0
  31. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.d.ts +3 -0
  32. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.d.ts.map +1 -0
  33. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js +80 -0
  34. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js.map +1 -0
  35. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.d.ts +2 -0
  36. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.d.ts.map +1 -0
  37. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js +45 -0
  38. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js.map +1 -0
  39. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.d.ts +38 -0
  40. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.d.ts.map +1 -0
  41. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js +2 -0
  42. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js.map +1 -0
  43. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts +38 -0
  44. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts.map +1 -0
  45. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js +378 -0
  46. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js.map +1 -0
  47. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts +16 -0
  48. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts.map +1 -0
  49. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js +2 -0
  50. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js.map +1 -0
  51. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.d.ts +41 -0
  52. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.d.ts.map +1 -0
  53. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js +6 -0
  54. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js.map +1 -0
  55. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.d.ts +12 -0
  56. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.d.ts.map +1 -0
  57. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js +61 -0
  58. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js.map +1 -0
  59. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts +15 -0
  60. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts.map +1 -0
  61. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js +49 -0
  62. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js.map +1 -0
  63. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts +18 -0
  64. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts.map +1 -0
  65. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js +412 -0
  66. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js.map +1 -0
  67. package/node_modules/@rdlabo/ionic-theme-utils/package.json +64 -0
  68. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.spec.ts +19 -0
  69. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.ts +16 -0
  70. package/node_modules/@rdlabo/ionic-theme-utils/src/focus-controller.ts +38 -0
  71. package/node_modules/@rdlabo/ionic-theme-utils/src/index.ts +59 -0
  72. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/animations/ios.enter.ts +259 -0
  73. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/animations/ios.leave.ts +99 -0
  74. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/callout-surface.ts +54 -0
  75. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/popover-interface.ts +44 -0
  76. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.spec.ts +58 -0
  77. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.ts +698 -0
  78. package/node_modules/@rdlabo/ionic-theme-utils/src/sheets-of-glass.ts +17 -0
  79. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/index.scss +1 -0
  80. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/md-ion-list-inset.scss +20 -0
  81. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/structured-list.scss +310 -0
  82. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/interfaces.ts +28 -0
  83. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.spec.ts +56 -0
  84. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.ts +75 -0
  85. package/node_modules/@rdlabo/ionic-theme-utils/src/theme.ts +51 -0
  86. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.spec.ts +52 -0
  87. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.ts +542 -0
  88. package/package.json +7 -4
  89. package/src/native/components/searchable-tabs.ts +142 -22
  90. package/src/native/runtime.ts +11 -6
package/README.md CHANGED
@@ -1,14 +1,12 @@
1
1
  # Ionic Theme iOS27
2
2
 
3
- A CSS/JS theme library that brings the iOS 27 Liquid Glass appearance and navigation animations to Ionic applications while preserving Ionic components and conventions.
3
+ A theme for Ionic apps that brings iOS 27 Liquid Glass and motion to the Web, with an optional way to project existing Ionic controls into native UI.
4
4
 
5
5
  > [!IMPORTANT]
6
6
  > This `main` branch contains the iOS 27 theme under the package name `@rdlabo/ionic-theme-ios27`. For the iOS 26 theme (`@rdlabo/ionic-theme-ios26`), see the [`ios26` branch](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/ios26).
7
7
 
8
8
  > All versions before 1.0.0 are release candidates (RC). APIs, CSS variables, classes, styling, and behavior may change without backward compatibility, including in minor and patch releases. A stable compatibility commitment starts with 1.0.0.
9
9
 
10
- [Try the iOS 27 demo](https://ionic-theme-ios27.rdlabo.dev/).
11
-
12
10
  <!-- rdlabo-docs-pick -->
13
11
 
14
12
  <p>
@@ -19,9 +17,27 @@ A CSS/JS theme library that brings the iOS 27 Liquid Glass appearance and naviga
19
17
 
20
18
  <!-- /rdlabo-docs-pick -->
21
19
 
20
+ ## Features
21
+
22
+ ### Bring the iOS 27 look to Ionic
23
+
24
+ Give familiar Ionic screens the iOS 27 visual language: Liquid Glass, styled toolbars and tabs, lists, buttons, search, overlays, page transitions, and coordinated light and dark appearances. See the result in the [Ionic 9 demo](https://ionic-theme-ios27.rdlabo.dev/) and [Ionic 8 demo](https://ionic8-theme-ios27.rdlabo.dev/).
25
+
26
+ ### Project your Ionic UI into Native UI
27
+
28
+ On Capacitor iOS, the optional, experimental [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) reads supported fixed controls from your existing Ionic markup. It projects their text, resolved `ion-icon` artwork or supported static SVGs, and selection state into UIKit controls with system Liquid Glass. Changes and native actions flow through the original Ionic components, so the Web and native presentations share one UI definition. Page content and routing stay in the WebView; unsupported layouts keep their Web presentation.
29
+
30
+ **Tab drag on iOS 27:** The same Library screen with Native UI Shell off (Web) and on (UIKit). Both frames were captured while dragging the selected tab; the lower panels enlarge the glass around the tab bar.
31
+
32
+ [![Native UI Shell off and on during the same Library tab drag, with enlarged tab bars](./screenshots/native-ui-shell-drag/comparison.png)](./screenshots/native-ui-shell-drag/comparison.png)
33
+
34
+ ### Follow the user's device
35
+
36
+ Pair the iOS 26 and iOS 27 themes so supported Safari versions can present the design of each generation: the iOS 26 look for iOS 26 users and the iOS 27 look for iOS 27 users. The [adaptive setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/ios-adaptive) uses browser feature checks to select the corresponding **styles**; it does not read the iOS version. When both packages are installed, keep the **page transition** on the iOS 27 animation. On even earlier iOS versions, Ionic's default iOS appearance remains when Safari supports neither feature. In a Capacitor iOS app, Native UI Shell's UIKit material follows the installed iOS version.
37
+
22
38
  ## Installation
23
39
 
24
- Capacitor apps can optionally use an experimental [Native UI Shell](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell): fixed Ionic navigation and action controls rendered by UIKit with system Liquid Glass, while page content, application logic and routing remain in the WebView. The guide covers the idea's origins, the one-time startup call, supported components and fallback behavior.
40
+ The steps below install the iOS 27 theme on its own. To switch between the iOS 26 and iOS 27 themes, follow [Adaptive iOS themes](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/ios-adaptive) instead of using the unconditional stylesheet imports and animation setup below.
25
41
 
26
42
  Requires `@ionic/core` 8.8.1 or later (Ionic 8 and 9). Install it in an existing Ionic project:
27
43
 
@@ -92,6 +108,8 @@ createApp(App)
92
108
  })
93
109
  ```
94
110
 
111
+ To enable Native UI Shell, follow its [setup guide](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). Importing the theme's styles alone does not turn on native controls.
112
+
95
113
  The page-transition radius defaults to `0`. Native apps can update it after measuring the web view:
96
114
 
97
115
  ```ts
@@ -175,13 +193,13 @@ createApp(App)
175
193
 
176
194
  **Full documentation:** [Ionic Theme iOS27](https://docs.rdlabo.dev/projects/ionic-theme-ios27)
177
195
 
178
- - [Adaptive iOS themes](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/ios-adaptive) — conditionally load iOS 26 or iOS 27 styles.
196
+ - [Adaptive iOS themes](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/ios-adaptive) — select iOS 26 or iOS 27 styles by browser capabilities; keep the iOS 27 page transition when both packages are installed.
179
197
  - [Using ion-item-group](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/using-ion-item-group) — required markup for inset lists.
180
198
  - [Special markup and classes](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/special-markup) — opt-in markup and utility classes used by the theme.
181
199
  - [ESLint](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/eslint) — check list structure with ESLint rules.
182
200
  - [Features](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/features) — CSS variables, Liquid Glass, selective imports, and dark mode.
183
- - [Native UI Shell (Experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) — native controls around Web content, using the included Capacitor plugin.
184
- - [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/experimental-animation) — tab bar and searchable effects.
201
+ - [Native UI Shell (Experimental)](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell) — project supported Ionic controls, text, and icons into UIKit.
202
+ - [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/experimental-animation) — tab, segment, and searchable effects.
185
203
  - [Migration](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/migration) — stylesheet, class, and CSS variable naming changes.
186
204
  - [iOS 26 migration history](https://docs.rdlabo.dev/projects/ionic-theme-ios26/docs/migration) — earlier major-version changes for the previous package.
187
205
 
@@ -208,7 +226,7 @@ npm start
208
226
 
209
227
  ### Visual Regression Testing
210
228
 
211
- We use Playwright for visual regression testing to ensure consistent styling across all components. The test suite automatically captures screenshots of all routes in both light and dark modes.
229
+ Playwright compares screenshots of demo routes in light and dark modes against stored baselines to catch unintended visual changes. These regression tests do not measure similarity to iOS 27 reference screens.
212
230
 
213
231
  #### Running Tests
214
232
 
@@ -6,7 +6,9 @@ export declare const createSearchSupport: (doc: Document, id: (element: Element)
6
6
  keepSearchTabsVisible(): void;
7
7
  decorate(candidates: Candidate[], blocked: (element: HTMLElement) => boolean): Candidate[];
8
8
  projected: (binding: NativeSearchBinding) => boolean;
9
+ hasActive(): boolean;
9
10
  begin(binding: NativeSearchBinding, revision: number): void;
11
+ setStatus(binding: NativeSearchBinding, active: boolean, focused?: boolean): void;
10
12
  release(element: HTMLElement): void;
11
13
  reject(ids: string[]): void;
12
14
  event(event: ShellSearchEvent): void;
@@ -1 +1 @@
1
- {"version":3,"file":"searchable-tabs.d.ts","sourceRoot":"","sources":["../../../src/native/components/searchable-tabs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAA0B,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAmB/E,eAAO,MAAM,mBAAmB,GAAI,KAAK,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,EAAE,UAAU,MAAM,IAAI;sBA8FnF,OAAO;;yBAgBJ,SAAS,EAAE,WAAW,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,GAAG,SAAS,EAAE;yBA3GhE,mBAAmB;mBAuO9B,mBAAmB,YAAY,MAAM;qBAInC,WAAW;gBAGhB,MAAM,EAAE;iBAUP,gBAAgB;oBAqCb,mBAAmB;;CAkBtC,CAAC"}
1
+ {"version":3,"file":"searchable-tabs.d.ts","sourceRoot":"","sources":["../../../src/native/components/searchable-tabs.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAGpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAA0B,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAmB/E,eAAO,MAAM,mBAAmB,GAAI,KAAK,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,KAAK,MAAM,EAAE,UAAU,MAAM,IAAI;sBA8InF,OAAO;;yBAmBJ,SAAS,EAAE,WAAW,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,GAAG,SAAS,EAAE;yBAlJhE,mBAAmB;;mBAuU9B,mBAAmB,YAAY,MAAM;uBAKjC,mBAAmB,UAAU,OAAO;qBAUtC,WAAW;gBAGhB,MAAM,EAAE;iBASP,gBAAgB;oBAqCb,mBAAmB;;CAoBtC,CAAC"}
@@ -4,7 +4,52 @@ import { iconSource } from '../shared/icons';
4
4
  export const createSearchSupport = (doc, id, schedule) => {
5
5
  const states = new Map();
6
6
  let keyboardVisible = false;
7
+ let heldKeyboardResize = null;
8
+ let keyboardResizeGeneration = 0;
9
+ const keyboardPlugin = () => doc.defaultView?.Capacitor?.Plugins?.Keyboard;
7
10
  const projected = (binding) => binding.footer.hasAttribute(marker);
11
+ const holdKeyboardResize = () => {
12
+ const keyboard = keyboardPlugin();
13
+ if (!keyboard?.setResizeMode || heldKeyboardResize !== null)
14
+ return;
15
+ const setResizeMode = keyboard.setResizeMode.bind(keyboard);
16
+ const generation = ++keyboardResizeGeneration;
17
+ heldKeyboardResize = 'native';
18
+ void (async () => {
19
+ let restoreMode = 'native';
20
+ try {
21
+ const current = await keyboard.getResizeMode?.();
22
+ if (typeof current?.mode === 'string')
23
+ restoreMode = current.mode;
24
+ }
25
+ catch {
26
+ }
27
+ if (generation !== keyboardResizeGeneration)
28
+ return;
29
+ heldKeyboardResize = restoreMode;
30
+ try {
31
+ await setResizeMode({ mode: 'none' });
32
+ }
33
+ catch {
34
+ if (generation === keyboardResizeGeneration)
35
+ heldKeyboardResize = null;
36
+ return;
37
+ }
38
+ if (generation !== keyboardResizeGeneration)
39
+ void setResizeMode({ mode: restoreMode }).catch(() => undefined);
40
+ })();
41
+ };
42
+ const hasActiveProjection = () => [...states.keys()].some((binding) => binding.active && projected(binding));
43
+ const releaseKeyboardResize = () => {
44
+ if (hasActiveProjection())
45
+ return;
46
+ keyboardResizeGeneration++;
47
+ const mode = heldKeyboardResize;
48
+ heldKeyboardResize = null;
49
+ if (!mode)
50
+ return;
51
+ void keyboardPlugin()?.setResizeMode?.({ mode });
52
+ };
8
53
  const isCurrent = (state) => state.bar.isConnected &&
9
54
  state.binding.footer.querySelector('ion-searchbar') === state.bar &&
10
55
  state.bar.querySelector('input.searchbar-input') === state.input;
@@ -105,8 +150,10 @@ export const createSearchSupport = (doc, id, schedule) => {
105
150
  return {
106
151
  keyboard(visible) {
107
152
  keyboardVisible = visible;
108
- if (!visible)
153
+ if (!visible) {
109
154
  states.forEach((state) => (state.keyboardHidden = false));
155
+ releaseKeyboardResize();
156
+ }
110
157
  schedule();
111
158
  },
112
159
  keepSearchTabsVisible() {
@@ -130,6 +177,11 @@ export const createSearchSupport = (doc, id, schedule) => {
130
177
  }
131
178
  }
132
179
  const inactive = [];
180
+ const pageHidden = (element) => !!element.closest('.ion-page-hidden, .ion-page-invisible');
181
+ const retainInactive = (candidate, state, keepAvailable = false) => {
182
+ if (state?.last)
183
+ inactive.push([candidate, { ...state.last, active: false, available: keepAvailable, focused: false }]);
184
+ };
133
185
  for (const binding of bindings) {
134
186
  const candidate = candidates.find((c) => c.element === binding.tabBar);
135
187
  if (!candidate || candidate.control.search || binding.tabBar.getAttribute('slot') !== 'bottom')
@@ -140,10 +192,36 @@ export const createSearchSupport = (doc, id, schedule) => {
140
192
  continue;
141
193
  if (isShellDisabled(fab) || isShellDisabled(binding.footer))
142
194
  continue;
143
- if (blocked(binding.footer) || !visible(binding.trigger) || binding.footer.closest(excluded)) {
144
- const state = states.get(binding);
145
- if (state?.last)
146
- inactive.push([candidate, { ...state.last, active: false, available: false, focused: false }]);
195
+ const pageUnavailable = blocked(binding.footer) || !!binding.footer.closest(excluded);
196
+ const existing = states.get(binding);
197
+ if (pageUnavailable) {
198
+ retainInactive(candidate, existing, !!existing?.last?.available && !pageHidden(binding.footer));
199
+ if (existing?.last)
200
+ continue;
201
+ }
202
+ if (binding.active && existing && isCurrent(existing) && existing.last && !pageUnavailable) {
203
+ const back = binding.footer.querySelector('ion-buttons[slot=start] ion-button');
204
+ if (back && !back.disabled && !back.closest(excluded)) {
205
+ candidate.control.search = {
206
+ ...existing.last,
207
+ active: true,
208
+ available: true,
209
+ focused: binding.focused,
210
+ value: existing.bar.value ?? '',
211
+ placeholder: existing.bar.placeholder ?? '',
212
+ disabled: existing.bar.disabled,
213
+ editSequence: existing.editSequence,
214
+ valueVersion: existing.valueVersion,
215
+ };
216
+ existing.last = candidate.control.search;
217
+ candidate.sources = [binding.tabBar, binding.trigger.closest('ion-fab') ?? binding.trigger, binding.footer];
218
+ candidate.actions.set(existing.last.trigger.id, binding.trigger);
219
+ candidate.actions.set(existing.last.closeId, back);
220
+ continue;
221
+ }
222
+ }
223
+ if (!visible(binding.trigger) && !pageUnavailable) {
224
+ retainInactive(candidate, existing);
147
225
  continue;
148
226
  }
149
227
  const bar = binding.footer.querySelector('ion-searchbar');
@@ -155,10 +233,12 @@ export const createSearchSupport = (doc, id, schedule) => {
155
233
  !binding.footer.matches('ion-footer') ||
156
234
  !inFixedToolbar(bar) ||
157
235
  !inFixedToolbar(back) ||
158
- bar.closest(excluded) ||
236
+ (!pageUnavailable && bar.closest(excluded)) ||
159
237
  !bar.classList.contains('ios') ||
160
- bar.classList.contains('searchbar-classic'))
238
+ bar.classList.contains('searchbar-classic')) {
239
+ retainInactive(candidate, existing);
161
240
  continue;
241
+ }
162
242
  if (bar.color ||
163
243
  back.fill !== 'default' ||
164
244
  back.color ||
@@ -171,14 +251,18 @@ export const createSearchSupport = (doc, id, schedule) => {
171
251
  ![false, 'off', undefined].includes(bar.autocorrect) ||
172
252
  bar.spellcheck ||
173
253
  bar.showClearButton !== 'always' ||
174
- bar.clearIcon)
254
+ bar.clearIcon) {
255
+ retainInactive(candidate, existing);
175
256
  continue;
176
- if (bar.showCancelButton !== 'never' || bar.type !== 'search' || !getComputedStyle(input).backdropFilter.includes('blur'))
257
+ }
258
+ if (bar.showCancelButton !== 'never' || bar.type !== 'search' || !getComputedStyle(input).backdropFilter.includes('blur')) {
259
+ retainInactive(candidate, existing);
177
260
  continue;
261
+ }
178
262
  const fabButton = binding.trigger;
179
263
  const nativeTrigger = fabButton.shadowRoot?.querySelector('[part=native]');
180
- if (binding.trigger.closest(excluded) ||
181
- back.closest(excluded) ||
264
+ if ((!pageUnavailable && binding.trigger.closest(excluded)) ||
265
+ (!pageUnavailable && back.closest(excluded)) ||
182
266
  back.disabled ||
183
267
  fabButton.disabled ||
184
268
  fabButton.color ||
@@ -186,16 +270,20 @@ export const createSearchSupport = (doc, id, schedule) => {
186
270
  fabButton.href ||
187
271
  !nativeTrigger ||
188
272
  getComputedStyle(fabButton).backgroundColor !== 'rgba(0, 0, 0, 0)' ||
189
- !getComputedStyle(nativeTrigger).backdropFilter.includes('blur'))
273
+ !getComputedStyle(nativeTrigger).backdropFilter.includes('blur')) {
274
+ retainInactive(candidate, existing);
190
275
  continue;
276
+ }
277
+ const state = existing ?? install(binding, bar, input);
191
278
  const trigger = readIcon(binding.trigger, binding.trigger, candidate);
192
279
  const searchIcon = bar.querySelector('.searchbar-search-icon');
193
280
  const field = searchIcon && readIcon(searchIcon, bar, candidate);
194
- if (!trigger || !field)
281
+ if (!trigger || !field) {
282
+ retainInactive(candidate, state);
195
283
  continue;
284
+ }
196
285
  field.label = '';
197
286
  field.accessibilityLabel = bar.getAttribute('aria-label') ?? input.getAttribute('aria-label') ?? 'Search';
198
- const state = states.get(binding) ?? install(binding, bar, input);
199
287
  state.layout = JSON.stringify([
200
288
  innerWidth,
201
289
  innerHeight,
@@ -215,16 +303,19 @@ export const createSearchSupport = (doc, id, schedule) => {
215
303
  bar.placeholder,
216
304
  bar.disabled,
217
305
  ]);
218
- if (state.rejectedLayout === state.layout)
306
+ if (state.rejectedLayout === state.layout) {
307
+ retainInactive(candidate, state);
219
308
  continue;
309
+ }
310
+ const available = !pageUnavailable && visible(binding.trigger);
220
311
  candidate.control.search = {
221
312
  id: id(input),
222
313
  field,
223
314
  trigger,
224
315
  closeId: id(back),
225
- active: binding.active,
226
- available: true,
227
- focused: binding.focused,
316
+ active: available && binding.active,
317
+ available,
318
+ focused: available && binding.focused,
228
319
  value: bar.value ?? '',
229
320
  placeholder: bar.placeholder ?? '',
230
321
  disabled: bar.disabled,
@@ -232,9 +323,11 @@ export const createSearchSupport = (doc, id, schedule) => {
232
323
  valueVersion: state.valueVersion,
233
324
  };
234
325
  state.last = candidate.control.search;
235
- candidate.sources = [binding.tabBar, binding.trigger.closest('ion-fab') ?? binding.trigger, binding.footer];
236
- candidate.actions.set(id(binding.trigger), binding.trigger);
237
- candidate.actions.set(id(back), back);
326
+ if (available) {
327
+ candidate.sources = [binding.tabBar, binding.trigger.closest('ion-fab') ?? binding.trigger, binding.footer];
328
+ candidate.actions.set(id(binding.trigger), binding.trigger);
329
+ candidate.actions.set(id(back), back);
330
+ }
238
331
  }
239
332
  for (const [candidate, configuration] of inactive)
240
333
  (_a = candidate.control).search ?? (_a.search = configuration);
@@ -242,11 +335,26 @@ export const createSearchSupport = (doc, id, schedule) => {
242
335
  return candidates.filter((c) => !groups.some((group) => group !== c && group.sources?.some((source) => source.contains(c.element))));
243
336
  },
244
337
  projected,
338
+ hasActive() {
339
+ for (const [binding] of states)
340
+ if (binding.active && projected(binding))
341
+ return true;
342
+ return false;
343
+ },
245
344
  begin(binding, revision) {
246
345
  const state = states.get(binding);
247
346
  if (state)
248
347
  state.minimumRevision = revision;
249
348
  },
349
+ setStatus(binding, active, focused = false) {
350
+ if (!active) {
351
+ this.retire(binding);
352
+ return;
353
+ }
354
+ binding.active = true;
355
+ binding.focused = focused;
356
+ holdKeyboardResize();
357
+ },
250
358
  release(element) {
251
359
  for (const binding of states.keys())
252
360
  if (binding.footer === element)
@@ -309,6 +417,7 @@ export const createSearchSupport = (doc, id, schedule) => {
309
417
  state.input.dispatchEvent(new FocusEvent('blur'));
310
418
  }
311
419
  binding.active = binding.focused = false;
420
+ releaseKeyboardResize();
312
421
  },
313
422
  destroy() {
314
423
  states.forEach((state) => {
@@ -316,6 +425,7 @@ export const createSearchSupport = (doc, id, schedule) => {
316
425
  state.restore();
317
426
  });
318
427
  states.clear();
428
+ releaseKeyboardResize();
319
429
  },
320
430
  };
321
431
  };
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,eAAe,CAAC;AAkBvB,eAAO,MAAM,aAAa,GACxB,KAAK,QAAQ,EACb,QAAQ,mBAAmB,EAC3B,UAAS,oBAAyB,KACjC,OAAO,CAAC,mBAAmB,CA+e7B,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,eAAe,CAAC;AAkBvB,eAAO,MAAM,aAAa,GACxB,KAAK,QAAQ,EACb,QAAQ,mBAAmB,EAC3B,UAAS,oBAAyB,KACjC,OAAO,CAAC,mBAAmB,CAof7B,CAAC"}
@@ -137,7 +137,9 @@ export const createRuntime = async (doc, plugin, options = {}) => {
137
137
  for (const surface of moving.keys())
138
138
  if (!surface.isConnected)
139
139
  moving.delete(surface);
140
- if (doc.hidden || overlayOpen() || (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0)))
140
+ if (doc.hidden || overlayOpen())
141
+ return [];
142
+ if (win.visualViewport && (win.visualViewport.scale !== 1 || win.visualViewport.offsetTop !== 0) && !search.hasActive())
141
143
  return [];
142
144
  return unprojected(sources.keys(), () => search
143
145
  .decorate(Array.from(doc.querySelectorAll(selector))
@@ -230,6 +232,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
230
232
  }
231
233
  if (result.rejectedSearches?.length) {
232
234
  search.reject(result.rejectedSearches);
235
+ lastSnapshot = '';
233
236
  dirty = true;
234
237
  }
235
238
  for (const candidate of candidates) {
@@ -480,12 +483,16 @@ export const createRuntime = async (doc, plugin, options = {}) => {
480
483
  async search(binding, active, focus) {
481
484
  if (!getNativeSearchBindings(doc).includes(binding) || !search.projected(binding))
482
485
  return false;
483
- if (active && !binding.active)
484
- search.begin(binding, revision + 1);
485
- binding.active = active;
486
- binding.focused = focus ?? (active && binding.focused);
487
- if (!active)
488
- search.retire(binding);
486
+ if (!active) {
487
+ search.setStatus(binding, false);
488
+ }
489
+ else {
490
+ const entering = !binding.active;
491
+ if (entering)
492
+ search.begin(binding, revision + 1);
493
+ const nextFocused = focus === true ? true : focus === false ? false : entering ? false : binding.focused;
494
+ search.setStatus(binding, true, nextFocused);
495
+ }
489
496
  await flush();
490
497
  return !stopped && search.projected(binding);
491
498
  },
@@ -4,7 +4,7 @@ title: Adaptive iOS themes
4
4
 
5
5
  # Adaptive iOS themes
6
6
 
7
- Use CSS feature queries to choose a theme while keeping Ionic's default iOS styles on older browsers. Safari 27 adds `:heading` support; see the [WebKit announcement](https://webkit.org/blog/17967/news-from-wwdc26-webkit-in-safari-27-beta/).
7
+ Use CSS feature queries to choose a theme while keeping Ionic's default iOS styles on older browsers. The newer theme branch uses `overflow-anchor` support; see the [MDN `overflow-anchor` documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-anchor).
8
8
 
9
9
  These checks detect browser capabilities, not the operating system version. Other browsers may support the same features. Keep Ionic mode and platform-specific animation configuration consistent with your application.
10
10
 
@@ -15,7 +15,7 @@ In your global Sass stylesheet, use `meta.load-css` inside the feature query. Un
15
15
  ```scss
16
16
  @use 'sass:meta';
17
17
 
18
- @supports selector(:heading) {
18
+ @supports (overflow-anchor: auto) {
19
19
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/default-variables');
20
20
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27');
21
21
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class');
@@ -23,6 +23,8 @@ In your global Sass stylesheet, use `meta.load-css` inside the feature query. Un
23
23
  }
24
24
  ```
25
25
 
26
+ Configure animations from the same package as in the [README](https://docs.rdlabo.dev/projects/ionic-theme-ios27#configure-animations).
27
+
26
28
  ## Use the iOS 26 and iOS 27 themes together
27
29
 
28
30
  Install both packages:
@@ -36,14 +38,14 @@ Use this global Sass configuration instead of unconditional theme imports:
36
38
  ```scss
37
39
  @use 'sass:meta';
38
40
 
39
- @supports selector(:heading) {
41
+ @supports (overflow-anchor: auto) {
40
42
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/default-variables');
41
43
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27');
42
44
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class');
43
45
  @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect');
44
46
  }
45
47
 
46
- @supports (text-wrap: pretty) and (not selector(:heading)) {
48
+ @supports (text-wrap: pretty) and (not (overflow-anchor: auto)) {
47
49
  @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/default-variables');
48
50
  @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26');
49
51
  @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class');
@@ -51,40 +53,37 @@ Use this global Sass configuration instead of unconditional theme imports:
51
53
  }
52
54
  ```
53
55
 
54
- The branches are mutually exclusive. Browsers without either feature retain Ionic's default styling. The `text-wrap: pretty` check preserves the earlier theme's capability-based fallback for older Safari versions.
56
+ The branches are mutually exclusive. Browsers without either feature retain Ionic's default styling. The `text-wrap: pretty` check preserves the earlier theme's capability-based fallback when `overflow-anchor` is unavailable.
55
57
 
56
58
  Both examples use class-based dark mode. Load Ionic's matching dark palette in your application; for system or always-dark mode, replace the `-dark-class` imports with the matching variant. If you also use `md-ion-list-inset`, load the corresponding package's stylesheet inside each branch.
57
59
 
58
- ## Select matching JavaScript animations
60
+ ### Page transition and overlay animations
59
61
 
60
- Use the same feature checks for dynamic imports before initializing Ionic. Call this loader for the iOS platform; leave other platforms on their existing animation configuration.
62
+ When both packages are installed, keep **page transition** (`navAnimation`) on the iOS 27 animation. Styles still follow the feature queries above; only the transition stays on `@rdlabo/ionic-theme-ios27`. Use the same package for popover enter/leave so overlay motion stays consistent with that transition.
61
63
 
62
64
  ```ts
63
- async function loadIOSAnimations() {
65
+ import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27';
66
+
67
+ function loadIOSAnimations() {
64
68
  if (typeof CSS === 'undefined') {
65
69
  return {};
66
70
  }
67
71
 
68
- const theme = CSS.supports('selector(:heading)')
69
- ? await import('@rdlabo/ionic-theme-ios27')
70
- : CSS.supports('text-wrap: pretty')
71
- ? await import('@rdlabo/ionic-theme-ios26')
72
- : undefined;
73
-
74
- if (!theme) {
72
+ // Styles are adaptive; apply iOS theme animations when either generation matches.
73
+ if (!CSS.supports('overflow-anchor: auto') && !CSS.supports('text-wrap: pretty')) {
75
74
  return {};
76
75
  }
77
76
 
78
77
  return {
79
- navAnimation: theme.iosTransitionAnimation,
80
- popoverEnter: theme.popoverEnterAnimation,
81
- popoverLeave: theme.popoverLeaveAnimation,
78
+ navAnimation: iosTransitionAnimation,
79
+ popoverEnter: popoverEnterAnimation,
80
+ popoverLeave: popoverLeaveAnimation,
82
81
  };
83
82
  }
84
83
 
85
- // In your browser bootstrap, before initializing Ionic:
86
- const animations = isPlatform('ios') ? await loadIOSAnimations() : {};
84
+ // Resolve synchronously before bootstrapping so Ionic's global config exists before any element upgrades.
85
+ const animations = isPlatform('ios') ? loadIOSAnimations() : {};
87
86
  provideIonicAngular({ ...animations });
88
87
  ```
89
88
 
90
- Import `isPlatform` and `provideIonicAngular` from your Ionic Angular entry point as shown in the README. Pass the same options to `setupIonicReact` or `IonicVue` when using those frameworks. The imports are selected at runtime; the bundler may still emit chunks for both packages. In server-rendered apps, run the selection during browser initialization.
89
+ Import `isPlatform` and `provideIonicAngular` from your Ionic Angular entry point as shown in the README. Pass the same options to `setupIonicReact` or `IonicVue` when using those frameworks. In server-rendered apps, run the selection during browser initialization.
@@ -46,7 +46,7 @@ Native appearance follows the applied class, system or always-dark theme CSS. Sy
46
46
  | `ion-buttons` | Fixed toolbar, two or more direct clear `ion-button` / `ion-menu-button` children sharing the theme glass | One `UIGlassEffect` surface with independent native buttons |
47
47
  | `ion-back-button` | Standard icon and color in a fixed header/footer toolbar | Glass `UIButton`, using the resolved Ionic label/icon |
48
48
  | `ion-menu-button` | Fixed toolbar, inside the theme glass `ion-buttons` | Glass `UIButton`; original Ionic menu toggle |
49
- | `ion-tab-bar` | Fixed tabs, icon-only/label-only items, one icon per item, dot/text badges, selection and disabled state | `UITabBar` and `UITabBarItem` |
49
+ | `ion-tab-bar` | Fixed tabs, icon-only/label-only items, one icon per item, dot/text badges, selection and disabled state | `UITabBar` and `UITabBarItem` |
50
50
  | `ion-segment` | Fixed toolbar, non-scrollable, text **or** one icon per item | `UISegmentedControl` |
51
51
  | `ion-fab` / `ion-fab-button` / `ion-fab-list` | Glass FAB in an `ion-content` fixed slot; one main button and optional directional lists | Persistent glass `UIButton` per button; one FAB synchronization group |
52
52
 
@@ -92,7 +92,9 @@ Existing `attachTabBarSearchable(tabBar, fabButton, footer)` registrations autom
92
92
 
93
93
  Search registration does not bypass placement restrictions. Its searchbar and close button must be in fixed footer toolbars. Its trigger must belong to an `ion-fab[slot="fixed"]` directly inside `ion-content`, or directly on the existing non-scrolling `.ion-page` layout. A wrapper inside scrolling content is not a fixed slot.
94
94
 
95
- Opening search preserves the selected Ionic tab and does not automatically show the keyboard. Tap the search field or call the existing `ion-searchbar.setFocus()`. UIKit owns the search expansion, keyboard placement and return to the ordinary tabs. The trigger's resolved SVG and the search icon are projected from Ionic, including `ion-icon name`. The resting layout is measured against `ion-tab-bar` and both coordinates of the original FAB's center. A layout that cannot preserve those origins uses Web search.
95
+ While a registration is alive, Native UI Shell keeps the searchable controller even when the page is transitioning or temporarily unavailable (`available: false`), instead of demoting back to a separate ordinary control identity. Resting chrome (including Album before search opens) uses the same `UITabBar` + `ShellTabBar.fit` path as ordinary tabs, with the search trigger pinned to the FAB when available; `UISearchTab` is shown only while the search session is active. Flipping availability or entering/leaving search crossfades between those layers. Register before the destination page finishes entering (for example in `ionViewWillEnter`) so the first visit does not paint ordinary tabs and then swap.
96
+
97
+ Opening search preserves the selected Ionic tab and does not automatically show the keyboard (`automaticallyActivatesSearch` stays off). Tap the field or call `ion-searchbar.setFocus()` for the keyboard. While search is active, Native UI Shell freezes Web layout projection and holds Capacitor Keyboard resize at `none`; UIKit owns tab/search chrome (no Ionic `fit` mid-session). Leave re-fits ordinary tabs to `ion-tab-bar` and search to the FAB. Ordinary native tabs keep an optimistic selection until the Web `selected` state catches up. Input events and application `value` updates continue across the bridge until search closes. The trigger's resolved SVG and the search icon are projected from Ionic, including `ion-icon name`.
96
98
 
97
99
  Native edits pass through Ionic's input handlers, preserving `ionInput` debounce, `ionChange`, `ionFocus`, `ionBlur`, and `ionClear`. Programmatic `value` changes do not emit `ionInput`; synchronous application corrections and stale native input are distinguished. Native editing owns marked text and the caret. Returning through the footer's close action retains the value and does not emit `ionCancel` or `ionClear`.
98
100
 
@@ -247,5 +249,4 @@ On iOS, [`Components`](../ios/Sources/IonicNativeUIShellPlugin/Components) owns
247
249
 
248
250
  See the [demo and verification guide](../demo/native-ui-shell.md) for browser tests, Simulator tests and an independent SPM consumer built from the npm package.
249
251
 
250
-
251
252
  Search controllers retain their UIKit-managed transition and are excluded from the ordinary control acquisition crossfade.