@rdlabo/ionic-theme-ios27 0.2.1 → 1.0.1-beta.pr194.sha00da872ba249
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 +134 -122
- package/dist/css/components/ion-segment.css +1 -1
- package/dist/css/components/ion-toolbar.css +1 -1
- package/dist/css/ionic-theme-ios27-dark-always.css +1 -1
- package/dist/css/ionic-theme-ios27-dark-class.css +1 -1
- package/dist/css/ionic-theme-ios27-dark-system.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/utils/structured-list.css +1 -0
- 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 +15 -3
- package/docs/special-markup.md +9 -0
- 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/package.json +4 -0
- package/node_modules/@rdlabo/ionic-theme-utils/scripts/copy-structured-list.mjs +17 -0
- package/package.json +4 -3
- package/src/native/components/searchable-tabs.ts +142 -22
- package/src/native/runtime.ts +11 -6
- package/src/styles/components/ion-list.scss +1 -1
- package/src/styles/components/ion-segment.scss +19 -0
- package/src/styles/components/ion-toolbar.scss +11 -0
- package/src/styles/md-ion-list-inset.scss +1 -1
- package/src/styles/utils/dark/ion-list.scss +1 -1
- package/src/styles/utils/dark/ion-segment.scss +20 -0
- package/src/styles/utils/structured-list.scss +310 -0
- package/docs/ios-adaptive.md +0 -90
- /package/docs/{experimental-animation.md → animation.md} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -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/migration.md
CHANGED
|
@@ -4,9 +4,111 @@ title: Migration
|
|
|
4
4
|
|
|
5
5
|
# Migration
|
|
6
6
|
|
|
7
|
+
## From the iOS 26 theme
|
|
8
|
+
|
|
9
|
+
For an app using `@rdlabo/ionic-theme-ios26`, the recommended migration keeps that package and adds `@rdlabo/ionic-theme-ios27`. The [README setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27#get-started) selects iOS 27 or iOS 26 styles by browser capability and leaves Ionic's default iOS appearance on older browsers.
|
|
10
|
+
|
|
11
|
+
### 1. Add the new package
|
|
12
|
+
|
|
13
|
+
Keep the iOS 26 package and add iOS 27. The new theme requires `@ionic/core` 8.8.1 or later (Ionic 8 or 9).
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @rdlabo/ionic-theme-ios27
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. Make the styles adaptive
|
|
20
|
+
|
|
21
|
+
Replace unconditional iOS 26 imports in your global Sass stylesheet with two mutually exclusive branches. This example uses class-based dark mode:
|
|
22
|
+
|
|
23
|
+
```diff
|
|
24
|
+
+ @use 'sass:meta';
|
|
25
|
+
+
|
|
26
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss';
|
|
27
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss';
|
|
28
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss';
|
|
29
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss';
|
|
30
|
+
+ @supports (overflow-anchor: auto) {
|
|
31
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/default-variables');
|
|
32
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27');
|
|
33
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class');
|
|
34
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect');
|
|
35
|
+
+ }
|
|
36
|
+
+
|
|
37
|
+
+ @supports (text-wrap: pretty) and (not (overflow-anchor: auto)) {
|
|
38
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/default-variables');
|
|
39
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26');
|
|
40
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class');
|
|
41
|
+
+ @include meta.load-css('@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect');
|
|
42
|
+
+ }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Keep Ionic's matching dark palette. For system or always-dark mode, replace both `-dark-class` imports with the matching variant. If you use `md-ion-list-inset`, load the corresponding package's stylesheet inside each branch. Browsers without either feature retain Ionic's default styling.
|
|
46
|
+
|
|
47
|
+
### 3. Switch the animations
|
|
48
|
+
|
|
49
|
+
Replace the iOS 26 animation import and gate the iOS 27 animations on the same browser features. Resolve the options before Ionic initializes:
|
|
50
|
+
|
|
51
|
+
```diff
|
|
52
|
+
- import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios26';
|
|
53
|
+
+ import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27';
|
|
54
|
+
+
|
|
55
|
+
+ function loadIOSAnimations() {
|
|
56
|
+
+ if (typeof CSS === 'undefined') return {};
|
|
57
|
+
+ if (!CSS.supports('overflow-anchor: auto') && !CSS.supports('text-wrap: pretty')) return {};
|
|
58
|
+
+
|
|
59
|
+
+ return {
|
|
60
|
+
+ navAnimation: iosTransitionAnimation,
|
|
61
|
+
+ popoverEnter: popoverEnterAnimation,
|
|
62
|
+
+ popoverLeave: popoverLeaveAnimation,
|
|
63
|
+
+ };
|
|
64
|
+
+ }
|
|
65
|
+
|
|
66
|
+
provideIonicAngular({
|
|
67
|
+
- navAnimation: isPlatform('ios') ? iosTransitionAnimation : undefined,
|
|
68
|
+
- popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined,
|
|
69
|
+
- popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined,
|
|
70
|
+
+ ...(isPlatform('ios') ? loadIOSAnimations() : {}),
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The iOS 27 page transition and popover animations serve both styled generations. Older browsers keep Ionic's defaults. The example uses Angular; pass the same options to React's `setupIonicReact` or Vue's `IonicVue`.
|
|
75
|
+
|
|
76
|
+
### 4. Update customizations
|
|
77
|
+
|
|
78
|
+
Rename theme variables and opt-out classes used by your app. For example:
|
|
79
|
+
|
|
80
|
+
```diff
|
|
81
|
+
ion-content {
|
|
82
|
+
- --ios26-content-box-shadow-rgb: 0, 0, 0;
|
|
83
|
+
+ --ios-theme-content-box-shadow-rgb: 0, 0, 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
- <ion-button class="ios26-disabled">Standard Ionic button</ion-button>
|
|
87
|
+
+ <ion-button class="ios-theme-disabled">Standard Ionic button</ion-button>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The old names remain as deprecated fallbacks. Check the resulting screens in light and dark modes on the browsers you support.
|
|
91
|
+
|
|
92
|
+
### iOS 27 only
|
|
93
|
+
|
|
94
|
+
To switch entirely to iOS 27, remove the iOS 26 package and replace its stylesheet and animation imports. The stylesheet changes are:
|
|
95
|
+
|
|
96
|
+
```diff
|
|
97
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/default-variables.scss';
|
|
98
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26.scss';
|
|
99
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/ionic-theme-ios26-dark-class.scss';
|
|
100
|
+
- @use '@rdlabo/ionic-theme-ios26/src/styles/md-remove-ios-class-effect.scss';
|
|
101
|
+
+ @use '@rdlabo/ionic-theme-ios27/src/styles/default-variables.scss';
|
|
102
|
+
+ @use '@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27.scss';
|
|
103
|
+
+ @use '@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class.scss';
|
|
104
|
+
+ @use '@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect.scss';
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Change the animation import from `@rdlabo/ionic-theme-ios26` to `@rdlabo/ionic-theme-ios27`; the existing `isPlatform('ios')` configuration can stay. See the README's [iOS 27-only setup](https://docs.rdlabo.dev/projects/ionic-theme-ios27#use-only-the-ios-27-theme). Unconditional imports apply the new styles on every browser using Ionic iOS mode.
|
|
108
|
+
|
|
7
109
|
## iOS 27 naming
|
|
8
110
|
|
|
9
|
-
|
|
111
|
+
For the iOS 27 branch or an iOS 27-only app, use `@rdlabo/ionic-theme-ios27`. Its stylesheets are `ionic-theme-ios27.scss` or `ionic-theme-ios27.css`, including the `-dark-always`, `-dark-system`, and `-dark-class` variants. Keep the iOS 26 stylesheet names in the iOS 26 branch of an adaptive setup.
|
|
10
112
|
|
|
11
113
|
Use the version-independent `--ios-theme-*` CSS variables. The corresponding `--ios26-*` variables remain supported as deprecated fallbacks. When both are set, the new name takes precedence. For example, use `--ios-theme-content-box-shadow-rgb` instead of `--ios26-content-box-shadow-rgb`.
|
|
12
114
|
|
|
@@ -22,7 +124,7 @@ Submit buttons now use each Ionic color's standard contrast value and an iOS 27
|
|
|
22
124
|
|
|
23
125
|
```diff
|
|
24
126
|
:root {
|
|
25
|
-
-
|
|
26
|
-
-
|
|
127
|
+
- --ion-color-primary-brightness-rgb: 130, 255, 255;
|
|
128
|
+
- --ion-color-primary-brightness: #96feff;
|
|
27
129
|
}
|
|
28
130
|
```
|
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
|
|
|
@@ -245,7 +247,17 @@ On iOS, [`Components`](../ios/Sources/IonicNativeUIShellPlugin/Components) owns
|
|
|
245
247
|
|
|
246
248
|
## Demo and verification
|
|
247
249
|
|
|
248
|
-
|
|
250
|
+
The demo includes a `native-ui-shell` page for exercising fixed controls. From the repository root, build the library and run the browser tests:
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
npm ci
|
|
254
|
+
npm run build
|
|
255
|
+
cd demo
|
|
256
|
+
npm ci
|
|
257
|
+
npx --no-install playwright install chromium
|
|
258
|
+
npx --no-install playwright test e2e/native-ui-shell.spec.ts e2e/native-ui-shell-edge.spec.ts
|
|
259
|
+
```
|
|
249
260
|
|
|
261
|
+
For native interaction and placement tests, use Xcode 26 or later, XcodeGen and a booted iOS 26+ Simulator. From the repository root, run `sh scripts/verify-native-ui-shell.sh SIMULATOR_UDID`. This also builds an independent Swift Package Manager consumer from the npm package. Run `sh scripts/verify-native-search.sh SIMULATOR_UDID` for searchable-tab integration, or add `edge` to that command for placement, navigation and keyboard edge cases. The scripts print the location of their local test artifacts.
|
|
250
262
|
|
|
251
263
|
Search controllers retain their UIKit-managed transition and are excluded from the ordinary control acquisition crossfade.
|
package/docs/special-markup.md
CHANGED
|
@@ -92,6 +92,15 @@ This is an introductory group. Place regular list items in a separate `ion-item-
|
|
|
92
92
|
|
|
93
93
|
## Full-width segments
|
|
94
94
|
|
|
95
|
+
Add `.segment-style-glass` to give a segment the same glass surface and selected indicator treatment as the tab bar. The class preserves the segment's existing dimensions and text colors, supports scrollable segments, and respects Ionic's public `--background` property.
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<ion-segment class="segment-style-glass" value="available">
|
|
99
|
+
<ion-segment-button value="available">Available</ion-segment-button>
|
|
100
|
+
<ion-segment-button value="away">Away</ion-segment-button>
|
|
101
|
+
</ion-segment>
|
|
102
|
+
```
|
|
103
|
+
|
|
95
104
|
For colored segments, use Ionic's `color` property (for example, `color="primary"` or `color="secondary"`). The selected surface uses the palette's base color and the selected label uses its contrast color. The optional moving glass inherits the same surface color; custom Ionic palettes work without additional registration.
|
|
96
105
|
|
|
97
106
|
Add `.segment-expand` when segment buttons should divide the available width evenly. The class also changes the Liquid Glass effect sizing when `registerSegmentEffect` is used.
|