@rdlabo/ionic-theme-ios27 0.2.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,8 @@
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. Capacitor iOS apps can also opt into an experimental Native UI Shell for supported controls.
4
4
 
5
- > [!IMPORTANT]
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
-
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
-
10
- [Try the iOS 27 demo](https://ionic-theme-ios27.rdlabo.dev/).
5
+ **[Ionic 9 demo](https://ionic-theme-ios27.rdlabo.dev/) · [Ionic 8 demo](https://ionic8-theme-ios27.rdlabo.dev/) · [Documentation](https://docs.rdlabo.dev/projects/ionic-theme-ios27)**
11
6
 
12
7
  <!-- rdlabo-docs-pick -->
13
8
 
@@ -19,80 +14,79 @@ A CSS/JS theme library that brings the iOS 27 Liquid Glass appearance and naviga
19
14
 
20
15
  <!-- /rdlabo-docs-pick -->
21
16
 
22
- ## Installation
17
+ ## Features
23
18
 
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.
19
+ ### Bring the iOS 27 look to Ionic
25
20
 
26
- Requires `@ionic/core` 8.8.1 or later (Ionic 8 and 9). Install it in an existing Ionic project:
21
+ 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/).
27
22
 
28
- ```bash
29
- npm install @rdlabo/ionic-theme-ios27
30
- ```
23
+ ### Project your Ionic UI into Native UI
31
24
 
32
- And import the theme in your project's main CSS file (e.g., `src/styles.scss`).
25
+ 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.
33
26
 
34
- ```css
35
- @import '@rdlabo/ionic-theme-ios27/dist/css/default-variables.css';
36
- @import '@rdlabo/ionic-theme-ios27/dist/css/ionic-theme-ios27.css';
27
+ **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.
37
28
 
38
- /**
39
- * Keep Material Design mode unaffected by the iOS theme
40
- * when the same markup is used in both modes.
41
- * Note: This stylesheet is not included in `@rdlabo/ionic-theme-md3`.
42
- */
43
- @import '@rdlabo/ionic-theme-ios27/dist/css/md-remove-ios-class-effect.css';
29
+ [![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)
30
+
31
+ ### Follow the user's device
32
+
33
+ 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 [default setup](#get-started) 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.
34
+
35
+ ## Get started
44
36
 
45
- /**
46
- * If you will use the design of ion-item-group with ion-list on Android as well, import it.
47
- * More info: https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/using-ion-item-group
48
- * Note: This stylesheet is included in `@rdlabo/ionic-theme-md3`.
49
- * @import '@rdlabo/ionic-theme-ios27/dist/css/md-ion-list-inset.css';
50
- */
51
-
52
- /*
53
- * Support Dark Mode
54
- * We support Ionic Dark Mode. More information is here: https://ionicframework.com/docs/theming/dark-mode
55
- * use Always: @import '@rdlabo/ionic-theme-ios27/dist/css/ionic-theme-ios27-dark-always.css'
56
- * use System: @import '@rdlabo/ionic-theme-ios27/dist/css/ionic-theme-ios27-dark-system.css'
57
- * use CSS Class: @import '@rdlabo/ionic-theme-ios27/dist/css/ionic-theme-ios27-dark-class.css'
58
- */
37
+ Install both themes in an existing Ionic 8 or 9 app (`@ionic/core` 8.8.1 or later):
38
+
39
+ ```bash
40
+ npm install @rdlabo/ionic-theme-ios26 @rdlabo/ionic-theme-ios27
41
+ ```
42
+
43
+ In your global Sass stylesheet (for example, `src/styles.scss`), load the styles by browser capability:
44
+
45
+ ```scss
46
+ @use 'sass:meta';
47
+
48
+ @supports (overflow-anchor: auto) {
49
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/default-variables');
50
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27');
51
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class');
52
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect');
53
+ }
54
+
55
+ @supports (text-wrap: pretty) and (not (overflow-anchor: auto)) {
56
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/default-variables');
57
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26');
58
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class');
59
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect');
60
+ }
59
61
  ```
60
62
 
63
+ These checks select styles by browser features, not by iOS version. Browsers without either feature retain Ionic's default iOS appearance. The example uses class-based dark mode: also load [Ionic's matching dark palette](https://ionicframework.com/docs/theming/dark-mode). For system or always-dark mode, replace both `-dark-class` imports with the matching variant. The `md-remove-ios-class-effect` styles prevent iOS-specific classes from affecting Material Design mode.
64
+
61
65
  ### Configure animations
62
66
 
63
- If you installed only the iOS 27 theme, configure its animations as follows.
67
+ Keep the iOS 27 page transition and popover animations for both styled generations. Resolve these options before Ionic initializes. For Angular:
64
68
 
65
69
  ```ts
66
- import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue
70
+ import { isPlatform, provideIonicAngular } from '@ionic/angular/standalone'; // Ionic 8
67
71
  import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27';
68
72
 
69
- // Angular
70
- provideIonicAngular({
71
- ...
72
- navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,
73
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,
74
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,
75
- });
76
-
77
- // React
78
- setupIonicReact({
79
- ...
80
- navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,
81
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,
82
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,
83
- });
84
-
85
- // Vue
86
- createApp(App)
87
- .use(IonicVue, {
88
- ...
89
- navAnimation: isPlatform('ios') ? iosTransitionAnimation: undefined,
90
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation: undefined,
91
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation: undefined,
92
- })
73
+ function loadIOSAnimations() {
74
+ if (typeof CSS === 'undefined') return {};
75
+ if (!CSS.supports('overflow-anchor: auto') && !CSS.supports('text-wrap: pretty')) return {};
76
+
77
+ return {
78
+ navAnimation: iosTransitionAnimation,
79
+ popoverEnter: popoverEnterAnimation,
80
+ popoverLeave: popoverLeaveAnimation,
81
+ };
82
+ }
83
+
84
+ provideIonicAngular(isPlatform('ios') ? loadIOSAnimations() : {});
93
85
  ```
94
86
 
95
- The page-transition radius defaults to `0`. Native apps can update it after measuring the web view:
87
+ For Ionic 9 Angular, import `isPlatform` and `provideIonicAngular` from `@ionic/angular`. React and Vue can pass the same options to `setupIonicReact` or `IonicVue` during initialization. In server-rendered apps, run the selection during browser initialization.
88
+
89
+ The page-transition radius defaults to `0`. Native apps can update it after measuring the WebView:
96
90
 
97
91
  ```ts
98
92
  import { setConfig } from '@rdlabo/ionic-theme-ios27';
@@ -115,17 +109,29 @@ Use this markup to preview the inset grouped list look. For the list structure t
115
109
  </ion-list>
116
110
  ```
117
111
 
118
- ### Optional: use the iOS 27 and MD3 themes together
112
+ ## Optional setups
119
113
 
120
- Install the MD3 theme to style both Ionic modes from the same application.
114
+ ### Use only the iOS 27 theme
121
115
 
122
- The current releases of both themes require `@ionic/core` 8.8.1 or later.
116
+ Install only `@rdlabo/ionic-theme-ios27` and import its styles unconditionally in your global stylesheet:
123
117
 
124
- ```bash
125
- npm install @rdlabo/ionic-theme-md3
118
+ ```css
119
+ @import '@rdlabo/ionic-theme-ios27/dist/css/default-variables.css';
120
+ @import '@rdlabo/ionic-theme-ios27/dist/css/ionic-theme-ios27.css';
121
+ @import '@rdlabo/ionic-theme-ios27/dist/css/md-remove-ios-class-effect.css';
122
+ @import '@rdlabo/ionic-theme-ios27/dist/css/ionic-theme-ios27-dark-class.css';
126
123
  ```
127
124
 
128
- When your global stylesheet uses Sass, initialize the themes in this order:
125
+ The last import uses class-based dark mode; choose the `-dark-system` or `-dark-always` variant and matching Ionic palette for another mode. Configure the iOS 27 animations with `isPlatform('ios')` as above, without the browser feature checks.
126
+
127
+ ### Other options
128
+
129
+ - **Native controls:** Follow the [Native UI Shell setup guide](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/native-ui-shell). Stylesheet imports alone do not enable it.
130
+ - **Inset lists on Android:** Load the matching package's `md-ion-list-inset` stylesheet inside each `@supports` branch if needed. It is already included in `@rdlabo/ionic-theme-md3`.
131
+
132
+ ### Use with the MD3 theme
133
+
134
+ For the iOS 27-only setup above, install `@rdlabo/ionic-theme-md3` to style both Ionic modes. Both themes require `@ionic/core` 8.8.1 or later. In a global Sass stylesheet, load the iOS 27 styles before MD3:
129
135
 
130
136
  ```scss
131
137
  @use '@rdlabo/ionic-theme-ios27/src/styles/default-variables.scss' as ios27-vars;
@@ -136,58 +142,24 @@ When your global stylesheet uses Sass, initialize the themes in this order:
136
142
  @use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css';
137
143
  ```
138
144
 
139
- The example uses Ionic's class-based dark mode. Your global stylesheet must also load Ionic's matching dark palette, such as `@ionic/angular/css/palettes/dark.class.css` for Angular. When using `dark-system` or `dark-always`, select the same variant for both Ionic's palette and the iOS 27 theme. See Ionic's [Dark Mode documentation](https://ionicframework.com/docs/theming/dark-mode). The explicit `ios27-vars` and `md3-vars` namespaces prevent the two variable modules from using the same default namespace.
140
-
141
- Configure both transition implementations when both themes are installed:
142
-
143
- ```ts
144
- import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue
145
- import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27';
146
- import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3';
147
-
148
- // Angular
149
- provideIonicAngular({
150
- ...
151
- navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,
152
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,
153
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,
154
- });
155
-
156
- // React
157
- setupIonicReact({
158
- ...
159
- navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,
160
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,
161
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,
162
- });
163
-
164
- // Vue
165
- createApp(App)
166
- .use(IonicVue, {
167
- ...
168
- navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation,
169
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,
170
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,
171
- });
172
- ```
145
+ Load Ionic's matching dark palette too. To use MD3's page transition in Material Design mode, set `navAnimation` to `isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation`, importing the latter from `@rdlabo/ionic-theme-md3`.
173
146
 
174
147
  ## Documentation
175
148
 
176
149
  **Full documentation:** [Ionic Theme iOS27](https://docs.rdlabo.dev/projects/ionic-theme-ios27)
177
150
 
178
- - [Adaptive iOS themes](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/ios-adaptive) — conditionally load iOS 26 or iOS 27 styles.
179
151
  - [Using ion-item-group](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/using-ion-item-group) — required markup for inset lists.
180
152
  - [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
153
  - [ESLint](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/eslint) — check list structure with ESLint rules.
182
154
  - [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.
185
- - [Migration](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/migration) — stylesheet, class, and CSS variable naming changes.
155
+ - [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.
156
+ - [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/experimental-animation) — tab, segment, and searchable effects.
157
+ - [Migration from iOS 26](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/migration) — upgrade an existing app, including stylesheet, class, and CSS variable changes.
186
158
  - [iOS 26 migration history](https://docs.rdlabo.dev/projects/ionic-theme-ios26/docs/migration) — earlier major-version changes for the previous package.
187
159
 
188
160
  <!-- rdlabo-docs-omit -->
189
161
 
190
- **iOS 26 documentation:** See the [iOS 26 documentation](https://docs.rdlabo.dev/projects/ionic-theme-ios26) for the previous theme.
162
+ **iOS 26 theme:** See the [`ios26` branch](https://github.com/rdlabo-dev/ionic-theme-ios27/tree/ios26) and [iOS 26 documentation](https://docs.rdlabo.dev/projects/ionic-theme-ios26).
191
163
 
192
164
  ## Development & Testing
193
165
 
@@ -208,7 +180,7 @@ npm start
208
180
 
209
181
  ### Visual Regression Testing
210
182
 
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.
183
+ 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
184
 
213
185
  #### Running Tests
214
186
 
@@ -228,23 +200,9 @@ npm run test:e2e:debug
228
200
  npm run test:e2e:update
229
201
  ```
230
202
 
231
- ### Prerelease channels
232
-
233
- An open, non-draft pull request can be published to the npm `beta` dist-tag after its `Lint`, `E2E Screenshot Tests Pull Request`, and `Package Candidate` workflows pass. A repository owner or maintainer must add a comment whose entire body is:
234
-
235
- ```text
236
- /beta
237
- ```
238
-
239
- The request authorizes only the pull request head SHA and base branch that existed when the comment was added. The workflow revalidates the owner or maintainer permission, head SHA, and base branch immediately before publishing. Any new commit or retargeting invalidates the request; the new state must pass CI and receive a fresh owner or maintainer `/beta` comment. Fork pull requests are supported. Pull requests that change a release-gating workflow cannot be beta-published until those workflow changes land on their target branch.
240
-
241
- Beta versions use `<base>-beta.pr<PR number>.sha<12-character SHA>`. The pull request receives a comment containing the immutable version and exact `npm install` command.
242
-
243
- When a pull request is merged into `main` or `ios26`, it is automatically published to the npm `beta` dist-tag only after `Lint`, `E2E Screenshot Tests`, and `Package Candidate` all succeed for that exact merge commit. Direct pushes do not publish a candidate. Merge candidates use `<base>-beta.pr<PR number>.sha<12-character SHA>` and the merged pull request receives the exact install command.
244
-
245
- Candidate code is built in a read-only workflow without npm publishing credentials. The privileged release workflow never checks out or executes pull request code; it revalidates the source workflow and package identity, then publishes only the immutable packed artifact with lifecycle scripts disabled. The install-command comment is a separate best-effort notification and cannot invalidate a successful npm publish.
203
+ ### Releases
246
204
 
247
- Only `npm run release` can create a release tag. Stable `ios27-vX.Y.Z` tags (major, minor, or patch releases) publish to npm `latest`; revision/prerelease tags publish to `next`. Neither `beta` nor `next` publishing changes the npm `latest` dist-tag.
205
+ Stable `ios27-vX.Y.Z` tags publish to npm `latest` through the [release workflow](./.github/workflows/release.yml). Maintainers create release tags with `npm run release`. Pull request and merge candidates use the npm `beta` tag; prerelease tags use `next`.
248
206
 
249
207
  <!-- /rdlabo-docs-omit -->
250
208
 
@@ -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
  },