@rdlabo/ionic-theme-ios27 1.0.4 → 1.1.0-beta.pr224.shac63de393e27b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/dist/css/components/ion-toggle.css +1 -1
  2. package/dist/css/ionic-theme-ios27.css +1 -1
  3. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  4. package/dist/native/components/searchable-tabs.js +2 -0
  5. package/dist/native/runtime.d.ts.map +1 -1
  6. package/dist/native/runtime.js +73 -7
  7. package/dist/native/shared/crossfade.d.ts +2 -2
  8. package/dist/native/shared/crossfade.d.ts.map +1 -1
  9. package/dist/native/shared/crossfade.js +3 -3
  10. package/dist/segment/index.d.ts.map +1 -1
  11. package/dist/segment/index.js +7 -0
  12. package/docs/animation.md +2 -2
  13. package/docs/native-ui-shell.md +2 -0
  14. package/package.json +2 -5
  15. package/src/native/components/searchable-tabs.ts +3 -0
  16. package/src/native/runtime.ts +81 -6
  17. package/src/native/shared/crossfade.ts +4 -3
  18. package/src/segment/index.ts +11 -0
  19. package/src/styles/components/ion-toggle.scss +8 -2
  20. package/src/tab-bar-searchable/index.ts +1 -1
  21. package/node_modules/@rdlabo/ionic-theme-utils/LICENSE +0 -21
  22. package/node_modules/@rdlabo/ionic-theme-utils/README.md +0 -28
  23. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts +0 -3
  24. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts.map +0 -1
  25. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js +0 -13
  26. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js.map +0 -1
  27. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts +0 -6
  28. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts.map +0 -1
  29. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js +0 -37
  30. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js.map +0 -1
  31. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts +0 -13
  32. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts.map +0 -1
  33. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js +0 -11
  34. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js.map +0 -1
  35. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts +0 -3
  36. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts.map +0 -1
  37. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js +0 -196
  38. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js.map +0 -1
  39. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.d.ts +0 -3
  40. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.d.ts.map +0 -1
  41. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js +0 -80
  42. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js.map +0 -1
  43. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.d.ts +0 -2
  44. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.d.ts.map +0 -1
  45. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js +0 -45
  46. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js.map +0 -1
  47. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.d.ts +0 -38
  48. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.d.ts.map +0 -1
  49. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js +0 -2
  50. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js.map +0 -1
  51. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts +0 -38
  52. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts.map +0 -1
  53. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js +0 -378
  54. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js.map +0 -1
  55. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts +0 -16
  56. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts.map +0 -1
  57. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js +0 -2
  58. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js.map +0 -1
  59. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.d.ts +0 -41
  60. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.d.ts.map +0 -1
  61. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js +0 -6
  62. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js.map +0 -1
  63. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.d.ts +0 -12
  64. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.d.ts.map +0 -1
  65. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js +0 -61
  66. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js.map +0 -1
  67. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts +0 -15
  68. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts.map +0 -1
  69. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js +0 -49
  70. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js.map +0 -1
  71. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts +0 -18
  72. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts.map +0 -1
  73. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js +0 -412
  74. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js.map +0 -1
  75. package/node_modules/@rdlabo/ionic-theme-utils/package.json +0 -68
  76. package/node_modules/@rdlabo/ionic-theme-utils/scripts/copy-structured-list.mjs +0 -17
  77. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.spec.ts +0 -19
  78. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.ts +0 -16
  79. package/node_modules/@rdlabo/ionic-theme-utils/src/focus-controller.ts +0 -38
  80. package/node_modules/@rdlabo/ionic-theme-utils/src/index.ts +0 -59
  81. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/animations/ios.enter.ts +0 -259
  82. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/animations/ios.leave.ts +0 -99
  83. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/callout-surface.ts +0 -54
  84. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/popover-interface.ts +0 -44
  85. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.spec.ts +0 -58
  86. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.ts +0 -698
  87. package/node_modules/@rdlabo/ionic-theme-utils/src/sheets-of-glass.ts +0 -17
  88. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/index.scss +0 -1
  89. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/md-ion-list-inset.scss +0 -20
  90. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/structured-list.scss +0 -310
  91. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/interfaces.ts +0 -28
  92. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.spec.ts +0 -56
  93. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.ts +0 -75
  94. package/node_modules/@rdlabo/ionic-theme-utils/src/theme.ts +0 -51
  95. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.spec.ts +0 -52
  96. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.ts +0 -542
