@rdlabo/ionic-theme-ios27 0.1.0 → 0.2.1

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 (140) hide show
  1. package/README.md +10 -2
  2. package/dist/css/components/ion-button.css +1 -1
  3. package/dist/css/components/ion-fab.css +1 -1
  4. package/dist/css/components/ion-list.css +1 -1
  5. package/dist/css/components/ion-menu.css +1 -1
  6. package/dist/css/components/ion-searchbar.css +1 -1
  7. package/dist/css/components/ion-tabs.css +1 -1
  8. package/dist/css/components/ion-toolbar.css +1 -1
  9. package/dist/css/ionic-theme-ios27.css +1 -1
  10. package/dist/css/md-ion-list-inset.css +1 -1
  11. package/dist/css/utils/translucent.css +1 -1
  12. package/dist/index.d.ts +1 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +1 -2
  15. package/dist/native/definitions.d.ts +19 -0
  16. package/dist/native/definitions.d.ts.map +1 -1
  17. package/dist/native/index.d.ts +4 -3
  18. package/dist/native/index.d.ts.map +1 -1
  19. package/dist/native/index.js +22 -9
  20. package/dist/native/lifecycle.d.ts +4 -0
  21. package/dist/native/lifecycle.d.ts.map +1 -0
  22. package/dist/native/lifecycle.js +34 -0
  23. package/dist/native/runtime.d.ts +2 -2
  24. package/dist/native/runtime.d.ts.map +1 -1
  25. package/dist/native/runtime.js +43 -4
  26. package/dist/tab-bar-searchable/animations/enter.d.ts +1 -1
  27. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -1
  28. package/dist/tab-bar-searchable/animations/enter.js +1 -1
  29. package/dist/tab-bar-searchable/animations/leave.d.ts +1 -1
  30. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -1
  31. package/dist/tab-bar-searchable/animations/leave.js +1 -1
  32. package/dist/tab-bar-searchable/index.d.ts +2 -2
  33. package/dist/tab-bar-searchable/index.d.ts.map +1 -1
  34. package/dist/tab-bar-searchable/index.js +2 -3
  35. package/dist/transition/ios.transition.d.ts +6 -1
  36. package/dist/transition/ios.transition.d.ts.map +1 -1
  37. package/dist/transition/ios.transition.js +10 -385
  38. package/docs/features.md +3 -1
  39. package/docs/native-ui-shell.md +113 -0
  40. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +28 -0
  41. package/node_modules/@rdlabo/ionic-theme-utils/LICENSE +21 -0
  42. package/node_modules/@rdlabo/ionic-theme-utils/README.md +28 -0
  43. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts +3 -0
  44. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.d.ts.map +1 -0
  45. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js +13 -0
  46. package/node_modules/@rdlabo/ionic-theme-utils/dist/dom.js.map +1 -0
  47. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts +6 -0
  48. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.d.ts.map +1 -0
  49. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js +37 -0
  50. package/node_modules/@rdlabo/ionic-theme-utils/dist/focus-controller.js.map +1 -0
  51. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts +13 -0
  52. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.d.ts.map +1 -0
  53. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js +11 -0
  54. package/node_modules/@rdlabo/ionic-theme-utils/dist/index.js.map +1 -0
  55. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.enter.d.ts +0 -1
  56. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.d.ts.map +1 -0
  57. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.enter.js +8 -5
  58. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.enter.js.map +1 -0
  59. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.leave.d.ts.map +1 -1
  60. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.leave.js +11 -5
  61. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/animations/ios.leave.js.map +1 -0
  62. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/callout-surface.js +1 -0
  63. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/callout-surface.js.map +1 -0
  64. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js +2 -0
  65. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/popover-interface.js.map +1 -0
  66. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/utils.d.ts +1 -0
  67. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.d.ts.map +1 -0
  68. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/utils.js +3 -2
  69. package/node_modules/@rdlabo/ionic-theme-utils/dist/popover/utils.js.map +1 -0
  70. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts +16 -0
  71. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.d.ts.map +1 -0
  72. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js +2 -0
  73. package/node_modules/@rdlabo/ionic-theme-utils/dist/sheets-of-glass.js.map +1 -0
  74. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/interfaces.js +1 -0
  75. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/interfaces.js.map +1 -0
  76. package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/utils.js +1 -0
  77. package/node_modules/@rdlabo/ionic-theme-utils/dist/tab-bar-searchable/utils.js.map +1 -0
  78. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts +15 -0
  79. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.d.ts.map +1 -0
  80. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js +49 -0
  81. package/node_modules/@rdlabo/ionic-theme-utils/dist/theme.js.map +1 -0
  82. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts +18 -0
  83. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.d.ts.map +1 -0
  84. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js +412 -0
  85. package/node_modules/@rdlabo/ionic-theme-utils/dist/transition/ios.transition.js.map +1 -0
  86. package/node_modules/@rdlabo/ionic-theme-utils/package.json +64 -0
  87. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.spec.ts +19 -0
  88. package/node_modules/@rdlabo/ionic-theme-utils/src/dom.ts +16 -0
  89. package/node_modules/@rdlabo/ionic-theme-utils/src/focus-controller.ts +38 -0
  90. package/node_modules/@rdlabo/ionic-theme-utils/src/index.ts +59 -0
  91. package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/animations/ios.enter.ts +7 -5
  92. package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/animations/ios.leave.ts +14 -5
  93. package/node_modules/@rdlabo/ionic-theme-utils/src/popover/utils.spec.ts +58 -0
  94. package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/utils.ts +4 -2
  95. package/node_modules/@rdlabo/ionic-theme-utils/src/sheets-of-glass.ts +17 -0
  96. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/index.scss +1 -0
  97. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/md-ion-list-inset.scss +20 -0
  98. package/node_modules/@rdlabo/ionic-theme-utils/src/styles/structured-list.scss +310 -0
  99. package/node_modules/@rdlabo/ionic-theme-utils/src/tab-bar-searchable/utils.spec.ts +56 -0
  100. package/node_modules/@rdlabo/ionic-theme-utils/src/theme.ts +51 -0
  101. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.spec.ts +52 -0
  102. package/node_modules/@rdlabo/ionic-theme-utils/src/transition/ios.transition.ts +542 -0
  103. package/package.json +13 -5
  104. package/src/index.ts +4 -2
  105. package/src/native/definitions.ts +34 -0
  106. package/src/native/index.ts +38 -11
  107. package/src/native/lifecycle.ts +38 -0
  108. package/src/native/runtime.ts +49 -4
  109. package/src/styles/components/ion-button.scss +21 -10
  110. package/src/styles/components/ion-fab.scss +10 -4
  111. package/src/styles/components/ion-list.scss +12 -3
  112. package/src/styles/components/ion-menu.scss +17 -10
  113. package/src/styles/components/ion-searchbar.scss +1 -1
  114. package/src/styles/components/ion-tabs.scss +14 -10
  115. package/src/styles/components/ion-toolbar.scss +9 -10
  116. package/src/styles/md-ion-list-inset.scss +2 -2
  117. package/src/styles/utils/dark/ion-list.scss +1 -1
  118. package/src/styles/utils/translucent.scss +44 -0
  119. package/src/tab-bar-searchable/animations/enter.ts +1 -2
  120. package/src/tab-bar-searchable/animations/leave.ts +1 -2
  121. package/src/tab-bar-searchable/index.ts +12 -4
  122. package/src/transition/ios.transition.ts +14 -473
  123. package/dist/css/utils/structured-list.css +0 -1
  124. package/dist/popover/animations/ios.enter.d.ts.map +0 -1
  125. package/dist/popover/popover-interface.js +0 -1
  126. package/dist/popover/utils.d.ts.map +0 -1
  127. package/src/styles/utils/structured-list.scss +0 -223
  128. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/animations/ios.leave.d.ts +0 -0
  129. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/callout-surface.d.ts +0 -0
  130. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/callout-surface.d.ts.map +0 -0
  131. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/popover-interface.d.ts +0 -0
  132. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/popover/popover-interface.d.ts.map +0 -0
  133. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/interfaces.d.ts +0 -0
  134. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/interfaces.d.ts.map +0 -0
  135. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/utils.d.ts +0 -0
  136. /package/{dist → node_modules/@rdlabo/ionic-theme-utils/dist}/tab-bar-searchable/utils.d.ts.map +0 -0
  137. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/callout-surface.ts +0 -0
  138. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/popover/popover-interface.ts +0 -0
  139. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/tab-bar-searchable/interfaces.ts +0 -0
  140. /package/{src → node_modules/@rdlabo/ionic-theme-utils/src}/tab-bar-searchable/utils.ts +0 -0
