@rdlabo/ionic-theme-ios26 0.4.5 → 1.0.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.
@@ -1,8 +1,9 @@
1
- import { EffectScales, registeredEffect } from './interfaces';
2
- import { createGesture, GestureDetail, createAnimation } from '@ionic/core';
1
+ import { AnimationPosition, EffectScales, registeredEffect } from './interfaces';
2
+ import { createAnimation, createGesture, GestureDetail } from '@ionic/core';
3
3
  import type { Animation } from '@ionic/core/dist/types/utils/animation/animation-interface';
4
4
  import { Gesture } from '@ionic/core/dist/types/utils/gesture';
5
- import { cloneElement, getTransform } from './utils';
5
+ import { changeSelectedElement, cloneElement, getStep } from './utils';
6
+ import { createMoveAnimation, createPreMoveAnimation, getMoveAnimationKeyframe, getScaleAnimation } from './animations';
6
7
 
7
8
  const GESTURE_NAME = 'ios26-enable-gesture';
8
9
  const ANIMATED_NAME = 'ios26-animated';
@@ -20,13 +21,11 @@ export const registerEffect = (
20
21
  let gesture!: Gesture;
21
22
  let moveAnimation: Animation | undefined;
22
23
  let currentTouchedElement: HTMLElement | undefined;
23
- let animationLatestX: number | undefined;
24
- let effectElementPositionY: number | undefined;
25
-
26
- let enterAnimationPromise: Promise<void> | undefined;
27
- let moveAnimationPromise: Promise<void> | undefined;
28
24
  let clearActivatedTimer: ReturnType<typeof setTimeout> | undefined;
29
-
25
+ let animationPosition: AnimationPosition | undefined = undefined;
26
+ let scaleAnimationPromise: Promise<void> | undefined;
27
+ let startAnimationPromise: Promise<void> | undefined;
28
+ let maxVelocity = 0;
30
29
  const effectElement = cloneElement(effectTagName);
31
30
 
32
31
  /**
@@ -35,14 +34,12 @@ export const registerEffect = (
35
34
  */
36
35
  const onPointerDown = () => {
37
36
  clearActivated();
38
- currentTouchedElement?.classList.remove('ion-activated');
39
37
  gesture.destroy();
40
38
  createAnimationGesture();
41
39
  };
42
40
  const onPointerUp = (event: PointerEvent) => {
43
- clearActivatedTimer = setTimeout(() => {
44
- onEndGesture();
45
- currentTouchedElement?.classList.remove('ion-activated');
41
+ clearActivatedTimer = setTimeout(async () => {
42
+ await onEndGesture();
46
43
  gesture.destroy();
47
44
  createAnimationGesture();
48
45
  });
@@ -59,7 +56,9 @@ export const registerEffect = (
59
56
  gestureName: `${GESTURE_NAME}_${effectTagName}_${crypto.randomUUID()}`,
60
57
  onStart: (event) => onStartGesture(event),
61
58
  onMove: (event) => onMoveGesture(event),
62
- onEnd: () => onEndGesture(),
59
+ onEnd: () => {
60
+ onEndGesture().then();
61
+ },
63
62
  });
64
63
  gesture.enable(true);
65
64
  };
@@ -69,197 +68,106 @@ export const registerEffect = (
69
68
  if (!currentTouchedElement) {
70
69
  return;
71
70
  }
72
- requestAnimationFrame(() => {
73
- effectElement.style.display = 'none';
74
- effectElement.innerHTML = '';
75
- effectElement.style.transform = 'none';
76
- });
77
-
78
- targetElement.classList.remove(ANIMATED_NAME);
71
+ currentTouchedElement!.click();
72
+ currentTouchedElement?.classList.remove('ion-activated');
79
73
  currentTouchedElement = undefined;
80
- moveAnimation = undefined; // 次回のために破棄
81
- moveAnimationPromise = undefined;
82
- enterAnimationPromise = undefined; // 次回のためにリセット
74
+ effectElement.style.display = 'none';
75
+ maxVelocity = 0;
76
+ targetElement.classList.remove(ANIMATED_NAME);
83
77
  };
84
78
 
85
79
  const onStartGesture = (detail: GestureDetail): boolean | undefined => {
86
- enterAnimationPromise = undefined;
87
80
  currentTouchedElement = ((detail.event.target as HTMLElement).closest(effectTagName) as HTMLElement) || undefined;
88
81
  const tabSelectedElement = targetElement.querySelector(`${effectTagName}.${selectedClassName}`);
89
82
  if (currentTouchedElement === undefined || tabSelectedElement === null) {
90
83
  return false;
91
84
  }
92
- effectElementPositionY = tabSelectedElement.getBoundingClientRect().top;
93
-
94
- const startTransform = getTransform(
95
- tabSelectedElement.getBoundingClientRect().left + tabSelectedElement.clientWidth / 2,
96
- effectElementPositionY,
97
- tabSelectedElement,
98
- );
99
- const middleTransform = getTransform(
100
- (tabSelectedElement.getBoundingClientRect().left + tabSelectedElement.clientWidth / 2 + detail.currentX) / 2,
101
- effectElementPositionY,
102
- currentTouchedElement,
103
- );
104
- const endTransform = getTransform(detail.currentX, effectElementPositionY, currentTouchedElement);
105
- const enterAnimation = createAnimation();
106
- enterAnimation
107
- .addElement(effectElement)
108
- .delay(70)
109
- .beforeStyles({
110
- width: `${tabSelectedElement.clientWidth}px`,
111
- height: `${tabSelectedElement.clientHeight}px`,
112
- display: 'block',
113
- })
114
- .beforeAddWrite(() => {
115
- tabSelectedElement.childNodes.forEach((node) => {
116
- effectElement.appendChild(node.cloneNode(true));
117
- });
118
- targetElement.classList.add(ANIMATED_NAME);
119
- currentTouchedElement!.classList.add('ion-activated');
120
- currentTouchedElement!.click();
121
- });
122
-
123
- if (currentTouchedElement === tabSelectedElement) {
124
- enterAnimation
125
- .keyframes([
126
- {
127
- transform: `${startTransform} ${scales.small}`,
128
- opacity: 1,
129
- offset: 0,
130
- },
131
- {
132
- transform: `${middleTransform} ${scales.large}`,
133
- opacity: 1,
134
- offset: 0.6,
135
- },
136
- {
137
- transform: `${endTransform} ${scales.medium}`,
138
- opacity: 1,
139
- offset: 1,
140
- },
141
- ])
142
- .duration(160);
143
- } else {
144
- enterAnimation
145
- .keyframes([
146
- {
147
- transform: `${startTransform} ${scales.small}`,
148
- opacity: 1,
149
- offset: 0,
150
- },
151
- {
152
- transform: `${middleTransform} ${scales.large}`,
153
- opacity: 1,
154
- offset: 0.65,
155
- },
156
- {
157
- transform: `${endTransform} ${scales.medium}`,
158
- opacity: 1,
159
- offset: 1,
160
- },
161
- ])
162
- .duration(280);
163
- }
164
- animationLatestX = detail.currentX;
165
- enterAnimationPromise = enterAnimation.play().then(() => {
166
- enterAnimationPromise = undefined;
85
+ animationPosition = {
86
+ minPositionX: targetElement.getBoundingClientRect().left,
87
+ maxPositionX: targetElement.getBoundingClientRect().right - tabSelectedElement.clientWidth,
88
+ width: tabSelectedElement.clientWidth,
89
+ positionY: tabSelectedElement.getBoundingClientRect().top,
90
+ };
91
+ targetElement.classList.add(ANIMATED_NAME);
92
+ changeSelectedElement(targetElement, currentTouchedElement, effectTagName, selectedClassName);
93
+
94
+ startAnimationPromise = (() => {
95
+ if (tabSelectedElement === currentTouchedElement) {
96
+ return new Promise<void>((resolve) => resolve());
97
+ } else {
98
+ const preMoveAnimation = createPreMoveAnimation(effectElement, tabSelectedElement, currentTouchedElement, animationPosition!);
99
+ return preMoveAnimation.play().finally(() => preMoveAnimation.destroy());
100
+ }
101
+ })();
102
+ startAnimationPromise.then(() => {
103
+ moveAnimation = createMoveAnimation(effectElement, detail, tabSelectedElement, animationPosition!);
104
+ moveAnimation.progressStart(
105
+ true,
106
+ getStep(currentTouchedElement!.getBoundingClientRect().left + currentTouchedElement!.clientWidth / 2, animationPosition!),
107
+ );
167
108
  });
109
+ getScaleAnimation(effectElement).duration(200).to('opacity', 1).to('transform', scales.large).play();
168
110
  return true;
169
111
  };
170
112
 
171
113
  const onMoveGesture = (detail: GestureDetail): boolean | undefined => {
172
- if (currentTouchedElement === undefined || enterAnimationPromise || moveAnimationPromise) {
173
- return true; // Skip Animation
114
+ if (currentTouchedElement === undefined || !moveAnimation) {
115
+ return false; // Skip Animation
174
116
  }
117
+ if (scaleAnimationPromise === undefined) {
118
+ if (Math.abs(detail.velocityX) > maxVelocity) {
119
+ maxVelocity = Math.abs(detail.velocityX);
120
+ }
121
+ if (Math.abs(detail.velocityX) > 0.2) {
122
+ scaleAnimationPromise = getScaleAnimation(effectElement)
123
+ .duration(720)
124
+ .keyframes(getMoveAnimationKeyframe('slowly', scales))
125
+ .play()
126
+ .finally(() => (scaleAnimationPromise = undefined));
127
+ }
128
+ if (maxVelocity > 0.2 && Math.abs(detail.velocityX) < 0.15 && Math.abs(detail.startX - detail.currentX) > 100) {
129
+ scaleAnimationPromise = getScaleAnimation(effectElement)
130
+ .duration(720)
131
+ .keyframes(getMoveAnimationKeyframe(detail.velocityX > 0 ? 'moveRight' : 'moveLeft', scales))
132
+ .play()
133
+ .finally(() => (scaleAnimationPromise = undefined));
134
+ maxVelocity = 0;
135
+ }
136
+ }
137
+ const latestTouchedElement = ((detail.event.target as HTMLElement).closest(effectTagName) as HTMLElement) || undefined;
175
138
 
176
- const startTransform = getTransform(animationLatestX!, effectElementPositionY!, currentTouchedElement);
177
- const endTransform = getTransform(detail.currentX, effectElementPositionY!, currentTouchedElement);
178
-
179
- // Move用のアニメーションオブジェクトを初回のみ作成し、再利用する
180
- if (!moveAnimation) {
181
- moveAnimation = createAnimation();
182
- moveAnimation
183
- .addElement(effectElement)
184
- .duration(800)
185
- .easing('ease-in-out')
186
- .keyframes([
187
- {
188
- transform: `${startTransform} ${scales.medium}`,
189
- opacity: 1,
190
- offset: 0,
191
- },
192
- {
193
- transform: `${startTransform} ${scales.xlarge}`,
194
- opacity: 1,
195
- offset: 0.2,
196
- },
197
- {
198
- transform: `${endTransform} ${scales.medium}`,
199
- opacity: 1,
200
- offset: 1,
201
- },
202
- ]);
203
- } else {
204
- moveAnimation.duration(0).keyframes([
205
- {
206
- transform: `${endTransform} ${scales.medium}`,
207
- opacity: 1,
208
- offset: 1,
209
- },
210
- {
211
- transform: `${endTransform} ${scales.medium}`,
212
- opacity: 1,
213
- offset: 1,
214
- },
215
- ]);
139
+ if (latestTouchedElement && currentTouchedElement !== latestTouchedElement) {
140
+ currentTouchedElement = latestTouchedElement;
141
+ changeSelectedElement(targetElement, currentTouchedElement, effectTagName, selectedClassName);
216
142
  }
217
- animationLatestX = detail.currentX;
218
- moveAnimationPromise = moveAnimation.play().then(() => {
219
- moveAnimationPromise = undefined;
220
- });
143
+ moveAnimation.progressStep(getStep(detail.currentX, animationPosition!));
221
144
  return true;
222
145
  };
223
146
 
224
- const onEndGesture = (): boolean | undefined => {
147
+ const onEndGesture = async (): Promise<boolean | undefined> => {
225
148
  // タイマーをクリア(正常にonEndGestureが実行された場合)
226
149
  if (clearActivatedTimer !== undefined) {
227
150
  clearTimeout(clearActivatedTimer);
228
151
  clearActivatedTimer = undefined;
229
152
  }
230
153
 
231
- if (currentTouchedElement === undefined) {
154
+ if (startAnimationPromise) {
155
+ await startAnimationPromise;
156
+ }
157
+
158
+ if (currentTouchedElement === undefined || !moveAnimation) {
232
159
  return false;
233
160
  }
234
161
 
235
- const transform = getTransform(animationLatestX!, effectElementPositionY!, currentTouchedElement);
162
+ setTimeout(() => {
163
+ const targetX = currentTouchedElement!.getBoundingClientRect().left + currentTouchedElement!.clientWidth / 2;
164
+ const step = getStep(targetX, animationPosition!);
165
+ moveAnimation!.progressStep(step);
166
+ });
167
+ await getScaleAnimation(effectElement).duration(120).to('transform', `scale(1, 0.92)`).play();
168
+ moveAnimation!.destroy();
236
169
 
237
- const leaveAnimation = createAnimation();
238
- leaveAnimation.addElement(effectElement);
239
- leaveAnimation
240
- .onFinish(() => clearActivated())
241
- .easing('ease-in')
242
- .duration(80)
243
- .keyframes([
244
- {
245
- transform: `${transform} ${scales.medium}`,
246
- opacity: 1,
247
- },
248
- {
249
- transform: `${transform} ${scales.small}`,
250
- opacity: 0,
251
- },
252
- ]);
253
- (async () => {
254
- // Wait for enter animation to complete before playing leave animation
255
- if (enterAnimationPromise) {
256
- setTimeout(() => currentTouchedElement!.classList.remove('ion-activated'), 50);
257
- await enterAnimationPromise;
258
- } else {
259
- currentTouchedElement!.classList.remove('ion-activated');
260
- }
261
- leaveAnimation.play();
262
- })();
170
+ clearActivated();
263
171
  return true;
264
172
  };
265
173
 
@@ -282,7 +190,6 @@ export const registerEffect = (
282
190
  if (gesture) {
283
191
  gesture.destroy();
284
192
  }
285
-
286
193
  // Remove gesture class
287
194
  targetElement.classList.remove(GESTURE_NAME);
288
195
  },
@@ -8,3 +8,10 @@ export interface EffectScales {
8
8
  export interface registeredEffect {
9
9
  destroy: () => void;
10
10
  }
11
+
12
+ export interface AnimationPosition {
13
+ minPositionX: number;
14
+ maxPositionX: number;
15
+ width: number;
16
+ positionY: number;
17
+ }
@@ -1,3 +1,5 @@
1
+ import { AnimationPosition } from './interfaces';
2
+
1
3
  export const cloneElement = (tagName: string): HTMLElement => {
2
4
  const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
3
5
  if (getCachedEl !== null) {
@@ -12,17 +14,25 @@ export const cloneElement = (tagName: string): HTMLElement => {
12
14
  return clonedEl;
13
15
  };
14
16
 
15
- export const getTransform = (detailCurrentX: number, tabEffectElY: number, tabSelectedActual: Element): string => {
16
- const diff = -2;
17
- const currentX = detailCurrentX - tabSelectedActual.clientWidth / 2;
18
- const maxLeft = tabSelectedActual.getBoundingClientRect().left + diff;
19
- const maxRight = tabSelectedActual.getBoundingClientRect().right - diff - tabSelectedActual.clientWidth;
20
-
21
- if (maxLeft < currentX && currentX < maxRight) {
22
- return `translate3d(${currentX}px, ${tabEffectElY}px, 0)`;
23
- }
24
- if (maxLeft > currentX) {
25
- return `translate3d(${maxLeft}px, ${tabEffectElY}px, 0)`;
17
+ export const getStep = (targetX: number, animationPosition: AnimationPosition) => {
18
+ if (animationPosition === undefined) {
19
+ return 0;
26
20
  }
27
- return `translate3d(${maxRight}px, ${tabEffectElY}px, 0)`;
21
+ const currentX = targetX - animationPosition.width / 2;
22
+ let progress = (currentX - animationPosition.minPositionX) / (animationPosition.maxPositionX - animationPosition.minPositionX);
23
+ progress = Math.max(0, Math.min(1, progress)); // clamp 0〜1
24
+ return progress;
25
+ };
26
+
27
+ export const changeSelectedElement = (
28
+ targetElement: HTMLElement,
29
+ selectedElement: HTMLElement,
30
+ effectTagName: string,
31
+ selectedClassName: string,
32
+ ): void => {
33
+ targetElement.querySelectorAll(effectTagName).forEach((element) => {
34
+ element.classList.remove(selectedClassName);
35
+ element.classList.remove('ion-activated');
36
+ });
37
+ selectedElement.classList.add(selectedClassName, 'ion-activated');
28
38
  };
package/src/index.ts CHANGED
@@ -4,10 +4,10 @@ export * from './gestures/interfaces';
4
4
 
5
5
  export const registerTabBarEffect = (targetElement: HTMLElement): registeredEffect | undefined => {
6
6
  return registerEffect(targetElement, 'ion-tab-button', 'tab-selected', {
7
- small: 'scale(1.1)',
7
+ small: 'scale(1.1, 1)',
8
8
  medium: 'scale(1.2)',
9
9
  large: 'scale(1.3)',
10
- xlarge: 'scale(1.3, 1.5)',
10
+ xlarge: 'scale(1.15, 1.4)',
11
11
  });
12
12
  };
13
13
 
@@ -175,9 +175,19 @@ $scaleup-large-icon-only: 1.22;
175
175
  * fill=solid && type=submit button has edge brightness.
176
176
  */
177
177
  &.button-solid[type='submit'] {
178
+ /*
179
+ * If not set ion-color-**, button color set primary.
180
+ * https://github.com/ionic-team/ionic-framework/blob/main/core/src/components/button/button.scss#L78-L81
181
+ */
182
+ --color: var(--ion-color-primary-brightness, var(--ion-color-contrast));
183
+ --color-activated: var(--ion-color-primary-brightness);
184
+ --border-color: var(--ion-color-primary-brightness, transparent);
185
+ --border-width: 0.5px;
186
+ --border-style: solid;
187
+
178
188
  @each $item in primary, secondary, tertiary, success, warning, danger, light, medium, dark {
179
189
  &.ion-color-#{$item} {
180
- --color: var(--ion-color-#{$item}-brightness, var(--ion-color-contrast));
190
+ --color: var(--ion-color-#{$item}-brightness, var(--ion-color-#{$item}-contrast, var(--ion-color-contrast)));
181
191
  --color-activated: var(--ion-color-#{$item}-brightness);
182
192
  --border-color: var(--ion-color-#{$item}-brightness, transparent);
183
193
  --border-width: 0.5px;