@@ -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;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"}
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;;mBA0U9B,mBAAmB,YAAY,MAAM;uBAKjC,mBAAmB,UAAU,OAAO;qBAUtC,WAAW;gBAGhB,MAAM,EAAE;iBASP,gBAAgB;oBAqCb,mBAAmB;;CAoBtC,CAAC"}
@@ -284,6 +284,8 @@ export const createSearchSupport = (doc, id, schedule) => {
284
284
  }
285
285
  field.label = '';
286
286
  field.accessibilityLabel = bar.getAttribute('aria-label') ?? input.getAttribute('aria-label') ?? 'Search';
287
+ if (!trigger.accessibilityLabel)
288
+ trigger.accessibilityLabel = field.accessibilityLabel;
287
289
  state.layout = JSON.stringify([
288
290
  innerWidth,
289
291
  innerHeight,
@@ -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,CAof7B,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/native/runtime.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EAEpB,MAAM,eAAe,CAAC;AAkBvB,eAAO,MAAM,aAAa,GACxB,KAAK,QAAQ,EACb,QAAQ,mBAAmB,EAC3B,UAAS,oBAAyB,KACjC,OAAO,CAAC,mBAAmB,CA+jB7B,CAAC"}
@@ -39,6 +39,11 @@ export const createRuntime = async (doc, plugin, options = {}) => {
39
39
  let dirty = false;
40
40
  let pending = false;
41
41
  let stopped = false;
42
+ let tabSwitchHandoff = false;
43
+ let handoffUntil = 0;
44
+ let handoffAcrossPending = false;
45
+ let handoffInstant = false;
46
+ let modalPresentHandoff = false;
42
47
  let frame = 0;
43
48
  let updates = 0;
44
49
  let reason;
@@ -75,7 +80,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
75
80
  search.release(element);
76
81
  element.removeAttribute(marker);
77
82
  if (!stopped)
78
- crossfade.play(element, false);
83
+ crossfade.play(element, false, handoffInstant);
79
84
  if (element.getAttribute('aria-hidden') === 'true') {
80
85
  const previous = sources.get(element);
81
86
  if (previous == null)
@@ -186,6 +191,8 @@ export const createRuntime = async (doc, plugin, options = {}) => {
186
191
  frame = 0;
187
192
  dirty = false;
188
193
  pending = true;
194
+ handoffInstant = tabSwitchHandoff || handoffAcrossPending || modalPresentHandoff || win.performance.now() < handoffUntil;
195
+ modalPresentHandoff = false;
189
196
  try {
190
197
  const size = `${win.innerWidth}:${win.innerHeight}`;
191
198
  if (viewport !== size) {
@@ -208,15 +215,17 @@ export const createRuntime = async (doc, plugin, options = {}) => {
208
215
  const removed = Array.from(sources.keys()).filter((element) => !retained.has(element));
209
216
  if (removed.length) {
210
217
  removed.forEach(restore);
211
- await painted();
212
- if (stopped || dirty)
213
- return;
218
+ if (!handoffInstant) {
219
+ await painted();
220
+ if (stopped || dirty)
221
+ return;
222
+ }
214
223
  }
215
224
  const data = { viewportWidth: win.innerWidth, controls: candidates.map((candidate) => candidate.control) };
216
225
  const serialized = JSON.stringify(data);
217
226
  if (serialized === lastSnapshot && !forceRefresh)
218
227
  return;
219
- const snapshot = { ...data, revision: ++revision, transitionDuration: crossfade.duration() };
228
+ const snapshot = { ...data, revision: ++revision, transitionDuration: crossfade.duration(handoffInstant) };
220
229
  forceRefresh = false;
221
230
  updates++;
222
231
  updatingRevision = snapshot.revision;
@@ -255,7 +264,7 @@ export const createRuntime = async (doc, plugin, options = {}) => {
255
264
  for (const element of accepted.flatMap(candidateSources)) {
256
265
  if (!sources.has(element)) {
257
266
  sources.set(element, element.getAttribute('aria-hidden'));
258
- crossfade.play(element, true);
267
+ crossfade.play(element, true, handoffInstant);
259
268
  element.setAttribute(marker, '');
260
269
  element.setAttribute('aria-hidden', 'true');
261
270
  element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
@@ -278,6 +287,10 @@ export const createRuntime = async (doc, plugin, options = {}) => {
278
287
  pendingActivations = [];
279
288
  if (forceRefresh)
280
289
  rejected = new WeakMap();
290
+ if (!stopped && (handoffAcrossPending || (handoffInstant && dirty))) {
291
+ handoffUntil = Math.max(handoffUntil, win.performance.now() + 400);
292
+ }
293
+ handoffAcrossPending = false;
281
294
  pending = false;
282
295
  if (dirty && !stopped)
283
296
  schedule();
@@ -301,12 +314,56 @@ export const createRuntime = async (doc, plugin, options = {}) => {
301
314
  getNativeSearchBindings(doc)
302
315
  .filter((binding) => page.contains(binding.footer))
303
316
  .forEach((binding) => search.retire(binding));
317
+ if (event.type === LIFECYCLE_WILL_LEAVE)
318
+ beginTabSwitchHandoffIfNeeded(page);
304
319
  pages.add(page);
305
320
  schedule();
306
321
  };
307
322
  const pageDid = (event) => {
308
323
  pages.delete(event.target);
309
324
  schedule();
325
+ if (tabSwitchHandoff && pages.size === 0)
326
+ endTabSwitchHandoff();
327
+ };
328
+ const tabOfPath = (tabs, pathname) => {
329
+ let best;
330
+ for (const button of Array.from(tabs.querySelectorAll('ion-tab-button'))) {
331
+ const tab = button.getAttribute('tab');
332
+ const href = button.getAttribute('href')?.split(/[?#]/)[0];
333
+ if (!tab || !href)
334
+ continue;
335
+ if (pathname === href || pathname.startsWith(`${href}/`)) {
336
+ if (!best || href.length > best.length)
337
+ best = { tab, length: href.length };
338
+ }
339
+ }
340
+ return best?.tab;
341
+ };
342
+ const armTabSwitchHandoff = () => {
343
+ tabSwitchHandoff = true;
344
+ handoffUntil = Math.max(handoffUntil, win.performance.now() + 400);
345
+ if (pending)
346
+ handoffAcrossPending = true;
347
+ };
348
+ const beginTabSwitchHandoffIfNeeded = (page) => {
349
+ const tabs = page.closest('ion-tabs');
350
+ if (!tabs)
351
+ return;
352
+ const bar = tabs.querySelector('ion-tab-bar');
353
+ const selected = (bar && 'selectedTab' in bar ? String(bar.selectedTab ?? '') : '') ||
354
+ tabs.querySelector('ion-tab-button.tab-selected')?.getAttribute('tab') ||
355
+ undefined;
356
+ const destination = tabOfPath(tabs, win.location.pathname);
357
+ if (selected && destination && selected !== destination)
358
+ armTabSwitchHandoff();
359
+ };
360
+ const endTabSwitchHandoff = () => {
361
+ win.requestAnimationFrame(() => {
362
+ if (stopped || !tabSwitchHandoff)
363
+ return;
364
+ tabSwitchHandoff = false;
365
+ schedule();
366
+ });
310
367
  };
311
368
  const motion = (event) => {
312
369
  const target = event.target;
@@ -331,9 +388,19 @@ export const createRuntime = async (doc, plugin, options = {}) => {
331
388
  on(doc, name, pageWill);
332
389
  for (const name of [LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE])
333
390
  on(doc, name, pageDid);
391
+ on(doc, 'ionTabsWillChange', () => {
392
+ armTabSwitchHandoff();
393
+ schedule();
394
+ });
395
+ on(doc, 'ionTabsDidChange', endTabSwitchHandoff);
334
396
  for (const name of overlayNames) {
335
397
  on(doc, `ion${name}WillPresent`, (event) => {
336
398
  presented.add(event.target);
399
+ if (name === 'Modal') {
400
+ modalPresentHandoff = true;
401
+ if (pending)
402
+ handoffInstant = true;
403
+ }
337
404
  schedule();
338
405
  });
339
406
  on(doc, `ion${name}DidDismiss`, (event) => {
@@ -352,7 +419,6 @@ export const createRuntime = async (doc, plugin, options = {}) => {
352
419
  for (const name of [
353
420
  'ionChange',
354
421
  'ionSelect',
355
- 'ionTabsDidChange',
356
422
  'ionImgDidLoad',
357
423
  CSS_MOTION_EVENTS.transitionEnd,
358
424
  CSS_MOTION_EVENTS.animationEnd,
@@ -1,7 +1,7 @@
1
1
  export declare const fadeMarker = "data-native-ui-shell-fading";
2
2
  export declare const createCrossfade: (win: Window) => {
3
- duration: () => 0 | 180;
4
- play(element: HTMLElement, toNative: boolean): void;
3
+ duration: (instant?: boolean) => 0 | 180;
4
+ play(element: HTMLElement, toNative: boolean, instant?: boolean): void;
5
5
  settled(): Promise<void>;
6
6
  destroy(): void;
7
7
  };
@@ -1 +1 @@
1
- {"version":3,"file":"crossfade.d.ts","sourceRoot":"","sources":["../../../src/native/shared/crossfade.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,gCAAgC,CAAC;AAGxD,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM;;kBAezB,WAAW,YAAY,OAAO;;;CA8B/C,CAAC"}
1
+ {"version":3,"file":"crossfade.d.ts","sourceRoot":"","sources":["../../../src/native/shared/crossfade.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,gCAAgC,CAAC;AAGxD,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM;;kBAezB,WAAW,YAAY,OAAO;;;CA+B/C,CAAC"}
@@ -15,14 +15,14 @@ export const createCrossfade = (win) => {
15
15
  };
16
16
  reduced.addEventListener('change', cancelAll);
17
17
  return {
18
- duration: () => (reduced.matches ? 0 : 180),
19
- play(element, toNative) {
18
+ duration: (instant = false) => (reduced.matches || instant ? 0 : 180),
19
+ play(element, toNative, instant = false) {
20
20
  const previous = active.get(element);
21
21
  const current = Number(win.getComputedStyle(element).opacity);
22
22
  previous?.cancel();
23
23
  active.delete(element);
24
24
  const opacity = Number(win.getComputedStyle(element).opacity);
25
- if (reduced.matches || !element.isConnected) {
25
+ if (reduced.matches || instant || !element.isConnected) {
26
26
  element.removeAttribute(fadeMarker);
27
27
  return;
28
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/segment/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA+CtE,eAAO,MAAM,qBAAqB,GAAI,eAAe,WAAW,KAAG,gBAAgB,GAAG,SAkUrF,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/segment/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AA+CtE,eAAO,MAAM,qBAAqB,GAAI,eAAe,WAAW,KAAG,gBAAgB,GAAG,SA6UrF,CAAC"}
@@ -102,6 +102,13 @@ export const registerSegmentEffect = (targetElement) => {
102
102
  animation?.cancel();
103
103
  handoff.forEach((effect) => effect.cancel());
104
104
  handoff = [];
105
+ const first = frames[0];
106
+ if (first) {
107
+ lens.style.transform = String(first['transform']);
108
+ lens.style.width = String(first['width']);
109
+ lens.style.height = String(first['height']);
110
+ lens.style.backgroundColor = String(first['backgroundColor']);
111
+ }
105
112
  segment.classList.add('ios27-animated');
106
113
  lens.hidden = false;
107
114
  const running = lens.animate(frames, { duration, easing: 'linear', fill: 'forwards' });
package/docs/animation.md CHANGED
@@ -78,7 +78,7 @@ Use the following structure inside `ion-tabs` to animate a search button into a
78
78
  ```html
79
79
  <ion-content>...</ion-content>
80
80
  <ion-fab vertical="bottom" horizontal="end" slot="fixed">
81
- <ion-fab-button (click)="present($event)">
81
+ <ion-fab-button aria-label="Search" (click)="present($event)">
82
82
  <ion-icon name="search"></ion-icon>
83
83
  </ion-fab-button>
84
84
  </ion-fab>
@@ -89,7 +89,7 @@ Use the following structure inside `ion-tabs` to animate a search button into a
89
89
  <ion-button fill="default"><ion-icon slot="icon-only"></ion-icon> </ion-button>
90
90
  </ion-buttons>
91
91
  <!-- User set `ionChange` or other events. -->
92
- <ion-searchbar (ionChange)="example($event)"></ion-searchbar>
92
+ <ion-searchbar aria-label="Search" (ionChange)="example($event)"></ion-searchbar>
93
93
  </ion-toolbar>
94
94
  </ion-footer>
95
95
  ```
@@ -118,6 +118,8 @@ Custom host animation or transition declarations on the FAB, list or button keep
118
118
 
119
119
  The connection in `src/transition/ios.transition.ts` waits for native retirement before starting the Web animation. Interactive progress and completion/cancellation are queued while that retirement is in progress. Stationary shared tabs are retained. First render and transitions without an animation builder are covered by the startup runtime and Ionic lifecycle events.
120
120
 
121
+ Tab switches skip the Web/native crossfade so a retiring UIKit snapshot cannot linger over the next tab. Detection uses the router URL versus the still-selected tab on `ionViewWillLeave` (and vanilla `ionTabsWillChange` / `ionTabsDidChange` DOM events). Stack pushes and pops keep the normal 180ms handoff.
122
+
121
123
  Standard Ionic overlays suspend native projection until dismissal. Unsupported searchable-tab configurations use the existing Web animation and share this ownership with Web glass gestures. CSS motion of supported containing surfaces also causes temporary Web rendering.
122
124
 
123
125
  During retirement, the source is restored and allowed to paint before its native cover is removed. During acquisition, the source is hidden only after a successful, current native response. Delayed responses are revalidated per control: existing eligible controls retain their native cover while content updates catch up. Only removed or ineligible sources return to Web; newly acquired sources require an exact acknowledgement. Ordinary page mutations never call the global clear operation. UIKit tab instances and items are retained, and equal frames/selections are not reapplied. Duplicate/stale activations are discarded. WebKit and UIKit still render separately: the implementation avoids an intentional blank frame, but does not provide an OS-level atomic compositing guarantee. Validate custom transitions and overlays on the app's supported simulators before rollout; unknown overlay systems are outside the automatic integration contract.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-theme-ios27",
3
3
  "private": false,
4
- "version": "1.0.4",
4
+ "version": "1.1.0-beta.pr224.shac63de393e27b",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -56,11 +56,8 @@
56
56
  },
57
57
  "homepage": "https://docs.rdlabo.dev/projects/ionic-theme-ios27",
58
58
  "dependencies": {
59
- "@rdlabo/ionic-theme-utils": "git+https://github.com/rdlabo-dev/ionic-theme-utils.git#e82be4389d9209750229f5fee990046b5b1217da"
59
+ "@rdlabo/ionic-theme-utils": "0.1.1"
60
60
  },
61
- "bundleDependencies": [
62
- "@rdlabo/ionic-theme-utils"
63
- ],
64
61
  "devDependencies": {
65
62
  "@capacitor/core": "^8.5.2",
66
63
  "@ionic/angular": "^9.0.0",
@@ -310,6 +310,9 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
310
310
  }
311
311
  field.label = '';
312
312
  field.accessibilityLabel = bar.getAttribute('aria-label') ?? input.getAttribute('aria-label') ?? 'Search';
313
+ // Icon-only search FABs often rely on the searchbar for their accessible
314
+ // name. Carry that semantic name into the independent resting trigger.
315
+ if (!trigger.accessibilityLabel) trigger.accessibilityLabel = field.accessibilityLabel;
313
316
  state.layout = JSON.stringify([
314
317
  innerWidth,
315
318
  innerHeight,
@@ -58,6 +58,16 @@ export const createRuntime = async (
58
58
  let dirty = false;
59
59
  let pending = false;
60
60
  let stopped = false;
61
+ /** True while a tab-switch handoff should skip crossfade. */
62
+ let tabSwitchHandoff = false;
63
+ /** Keep instant updates until this time — DidLeave often precedes the retire sync. */
64
+ let handoffUntil = 0;
65
+ /** Tab switch armed while a sync was already in flight; extend instant past that sync. */
66
+ let handoffAcrossPending = false;
67
+ /** Captured at the start of each sync so an in-flight update keeps a stable duration. */
68
+ let handoffInstant = false;
69
+ /** Retire native controls immediately as a modal starts covering the page. */
70
+ let modalPresentHandoff = false;
61
71
  let frame = 0;
62
72
  let updates = 0;
63
73
  let reason: string | undefined;
@@ -91,7 +101,7 @@ export const createRuntime = async (
91
101
  lastSnapshot = '';
92
102
  search.release(element);
93
103
  element.removeAttribute(marker);
94
- if (!stopped) crossfade.play(element, false);
104
+ if (!stopped) crossfade.play(element, false, handoffInstant);
95
105
  if (element.getAttribute('aria-hidden') === 'true') {
96
106
  const previous = sources.get(element);
97
107
  if (previous == null) element.removeAttribute('aria-hidden');
@@ -197,6 +207,8 @@ export const createRuntime = async (
197
207
  frame = 0;
198
208
  dirty = false;
199
209
  pending = true;
210
+ handoffInstant = tabSwitchHandoff || handoffAcrossPending || modalPresentHandoff || win.performance.now() < handoffUntil;
211
+ modalPresentHandoff = false;
200
212
  try {
201
213
  const size = `${win.innerWidth}:${win.innerHeight}`;
202
214
  // WebKit can resize before Ionic's fixed DOM positions catch up.
@@ -225,13 +237,17 @@ export const createRuntime = async (
225
237
  if (removed.length) {
226
238
  // Restore the source and let WebKit paint before removing its native cover.
227
239
  removed.forEach(restore);
228
- await painted();
229
- if (stopped || dirty) return;
240
+ // The outgoing tab is no longer visible, so waiting two frames only leaves its
241
+ // native snapshot over the destination. Stack transitions still need the paint.
242
+ if (!handoffInstant) {
243
+ await painted();
244
+ if (stopped || dirty) return;
245
+ }
230
246
  }
231
247
  const data = { viewportWidth: win.innerWidth, controls: candidates.map((candidate) => candidate.control) };
232
248
  const serialized = JSON.stringify(data);
233
249
  if (serialized === lastSnapshot && !forceRefresh) return;
234
- const snapshot: ShellSnapshot = { ...data, revision: ++revision, transitionDuration: crossfade.duration() };
250
+ const snapshot: ShellSnapshot = { ...data, revision: ++revision, transitionDuration: crossfade.duration(handoffInstant) };
235
251
  // A native visibility notification during this update must survive its ack.
236
252
  forceRefresh = false;
237
253
  updates++;
@@ -272,7 +288,7 @@ export const createRuntime = async (
272
288
  for (const element of accepted.flatMap(candidateSources)) {
273
289
  if (!sources.has(element)) {
274
290
  sources.set(element, element.getAttribute('aria-hidden'));
275
- crossfade.play(element, true);
291
+ crossfade.play(element, true, handoffInstant);
276
292
  element.setAttribute(marker, '');
277
293
  element.setAttribute('aria-hidden', 'true');
278
294
  element.dispatchEvent(new CustomEvent('nativeUIShellChange'));
@@ -294,6 +310,11 @@ export const createRuntime = async (
294
310
  pendingActivations = [];
295
311
  // A refresh received before the ack must also invalidate that ack's rejects.
296
312
  if (forceRefresh) rejected = new WeakMap();
313
+ // A tab switch mid-update (or a dirty follow-up) must keep instant through the retire sync.
314
+ if (!stopped && (handoffAcrossPending || (handoffInstant && dirty))) {
315
+ handoffUntil = Math.max(handoffUntil, win.performance.now() + 400);
316
+ }
317
+ handoffAcrossPending = false;
297
318
  pending = false;
298
319
  if (dirty && !stopped) schedule();
299
320
  else finishWaiters();
@@ -323,12 +344,55 @@ export const createRuntime = async (
323
344
  getNativeSearchBindings(doc)
324
345
  .filter((binding) => page.contains(binding.footer))
325
346
  .forEach((binding) => search.retire(binding));
347
+ // Angular updates the URL before willLeave, but keeps ion-tab-bar.selectedTab on the
348
+ // leaving tab until DidChange. A mismatch means this leave is a tab switch, not a stack push.
349
+ if (event.type === LIFECYCLE_WILL_LEAVE) beginTabSwitchHandoffIfNeeded(page);
326
350
  pages.add(page);
327
351
  schedule();
328
352
  };
329
353
  const pageDid: EventListener = (event) => {
330
354
  pages.delete(event.target as HTMLElement);
331
355
  schedule();
356
+ if (tabSwitchHandoff && pages.size === 0) endTabSwitchHandoff();
357
+ };
358
+ const tabOfPath = (tabs: Element, pathname: string) => {
359
+ let best: { tab: string; length: number } | undefined;
360
+ for (const button of Array.from(tabs.querySelectorAll('ion-tab-button'))) {
361
+ const tab = button.getAttribute('tab');
362
+ const href = button.getAttribute('href')?.split(/[?#]/)[0];
363
+ if (!tab || !href) continue;
364
+ if (pathname === href || pathname.startsWith(`${href}/`)) {
365
+ if (!best || href.length > best.length) best = { tab, length: href.length };
366
+ }
367
+ }
368
+ return best?.tab;
369
+ };
370
+ const armTabSwitchHandoff = () => {
371
+ tabSwitchHandoff = true;
372
+ // Retiring outgoing controls often lands in a later async sync than WillLeave/DidChange.
373
+ handoffUntil = Math.max(handoffUntil, win.performance.now() + 400);
374
+ // The in-flight sync already captured handoffInstant=false; hold instant for the next one.
375
+ if (pending) handoffAcrossPending = true;
376
+ };
377
+ const beginTabSwitchHandoffIfNeeded = (page: HTMLElement) => {
378
+ const tabs = page.closest('ion-tabs');
379
+ if (!tabs) return;
380
+ const bar = tabs.querySelector('ion-tab-bar');
381
+ const selected =
382
+ (bar && 'selectedTab' in bar ? String((bar as HTMLElement & { selectedTab?: string }).selectedTab ?? '') : '') ||
383
+ tabs.querySelector('ion-tab-button.tab-selected')?.getAttribute('tab') ||
384
+ undefined;
385
+ const destination = tabOfPath(tabs, win.location.pathname);
386
+ if (selected && destination && selected !== destination) armTabSwitchHandoff();
387
+ };
388
+ // DidChange / pageDid can land in the same turn as WillLeave. Clear the sticky flag on the
389
+ // next frame; handoffUntil / handoffAcrossPending still cover the async retire sync.
390
+ const endTabSwitchHandoff = () => {
391
+ win.requestAnimationFrame(() => {
392
+ if (stopped || !tabSwitchHandoff) return;
393
+ tabSwitchHandoff = false;
394
+ schedule();
395
+ });
332
396
  };
333
397
  const motion: EventListener = (event) => {
334
398
  const target = event.target as HTMLElement;
@@ -347,9 +411,21 @@ export const createRuntime = async (
347
411
  for (const name of Object.values(CSS_MOTION_EVENTS)) on(doc, name, motion);
348
412
  for (const name of [LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE]) on(doc, name, pageWill);
349
413
  for (const name of [LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE]) on(doc, name, pageDid);
414
+ on(doc, 'ionTabsWillChange', () => {
415
+ // Vanilla ion-tabs dispatches DOM events; @ionic/angular uses EventEmitters instead.
416
+ armTabSwitchHandoff();
417
+ schedule();
418
+ });
419
+ on(doc, 'ionTabsDidChange', endTabSwitchHandoff);
350
420
  for (const name of overlayNames) {
351
421
  on(doc, `ion${name}WillPresent`, (event) => {
352
422
  presented.add(event.target as HTMLElement);
423
+ if (name === 'Modal') {
424
+ modalPresentHandoff = true;
425
+ // WillPresent may arrive while another bridge update is awaiting its ack.
426
+ // Make that retirement instant too instead of racing the modal animation.
427
+ if (pending) handoffInstant = true;
428
+ }
353
429
  schedule();
354
430
  });
355
431
  on(doc, `ion${name}DidDismiss`, (event) => {
@@ -368,7 +444,6 @@ export const createRuntime = async (
368
444
  for (const name of [
369
445
  'ionChange',
370
446
  'ionSelect',
371
- 'ionTabsDidChange',
372
447
  'ionImgDidLoad',
373
448
  CSS_MOTION_EVENTS.transitionEnd,
374
449
  CSS_MOTION_EVENTS.animationEnd,
@@ -15,14 +15,15 @@ export const createCrossfade = (win: Window) => {
15
15
  };
16
16
  reduced.addEventListener('change', cancelAll);
17
17
  return {
18
- duration: () => (reduced.matches ? 0 : 180),
19
- play(element: HTMLElement, toNative: boolean) {
18
+ duration: (instant = false) => (reduced.matches || instant ? 0 : 180),
19
+ play(element: HTMLElement, toNative: boolean, instant = false) {
20
20
  const previous = active.get(element);
21
21
  const current = Number(win.getComputedStyle(element).opacity);
22
22
  previous?.cancel();
23
23
  active.delete(element);
24
24
  const opacity = Number(win.getComputedStyle(element).opacity);
25
- if (reduced.matches || !element.isConnected) {
25
+ // Tab switches skip the opacity handoff so a retiring native snapshot cannot linger.
26
+ if (reduced.matches || instant || !element.isConnected) {
26
27
  element.removeAttribute(fadeMarker);
27
28
  return;
28
29
  }
@@ -119,6 +119,17 @@ export const registerSegmentEffect = (targetElement: HTMLElement): registeredEff
119
119
  animation?.cancel();
120
120
  handoff.forEach((effect) => effect.cancel());
121
121
  handoff = [];
122
+ // WebKit can paint the newly unhidden element once at its underlying
123
+ // top-left position before the replacement WAAPI effect is sampled.
124
+ // Prime that underlying style so interrupted/reversed selections always
125
+ // hand off from the first frame instead of briefly flashing at (0, 0).
126
+ const first = frames[0];
127
+ if (first) {
128
+ lens.style.transform = String(first['transform']);
129
+ lens.style.width = String(first['width']);
130
+ lens.style.height = String(first['height']);
131
+ lens.style.backgroundColor = String(first['backgroundColor']);
132
+ }
122
133
  segment.classList.add('ios27-animated');
123
134
  lens.hidden = false;
124
135
  const running = lens.animate(frames, { duration, easing: 'linear', fill: 'forwards' });
@@ -5,7 +5,10 @@ ion-toggle.ios:not(.ios-theme-disabled, .ios26-disabled) {
5
5
  --handle-max-width: none;
6
6
  --handle-max-height: none;
7
7
  --border-radius: 14px;
8
- --track-background-checked: var(--ion-color-base, #34c759);
8
+ --track-background-checked: var(--ion-color-primary, #3880ff);
9
+ &.ion-color {
10
+ --track-background-checked: var(--ion-color-base);
11
+ }
9
12
  &::part(track) {
10
13
  width: 63px;
11
14
  height: 28px;
@@ -48,7 +51,10 @@ ion-toggle.ios:not(.ios-theme-disabled, .ios26-disabled) {
48
51
  &:not(.ion-color) {
49
52
  --track-background: rgba(var(--ion-color-contrast-rgb, var(--ion-text-color-rgb, 0, 0, 0)), 0.23);
50
53
  }
51
- --track-background-checked: var(--ion-color-base, #34c759);
54
+ --track-background-checked: var(--ion-color-primary, #3880ff);
55
+ &.ion-color {
56
+ --track-background-checked: var(--ion-color-base);
57
+ }
52
58
  // Measured from the iOS 27 UISwitch presentation layer (37 -> 58pt).
53
59
  --ios27-toggle-release: linear(
54
60
  0,
@@ -48,7 +48,7 @@ export {
48
48
  * </ion-button>
49
49
  * </ion-buttons>
50
50
  * <!-- User set `ionChange` or other events. -->
51
- * <ion-searchbar (ionChange)="example($event)"></ion-searchbar>
51
+ * <ion-searchbar aria-label="Search" (ionChange)="example($event)"></ion-searchbar>
52
52
  * </ion-toolbar>
53
53
  * </ion-footer>
54
54
  **/
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Masahiko Sakakibara
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
@@ -1,28 +0,0 @@
1
- # @rdlabo/ionic-theme-utils
2
-
3
- Shared, theme-neutral utilities for Rdlabo Ionic themes.
4
-
5
- TypeScript and Sass sources are exported directly. Consumers are expected to
6
- compile them as part of an Angular/Ionic application build.
7
-
8
- ```typescript
9
- import { createIosTransitionAnimation, getPopoverPosition } from '@rdlabo/ionic-theme-utils';
10
- ```
11
-
12
- ```scss
13
- @use 'pkg:@rdlabo/ionic-theme-utils/styles/structured-list';
14
- ```
15
-
16
- The package currently contains shared iOS transition construction, popover
17
- positioning, searchable tab-bar utilities, and structured-list Sass mixins.
18
-
19
- ## Development
20
-
21
- ```shell
22
- npm install
23
- npm run build
24
- npm run lint
25
- ```
26
-
27
- Use `npm link` while developing this package together with a theme repository.
28
- Theme repositories should use a tagged Git dependency for reproducible installs.
@@ -1,3 +0,0 @@
1
- export declare const getElementRoot: (el: HTMLElement, fallback?: HTMLElement) => HTMLElement | ShadowRoot;
2
- export declare const raf: (h: FrameRequestCallback) => any;
3
- //# sourceMappingURL=dom.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,cAAc,GAAI,IAAI,WAAW,EAAE,WAAU,WAAgB,6BAEzE,CAAC;AAEF,eAAO,MAAM,GAAG,GAAI,GAAG,oBAAoB,QAQ1C,CAAC"}
@@ -1,13 +0,0 @@
1
- export const getElementRoot = (el, fallback = el) => {
2
- return el.shadowRoot || fallback;
3
- };
4
- export const raf = (h) => {
5
- if (typeof __zone_symbol__requestAnimationFrame === 'function') {
6
- return __zone_symbol__requestAnimationFrame(h);
7
- }
8
- if (typeof requestAnimationFrame === 'function') {
9
- return requestAnimationFrame(h);
10
- }
11
- return setTimeout(h);
12
- };
13
- //# sourceMappingURL=dom.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"dom.js","sourceRoot":"","sources":["../src/dom.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,EAAe,EAAE,WAAwB,EAAE,EAAE,EAAE;IAC5E,OAAO,EAAE,CAAC,UAAU,IAAI,QAAQ,CAAC;AACnC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,GAAG,GAAG,CAAC,CAAuB,EAAE,EAAE;IAC7C,IAAI,OAAO,oCAAoC,KAAK,UAAU,EAAE,CAAC;QAC/D,OAAO,oCAAoC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,OAAO,qBAAqB,KAAK,UAAU,EAAE,CAAC;QAChD,OAAO,qBAAqB,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;AACvB,CAAC,CAAC"}
@@ -1,6 +0,0 @@
1
- export interface FocusController {
2
- saveViewFocus(referenceEl?: HTMLElement): void;
3
- setViewFocus(referenceEl: HTMLElement): void;
4
- }
5
- export declare const createFocusController: () => FocusController;
6
- //# sourceMappingURL=focus-controller.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"focus-controller.d.ts","sourceRoot":"","sources":["../src/focus-controller.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,eAAe;IAC9B,aAAa,CAAC,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,YAAY,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAAC;CAC9C;AAED,eAAO,MAAM,qBAAqB,QAAO,eAuBvC,CAAC"}