@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.
- package/README.md +26 -8
- 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/ios-adaptive.md +20 -21
- 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/node_modules/@rdlabo/ionic-theme-utils/LICENSE +21 -0
- package/node_modules/@rdlabo/ionic-theme-utils/README.md +28 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts +3 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js +13 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts +6 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js +37 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts +13 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js +11 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts +3 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js +196 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.d.ts +3 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js +80 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.d.ts +2 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js +45 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.d.ts +38 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js +2 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts +38 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js +378 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts +16 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js +2 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.d.ts +41 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js +6 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.d.ts +12 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js +61 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts +15 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js +49 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts +18 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js +412 -0
- package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js.map +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/package.json +64 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/dom.spec.ts +19 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/dom.ts +16 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/focus-controller.ts +38 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/index.ts +59 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/popover/animations/ios.enter.ts +259 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/popover/animations/ios.leave.ts +99 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/popover/callout-surface.ts +54 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/popover/popover-interface.ts +44 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.spec.ts +58 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.ts +698 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/sheets-of-glass.ts +17 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/styles/index.scss +1 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/styles/md-ion-list-inset.scss +20 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/styles/structured-list.scss +310 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/interfaces.ts +28 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.spec.ts +56 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.ts +75 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/theme.ts +51 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.spec.ts +52 -0
- package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.ts +542 -0
- package/package.json +7 -4
- package/src/native/components/searchable-tabs.ts +142 -22
- package/src/native/runtime.ts +11 -6
package/README.md
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
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, 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
|
+
[](./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
|
-
|
|
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) —
|
|
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) —
|
|
184
|
-
- [Animation](https://docs.rdlabo.dev/projects/ionic-theme-ios27/docs/experimental-animation) — tab
|
|
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
|
-
|
|
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;
|
|
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
|
},
|
package/docs/ios-adaptive.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
60
|
+
### Page transition and overlay animations
|
|
59
61
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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:
|
|
80
|
-
popoverEnter:
|
|
81
|
-
popoverLeave:
|
|
78
|
+
navAnimation: iosTransitionAnimation,
|
|
79
|
+
popoverEnter: popoverEnterAnimation,
|
|
80
|
+
popoverLeave: popoverLeaveAnimation,
|
|
82
81
|
};
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
//
|
|
86
|
-
const animations = isPlatform('ios') ?
|
|
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.
|
|
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.
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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.
|