@rdlabo/ionic-theme-ios27 0.1.0-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/css/components/ion-action-sheet.css +1 -0
  4. package/dist/css/components/ion-alert.css +1 -0
  5. package/dist/css/components/ion-button.css +1 -0
  6. package/dist/css/components/ion-card.css +1 -0
  7. package/dist/css/components/ion-content.css +1 -0
  8. package/dist/css/components/ion-datetime.css +1 -0
  9. package/dist/css/components/ion-fab.css +1 -0
  10. package/dist/css/components/ion-inputs.css +1 -0
  11. package/dist/css/components/ion-list.css +1 -0
  12. package/dist/css/components/ion-loading.css +1 -0
  13. package/dist/css/components/ion-menu.css +1 -0
  14. package/dist/css/components/ion-modal.css +1 -0
  15. package/dist/css/components/ion-picker.css +1 -0
  16. package/dist/css/components/ion-popover.css +1 -0
  17. package/dist/css/components/ion-range.css +1 -0
  18. package/dist/css/components/ion-searchbar.css +1 -0
  19. package/dist/css/components/ion-segment.css +1 -0
  20. package/dist/css/components/ion-tabs.css +1 -0
  21. package/dist/css/components/ion-toast.css +1 -0
  22. package/dist/css/components/ion-toggle.css +1 -0
  23. package/dist/css/components/ion-toolbar.css +1 -0
  24. package/dist/css/default-variables.css +1 -0
  25. package/dist/css/ionic-theme-ios26-dark-always.css +1 -0
  26. package/dist/css/ionic-theme-ios26-dark-class.css +1 -0
  27. package/dist/css/ionic-theme-ios26-dark-system.css +1 -0
  28. package/dist/css/ionic-theme-ios26.css +1 -0
  29. package/dist/css/md-ion-list-inset.css +1 -0
  30. package/dist/css/md-remove-ios-class-effect.css +1 -0
  31. package/dist/css/utils/api.css +1 -0
  32. package/dist/css/utils/dark/ion-button.css +1 -0
  33. package/dist/css/utils/dark/ion-fab.css +1 -0
  34. package/dist/css/utils/dark/ion-segment.css +1 -0
  35. package/dist/css/utils/dark/ion-tabs.css +1 -0
  36. package/dist/css/utils/searchable.css +1 -0
  37. package/dist/css/utils/structured-list.css +1 -0
  38. package/dist/css/utils/theme-dark.css +1 -0
  39. package/dist/css/utils/translucent.css +1 -0
  40. package/dist/docs.json +234 -0
  41. package/dist/focus-controller/index.d.ts +6 -0
  42. package/dist/focus-controller/index.d.ts.map +1 -0
  43. package/dist/focus-controller/index.js +62 -0
  44. package/dist/focus-controller /index.d.ts +6 -0
  45. package/dist/focus-controller /index.d.ts.map +1 -0
  46. package/dist/focus-controller /index.js +62 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +30 -0
  50. package/dist/popover/animations/ios.enter.d.ts +4 -0
  51. package/dist/popover/animations/ios.enter.d.ts.map +1 -0
  52. package/dist/popover/animations/ios.enter.js +97 -0
  53. package/dist/popover/animations/ios.leave.d.ts +3 -0
  54. package/dist/popover/animations/ios.leave.d.ts.map +1 -0
  55. package/dist/popover/animations/ios.leave.js +60 -0
  56. package/dist/popover/popover-interface.d.ts +38 -0
  57. package/dist/popover/popover-interface.d.ts.map +1 -0
  58. package/dist/popover/popover-interface.js +1 -0
  59. package/dist/popover/utils.d.ts +37 -0
  60. package/dist/popover/utils.d.ts.map +1 -0
  61. package/dist/popover/utils.js +377 -0
  62. package/dist/sheets-of-glass/animations.d.ts +8 -0
  63. package/dist/sheets-of-glass/animations.d.ts.map +1 -0
  64. package/dist/sheets-of-glass/animations.js +97 -0
  65. package/dist/sheets-of-glass/index.d.ts +3 -0
  66. package/dist/sheets-of-glass/index.d.ts.map +1 -0
  67. package/dist/sheets-of-glass/index.js +174 -0
  68. package/dist/sheets-of-glass/interfaces.d.ts +16 -0
  69. package/dist/sheets-of-glass/interfaces.d.ts.map +1 -0
  70. package/dist/sheets-of-glass/interfaces.js +1 -0
  71. package/dist/tab-bar-searchable/animations/enter.d.ts +8 -0
  72. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -0
  73. package/dist/tab-bar-searchable/animations/enter.js +73 -0
  74. package/dist/tab-bar-searchable/animations/leave.d.ts +8 -0
  75. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -0
  76. package/dist/tab-bar-searchable/animations/leave.js +66 -0
  77. package/dist/tab-bar-searchable/index.d.ts +4 -0
  78. package/dist/tab-bar-searchable/index.d.ts.map +1 -0
  79. package/dist/tab-bar-searchable/index.js +75 -0
  80. package/dist/tab-bar-searchable/interfaces.d.ts +41 -0
  81. package/dist/tab-bar-searchable/interfaces.d.ts.map +1 -0
  82. package/dist/tab-bar-searchable/interfaces.js +5 -0
  83. package/dist/tab-bar-searchable/utils.d.ts +12 -0
  84. package/dist/tab-bar-searchable/utils.d.ts.map +1 -0
  85. package/dist/tab-bar-searchable/utils.js +60 -0
  86. package/dist/transition/index.d.ts +19 -0
  87. package/dist/transition/index.d.ts.map +1 -0
  88. package/dist/transition/index.js +219 -0
  89. package/dist/transition/ios.transition.d.ts +5 -0
  90. package/dist/transition/ios.transition.d.ts.map +1 -0
  91. package/dist/transition/ios.transition.js +609 -0
  92. package/dist/utils.d.ts +17 -0
  93. package/dist/utils.d.ts.map +1 -0
  94. package/dist/utils.js +69 -0
  95. package/docs/e2e-testing.md +51 -0
  96. package/docs/eslint.md +73 -0
  97. package/docs/experimental-animation.md +85 -0
  98. package/docs/features.md +88 -0
  99. package/docs/feedback.md +65 -0
  100. package/docs/ios-18.md +16 -0
  101. package/docs/migration.md +94 -0
  102. package/docs/special-markup.md +145 -0
  103. package/docs/using-ion-item-group.md +47 -0
  104. package/package.json +63 -0
  105. package/src/focus-controller/index.ts +123 -0
  106. package/src/index.ts +33 -0
  107. package/src/popover/animations/ios.enter.ts +144 -0
  108. package/src/popover/animations/ios.leave.ts +76 -0
  109. package/src/popover/popover-interface.ts +44 -0
  110. package/src/popover/utils.ts +696 -0
  111. package/src/sheets-of-glass/animations.ts +120 -0
  112. package/src/sheets-of-glass/index.ts +214 -0
  113. package/src/sheets-of-glass/interfaces.ts +17 -0
  114. package/src/styles/components/ion-action-sheet.scss +85 -0
  115. package/src/styles/components/ion-alert.scss +66 -0
  116. package/src/styles/components/ion-button.scss +443 -0
  117. package/src/styles/components/ion-card.scss +7 -0
  118. package/src/styles/components/ion-content.scss +22 -0
  119. package/src/styles/components/ion-datetime.scss +9 -0
  120. package/src/styles/components/ion-fab.scss +104 -0
  121. package/src/styles/components/ion-inputs.scss +82 -0
  122. package/src/styles/components/ion-list.scss +44 -0
  123. package/src/styles/components/ion-loading.scss +11 -0
  124. package/src/styles/components/ion-menu.scss +110 -0
  125. package/src/styles/components/ion-modal.scss +42 -0
  126. package/src/styles/components/ion-picker.scss +4 -0
  127. package/src/styles/components/ion-popover.scss +33 -0
  128. package/src/styles/components/ion-range.scss +90 -0
  129. package/src/styles/components/ion-searchbar.scss +68 -0
  130. package/src/styles/components/ion-segment.scss +150 -0
  131. package/src/styles/components/ion-tabs.scss +118 -0
  132. package/src/styles/components/ion-toast.scss +35 -0
  133. package/src/styles/components/ion-toggle.scss +65 -0
  134. package/src/styles/components/ion-toolbar.scss +76 -0
  135. package/src/styles/default-variables.scss +38 -0
  136. package/src/styles/ionic-theme-ios26-dark-always.scss +6 -0
  137. package/src/styles/ionic-theme-ios26-dark-class.scss +6 -0
  138. package/src/styles/ionic-theme-ios26-dark-system.scss +8 -0
  139. package/src/styles/ionic-theme-ios26.scss +23 -0
  140. package/src/styles/md-ion-list-inset.scss +22 -0
  141. package/src/styles/md-remove-ios-class-effect.scss +14 -0
  142. package/src/styles/utils/api.scss +146 -0
  143. package/src/styles/utils/dark/ion-button.scss +58 -0
  144. package/src/styles/utils/dark/ion-fab.scss +18 -0
  145. package/src/styles/utils/dark/ion-segment.scss +12 -0
  146. package/src/styles/utils/dark/ion-tabs.scss +21 -0
  147. package/src/styles/utils/searchable.scss +0 -0
  148. package/src/styles/utils/structured-list.scss +170 -0
  149. package/src/styles/utils/theme-dark.scss +25 -0
  150. package/src/styles/utils/translucent.scss +95 -0
  151. package/src/tab-bar-searchable/animations/enter.ts +92 -0
  152. package/src/tab-bar-searchable/animations/leave.ts +89 -0
  153. package/src/tab-bar-searchable/index.ts +141 -0
  154. package/src/tab-bar-searchable/interfaces.ts +28 -0
  155. package/src/tab-bar-searchable/utils.ts +75 -0
  156. package/src/transition/index.ts +369 -0
  157. package/src/transition/ios.transition.ts +966 -0
  158. package/src/utils.ts +91 -0
