@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 +84 -126
- package/dist/native/components/searchable-tabs.d.ts +2 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -1
- package/dist/native/components/searchable-tabs.js +131 -21
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +14 -7
- package/docs/migration.md +105 -3
- package/docs/native-ui-shell.md +4 -3
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +243 -64
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +77 -6
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +79 -12
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +258 -2
- package/package.json +1 -1
- package/src/native/components/searchable-tabs.ts +142 -22
- package/src/native/runtime.ts +11 -6
- package/docs/ios-adaptive.md +0 -90
package/README.md
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
# Ionic Theme iOS27
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
|
|
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
|
-
##
|
|
17
|
+
## Features
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
### Bring the iOS 27 look to Ionic
|
|
25
20
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
npm install @rdlabo/ionic-theme-ios27
|
|
30
|
-
```
|
|
23
|
+
### Project your Ionic UI into Native UI
|
|
31
24
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
[](./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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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/
|
|
70
|
+
import { isPlatform, provideIonicAngular } from '@ionic/angular/standalone'; // Ionic 8
|
|
67
71
|
import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27';
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
+
## Optional setups
|
|
119
113
|
|
|
120
|
-
|
|
114
|
+
### Use only the iOS 27 theme
|
|
121
115
|
|
|
122
|
-
|
|
116
|
+
Install only `@rdlabo/ionic-theme-ios27` and import its styles unconditionally in your global stylesheet:
|
|
123
117
|
|
|
124
|
-
```
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) —
|
|
184
|
-
- [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/experimental-animation) — tab
|
|
185
|
-
- [Migration](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/migration) — stylesheet, class, and CSS variable
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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,
|
|
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"}
|
package/dist/native/runtime.js
CHANGED
|
@@ -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()
|
|
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 (
|
|
484
|
-
search.
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
},
|