@@ -1,17 +1,16 @@
1
1
  import { createAnimation } from '@ionic/core';
2
2
  import type { Animation } from '@ionic/core';
3
- import { getElementRoot } from '../../utils';
4
- import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition } from '../utils';
3
+ import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition, POPOVER_IOS_BODY_MARGIN } from '../utils';
5
4
  import { createCalloutSurface } from '../callout-surface';
5
+ import { getElementRoot } from '../../dom';
6
6
 
7
7
  const POPOVER_IOS_BODY_PADDING = 5;
8
- export const POPOVER_IOS_BODY_MARGIN = 8;
9
8
 
10
9
  /**
11
10
  * iOS Popover Enter Animation
12
11
  */
13
12
  // TODO(FW-2832): types
14
- export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => {
13
+ export const iosEnterAnimation = (baseEl: HTMLElement, opts: any = {}): Animation => {
15
14
  const { event: ev, size, trigger, reference, side, align } = opts;
16
15
  const doc = baseEl.ownerDocument as any;
17
16
  const isRTL = doc.dir === 'rtl';
@@ -49,6 +48,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
49
48
  const availableWidth = Math.max(0, paneRight - paneLeft - paneMargin * 2);
50
49
  if (pane && availableWidth > 0 && contentEl.getBoundingClientRect().width > availableWidth) {
51
50
  contentEl.dataset['previousMaxWidth'] = contentEl.style.maxWidth;
51
+ contentEl.dataset['previousMaxWidthPriority'] = contentEl.style.getPropertyPriority('max-width');
52
52
  contentEl.style.maxWidth = `${availableWidth}px`;
53
53
  }
54
54
  const { contentWidth, contentHeight } = getPopoverDimensions(size, contentEl, referenceSizeEl);
@@ -189,7 +189,7 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
189
189
  .duration(200)
190
190
  .addElement(referenceSizeEl)
191
191
  .beforeStyles({ 'transform-origin': `${originY} ${originX}` })
192
- .beforeAddClass('ios27-replace-element')
192
+ .beforeAddClass('ios-theme-replace-element')
193
193
  .fromTo('transform', 'scale(1)', 'scale(1.05)')
194
194
  .fromTo('opacity', 1, 0);
195
195
  }
@@ -200,6 +200,8 @@ export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation =>
200
200
  .duration(100)
201
201
  .beforeAddWrite(() => {
202
202
  if (size === 'cover') {
203
+ baseEl.dataset['iosThemePreviousWidth'] = baseEl.style.getPropertyValue('--width');
204
+ baseEl.dataset['iosThemePreviousWidthPriority'] = baseEl.style.getPropertyPriority('--width');
203
205
  baseEl.style.setProperty('--width', `${contentWidth}px`);
204
206
  }
205
207
 
@@ -1,5 +1,5 @@
1
1
  import { createAnimation } from '@ionic/core';
2
- import { getElementRoot } from '../../utils';
2
+ import { getElementRoot } from '../../dom';
3
3
 
4
4
  import type { Animation } from '@ionic/core';
5
5
 
@@ -23,7 +23,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
23
23
  .fromTo('transform', 'scale(1)', 'scale(0)');
24
24
 
25
25
  const doc = baseEl.ownerDocument as any;
26
- const replaceElement = doc.querySelector('.ios27-replace-element') as HTMLElement | null;
26
+ const replaceElement = doc.querySelector('.ios-theme-replace-element') as HTMLElement | null;
27
27
 
28
28
  if (replaceElement) {
29
29
  const ratio = contentEl.getBoundingClientRect().width / contentEl.getBoundingClientRect().height;
@@ -33,7 +33,7 @@ export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
33
33
  .addElement(replaceElement)
34
34
  .delay(100)
35
35
  .duration(300)
36
- .afterRemoveClass('ios27-replace-element')
36
+ .afterRemoveClass('ios-theme-replace-element')
37
37
  .fromTo('transform', `scale(${scale})`, 'scale(1)')
38
38
  .fromTo('opacity', 0, 0.9);
39
39
  }
@@ -63,7 +63,11 @@ export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
63
63
  return baseAnimation
64
64
  .easing('ease')
65
65
  .afterAddWrite(() => {
66
- baseEl.style.removeProperty('--width');
66
+ if (baseEl.dataset['iosThemePreviousWidth'] !== undefined) {
67
+ baseEl.style.setProperty('--width', baseEl.dataset['iosThemePreviousWidth'], baseEl.dataset['iosThemePreviousWidthPriority'] ?? '');
68
+ delete baseEl.dataset['iosThemePreviousWidth'];
69
+ delete baseEl.dataset['iosThemePreviousWidthPriority'];
70
+ }
67
71
  baseEl.classList.remove('popover-bottom');
68
72
  baseEl.classList.remove('ios-theme-callout');
69
73
  root.querySelectorAll('.ios-theme-callout-layer').forEach((layer) => layer.remove());
@@ -73,8 +77,13 @@ export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
73
77
  contentEl.style.removeProperty('bottom');
74
78
  contentEl.style.removeProperty('transform-origin');
75
79
  if (contentEl.dataset['previousMaxWidth'] !== undefined) {
76
- contentEl.style.maxWidth = contentEl.dataset['previousMaxWidth'];
80
+ contentEl.style.setProperty(
81
+ 'max-width',
82
+ contentEl.dataset['previousMaxWidth'],
83
+ contentEl.dataset['previousMaxWidthPriority'] ?? '',
84
+ );
77
85
  delete contentEl.dataset['previousMaxWidth'];
86
+ delete contentEl.dataset['previousMaxWidthPriority'];
78
87
  }
79
88
 
80
89
  if (arrowEl) {
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { calculateWindowAdjustment, getIndexOfItem, getNextItem, getPopoverPosition, getPrevItem } from './utils';
4
+
5
+ describe('popover utilities', () => {
6
+ it('navigates only relative to ion-item elements', () => {
7
+ const items = [document.createElement('ion-item'), document.createElement('ion-item')] as HTMLIonItemElement[];
8
+
9
+ expect(getIndexOfItem(items, items[0])).toBe(0);
10
+ expect(getIndexOfItem(items, document.createElement('div'))).toBe(-1);
11
+ expect(getNextItem(items, items[0])).toBe(items[1]);
12
+ expect(getPrevItem(items, items[1])).toBe(items[0]);
13
+ });
14
+
15
+ it('keeps a popover inside the left safe-area boundary', () => {
16
+ expect(calculateWindowAdjustment('bottom', 20, -10, 16, 390, 844, 280, 140, 8, 'center', 'top')).toMatchObject({
17
+ top: 28,
18
+ left: 16,
19
+ originX: 'left',
20
+ checkSafeAreaLeft: true,
21
+ checkSafeAreaRight: false,
22
+ });
23
+ });
24
+
25
+ it('keeps a popover inside the right safe-area boundary', () => {
26
+ expect(calculateWindowAdjustment('bottom', 20, 150, 16, 390, 844, 280, 140, 8, 'center', 'top')).toMatchObject({
27
+ top: 28,
28
+ left: 94,
29
+ originX: 'right',
30
+ checkSafeAreaLeft: false,
31
+ checkSafeAreaRight: true,
32
+ });
33
+ });
34
+
35
+ it.each([
36
+ { isRTL: false, side: 'start' as const, left: 60, originX: 'right' },
37
+ { isRTL: false, side: 'end' as const, left: 140, originX: 'left' },
38
+ { isRTL: true, side: 'start' as const, left: 140, originX: 'left' },
39
+ { isRTL: true, side: 'end' as const, left: 60, originX: 'right' },
40
+ ])('maps the logical $side side in RTL=$isRTL', ({ isRTL, side, left, originX }) => {
41
+ const trigger = document.createElement('button');
42
+ trigger.getBoundingClientRect = () => ({
43
+ top: 20,
44
+ left: 100,
45
+ width: 40,
46
+ height: 30,
47
+ right: 140,
48
+ bottom: 50,
49
+ x: 100,
50
+ y: 20,
51
+ toJSON: () => ({}),
52
+ });
53
+
54
+ expect(
55
+ getPopoverPosition(isRTL, 40, 20, 'trigger', side, 'start', { top: 0, left: 0, originX: 'left', originY: 'top' }, trigger),
56
+ ).toMatchObject({ left, originX });
57
+ });
58
+ });
@@ -1,7 +1,9 @@
1
- import { getElementRoot, raf } from '../utils';
1
+ import { getElementRoot, raf } from '../dom';
2
2
 
3
3
  import type { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from './popover-interface';
4
- import { POPOVER_IOS_BODY_MARGIN } from './animations/ios.enter';
4
+
5
+ /** Shared with theme popover enter animations (iOS body margin). */
6
+ export const POPOVER_IOS_BODY_MARGIN = 8;
5
7
 
6
8
  interface InteractionCallback {
7
9
  eventName: string;
@@ -0,0 +1,17 @@
1
+ export interface EffectScales {
2
+ small: string;
3
+ medium: string;
4
+ large: string;
5
+ xlarge: string;
6
+ }
7
+
8
+ export interface registeredEffect {
9
+ destroy: () => void;
10
+ }
11
+
12
+ export interface AnimationPosition {
13
+ minPositionX: number;
14
+ maxPositionX: number;
15
+ width: number;
16
+ positionY: number;
17
+ }
@@ -0,0 +1 @@
1
+ @forward 'structured-list';
@@ -0,0 +1,20 @@
1
+ @use 'structured-list';
2
+
3
+ @mixin theme($variant: classic, $disabled-selector: null) {
4
+ ion-list.md {
5
+ @include structured-list.support-text($disabled-selector: $disabled-selector);
6
+
7
+ &.list-inset {
8
+ @include structured-list.layout(8px, 16px, $variant: $variant, $disabled-selector: $disabled-selector);
9
+ :is(#{structured-list.$groups}) ion-item > ion-input[labelplacement='floating'] {
10
+ transition: transform 200ms ease;
11
+ &:not(.label-floating) {
12
+ transform: translateY(-7px);
13
+ }
14
+ &.label-floating {
15
+ transform: translateY(-4px);
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,310 @@
1
+ $groups: ion-item-group, ion-reorder-group, ion-accordion-group, ion-radio-group;
2
+
3
+ @function _disabled-suffix($disabled-selector) {
4
+ @if $disabled-selector {
5
+ @return ':not(#{$disabled-selector})';
6
+ }
7
+
8
+ @return '';
9
+ }
10
+
11
+ // Derive neutral list colors from the application's Ionic medium palette.
12
+ @mixin colors($supporting-opacity, $detail-opacity) {
13
+ ion-item {
14
+ --detail-icon-color: rgb(var(--ion-color-medium-rgb));
15
+ --detail-icon-opacity: #{$detail-opacity};
16
+ }
17
+
18
+ &.list-inset {
19
+ ion-list-header ion-label:not(.ion-color) {
20
+ color: rgba(var(--ion-color-medium-rgb), $supporting-opacity);
21
+ }
22
+
23
+ ion-list-header ion-note:not(.ion-color),
24
+ > ion-note:not(.ion-color) {
25
+ --color: rgba(var(--ion-color-medium-rgb), #{$supporting-opacity});
26
+ }
27
+ }
28
+ }
29
+
30
+ @mixin support-text($disabled-selector: null) {
31
+ $d: _disabled-suffix($disabled-selector);
32
+
33
+ ion-item#{$d} {
34
+ &:has(ion-input.input-fill-outline),
35
+ &:has(ion-textarea.textarea-fill-outline),
36
+ &:has(ion-select:is([helpertext]:not([helpertext='']), [errortext]:not([errortext='']))),
37
+ &:has(ion-input .input-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))),
38
+ &:has(ion-textarea .textarea-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))) {
39
+ --inner-border-width: 0;
40
+ --inner-padding-bottom: 4px;
41
+ }
42
+
43
+ ion-input#{$d} .input-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))),
44
+ ion-textarea#{$d} .textarea-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))) {
45
+ display: none;
46
+ }
47
+ }
48
+ }
49
+
50
+ @mixin layout($outer-note-inset, $group-border-radius, $variant: classic, $disabled-selector: null) {
51
+ $d: _disabled-suffix($disabled-selector);
52
+ $is-ios27: $variant == ios27;
53
+
54
+ background: transparent;
55
+
56
+ ion-list-header#{$d} {
57
+ display: flex;
58
+ align-items: center;
59
+
60
+ ion-label#{$d} {
61
+ @if $is-ios27 {
62
+ font-size: 1rem;
63
+ line-height: 1.29rem;
64
+ font-weight: 600;
65
+ } @else {
66
+ font-size: 0.9rem;
67
+ }
68
+ margin: 8px 4px 4px;
69
+
70
+ &:not(.ion-color) {
71
+ color: var(--ion-color-medium-tint);
72
+ }
73
+ }
74
+
75
+ ion-button#{$d} {
76
+ margin-top: 8px;
77
+ }
78
+
79
+ ion-note#{$d} {
80
+ @if $is-ios27 {
81
+ font-size: 0.76rem;
82
+ line-height: 1.06rem;
83
+ font-weight: 400;
84
+ margin-block: 8px 4px;
85
+ margin-inline: 0 16px;
86
+
87
+ &:not(.ion-color) {
88
+ --color: var(--ion-color-medium-tint);
89
+ }
90
+ } @else {
91
+ font-size: 0.9rem;
92
+ margin-block: 8px 4px;
93
+ margin-inline: 0 16px;
94
+ }
95
+ }
96
+ }
97
+
98
+ @if $is-ios27 {
99
+ ion-item-group.item-group-header#{$d} {
100
+ border-radius: $group-border-radius;
101
+
102
+ > ion-item#{$d} {
103
+ --inner-padding-end: 0;
104
+ --inner-border-width: 0;
105
+
106
+ &::part(native) {
107
+ padding-inline: 0;
108
+ }
109
+ }
110
+
111
+ & > ion-item#{$d} > ion-label#{$d} {
112
+ width: 100%;
113
+ margin: 0;
114
+ text-align: start;
115
+ white-space: normal;
116
+ padding: 16px 20px;
117
+
118
+ /**
119
+ * ion-header.header-collapse-condenseを使って、スクロールによってHeaderを変化させるトリック
120
+ */
121
+ ion-header.header-collapse-condense > ion-toolbar {
122
+ --min-height: 0;
123
+ visibility: hidden;
124
+ }
125
+
126
+ ion-icon#{$d} {
127
+ font-size: 2.82rem;
128
+ border-radius: 20%;
129
+ padding: 6px;
130
+ vertical-align: top;
131
+
132
+ &:not(.ion-color) {
133
+ color: #ffffff;
134
+ }
135
+ }
136
+
137
+ h2 {
138
+ font-size: 1.29rem;
139
+ font-weight: 700;
140
+ line-height: 1.53rem;
141
+ margin: 20px 0 8px;
142
+ }
143
+
144
+ ion-text#{$d} {
145
+ display: block;
146
+ font-size: 1rem;
147
+ line-height: 1.29rem;
148
+
149
+ &:not(.ion-color) {
150
+ color: var(--ion-color-step-500, var(--ion-text-color-step-500, #808080));
151
+ }
152
+ }
153
+ }
154
+ }
155
+ } @else {
156
+ ion-item-group.item-group-header#{$d} {
157
+ & > ion-item#{$d} > ion-label#{$d} {
158
+ width: 100%;
159
+ text-align: center;
160
+ padding: 32px 16px 16px;
161
+ transform: translateX(5px);
162
+
163
+ &:dir(rtl) {
164
+ transform: translateX(-5px);
165
+ }
166
+
167
+ /**
168
+ * ion-header.header-collapse-condenseを使って、スクロールによってHeaderを変化させるトリック
169
+ */
170
+ ion-header.header-collapse-condense > ion-toolbar {
171
+ --min-height: 0;
172
+ visibility: hidden;
173
+ }
174
+
175
+ ion-icon#{$d} {
176
+ font-size: 2.8rem;
177
+ border-radius: 20%;
178
+ padding: 6px;
179
+
180
+ &:not(.ion-color) {
181
+ color: #ffffff;
182
+ }
183
+ }
184
+
185
+ h2 {
186
+ font-size: 1.2rem;
187
+ font-weight: bold;
188
+ margin-top: 8px;
189
+ }
190
+
191
+ ion-text#{$d} {
192
+ font-size: 0.9rem;
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ > :is(#{$groups})#{$d} {
199
+ display: block;
200
+ border-radius: $group-border-radius;
201
+ overflow: hidden;
202
+
203
+ &:has(> .radio-group-top) {
204
+ display: flex;
205
+ flex-direction: column;
206
+ border-radius: 0;
207
+ overflow: visible;
208
+
209
+ > .radio-group-top {
210
+ order: 1;
211
+ font-size: 0.9rem;
212
+ padding-block-start: 8px;
213
+ }
214
+
215
+ > ion-item#{$d}:first-of-type {
216
+ --border-radius: #{$group-border-radius} #{$group-border-radius} 0 0;
217
+ }
218
+
219
+ > ion-item#{$d}:last-of-type {
220
+ --border-radius: 0 0 #{$group-border-radius} #{$group-border-radius};
221
+ }
222
+ }
223
+
224
+ &:not(:first-of-type) {
225
+ margin-top: 16px;
226
+ }
227
+
228
+ ion-item#{$d} {
229
+ &:has(> ion-label:not([slot])):has(> ion-note:not([slot])) {
230
+ &::part(container) {
231
+ flex-direction: column;
232
+ justify-content: center;
233
+ padding-top: 14px;
234
+ padding-bottom: 14px;
235
+ min-height: 64px;
236
+ }
237
+ & > ion-label#{$d},
238
+ & > ion-note#{$d} {
239
+ width: auto;
240
+ align-self: flex-start;
241
+ }
242
+ & > ion-label#{$d} {
243
+ margin: 0;
244
+ font-weight: 500;
245
+ @if $is-ios27 {
246
+ font-size: 1rem;
247
+ line-height: 1.29rem;
248
+ } @else {
249
+ font-size: 1.15rem;
250
+ line-height: 0.9rem;
251
+ }
252
+ }
253
+ & > ion-note#{$d} {
254
+ @if $is-ios27 {
255
+ font-size: 0.88rem;
256
+ line-height: 1.18rem;
257
+ } @else {
258
+ font-size: 0.85rem;
259
+ }
260
+ }
261
+ }
262
+
263
+ ion-text[slot='end']#{$d} {
264
+ padding-inline-start: 8px;
265
+ }
266
+
267
+ &.item-disabled {
268
+ --detail-icon-opacity: 0.1;
269
+ opacity: 1;
270
+
271
+ & > * {
272
+ opacity: 0.4;
273
+ }
274
+ }
275
+
276
+ ion-button[slot='end']#{$d} {
277
+ &.ion-align-self-end {
278
+ transform: translateY(-7px);
279
+ }
280
+ ion-icon[slot='icon-only']#{$d} {
281
+ font-size: 1.2rem;
282
+ transform: translateY(4px);
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ & > ion-note#{$d} {
289
+ margin-block: 8px;
290
+ margin-inline: calc(var(--ion-safe-area-left, 0) + $outer-note-inset) calc(var(--ion-safe-area-right, 0) + $outer-note-inset);
291
+
292
+ &:dir(rtl) {
293
+ margin-inline: calc(var(--ion-safe-area-right, 0) + $outer-note-inset) calc(var(--ion-safe-area-left, 0) + $outer-note-inset);
294
+ }
295
+
296
+ @if $is-ios27 {
297
+ font-size: 0.76rem;
298
+ line-height: 1.06rem;
299
+ font-weight: 400;
300
+ display: block;
301
+ &:not(.ion-color) {
302
+ --color: var(--ion-color-medium-tint);
303
+ }
304
+ } @else {
305
+ --color: var(--ion-color-medium-tint);
306
+ font-size: 0.9rem;
307
+ display: block;
308
+ }
309
+ }
310
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { getElement, getElementReferences, getElementSizes } from './utils';
4
+
5
+ const rect = (width: number, height: number, top = 0, left = 0) => ({ width, height, top, left }) as DOMRect;
6
+
7
+ describe('tab bar searchable utilities', () => {
8
+ it('reports a missing required element with its selector', () => {
9
+ expect(() => getElement(document.createElement('div'), '.missing')).toThrow('Expected click element to be inside `.missing`');
10
+ });
11
+
12
+ it('collects the elements used by the searchable animation', () => {
13
+ const tabBar = document.createElement('ion-tab-bar');
14
+ tabBar.innerHTML = '<ion-tab-button class="tab-selected"><ion-icon></ion-icon></ion-tab-button>';
15
+ const footer = document.createElement('ion-footer');
16
+ footer.innerHTML =
17
+ '<ion-searchbar><div class="searchbar-input-container"></div></ion-searchbar><ion-buttons slot="start"><ion-button><ion-icon></ion-icon></ion-button></ion-buttons>';
18
+
19
+ const references = getElementReferences(tabBar, footer);
20
+
21
+ expect(references.selectedTabButton).toBe(tabBar.querySelector('.tab-selected'));
22
+ expect(references.searchContainer).toBe(footer.querySelector('.searchbar-input-container'));
23
+ expect(references.closeButtonIcon.tagName).toBe('ION-ICON');
24
+ });
25
+
26
+ it('normalizes bounding rectangles to the shared size model', () => {
27
+ const tabBar = document.createElement('ion-tab-bar');
28
+ const fab = document.createElement('ion-fab-button');
29
+ const closeButtons = document.createElement('ion-buttons');
30
+ const searchContainer = document.createElement('div');
31
+ const selectedTabButtonIcon = document.createElement('ion-icon');
32
+ const selectedTabButton = document.createElement('ion-tab-button');
33
+ const closeButtonIcon = document.createElement('ion-icon');
34
+ vi.spyOn(tabBar, 'getBoundingClientRect').mockReturnValue(rect(390, 80));
35
+ vi.spyOn(fab, 'getBoundingClientRect').mockReturnValue(rect(56, 56));
36
+ vi.spyOn(closeButtons, 'getBoundingClientRect').mockReturnValue(rect(44, 44));
37
+ vi.spyOn(searchContainer, 'getBoundingClientRect').mockReturnValue(rect(270, 44));
38
+ vi.spyOn(selectedTabButtonIcon, 'getBoundingClientRect').mockReturnValue(rect(24, 24, 12, 18));
39
+
40
+ expect(
41
+ getElementSizes(tabBar, fab, {
42
+ closeButtons,
43
+ searchContainer,
44
+ selectedTabButton,
45
+ selectedTabButtonIcon,
46
+ closeButtonIcon,
47
+ }),
48
+ ).toEqual({
49
+ tabBar: { width: 390, height: 80 },
50
+ closeButton: { width: 44, height: 44 },
51
+ fabButton: { width: 56, height: 56 },
52
+ searchContainer: { width: 270, height: 44 },
53
+ selectedTabButtonIcon: { width: 24, height: 24, top: 12, left: 18 },
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,51 @@
1
+ import type { IonicConfig } from '@ionic/core';
2
+ import type { AnimationPosition } from './sheets-of-glass';
3
+
4
+ export const cloneElement = (tagName: string, useCache: boolean = true): HTMLElement => {
5
+ if (useCache) {
6
+ const cachedElement = document.querySelector(`${tagName}.ion-cloned-element`);
7
+ if (cachedElement !== null) return cachedElement as HTMLElement;
8
+ }
9
+ const clonedEl = document.createElement(tagName);
10
+ clonedEl.classList.add('ion-cloned-element');
11
+ clonedEl.style.setProperty('display', 'none');
12
+ document.body.appendChild(clonedEl);
13
+ return clonedEl;
14
+ };
15
+
16
+ export const getStep = (targetX: number, position: AnimationPosition) => {
17
+ const currentX = targetX - position.width / 2;
18
+ return Math.max(0, Math.min(1, (currentX - position.minPositionX) / (position.maxPositionX - position.minPositionX)));
19
+ };
20
+
21
+ export const changeSelectedElement = (target: HTMLElement, selected: HTMLElement, tagName: string, selectedClass: string): void => {
22
+ target.querySelectorAll(tagName).forEach((element) => {
23
+ element.classList.remove(selectedClass, 'ion-activated');
24
+ });
25
+ selected.classList.add('ion-activated');
26
+ };
27
+
28
+ export class Config {
29
+ private values = new Map<keyof IonicConfig, unknown>();
30
+ reset(config: IonicConfig) {
31
+ this.values = new Map(Object.entries(config) as [keyof IonicConfig, unknown][]);
32
+ }
33
+ get<T = unknown>(key: keyof IonicConfig, fallback?: T): T {
34
+ const value = this.values.get(key);
35
+ return (value !== undefined ? value : fallback) as T;
36
+ }
37
+ getBoolean(key: keyof IonicConfig, fallback = false): boolean {
38
+ const value = this.values.get(key);
39
+ if (value === undefined) return fallback;
40
+ return typeof value === 'string' ? value === 'true' : !!value;
41
+ }
42
+ getNumber(key: keyof IonicConfig, fallback?: number): number {
43
+ const value = Number.parseFloat(String(this.values.get(key)));
44
+ return Number.isNaN(value) ? (fallback ?? Number.NaN) : value;
45
+ }
46
+ set(key: keyof IonicConfig, value: unknown) {
47
+ this.values.set(key, value);
48
+ }
49
+ }
50
+
51
+ export const config = /* @__PURE__ */ new Config();
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { createIosTransitionAnimation } from './ios.transition';
4
+
5
+ const createPage = (translucent = false) => {
6
+ const page = document.createElement('div');
7
+ const header = document.createElement('ion-header');
8
+ if (translucent) {
9
+ header.classList.add('header-translucent');
10
+ }
11
+ page.append(header, document.createElement('ion-content'));
12
+ return page;
13
+ };
14
+
15
+ const createTransition = (radius?: number) => {
16
+ const nav = document.createElement('ion-router-outlet');
17
+ const enteringEl = createPage(true);
18
+ const leavingEl = createPage();
19
+ nav.append(leavingEl, enteringEl);
20
+ document.body.append(nav);
21
+
22
+ const animation = createIosTransitionAnimation({
23
+ offLeftPercent: 30,
24
+ radius,
25
+ getIonPageElement: (element) => element,
26
+ })(nav, { enteringEl, leavingEl });
27
+
28
+ return { animation, enteringEl, nav };
29
+ };
30
+
31
+ describe('createIosTransitionAnimation radius', () => {
32
+ it('does not apply a page clip when radius is omitted', () => {
33
+ const { animation, enteringEl, nav } = createTransition();
34
+
35
+ animation.progressStart(true);
36
+
37
+ expect(enteringEl.style.clipPath).toBe('');
38
+ animation.destroy();
39
+ nav.remove();
40
+ });
41
+
42
+ it('applies the configured radius', () => {
43
+ const { animation, enteringEl, nav } = createTransition(48);
44
+ enteringEl.style.clipPath = 'inset(1px)';
45
+
46
+ animation.progressStart(true);
47
+
48
+ expect(enteringEl.style.clipPath).toBe('inset(0 round 48px)');
49
+ animation.destroy();
50
+ nav.remove();
51
+ });
52
+ });