@@ -0,0 +1,170 @@
1
+ $groups: ion-item-group, ion-reorder-group, ion-accordion-group, ion-radio-group;
2
+
3
+ @mixin support-text() {
4
+ ion-item {
5
+ &:has(ion-input.input-fill-outline),
6
+ &:has(ion-textarea.textarea-fill-outline),
7
+ &:has(ion-select:is([helpertext]:not([helpertext='']), [errortext]:not([errortext='']))),
8
+ &:has(ion-input .input-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))),
9
+ &:has(ion-textarea .textarea-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))) {
10
+ --inner-border-width: 0;
11
+ --inner-padding-bottom: 4px;
12
+ }
13
+
14
+ ion-input .input-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))),
15
+ ion-textarea .textarea-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))) {
16
+ display: none;
17
+ }
18
+ }
19
+ }
20
+
21
+ @mixin layout($outer-note-inset, $group-border-radius) {
22
+ background: transparent;
23
+
24
+ ion-list-header {
25
+ display: flex;
26
+ align-items: center;
27
+
28
+ ion-label {
29
+ font-size: 0.9rem;
30
+ margin: 8px 4px 4px;
31
+
32
+ &:not(.ion-color) {
33
+ color: var(--ion-color-medium-tint);
34
+ }
35
+ }
36
+
37
+ ion-button {
38
+ margin-top: 8px;
39
+ }
40
+
41
+ ion-note {
42
+ font-size: 0.9rem;
43
+ margin: 8px 16px 4px 0;
44
+ }
45
+ }
46
+
47
+ ion-item-group.item-group-header {
48
+ & > ion-item > ion-label {
49
+ width: 100%;
50
+ text-align: center;
51
+ padding: 32px 16px 16px;
52
+ transform: translateX(5px);
53
+
54
+ /**
55
+ * ion-header.header-collapse-condenseを使って、スクロールによってHeaderを変化させるトリック
56
+ */
57
+ ion-header.header-collapse-condense > ion-toolbar {
58
+ --min-height: 0;
59
+ visibility: hidden;
60
+ }
61
+
62
+ ion-icon {
63
+ font-size: 2.8rem;
64
+ border-radius: 20%;
65
+ padding: 6px;
66
+
67
+ &:not(.ion-color) {
68
+ color: #ffffff;
69
+ }
70
+ }
71
+
72
+ h2 {
73
+ font-size: 1.2rem;
74
+ font-weight: bold;
75
+ margin-top: 8px;
76
+ }
77
+
78
+ ion-text {
79
+ font-size: 0.9rem;
80
+ }
81
+ }
82
+ }
83
+
84
+ > :is(#{$groups}) {
85
+ display: block;
86
+ border-radius: $group-border-radius;
87
+ overflow: hidden;
88
+
89
+ &:has(> .radio-group-top) {
90
+ display: flex;
91
+ flex-direction: column;
92
+ border-radius: 0;
93
+ overflow: visible;
94
+
95
+ > .radio-group-top {
96
+ order: 1;
97
+ font-size: 0.9rem;
98
+ padding-block-start: 8px;
99
+ }
100
+
101
+ > ion-item:first-of-type {
102
+ --border-radius: #{$group-border-radius} #{$group-border-radius} 0 0;
103
+ }
104
+
105
+ > ion-item:last-of-type {
106
+ --border-radius: 0 0 #{$group-border-radius} #{$group-border-radius};
107
+ }
108
+ }
109
+
110
+ &:not(:first-of-type) {
111
+ margin-top: 16px;
112
+ }
113
+
114
+ ion-item {
115
+ &:has(> ion-label:not([slot])):has(> ion-note:not([slot])) {
116
+ &::part(container) {
117
+ flex-direction: column;
118
+ justify-content: center;
119
+ padding-top: 14px;
120
+ padding-bottom: 14px;
121
+ min-height: 64px;
122
+ }
123
+ & > ion-label,
124
+ & > ion-note {
125
+ width: auto;
126
+ align-self: flex-start;
127
+ }
128
+ & > ion-label {
129
+ margin: 0;
130
+ font-weight: 500;
131
+ font-size: 1.15rem;
132
+ line-height: 0.9rem;
133
+ }
134
+ & > ion-note {
135
+ font-size: 0.85rem;
136
+ }
137
+ }
138
+
139
+ ion-text[slot='end'] {
140
+ padding-left: 8px;
141
+ }
142
+
143
+ &.item-disabled {
144
+ --detail-icon-opacity: 0.1;
145
+ opacity: 1;
146
+
147
+ & > * {
148
+ opacity: 0.4;
149
+ }
150
+ }
151
+
152
+ ion-button[slot='end'] {
153
+ &.ion-align-self-end {
154
+ transform: translateY(-7px);
155
+ }
156
+ ion-icon[slot='icon-only'] {
157
+ font-size: 1.2rem;
158
+ transform: translateY(4px);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ & > ion-note {
165
+ --color: var(--ion-color-medium-tint);
166
+ font-size: 0.9rem;
167
+ display: block;
168
+ margin: 8px calc(var(--ion-safe-area-right, 0) + $outer-note-inset) 8px calc(var(--ion-safe-area-left, 0) + $outer-note-inset);
169
+ }
170
+ }
@@ -0,0 +1,25 @@
1
+ @forward 'dark/ion-button';
2
+ @use 'dark/ion-button' as *;
3
+ @forward 'dark/ion-fab';
4
+ @use 'dark/ion-fab' as *;
5
+ @forward 'dark/ion-tabs';
6
+ @use 'dark/ion-tabs' as *;
7
+ @forward 'dark/ion-segment';
8
+ @use 'dark/ion-segment' as *;
9
+
10
+ @mixin default-variables {
11
+ //--ios26-glass-background-rgb: 35, 35, 35;
12
+ --ios26-glass-background-rgb: 62, 62, 62;
13
+ --ios26-glass-box-shadow-color-rgb: 0, 0, 0 / 0;
14
+ --ios26-glass-border-color-rgb: 68, 68, 68;
15
+ --ios26-button-color-selected-rgb: 255, 255, 255;
16
+
17
+ --ios26-content-box-shadow-rgb: 0, 0, 0;
18
+ }
19
+
20
+ @mixin theme-dark {
21
+ @include ion-button;
22
+ @include ion-fab;
23
+ @include ion-tabs;
24
+ @include ion-segment;
25
+ }
@@ -0,0 +1,95 @@
1
+ @use 'api';
2
+
3
+ ion-header.ios:not(.ios26-disabled).header-translucent,
4
+ ion-footer.ios:not(.ios26-disabled).footer-translucent {
5
+ .header-background,
6
+ .footer-background {
7
+ backdrop-filter: none;
8
+ }
9
+
10
+ ion-toolbar {
11
+ --border-width: 0;
12
+ --background: transparent;
13
+ --opacity: 1;
14
+ &::part(background) {
15
+ --opacity: 0;
16
+ }
17
+ ion-title {
18
+ transform: translateY(0);
19
+ }
20
+ }
21
+ }
22
+
23
+ ion-header.ios:not(.ios26-disabled).header-translucent:not(.header-collapse-condense) {
24
+ &:has(> ion-toolbar.ios:not(.ios26-disabled) > *)::before {
25
+ height: 100%;
26
+ background: rgba(var(--ion-color-base-rgb, var(--ios26-content-box-shadow-rgb)), calc(0.2 * var(--opacity-scale, 1)));
27
+ backdrop-filter: blur(8px);
28
+ }
29
+
30
+ &:not(.ion-no-border) > ion-toolbar.ios:not(.ios26-disabled):has(> *):last-of-type {
31
+ --border-width: 0 0 0.5px;
32
+ --border-color: var(--ion-toolbar-border-color, rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.08));
33
+ &::part(background) {
34
+ --opacity: 1;
35
+ background: transparent;
36
+ }
37
+ }
38
+ }
39
+
40
+ ion-header.ios:not(.ios26-disabled).header-collapse-main ion-toolbar.in-toolbar ion-title {
41
+ transition:
42
+ opacity 0.5s ease-in-out,
43
+ color 0.5s ease-in-out,
44
+ text-shadow 0.5s ease-in-out,
45
+ transform 0.5s ease-in-out;
46
+ }
47
+
48
+ ion-header.ios:not(.ios26-disabled).header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title {
49
+ color: transparent;
50
+ text-shadow: 0 0 1px var(--ion-color-base, var(--ion-toolbar-color, var(--ion-text-color, #000)));
51
+ transform: translateY(8px);
52
+ }
53
+
54
+ ion-header.ios:not(.ios26-disabled).header-translucent::before,
55
+ ion-header.ios:not(.ios26-disabled).header-translucent::after,
56
+ ion-content.ios:not(.ios26-disabled).content-fullscreen::part(background)::before {
57
+ content: '';
58
+ position: absolute;
59
+ width: 100%;
60
+ pointer-events: none;
61
+ z-index: 1;
62
+ }
63
+
64
+ ion-header.ios:not(.ios26-disabled).header-translucent::before {
65
+ /**
66
+ * With safe-area effect, this is need.
67
+ */
68
+ z-index: 2;
69
+ top: 0;
70
+ height: var(--ion-safe-area-top);
71
+ }
72
+
73
+ ion-header.ios:not(.ios26-disabled).header-translucent::after {
74
+ z-index: 2;
75
+ top: 0;
76
+ height: 100%;
77
+ backdrop-filter: blur(2px);
78
+ mask-image: linear-gradient(rgba(0, 0, 0, 1) 80%, rgba(0, 0, 0, 0) 100%);
79
+ mask-composite: intersect;
80
+ }
81
+
82
+ /**
83
+ * ここから下は、ion-content::beforeが content:'' を持たない限り表示されない
84
+ */
85
+ .ion-page:has(> ion-header.header-translucent) > ion-content.content-fullscreen::part(background)::before {
86
+ z-index: 2;
87
+ top: 0;
88
+ height: var(--offset-top, 0px);
89
+ background: linear-gradient(
90
+ in oklab,
91
+ rgba(var(--ios26-content-box-shadow-rgb), 0.5) 0%,
92
+ rgba(var(--ios26-content-box-shadow-rgb), 0.2) 50%,
93
+ rgba(var(--ios26-content-box-shadow-rgb), 0) 100%
94
+ );
95
+ }
@@ -0,0 +1,92 @@
1
+ import { ElementReferences, ElementSizes } from '../interfaces';
2
+ import { Animation, createAnimation } from '@ionic/core';
3
+ import { cloneElement } from '../../utils';
4
+ import { ANIMATION_DELAY_CLOSE_BUTTONS, OPACITY_TRANSITION } from '../utils';
5
+
6
+ export const createEffectAnimation = (references: ElementReferences, sizes: ElementSizes): Animation => {
7
+ const effectElement = cloneElement('ion-icon');
8
+ const closeButtonRect = references.closeButtonIcon?.getBoundingClientRect();
9
+ const iconName = references.selectedTabButtonIcon?.getAttribute('name');
10
+ const selectedTabButtonIconRect = references.selectedTabButtonIcon?.getBoundingClientRect();
11
+
12
+ return createAnimation()
13
+ .addElement(effectElement)
14
+ .beforeAddWrite(() => {
15
+ effectElement.style.display = 'inline-block';
16
+ references.closeButtonIcon.style.opacity = '0';
17
+ if (iconName && closeButtonRect) {
18
+ effectElement.setAttribute('name', iconName);
19
+ effectElement.style.width = `${closeButtonRect.width}px`;
20
+ effectElement.style.height = `${closeButtonRect.height}px`;
21
+ }
22
+ })
23
+ .afterAddWrite(() => {
24
+ effectElement.style.display = 'none';
25
+ references.closeButtonIcon.style.opacity = '1';
26
+ })
27
+ .fromTo(
28
+ 'transform',
29
+ `translate3d(${selectedTabButtonIconRect.left}px, ${selectedTabButtonIconRect.top}px, 0)`,
30
+ `translate3d(${closeButtonRect.left}px, ${closeButtonRect.top}px, 0)`,
31
+ );
32
+ };
33
+
34
+ export const createSearchContainerAnimation = (references: ElementReferences, sizes: ElementSizes): Animation => {
35
+ return createAnimation()
36
+ .addElement(references.searchContainer)
37
+ .beforeAddWrite(() => (references.searchContainer.style.transformOrigin = 'right center'))
38
+ .fromTo(
39
+ 'transform',
40
+ `scale(${sizes.fabButton.width / sizes.searchContainer.width}, ${sizes.fabButton.height / sizes.searchContainer.height})`,
41
+ 'scale(1)',
42
+ )
43
+ .fromTo('opacity', '0.2', '1');
44
+ };
45
+
46
+ export const createCloseButtonsAnimation = (references: ElementReferences): Animation => {
47
+ return createAnimation()
48
+ .delay(ANIMATION_DELAY_CLOSE_BUTTONS)
49
+ .addElement(references.closeButtons)
50
+ .beforeAddWrite(() => (references.closeButtons.style.transformOrigin = 'left center'))
51
+ .afterClearStyles(['transform', 'transform-origin'])
52
+ .fromTo('transform', 'scale(1.5, 1)', 'scale(1)');
53
+ };
54
+
55
+ export const createTabBarAnimation = (ionTabBar: HTMLElement, references: ElementReferences, sizes: ElementSizes): Animation => {
56
+ return createAnimation()
57
+ .addElement(ionTabBar)
58
+ .beforeAddWrite(() => {
59
+ ionTabBar.querySelectorAll<HTMLElement>('ion-tab-button').forEach((element: HTMLElement) => {
60
+ element.style.transition = OPACITY_TRANSITION;
61
+ element.style.opacity = '0';
62
+ });
63
+ references.selectedTabButton.classList.remove('tab-selected');
64
+ const iconName = references.selectedTabButtonIcon?.getAttribute('name');
65
+ if (iconName) {
66
+ references.closeButtonIcon?.setAttribute('name', iconName);
67
+ }
68
+ })
69
+ .afterAddWrite(() => {
70
+ references.selectedTabButton.classList.add('tab-selected');
71
+ ionTabBar.style.pointerEvents = 'none';
72
+ })
73
+ .fromTo(
74
+ 'transform',
75
+ 'scale(1)',
76
+ `scale(${sizes.closeButton.width / sizes.tabBar.width}, ${sizes.closeButton.height / sizes.tabBar.height})`,
77
+ )
78
+ .fromTo('opacity', '1', '0');
79
+ };
80
+
81
+ export const createFabButtonAnimation = (ionFabButton: HTMLElement): Animation => {
82
+ return createAnimation()
83
+ .addElement(ionFabButton)
84
+ .beforeAddWrite(() => {
85
+ ionFabButton.style.transformOrigin = 'center right';
86
+ ionFabButton.querySelector<HTMLElement>('ion-icon')?.style.setProperty('opacity', '0');
87
+ })
88
+ .afterAddWrite(() => {
89
+ ionFabButton.style.pointerEvents = 'none';
90
+ })
91
+ .fromTo('opacity', '1', '0');
92
+ };
@@ -0,0 +1,89 @@
1
+ import { ElementReferences, ElementSizes } from '../interfaces';
2
+ import { Animation, createAnimation } from '@ionic/core';
3
+ import { cloneElement } from '../../utils';
4
+ import { ANIMATION_DELAY_CLOSE_BUTTONS, OPACITY_TRANSITION } from '../utils';
5
+
6
+ export const createReverseEffectAnimation = (
7
+ references: ElementReferences,
8
+ searchableElementSizes: ElementSizes,
9
+ colorSelected: string,
10
+ ): Animation => {
11
+ const effectElement = cloneElement('ion-icon');
12
+ const closeButtonRect = references.closeButtonIcon?.getBoundingClientRect();
13
+
14
+ return createAnimation()
15
+ .addElement(effectElement)
16
+ .beforeAddWrite(() => {
17
+ effectElement.style.display = 'inline-block';
18
+ references.closeButtonIcon.style.opacity = '0';
19
+ if (searchableElementSizes.selectedTabButtonIcon) {
20
+ effectElement.style.width = `${searchableElementSizes.selectedTabButtonIcon.width}px`;
21
+ effectElement.style.height = `${searchableElementSizes.selectedTabButtonIcon.height}px`;
22
+ effectElement.style.color = colorSelected;
23
+ }
24
+ })
25
+ .afterAddWrite(() => {
26
+ effectElement.style.display = 'none';
27
+ references.closeButtonIcon.style.opacity = '1';
28
+ effectElement.style.color = '';
29
+ })
30
+ .fromTo(
31
+ 'transform',
32
+ `translate3d(${closeButtonRect.left}px, ${closeButtonRect.top}px, 0)`,
33
+ `translate3d(${searchableElementSizes.selectedTabButtonIcon.left}px, ${searchableElementSizes.selectedTabButtonIcon.top}px, 0)`,
34
+ );
35
+ };
36
+
37
+ export const createReverseSearchContainerAnimation = (references: ElementReferences, sizes: ElementSizes): Animation => {
38
+ return createAnimation()
39
+ .addElement(references.searchContainer)
40
+ .beforeAddWrite(() => (references.searchContainer.style.transformOrigin = 'right center'))
41
+ .fromTo(
42
+ 'transform',
43
+ 'scale(1)',
44
+ `scale(${sizes.fabButton.width / sizes.searchContainer.width}, ${sizes.fabButton.height / sizes.searchContainer.height})`,
45
+ )
46
+ .fromTo('opacity', '1', '0.2');
47
+ };
48
+
49
+ export const createReverseCloseButtonsAnimation = (references: ElementReferences): Animation => {
50
+ return createAnimation()
51
+ .delay(ANIMATION_DELAY_CLOSE_BUTTONS)
52
+ .addElement(references.closeButtons)
53
+ .beforeAddWrite(() => (references.closeButtons.style.transformOrigin = 'left center'))
54
+ .afterClearStyles(['transform', 'transform-origin'])
55
+ .fromTo('transform', 'scale(1)', 'scale(1.5, 1)');
56
+ };
57
+
58
+ export const createReverseTabBarAnimation = (ionTabBar: HTMLElement, references: ElementReferences, sizes: ElementSizes): Animation => {
59
+ return createAnimation()
60
+ .addElement(ionTabBar)
61
+ .beforeAddWrite(() => {
62
+ ionTabBar.style.pointerEvents = 'auto';
63
+ ionTabBar.querySelectorAll<HTMLElement>('ion-tab-button').forEach((element: HTMLElement) => {
64
+ element.style.transition = OPACITY_TRANSITION;
65
+ element.style.opacity = '1';
66
+ });
67
+ })
68
+ .afterClearStyles(['transform', 'opacity'])
69
+ .fromTo(
70
+ 'transform',
71
+ `scale(${sizes.closeButton.width / sizes.tabBar.width}, ${sizes.closeButton.height / sizes.tabBar.height})`,
72
+ 'scale(1)',
73
+ )
74
+ .fromTo('opacity', '0', '1');
75
+ };
76
+
77
+ export const createReverseFabButtonAnimation = (ionFabButton: HTMLElement, references: ElementSizes): Animation => {
78
+ return createAnimation()
79
+ .addElement(ionFabButton)
80
+ .beforeAddWrite(() => {
81
+ ionFabButton.querySelector<HTMLElement>('ion-icon')?.style.setProperty('opacity', '1');
82
+ ionFabButton.style.transformOrigin = 'center right';
83
+ })
84
+ .afterAddWrite(() => {
85
+ ionFabButton.style.pointerEvents = 'auto';
86
+ })
87
+ .fromTo('opacity', '0', '1')
88
+ .fromTo('transform', `scale(${references.searchContainer.height / references.fabButton.height})`, 'scale(1)');
89
+ };
@@ -0,0 +1,141 @@
1
+ import { createAnimation } from '@ionic/core';
2
+ import {
3
+ ANIMATION_DELAY_BASE,
4
+ ANIMATION_DURATION,
5
+ ANIMATION_EASING,
6
+ getElementReferences,
7
+ getElementSizes,
8
+ throwErrorByFailedClickElement,
9
+ } from './utils';
10
+ import { SearchableEventCache, TabBarSearchableFunction, TabBarSearchableType } from './interfaces';
11
+ import {
12
+ createCloseButtonsAnimation,
13
+ createEffectAnimation,
14
+ createFabButtonAnimation,
15
+ createSearchContainerAnimation,
16
+ createTabBarAnimation,
17
+ } from './animations/enter';
18
+ import {
19
+ createReverseCloseButtonsAnimation,
20
+ createReverseEffectAnimation,
21
+ createReverseFabButtonAnimation,
22
+ createReverseSearchContainerAnimation,
23
+ createReverseTabBarAnimation,
24
+ } from './animations/leave';
25
+
26
+ export * from './interfaces';
27
+
28
+ /**
29
+ * <ion-fab vertical="bottom" horizontal="end" slot="fixed">
30
+ * <ion-fab-button (click)="present($event)">
31
+ * <ion-icon name="search"></ion-icon>
32
+ * </ion-fab-button>
33
+ * </ion-fab>
34
+ * <ion-footer [translucent]="true">
35
+ * <ion-toolbar>
36
+ * <ion-buttons slot="start">
37
+ * <!-- ion-icon does not need `name` attribute. -->
38
+ * <ion-button fill="default"><ion-icon slot="icon-only"></ion-icon>
39
+ * </ion-button>
40
+ * </ion-buttons>
41
+ * <!-- User set `ionChange` or other events. -->
42
+ * <ion-searchbar (ionChange)="example($event)"></ion-searchbar>
43
+ * </ion-toolbar>
44
+ * </ion-footer>
45
+ **/
46
+
47
+ export const attachTabBarSearchable = (
48
+ ionTabBar: HTMLElement,
49
+ ionFabButton: HTMLElement,
50
+ ionFooter: HTMLElement,
51
+ ): TabBarSearchableFunction => {
52
+ if (!ionTabBar || !ionFabButton || !ionFooter) {
53
+ throw new Error('TabBarSearchable should be defined with props');
54
+ }
55
+ // Initialize
56
+ ionFooter.style.pointerEvents = 'none';
57
+ ionFooter.style.opacity = '0';
58
+ ionTabBar.style.transformOrigin = 'left center';
59
+
60
+ // Saved Params
61
+ let searchableEventCache: SearchableEventCache | undefined;
62
+
63
+ return async (event: Event, type: TabBarSearchableType) => {
64
+ if (type === TabBarSearchableType.Enter) {
65
+ searchableEventCache = await enterEvent(event, ionTabBar, ionFabButton, ionFooter);
66
+ } else if (searchableEventCache !== undefined) {
67
+ await leaveEvent(event, searchableEventCache, ionTabBar, ionFabButton, ionFooter);
68
+ searchableEventCache = undefined;
69
+ } else {
70
+ throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');
71
+ }
72
+ };
73
+ };
74
+
75
+ const enterEvent = async (
76
+ event: Event,
77
+ ionTabBar: HTMLElement,
78
+ ionFabButton: HTMLElement,
79
+ ionFooter: HTMLElement,
80
+ ): Promise<SearchableEventCache> => {
81
+ if (!(event.target as HTMLElement)?.closest('ion-fab-button')) {
82
+ throw throwErrorByFailedClickElement('ion-fab-button');
83
+ }
84
+
85
+ const references = getElementReferences(ionTabBar, ionFooter);
86
+ const sizes = getElementSizes(ionTabBar, ionFabButton, references);
87
+ const colorSelected = references.selectedTabButton
88
+ ? getComputedStyle(references.selectedTabButton).getPropertyValue('--color-selected').trim()
89
+ : '';
90
+
91
+ const effectAnimation = createEffectAnimation(references, sizes);
92
+ const searchContainerAnimation = createSearchContainerAnimation(references, sizes);
93
+ const closeButtonsAnimation = createCloseButtonsAnimation(references);
94
+ const tabBarAnimation = createTabBarAnimation(ionTabBar, references, sizes);
95
+ const fabButtonAnimation = createFabButtonAnimation(ionFabButton);
96
+
97
+ await createAnimation()
98
+ .delay(ANIMATION_DELAY_BASE)
99
+ .duration(ANIMATION_DURATION)
100
+ .easing(ANIMATION_EASING)
101
+ .addElement(ionFooter)
102
+ .afterAddWrite(() => (ionFooter.style.pointerEvents = 'auto'))
103
+ .fromTo('opacity', '0.8', '1')
104
+ .addAnimation([tabBarAnimation, fabButtonAnimation, searchContainerAnimation, effectAnimation, closeButtonsAnimation])
105
+ .play();
106
+
107
+ return {
108
+ elementSizes: sizes,
109
+ colorSelected,
110
+ };
111
+ };
112
+
113
+ const leaveEvent = async (
114
+ event: Event,
115
+ searchableEventCache: SearchableEventCache,
116
+ ionTabBar: HTMLElement,
117
+ ionFabButton: HTMLElement,
118
+ ionFooter: HTMLElement,
119
+ ): Promise<void> => {
120
+ if (!(event.target as HTMLElement)?.closest('ion-buttons[slot=start] ion-button')) {
121
+ throw throwErrorByFailedClickElement('ion-buttons[slot=start] ion-button');
122
+ }
123
+
124
+ const references = getElementReferences(ionTabBar, ionFooter);
125
+
126
+ const effectAnimation = createReverseEffectAnimation(references, searchableEventCache.elementSizes, searchableEventCache.colorSelected);
127
+ const searchContainerAnimation = createReverseSearchContainerAnimation(references, searchableEventCache.elementSizes);
128
+ const closeButtonsAnimation = createReverseCloseButtonsAnimation(references);
129
+ const tabBarAnimation = createReverseTabBarAnimation(ionTabBar, references, searchableEventCache.elementSizes);
130
+ const fabButtonAnimation = createReverseFabButtonAnimation(ionFabButton, searchableEventCache.elementSizes);
131
+
132
+ await createAnimation()
133
+ .delay(ANIMATION_DELAY_BASE)
134
+ .duration(ANIMATION_DURATION)
135
+ .easing(ANIMATION_EASING)
136
+ .addElement(ionFooter)
137
+ .afterAddWrite(() => (ionFooter.style.pointerEvents = 'none'))
138
+ .fromTo('opacity', '1', '0')
139
+ .addAnimation([tabBarAnimation, fabButtonAnimation, searchContainerAnimation, effectAnimation, closeButtonsAnimation])
140
+ .play();
141
+ };
@@ -0,0 +1,28 @@
1
+ export enum TabBarSearchableType {
2
+ Enter = 'enter',
3
+ Leave = 'leave',
4
+ }
5
+
6
+ export type TabBarSearchableFunction = (event: Event, type: TabBarSearchableType) => Promise<void>;
7
+
8
+ export interface SearchableEventCache {
9
+ elementSizes: ElementSizes;
10
+ colorSelected: string;
11
+ }
12
+
13
+ // DOM要素とサイズ情報の型定義
14
+ export interface ElementSizes {
15
+ tabBar: { width: number; height: number };
16
+ closeButton: { width: number; height: number };
17
+ fabButton: { width: number; height: number };
18
+ searchContainer: { width: number; height: number };
19
+ selectedTabButtonIcon: { width: number; height: number; top: number; left: number };
20
+ }
21
+
22
+ export interface ElementReferences {
23
+ searchContainer: HTMLElement;
24
+ closeButtons: HTMLElement;
25
+ selectedTabButton: HTMLElement;
26
+ selectedTabButtonIcon: HTMLElement;
27
+ closeButtonIcon: HTMLElement;
28
+ }