@rdlabo/ionic-theme-ios26 1.3.2 → 2.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.
- package/README.md +35 -55
- package/dist/css/components/ion-button.css +1 -1
- package/dist/css/components/ion-toolbar.css +1 -1
- package/dist/css/ionic-theme-ios26.css +1 -1
- package/dist/focus-controller /index.d.ts +6 -0
- package/dist/focus-controller /index.d.ts.map +1 -0
- package/dist/focus-controller /index.js +62 -0
- package/dist/popover/animations/ios.enter.js +1 -1
- package/dist/popover/utils.d.ts.map +1 -1
- package/dist/popover/utils.js +6 -0
- package/dist/sheets-of-glass/index.d.ts.map +1 -1
- package/dist/sheets-of-glass/index.js +6 -0
- package/dist/transition/index.d.ts +19 -0
- package/dist/transition/index.d.ts.map +1 -0
- package/dist/transition/index.js +219 -0
- package/dist/transition/ios.transition.d.ts +5 -0
- package/dist/transition/ios.transition.d.ts.map +1 -0
- package/dist/transition/ios.transition.js +496 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +30 -0
- package/package.json +1 -1
- package/src/focus-controller /index.ts +123 -0
- package/src/popover/animations/ios.enter.ts +1 -1
- package/src/popover/utils.ts +6 -0
- package/src/sheets-of-glass/index.ts +8 -2
- package/src/styles/components/ion-button.scss +0 -4
- package/src/transition/index.ts +369 -0
- package/src/transition/ios.transition.ts +834 -0
- package/src/utils.ts +36 -0
- package/dist/gestures/animations.d.ts +0 -8
- package/dist/gestures/animations.d.ts.map +0 -1
- package/dist/gestures/animations.js +0 -97
- package/dist/gestures/gestures.d.ts +0 -3
- package/dist/gestures/gestures.d.ts.map +0 -1
- package/dist/gestures/gestures.js +0 -240
- package/dist/gestures/index.d.ts +0 -3
- package/dist/gestures/index.d.ts.map +0 -1
- package/dist/gestures/index.js +0 -160
- package/dist/gestures/interfaces.d.ts +0 -16
- package/dist/gestures/interfaces.d.ts.map +0 -1
- package/dist/gestures/interfaces.js +0 -1
- package/dist/gestures/utils.d.ts +0 -5
- package/dist/gestures/utils.d.ts.map +0 -1
- package/dist/gestures/utils.js +0 -27
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
import type { Animation } from '@ionic/core/dist/types/utils/animation/animation-interface';
|
|
2
|
+
import { createAnimation } from '@ionic/core';
|
|
3
|
+
import type { TransitionOptions } from './index';
|
|
4
|
+
import { getIonPageElement } from './index';
|
|
5
|
+
|
|
6
|
+
const DURATION = 540;
|
|
7
|
+
|
|
8
|
+
// TODO(FW-2832): types
|
|
9
|
+
|
|
10
|
+
const getClonedElement = <T extends HTMLIonBackButtonElement | HTMLIonTitleElement>(tagName: string) => {
|
|
11
|
+
return document.querySelector<T>(`${tagName}.ion-cloned-element`);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const shadow = <T extends Element>(el: T): ShadowRoot | T => {
|
|
15
|
+
return el.shadowRoot || el;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const getLargeTitle = (refEl: any) => {
|
|
19
|
+
const tabs = refEl.tagName === 'ION-TABS' ? refEl : refEl.querySelector('ion-tabs');
|
|
20
|
+
const query = 'ion-content ion-header:not(.header-collapse-condense-inactive) ion-title.title-large';
|
|
21
|
+
|
|
22
|
+
if (tabs != null) {
|
|
23
|
+
const activeTab = tabs.querySelector('ion-tab:not(.tab-hidden), .ion-page:not(.ion-page-hidden)');
|
|
24
|
+
return activeTab != null ? activeTab.querySelector(query) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return refEl.querySelector(query);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const getBackButton = (refEl: any, backDirection: boolean) => {
|
|
31
|
+
const tabs = refEl.tagName === 'ION-TABS' ? refEl : refEl.querySelector('ion-tabs');
|
|
32
|
+
let buttonsList = [];
|
|
33
|
+
|
|
34
|
+
if (tabs != null) {
|
|
35
|
+
const activeTab = tabs.querySelector('ion-tab:not(.tab-hidden), .ion-page:not(.ion-page-hidden)');
|
|
36
|
+
if (activeTab != null) {
|
|
37
|
+
buttonsList = activeTab.querySelectorAll('ion-buttons');
|
|
38
|
+
}
|
|
39
|
+
} else {
|
|
40
|
+
buttonsList = refEl.querySelectorAll('ion-buttons');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const buttons of buttonsList) {
|
|
44
|
+
const parentHeader = buttons.closest('ion-header');
|
|
45
|
+
const activeHeader = parentHeader && !parentHeader.classList.contains('header-collapse-condense-inactive');
|
|
46
|
+
const backButton = buttons.querySelector('ion-back-button');
|
|
47
|
+
const buttonsCollapse = buttons.classList.contains('buttons-collapse');
|
|
48
|
+
const startSlot = buttons.slot === 'start' || buttons.slot === '';
|
|
49
|
+
|
|
50
|
+
if (backButton !== null && startSlot && ((buttonsCollapse && activeHeader && backDirection) || !buttonsCollapse)) {
|
|
51
|
+
return backButton;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const createLargeTitleTransition = (
|
|
59
|
+
rootAnimation: Animation,
|
|
60
|
+
rtl: boolean,
|
|
61
|
+
backDirection: boolean,
|
|
62
|
+
enteringEl: HTMLElement,
|
|
63
|
+
leavingEl: HTMLElement | undefined,
|
|
64
|
+
) => {
|
|
65
|
+
const enteringBackButton = getBackButton(enteringEl, backDirection);
|
|
66
|
+
const leavingLargeTitle = getLargeTitle(leavingEl);
|
|
67
|
+
|
|
68
|
+
const enteringLargeTitle = getLargeTitle(enteringEl);
|
|
69
|
+
const leavingBackButton = getBackButton(leavingEl, backDirection);
|
|
70
|
+
|
|
71
|
+
const shouldAnimationForward = enteringBackButton !== null && leavingLargeTitle !== null && !backDirection;
|
|
72
|
+
const shouldAnimationBackward = enteringLargeTitle !== null && leavingBackButton !== null && backDirection;
|
|
73
|
+
|
|
74
|
+
if (shouldAnimationForward) {
|
|
75
|
+
const leavingLargeTitleBox = leavingLargeTitle.getBoundingClientRect();
|
|
76
|
+
const enteringBackButtonBox = enteringBackButton.getBoundingClientRect();
|
|
77
|
+
|
|
78
|
+
const enteringBackButtonTextEl = shadow(enteringBackButton).querySelector('.button-text');
|
|
79
|
+
|
|
80
|
+
// Text element not rendered if developers pass text="" to the back button
|
|
81
|
+
const enteringBackButtonTextBox = enteringBackButtonTextEl?.getBoundingClientRect();
|
|
82
|
+
|
|
83
|
+
const leavingLargeTitleTextEl = shadow(leavingLargeTitle).querySelector('.toolbar-title')!;
|
|
84
|
+
const leavingLargeTitleTextBox = leavingLargeTitleTextEl.getBoundingClientRect();
|
|
85
|
+
|
|
86
|
+
animateLargeTitle(
|
|
87
|
+
rootAnimation,
|
|
88
|
+
rtl,
|
|
89
|
+
backDirection,
|
|
90
|
+
leavingLargeTitle,
|
|
91
|
+
leavingLargeTitleBox,
|
|
92
|
+
leavingLargeTitleTextBox,
|
|
93
|
+
enteringBackButtonBox,
|
|
94
|
+
enteringBackButtonTextEl,
|
|
95
|
+
enteringBackButtonTextBox,
|
|
96
|
+
);
|
|
97
|
+
// animateBackButton(
|
|
98
|
+
// rootAnimation,
|
|
99
|
+
// rtl,
|
|
100
|
+
// backDirection,
|
|
101
|
+
// enteringBackButton,
|
|
102
|
+
// enteringBackButtonBox,
|
|
103
|
+
// enteringBackButtonTextEl,
|
|
104
|
+
// enteringBackButtonTextBox,
|
|
105
|
+
// leavingLargeTitle,
|
|
106
|
+
// leavingLargeTitleTextBox,
|
|
107
|
+
// );
|
|
108
|
+
} else if (shouldAnimationBackward) {
|
|
109
|
+
const enteringLargeTitleBox = enteringLargeTitle.getBoundingClientRect();
|
|
110
|
+
const leavingBackButtonBox = leavingBackButton.getBoundingClientRect();
|
|
111
|
+
|
|
112
|
+
const leavingBackButtonTextEl = shadow(leavingBackButton).querySelector('.button-text');
|
|
113
|
+
|
|
114
|
+
// Text element not rendered if developers pass text="" to the back button
|
|
115
|
+
const leavingBackButtonTextBox = leavingBackButtonTextEl?.getBoundingClientRect();
|
|
116
|
+
|
|
117
|
+
const enteringLargeTitleTextEl = shadow(enteringLargeTitle).querySelector('.toolbar-title')!;
|
|
118
|
+
const enteringLargeTitleTextBox = enteringLargeTitleTextEl.getBoundingClientRect();
|
|
119
|
+
|
|
120
|
+
animateLargeTitle(
|
|
121
|
+
rootAnimation,
|
|
122
|
+
rtl,
|
|
123
|
+
backDirection,
|
|
124
|
+
enteringLargeTitle,
|
|
125
|
+
enteringLargeTitleBox,
|
|
126
|
+
enteringLargeTitleTextBox,
|
|
127
|
+
leavingBackButtonBox,
|
|
128
|
+
leavingBackButtonTextEl,
|
|
129
|
+
leavingBackButtonTextBox,
|
|
130
|
+
);
|
|
131
|
+
// animateBackButton(
|
|
132
|
+
// rootAnimation,
|
|
133
|
+
// rtl,
|
|
134
|
+
// backDirection,
|
|
135
|
+
// leavingBackButton,
|
|
136
|
+
// leavingBackButtonBox,
|
|
137
|
+
// leavingBackButtonTextEl,
|
|
138
|
+
// leavingBackButtonTextBox,
|
|
139
|
+
// enteringLargeTitle,
|
|
140
|
+
// enteringLargeTitleTextBox,
|
|
141
|
+
// );
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
forward: shouldAnimationForward,
|
|
146
|
+
backward: shouldAnimationBackward,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const animateBackButton = (
|
|
151
|
+
rootAnimation: Animation,
|
|
152
|
+
rtl: boolean,
|
|
153
|
+
backDirection: boolean,
|
|
154
|
+
backButtonEl: HTMLIonBackButtonElement,
|
|
155
|
+
backButtonBox: DOMRect,
|
|
156
|
+
backButtonTextEl: HTMLElement | null,
|
|
157
|
+
backButtonTextBox: DOMRect | undefined,
|
|
158
|
+
largeTitleEl: HTMLIonTitleElement,
|
|
159
|
+
largeTitleTextBox: DOMRect,
|
|
160
|
+
) => {
|
|
161
|
+
const BACK_BUTTON_START_OFFSET = rtl ? `calc(100% - ${backButtonBox.right + 4}px)` : `${backButtonBox.left - 4}px`;
|
|
162
|
+
|
|
163
|
+
const TEXT_ORIGIN_X = rtl ? 'right' : 'left';
|
|
164
|
+
const ICON_ORIGIN_X = rtl ? 'left' : 'right';
|
|
165
|
+
|
|
166
|
+
const CONTAINER_ORIGIN_X = rtl ? 'right' : 'left';
|
|
167
|
+
let WIDTH_SCALE = 1;
|
|
168
|
+
let HEIGHT_SCALE = 1;
|
|
169
|
+
|
|
170
|
+
let TEXT_START_SCALE = `scale(${HEIGHT_SCALE})`;
|
|
171
|
+
const TEXT_END_SCALE = 'scale(1)';
|
|
172
|
+
|
|
173
|
+
if (backButtonTextEl && backButtonTextBox) {
|
|
174
|
+
/**
|
|
175
|
+
* When the title and back button texts match then they should overlap during the
|
|
176
|
+
* page transition. If the texts do not match up then the back button text scale
|
|
177
|
+
* adjusts to not perfectly match the large title text otherwise the proportions
|
|
178
|
+
* will be incorrect. When the texts match we scale both the width and height to
|
|
179
|
+
* account for font weight differences between the title and back button.
|
|
180
|
+
*/
|
|
181
|
+
const doTitleAndButtonTextsMatch = backButtonTextEl.textContent?.trim() === largeTitleEl.textContent?.trim();
|
|
182
|
+
WIDTH_SCALE = largeTitleTextBox.width / backButtonTextBox.width;
|
|
183
|
+
/**
|
|
184
|
+
* Subtract an offset to account for slight sizing/padding differences between the
|
|
185
|
+
* title and the back button.
|
|
186
|
+
*/
|
|
187
|
+
HEIGHT_SCALE = (largeTitleTextBox.height - LARGE_TITLE_SIZE_OFFSET) / backButtonTextBox.height;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Even though we set TEXT_START_SCALE to HEIGHT_SCALE above, we potentially need
|
|
191
|
+
* to re-compute this here since the HEIGHT_SCALE may have changed.
|
|
192
|
+
*/
|
|
193
|
+
TEXT_START_SCALE = doTitleAndButtonTextsMatch ? `scale(${WIDTH_SCALE}, ${HEIGHT_SCALE})` : `scale(${HEIGHT_SCALE})`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const backButtonIconEl = shadow(backButtonEl).querySelector('ion-icon')!;
|
|
197
|
+
const backButtonIconBox = backButtonIconEl.getBoundingClientRect();
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* We need to offset the container by the icon dimensions
|
|
201
|
+
* so that the back button text aligns with the large title
|
|
202
|
+
* text. Otherwise, the back button icon will align with the
|
|
203
|
+
* large title text but the back button text will not.
|
|
204
|
+
*/
|
|
205
|
+
const CONTAINER_START_TRANSLATE_X = rtl
|
|
206
|
+
? `${backButtonIconBox.width / 2 - (backButtonIconBox.right - backButtonBox.right)}px`
|
|
207
|
+
: `${backButtonBox.left - backButtonIconBox.width / 2}px`;
|
|
208
|
+
const CONTAINER_END_TRANSLATE_X = rtl ? `-${window.innerWidth - backButtonBox.right}px` : `${backButtonBox.left}px`;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Back button container should be
|
|
212
|
+
* aligned to the top of the title container
|
|
213
|
+
* so the texts overlap as the back button
|
|
214
|
+
* text begins to fade in.
|
|
215
|
+
*/
|
|
216
|
+
const CONTAINER_START_TRANSLATE_Y = `${largeTitleTextBox.top}px`;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The cloned back button should align exactly with the
|
|
220
|
+
* real back button on the entering page otherwise there will
|
|
221
|
+
* be a layout shift.
|
|
222
|
+
*/
|
|
223
|
+
const CONTAINER_END_TRANSLATE_Y = `${backButtonBox.top}px`;
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* In the forward direction, the cloned back button
|
|
227
|
+
* container should translate from over the large title
|
|
228
|
+
* to over the back button. In the backward direction,
|
|
229
|
+
* it should translate from over the back button to over
|
|
230
|
+
* the large title.
|
|
231
|
+
*/
|
|
232
|
+
const FORWARD_CONTAINER_KEYFRAMES = [
|
|
233
|
+
{ offset: 0, transform: `translate3d(${CONTAINER_START_TRANSLATE_X}, ${CONTAINER_START_TRANSLATE_Y}, 0)` },
|
|
234
|
+
{ offset: 1, transform: `translate3d(${CONTAINER_END_TRANSLATE_X}, ${CONTAINER_END_TRANSLATE_Y}, 0)` },
|
|
235
|
+
];
|
|
236
|
+
const BACKWARD_CONTAINER_KEYFRAMES = [
|
|
237
|
+
{ offset: 0, transform: `translate3d(${CONTAINER_END_TRANSLATE_X}, ${CONTAINER_END_TRANSLATE_Y}, 0)` },
|
|
238
|
+
{ offset: 1, transform: `translate3d(${CONTAINER_START_TRANSLATE_X}, ${CONTAINER_START_TRANSLATE_Y}, 0)` },
|
|
239
|
+
];
|
|
240
|
+
const CONTAINER_KEYFRAMES = backDirection ? BACKWARD_CONTAINER_KEYFRAMES : FORWARD_CONTAINER_KEYFRAMES;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* In the forward direction, the text in the cloned back button
|
|
244
|
+
* should start to be (roughly) the size of the large title
|
|
245
|
+
* and then scale down to be the size of the actual back button.
|
|
246
|
+
* The text should also translate, but that translate is handled
|
|
247
|
+
* by the container keyframes.
|
|
248
|
+
*/
|
|
249
|
+
const FORWARD_TEXT_KEYFRAMES = [
|
|
250
|
+
{ offset: 0, opacity: 0, transform: TEXT_START_SCALE },
|
|
251
|
+
{ offset: 1, opacity: 1, transform: TEXT_END_SCALE },
|
|
252
|
+
];
|
|
253
|
+
const BACKWARD_TEXT_KEYFRAMES = [
|
|
254
|
+
{ offset: 0, opacity: 1, transform: TEXT_END_SCALE },
|
|
255
|
+
{ offset: 1, opacity: 0, transform: TEXT_START_SCALE },
|
|
256
|
+
];
|
|
257
|
+
const TEXT_KEYFRAMES = backDirection ? BACKWARD_TEXT_KEYFRAMES : FORWARD_TEXT_KEYFRAMES;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The icon should scale in/out in the second
|
|
261
|
+
* half of the animation. The icon should also
|
|
262
|
+
* translate, but that translate is handled by the
|
|
263
|
+
* container keyframes.
|
|
264
|
+
*/
|
|
265
|
+
const FORWARD_ICON_KEYFRAMES = [
|
|
266
|
+
{ offset: 0, opacity: 0, transform: 'scale(0.6)' },
|
|
267
|
+
{ offset: 0.6, opacity: 0, transform: 'scale(0.6)' },
|
|
268
|
+
{ offset: 1, opacity: 1, transform: 'scale(1)' },
|
|
269
|
+
];
|
|
270
|
+
const BACKWARD_ICON_KEYFRAMES = [
|
|
271
|
+
{ offset: 0, opacity: 1, transform: 'scale(1)' },
|
|
272
|
+
{ offset: 0.2, opacity: 0, transform: 'scale(0.6)' },
|
|
273
|
+
{ offset: 1, opacity: 0, transform: 'scale(0.6)' },
|
|
274
|
+
];
|
|
275
|
+
const ICON_KEYFRAMES = backDirection ? BACKWARD_ICON_KEYFRAMES : FORWARD_ICON_KEYFRAMES;
|
|
276
|
+
|
|
277
|
+
const enteringBackButtonTextAnimation = createAnimation();
|
|
278
|
+
const enteringBackButtonIconAnimation = createAnimation();
|
|
279
|
+
const enteringBackButtonAnimation = createAnimation();
|
|
280
|
+
|
|
281
|
+
const clonedBackButtonEl = getClonedElement<HTMLIonBackButtonElement>('ion-back-button')!;
|
|
282
|
+
|
|
283
|
+
const clonedBackButtonTextEl = shadow(clonedBackButtonEl).querySelector('.button-text')!;
|
|
284
|
+
const clonedBackButtonIconEl = shadow(clonedBackButtonEl).querySelector('ion-icon')!;
|
|
285
|
+
|
|
286
|
+
clonedBackButtonEl.text = backButtonEl.text;
|
|
287
|
+
clonedBackButtonEl.mode = backButtonEl.mode;
|
|
288
|
+
clonedBackButtonEl.icon = backButtonEl.icon;
|
|
289
|
+
clonedBackButtonEl.color = backButtonEl.color;
|
|
290
|
+
clonedBackButtonEl.disabled = backButtonEl.disabled;
|
|
291
|
+
|
|
292
|
+
clonedBackButtonEl.style.setProperty('display', 'block');
|
|
293
|
+
clonedBackButtonEl.style.setProperty('position', 'fixed');
|
|
294
|
+
|
|
295
|
+
enteringBackButtonIconAnimation.addElement(clonedBackButtonIconEl);
|
|
296
|
+
enteringBackButtonTextAnimation.addElement(clonedBackButtonTextEl);
|
|
297
|
+
enteringBackButtonAnimation.addElement(clonedBackButtonEl);
|
|
298
|
+
|
|
299
|
+
enteringBackButtonAnimation
|
|
300
|
+
.beforeStyles({
|
|
301
|
+
position: 'absolute',
|
|
302
|
+
top: '0px',
|
|
303
|
+
[CONTAINER_ORIGIN_X]: '0px',
|
|
304
|
+
})
|
|
305
|
+
/**
|
|
306
|
+
* The write hooks must be set on this animation as it is guaranteed to run. Other
|
|
307
|
+
* animations such as the back button text animation will not run if the back button
|
|
308
|
+
* has no visible text.
|
|
309
|
+
*/
|
|
310
|
+
.beforeAddWrite(() => {
|
|
311
|
+
backButtonEl.style.setProperty('display', 'none');
|
|
312
|
+
clonedBackButtonEl.style.setProperty(TEXT_ORIGIN_X, BACK_BUTTON_START_OFFSET);
|
|
313
|
+
})
|
|
314
|
+
.afterAddWrite(() => {
|
|
315
|
+
backButtonEl.style.setProperty('display', '');
|
|
316
|
+
clonedBackButtonEl.style.setProperty('display', 'none');
|
|
317
|
+
clonedBackButtonEl.style.removeProperty(TEXT_ORIGIN_X);
|
|
318
|
+
})
|
|
319
|
+
.keyframes(CONTAINER_KEYFRAMES);
|
|
320
|
+
|
|
321
|
+
enteringBackButtonTextAnimation
|
|
322
|
+
.beforeStyles({
|
|
323
|
+
'transform-origin': `${TEXT_ORIGIN_X} top`,
|
|
324
|
+
})
|
|
325
|
+
.keyframes(TEXT_KEYFRAMES);
|
|
326
|
+
|
|
327
|
+
enteringBackButtonIconAnimation
|
|
328
|
+
.beforeStyles({
|
|
329
|
+
'transform-origin': `${ICON_ORIGIN_X} center`,
|
|
330
|
+
})
|
|
331
|
+
.keyframes(ICON_KEYFRAMES);
|
|
332
|
+
|
|
333
|
+
rootAnimation.addAnimation([enteringBackButtonTextAnimation, enteringBackButtonIconAnimation, enteringBackButtonAnimation]);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const animateLargeTitle = (
|
|
337
|
+
rootAnimation: Animation,
|
|
338
|
+
rtl: boolean,
|
|
339
|
+
backDirection: boolean,
|
|
340
|
+
largeTitleEl: HTMLIonTitleElement,
|
|
341
|
+
largeTitleBox: DOMRect,
|
|
342
|
+
largeTitleTextBox: DOMRect,
|
|
343
|
+
backButtonBox: DOMRect,
|
|
344
|
+
backButtonTextEl: HTMLElement | null,
|
|
345
|
+
backButtonTextBox: DOMRect | undefined,
|
|
346
|
+
) => {
|
|
347
|
+
/**
|
|
348
|
+
* The horizontal transform origin for the large title
|
|
349
|
+
*/
|
|
350
|
+
const ORIGIN_X = rtl ? 'right' : 'left';
|
|
351
|
+
|
|
352
|
+
const TITLE_START_OFFSET = rtl ? `calc(100% - ${largeTitleBox.right}px)` : `${largeTitleBox.left}px`;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The cloned large should align exactly with the
|
|
356
|
+
* real large title on the leaving page otherwise there will
|
|
357
|
+
* be a layout shift.
|
|
358
|
+
*/
|
|
359
|
+
const START_TRANSLATE_X = '0px';
|
|
360
|
+
const START_TRANSLATE_Y = `${largeTitleBox.top}px`;
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* How much to offset the large title translation by.
|
|
364
|
+
* This accounts for differences in sizing between the large
|
|
365
|
+
* title and the back button due to padding and font weight.
|
|
366
|
+
*/
|
|
367
|
+
const LARGE_TITLE_TRANSLATION_OFFSET = 8;
|
|
368
|
+
let END_TRANSLATE_X = rtl
|
|
369
|
+
? `-${window.innerWidth - backButtonBox.right - LARGE_TITLE_TRANSLATION_OFFSET}px`
|
|
370
|
+
: `${backButtonBox.x + LARGE_TITLE_TRANSLATION_OFFSET}px`;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* How much to scale the large title up/down by.
|
|
374
|
+
*/
|
|
375
|
+
let HEIGHT_SCALE = 0.5;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The large title always starts full size.
|
|
379
|
+
*/
|
|
380
|
+
const START_SCALE = 'scale(1)';
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* By default, we don't worry about having the large title scaled to perfectly
|
|
384
|
+
* match the back button because we don't know if the back button's text matches
|
|
385
|
+
* the large title's text.
|
|
386
|
+
*/
|
|
387
|
+
let END_SCALE = `scale(${HEIGHT_SCALE})`;
|
|
388
|
+
|
|
389
|
+
// Text element not rendered if developers pass text="" to the back button
|
|
390
|
+
if (backButtonTextEl && backButtonTextBox) {
|
|
391
|
+
/**
|
|
392
|
+
* The scaled title should (roughly) overlap the back button. This ensures that
|
|
393
|
+
* the back button and title overlap during the animation. Note that since both
|
|
394
|
+
* elements either fade in or fade out over the course of the animation, neither
|
|
395
|
+
* element will be fully visible on top of the other. As a result, the overlap
|
|
396
|
+
* does not need to be perfect, so approximate values are acceptable here.
|
|
397
|
+
*/
|
|
398
|
+
END_TRANSLATE_X = rtl
|
|
399
|
+
? `-${window.innerWidth - backButtonTextBox.right - LARGE_TITLE_TRANSLATION_OFFSET}px`
|
|
400
|
+
: `${backButtonTextBox.x - LARGE_TITLE_TRANSLATION_OFFSET}px`;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* In the forward direction, the large title should start at its normal size and
|
|
404
|
+
* then scale down to be (roughly) the size of the back button on the other view.
|
|
405
|
+
* In the backward direction, the large title should start at (roughly) the size
|
|
406
|
+
* of the back button and then scale up to its original size.
|
|
407
|
+
* Note that since both elements either fade in or fade out over the course of the
|
|
408
|
+
* animation, neither element will be fully visible on top of the other. As a result,
|
|
409
|
+
* the overlap does not need to be perfect, so approximate values are acceptable here.
|
|
410
|
+
*/
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* When the title and back button texts match then they should overlap during the
|
|
414
|
+
* page transition. If the texts do not match up then the large title text scale
|
|
415
|
+
* adjusts to not perfectly match the back button text otherwise the proportions
|
|
416
|
+
* will be incorrect. When the texts match we scale both the width and height to
|
|
417
|
+
* account for font weight differences between the title and back button.
|
|
418
|
+
*/
|
|
419
|
+
const doTitleAndButtonTextsMatch = backButtonTextEl.textContent?.trim() === largeTitleEl.textContent?.trim();
|
|
420
|
+
|
|
421
|
+
const WIDTH_SCALE = backButtonTextBox.width / largeTitleTextBox.width;
|
|
422
|
+
HEIGHT_SCALE = backButtonTextBox.height / (largeTitleTextBox.height - LARGE_TITLE_SIZE_OFFSET);
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Even though we set TEXT_START_SCALE to HEIGHT_SCALE above, we potentially need
|
|
426
|
+
* to re-compute this here since the HEIGHT_SCALE may have changed.
|
|
427
|
+
*/
|
|
428
|
+
END_SCALE = doTitleAndButtonTextsMatch ? `scale(${WIDTH_SCALE}, ${HEIGHT_SCALE})` : `scale(${HEIGHT_SCALE})`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The midpoints of the back button and the title should align such that the back
|
|
433
|
+
* button and title appear to be centered with each other.
|
|
434
|
+
*/
|
|
435
|
+
const backButtonMidPoint = backButtonBox.top + backButtonBox.height / 2;
|
|
436
|
+
const titleMidPoint = (largeTitleBox.height * HEIGHT_SCALE) / 2;
|
|
437
|
+
const END_TRANSLATE_Y = `${backButtonMidPoint - titleMidPoint}px`;
|
|
438
|
+
|
|
439
|
+
const BACKWARDS_KEYFRAMES = [
|
|
440
|
+
{ offset: 0, opacity: 0, transform: `translate3d(${END_TRANSLATE_X}, ${END_TRANSLATE_Y}, 0) ${END_SCALE}` },
|
|
441
|
+
{ offset: 0.1, opacity: 0 },
|
|
442
|
+
{ offset: 1, opacity: 1, transform: `translate3d(${START_TRANSLATE_X}, ${START_TRANSLATE_Y}, 0) ${START_SCALE}` },
|
|
443
|
+
];
|
|
444
|
+
const FORWARDS_KEYFRAMES = [
|
|
445
|
+
{
|
|
446
|
+
offset: 0,
|
|
447
|
+
opacity: 0.99,
|
|
448
|
+
transform: `translate3d(${START_TRANSLATE_X}, ${START_TRANSLATE_Y}, 0) ${START_SCALE}`,
|
|
449
|
+
},
|
|
450
|
+
{ offset: 0.6, opacity: 0 },
|
|
451
|
+
{ offset: 1, opacity: 0, transform: `translate3d(${END_TRANSLATE_X}, ${END_TRANSLATE_Y}, 0) ${END_SCALE}` },
|
|
452
|
+
];
|
|
453
|
+
|
|
454
|
+
const KEYFRAMES = backDirection ? BACKWARDS_KEYFRAMES : FORWARDS_KEYFRAMES;
|
|
455
|
+
|
|
456
|
+
const clonedTitleEl = getClonedElement<HTMLIonTitleElement>('ion-title')!;
|
|
457
|
+
const clonedLargeTitleAnimation = createAnimation();
|
|
458
|
+
|
|
459
|
+
clonedTitleEl.innerText = largeTitleEl.innerText;
|
|
460
|
+
clonedTitleEl.size = largeTitleEl.size;
|
|
461
|
+
clonedTitleEl.color = largeTitleEl.color;
|
|
462
|
+
|
|
463
|
+
clonedLargeTitleAnimation.addElement(clonedTitleEl);
|
|
464
|
+
|
|
465
|
+
clonedLargeTitleAnimation
|
|
466
|
+
.beforeStyles({
|
|
467
|
+
'transform-origin': `${ORIGIN_X} top`,
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Since font size changes will cause
|
|
471
|
+
* the dimension of the large title to change
|
|
472
|
+
* we need to set the cloned title height
|
|
473
|
+
* equal to that of the original large title height.
|
|
474
|
+
*/
|
|
475
|
+
height: `${largeTitleBox.height}px`,
|
|
476
|
+
display: '',
|
|
477
|
+
position: 'relative',
|
|
478
|
+
[ORIGIN_X]: TITLE_START_OFFSET,
|
|
479
|
+
})
|
|
480
|
+
.beforeAddWrite(() => {
|
|
481
|
+
largeTitleEl.style.setProperty('opacity', '0');
|
|
482
|
+
})
|
|
483
|
+
.afterAddWrite(() => {
|
|
484
|
+
largeTitleEl.style.setProperty('opacity', '');
|
|
485
|
+
clonedTitleEl.style.setProperty('display', 'none');
|
|
486
|
+
})
|
|
487
|
+
.keyframes(KEYFRAMES);
|
|
488
|
+
|
|
489
|
+
rootAnimation.addAnimation(clonedLargeTitleAnimation);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptions): Animation => {
|
|
493
|
+
try {
|
|
494
|
+
const EASING = 'cubic-bezier(0.32,0.72,0,1)';
|
|
495
|
+
const OPACITY = 'opacity';
|
|
496
|
+
const TRANSFORM = 'transform';
|
|
497
|
+
const CENTER = '0%';
|
|
498
|
+
const OFF_OPACITY = 0.8;
|
|
499
|
+
|
|
500
|
+
const isRTL = navEl.ownerDocument.dir === 'rtl';
|
|
501
|
+
const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%';
|
|
502
|
+
const OFF_LEFT = isRTL ? '33%' : '-33%';
|
|
503
|
+
|
|
504
|
+
const enteringEl = opts.enteringEl;
|
|
505
|
+
const leavingEl = opts.leavingEl;
|
|
506
|
+
|
|
507
|
+
const backDirection = opts.direction === 'back';
|
|
508
|
+
const contentEl = enteringEl.querySelector(':scope > ion-content');
|
|
509
|
+
const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
|
|
510
|
+
const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar');
|
|
511
|
+
|
|
512
|
+
const rootAnimation = createAnimation();
|
|
513
|
+
const enteringContentAnimation = createAnimation();
|
|
514
|
+
|
|
515
|
+
rootAnimation
|
|
516
|
+
.addElement(enteringEl)
|
|
517
|
+
.duration((opts.duration ?? 0) || DURATION)
|
|
518
|
+
.easing(opts.easing || EASING)
|
|
519
|
+
.fill('both')
|
|
520
|
+
.beforeRemoveClass('ion-page-invisible');
|
|
521
|
+
|
|
522
|
+
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
|
|
523
|
+
if (leavingEl && navEl !== null && navEl !== undefined) {
|
|
524
|
+
const navDecorAnimation = createAnimation();
|
|
525
|
+
navDecorAnimation.addElement(navEl);
|
|
526
|
+
rootAnimation.addAnimation(navDecorAnimation);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) {
|
|
530
|
+
enteringContentAnimation.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')!); // REVIEW
|
|
531
|
+
} else {
|
|
532
|
+
enteringContentAnimation.addElement(contentEl!); // REVIEW
|
|
533
|
+
enteringContentAnimation.addElement(headerEls);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
rootAnimation.addAnimation(enteringContentAnimation);
|
|
537
|
+
|
|
538
|
+
if (backDirection) {
|
|
539
|
+
enteringContentAnimation
|
|
540
|
+
.beforeClearStyles([OPACITY])
|
|
541
|
+
.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`)
|
|
542
|
+
.fromTo(OPACITY, OFF_OPACITY, 1);
|
|
543
|
+
} else {
|
|
544
|
+
// entering content, forward direction
|
|
545
|
+
enteringContentAnimation.beforeClearStyles([OPACITY]).fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (contentEl) {
|
|
549
|
+
const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect');
|
|
550
|
+
if (enteringTransitionEffectEl) {
|
|
551
|
+
const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover');
|
|
552
|
+
const enteringTransitionShadowEl = enteringTransitionEffectEl.querySelector('.transition-shadow');
|
|
553
|
+
|
|
554
|
+
const enteringTransitionEffect = createAnimation();
|
|
555
|
+
const enteringTransitionCover = createAnimation();
|
|
556
|
+
const enteringTransitionShadow = createAnimation();
|
|
557
|
+
|
|
558
|
+
enteringTransitionEffect
|
|
559
|
+
.addElement(enteringTransitionEffectEl)
|
|
560
|
+
.beforeStyles({ opacity: '1', display: 'block' })
|
|
561
|
+
.afterStyles({ opacity: '', display: '' });
|
|
562
|
+
|
|
563
|
+
enteringTransitionCover
|
|
564
|
+
.addElement(enteringTransitionCoverEl!) // REVIEW
|
|
565
|
+
.beforeClearStyles([OPACITY])
|
|
566
|
+
.fromTo(OPACITY, 0, 0.1);
|
|
567
|
+
|
|
568
|
+
enteringTransitionShadow
|
|
569
|
+
.addElement(enteringTransitionShadowEl!) // REVIEW
|
|
570
|
+
.beforeClearStyles([OPACITY])
|
|
571
|
+
.fromTo(OPACITY, 0.03, 0.7);
|
|
572
|
+
|
|
573
|
+
enteringTransitionEffect.addAnimation([enteringTransitionCover, enteringTransitionShadow]);
|
|
574
|
+
enteringContentAnimation.addAnimation([enteringTransitionEffect]);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-condense');
|
|
579
|
+
|
|
580
|
+
const { forward, backward } = createLargeTitleTransition(rootAnimation, isRTL, backDirection, enteringEl, leavingEl);
|
|
581
|
+
enteringToolBarEls.forEach((enteringToolBarEl) => {
|
|
582
|
+
const enteringToolBar = createAnimation();
|
|
583
|
+
enteringToolBar.addElement(enteringToolBarEl);
|
|
584
|
+
rootAnimation.addAnimation(enteringToolBar);
|
|
585
|
+
|
|
586
|
+
const enteringTitle = createAnimation();
|
|
587
|
+
enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title')!); // REVIEW
|
|
588
|
+
|
|
589
|
+
const enteringToolBarButtons = createAnimation();
|
|
590
|
+
const buttons = Array.from(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
|
|
591
|
+
|
|
592
|
+
const parentHeader = enteringToolBarEl.closest('ion-header');
|
|
593
|
+
const inactiveHeader = parentHeader?.classList.contains('header-collapse-condense-inactive');
|
|
594
|
+
|
|
595
|
+
let buttonsToAnimate;
|
|
596
|
+
if (backDirection) {
|
|
597
|
+
buttonsToAnimate = buttons.filter((button) => {
|
|
598
|
+
const isCollapseButton = button.classList.contains('buttons-collapse');
|
|
599
|
+
return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
|
|
600
|
+
});
|
|
601
|
+
} else {
|
|
602
|
+
buttonsToAnimate = buttons.filter((button) => !button.classList.contains('buttons-collapse'));
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
enteringToolBarButtons.addElement(buttonsToAnimate);
|
|
606
|
+
|
|
607
|
+
const enteringToolBarItems = createAnimation();
|
|
608
|
+
enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'));
|
|
609
|
+
|
|
610
|
+
const enteringToolBarBg = createAnimation();
|
|
611
|
+
enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background')!); // REVIEW
|
|
612
|
+
|
|
613
|
+
const enteringBackButton = createAnimation();
|
|
614
|
+
const backButtonEl = enteringToolBarEl.querySelector('ion-back-button');
|
|
615
|
+
|
|
616
|
+
if (backButtonEl) {
|
|
617
|
+
enteringBackButton.addElement(backButtonEl);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
enteringToolBar.addAnimation([enteringTitle, enteringToolBarButtons, enteringToolBarItems, enteringToolBarBg, enteringBackButton]);
|
|
621
|
+
enteringToolBarButtons.fromTo(OPACITY, 0.01, 1);
|
|
622
|
+
enteringToolBarItems.fromTo(OPACITY, 0.01, 1);
|
|
623
|
+
|
|
624
|
+
if (backDirection) {
|
|
625
|
+
if (!inactiveHeader) {
|
|
626
|
+
enteringTitle.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`).fromTo(OPACITY, 0.01, 1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
enteringToolBarItems.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
|
|
630
|
+
|
|
631
|
+
// back direction, entering page has a back button
|
|
632
|
+
enteringBackButton.fromTo(OPACITY, 0.01, 1);
|
|
633
|
+
} else {
|
|
634
|
+
// entering toolbar, forward direction
|
|
635
|
+
if (!enteringContentHasLargeTitle) {
|
|
636
|
+
enteringTitle.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`).fromTo(OPACITY, 0.01, 1);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
enteringToolBarItems.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
|
|
640
|
+
enteringToolBarBg.beforeClearStyles([OPACITY, 'transform']);
|
|
641
|
+
|
|
642
|
+
const translucentHeader = parentHeader?.translucent;
|
|
643
|
+
if (!translucentHeader) {
|
|
644
|
+
enteringToolBarBg.fromTo(OPACITY, 0.01, 'var(--opacity)');
|
|
645
|
+
} else {
|
|
646
|
+
enteringToolBarBg.fromTo('transform', isRTL ? 'translateX(-100%)' : 'translateX(100%)', 'translateX(0px)');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// forward direction, entering page has a back button
|
|
650
|
+
if (!forward) {
|
|
651
|
+
enteringBackButton.fromTo(OPACITY, 0.01, 1);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (backButtonEl && !forward) {
|
|
655
|
+
const enteringBackBtnText = createAnimation();
|
|
656
|
+
enteringBackBtnText
|
|
657
|
+
.addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW
|
|
658
|
+
.fromTo(`transform`, isRTL ? 'translateX(-100px)' : 'translateX(100px)', 'translateX(0px)');
|
|
659
|
+
|
|
660
|
+
enteringToolBar.addAnimation(enteringBackBtnText);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
// setup leaving view
|
|
666
|
+
if (leavingEl) {
|
|
667
|
+
const leavingContent = createAnimation();
|
|
668
|
+
const leavingContentEl = leavingEl.querySelector(':scope > ion-content');
|
|
669
|
+
const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
|
|
670
|
+
const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');
|
|
671
|
+
|
|
672
|
+
if (!leavingContentEl && leavingToolBarEls.length === 0 && leavingHeaderEls.length === 0) {
|
|
673
|
+
leavingContent.addElement(leavingEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')!); // REVIEW
|
|
674
|
+
} else {
|
|
675
|
+
leavingContent.addElement(leavingContentEl!); // REVIEW
|
|
676
|
+
leavingContent.addElement(leavingHeaderEls);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
rootAnimation.addAnimation(leavingContent);
|
|
680
|
+
|
|
681
|
+
if (backDirection) {
|
|
682
|
+
// leaving content, back direction
|
|
683
|
+
leavingContent
|
|
684
|
+
.beforeClearStyles([OPACITY])
|
|
685
|
+
.fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)');
|
|
686
|
+
|
|
687
|
+
const leavingPage = getIonPageElement(leavingEl) as HTMLElement;
|
|
688
|
+
rootAnimation.afterAddWrite(() => {
|
|
689
|
+
if (rootAnimation.getDirection() === 'normal') {
|
|
690
|
+
leavingPage.style.setProperty('display', 'none');
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
} else {
|
|
694
|
+
// leaving content, forward direction
|
|
695
|
+
leavingContent.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`).fromTo(OPACITY, 1, OFF_OPACITY);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (leavingContentEl) {
|
|
699
|
+
const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect');
|
|
700
|
+
|
|
701
|
+
if (leavingTransitionEffectEl) {
|
|
702
|
+
const leavingTransitionCoverEl = leavingTransitionEffectEl.querySelector('.transition-cover');
|
|
703
|
+
const leavingTransitionShadowEl = leavingTransitionEffectEl.querySelector('.transition-shadow');
|
|
704
|
+
|
|
705
|
+
const leavingTransitionEffect = createAnimation();
|
|
706
|
+
const leavingTransitionCover = createAnimation();
|
|
707
|
+
const leavingTransitionShadow = createAnimation();
|
|
708
|
+
|
|
709
|
+
leavingTransitionEffect
|
|
710
|
+
.addElement(leavingTransitionEffectEl)
|
|
711
|
+
.beforeStyles({ opacity: '1', display: 'block' })
|
|
712
|
+
.afterStyles({ opacity: '', display: '' });
|
|
713
|
+
|
|
714
|
+
leavingTransitionCover
|
|
715
|
+
.addElement(leavingTransitionCoverEl!) // REVIEW
|
|
716
|
+
.beforeClearStyles([OPACITY])
|
|
717
|
+
.fromTo(OPACITY, 0.1, 0);
|
|
718
|
+
|
|
719
|
+
leavingTransitionShadow
|
|
720
|
+
.addElement(leavingTransitionShadowEl!) // REVIEW
|
|
721
|
+
.beforeClearStyles([OPACITY])
|
|
722
|
+
.fromTo(OPACITY, 0.7, 0.03);
|
|
723
|
+
|
|
724
|
+
leavingTransitionEffect.addAnimation([leavingTransitionCover, leavingTransitionShadow]);
|
|
725
|
+
leavingContent.addAnimation([leavingTransitionEffect]);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
leavingToolBarEls.forEach((leavingToolBarEl) => {
|
|
730
|
+
const leavingToolBar = createAnimation();
|
|
731
|
+
leavingToolBar.addElement(leavingToolBarEl);
|
|
732
|
+
|
|
733
|
+
const leavingTitle = createAnimation();
|
|
734
|
+
leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title')!); // REVIEW
|
|
735
|
+
|
|
736
|
+
const leavingToolBarButtons = createAnimation();
|
|
737
|
+
const buttons = leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]');
|
|
738
|
+
|
|
739
|
+
const parentHeader = leavingToolBarEl.closest('ion-header');
|
|
740
|
+
const inactiveHeader = parentHeader?.classList.contains('header-collapse-condense-inactive');
|
|
741
|
+
|
|
742
|
+
const buttonsToAnimate = Array.from(buttons).filter((button) => {
|
|
743
|
+
const isCollapseButton = button.classList.contains('buttons-collapse');
|
|
744
|
+
return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
leavingToolBarButtons.addElement(buttonsToAnimate);
|
|
748
|
+
|
|
749
|
+
const leavingToolBarItems = createAnimation();
|
|
750
|
+
const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])');
|
|
751
|
+
if (leavingToolBarItemEls.length > 0) {
|
|
752
|
+
leavingToolBarItems.addElement(leavingToolBarItemEls);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const leavingToolBarBg = createAnimation();
|
|
756
|
+
leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background')!); // REVIEW
|
|
757
|
+
|
|
758
|
+
const leavingBackButton = createAnimation();
|
|
759
|
+
const backButtonEl = leavingToolBarEl.querySelector('ion-back-button');
|
|
760
|
+
if (backButtonEl) {
|
|
761
|
+
leavingBackButton.addElement(backButtonEl);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
leavingToolBar.addAnimation([leavingTitle, leavingToolBarButtons, leavingToolBarItems, leavingBackButton, leavingToolBarBg]);
|
|
765
|
+
rootAnimation.addAnimation(leavingToolBar);
|
|
766
|
+
|
|
767
|
+
// fade out leaving toolbar items
|
|
768
|
+
leavingBackButton.fromTo(OPACITY, 0.99, 0);
|
|
769
|
+
|
|
770
|
+
leavingToolBarButtons.fromTo(OPACITY, 0.99, 0);
|
|
771
|
+
leavingToolBarItems.fromTo(OPACITY, 0.99, 0);
|
|
772
|
+
|
|
773
|
+
if (backDirection) {
|
|
774
|
+
if (!inactiveHeader) {
|
|
775
|
+
// leaving toolbar, back direction
|
|
776
|
+
leavingTitle
|
|
777
|
+
.fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)')
|
|
778
|
+
.fromTo(OPACITY, 0.99, 0);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
leavingToolBarItems.fromTo('transform', `translateX(${CENTER})`, isRTL ? 'translateX(-100%)' : 'translateX(100%)');
|
|
782
|
+
leavingToolBarBg.beforeClearStyles([OPACITY, 'transform']);
|
|
783
|
+
// leaving toolbar, back direction, and there's no entering toolbar
|
|
784
|
+
// should just slide out, no fading out
|
|
785
|
+
const translucentHeader = parentHeader?.translucent;
|
|
786
|
+
if (!translucentHeader) {
|
|
787
|
+
leavingToolBarBg.fromTo(OPACITY, 'var(--opacity)', 0);
|
|
788
|
+
} else {
|
|
789
|
+
leavingToolBarBg.fromTo('transform', 'translateX(0px)', isRTL ? 'translateX(-100%)' : 'translateX(100%)');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (backButtonEl && !backward) {
|
|
793
|
+
const leavingBackBtnText = createAnimation();
|
|
794
|
+
leavingBackBtnText
|
|
795
|
+
.addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW
|
|
796
|
+
.fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`);
|
|
797
|
+
leavingToolBar.addAnimation(leavingBackBtnText);
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
// leaving toolbar, forward direction
|
|
801
|
+
if (!inactiveHeader) {
|
|
802
|
+
leavingTitle
|
|
803
|
+
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
|
|
804
|
+
.fromTo(OPACITY, 0.99, 0)
|
|
805
|
+
.afterClearStyles([TRANSFORM, OPACITY]);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
leavingToolBarItems
|
|
809
|
+
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
|
|
810
|
+
.afterClearStyles([TRANSFORM, OPACITY]);
|
|
811
|
+
|
|
812
|
+
leavingBackButton.afterClearStyles([OPACITY]);
|
|
813
|
+
leavingTitle.afterClearStyles([OPACITY]);
|
|
814
|
+
leavingToolBarButtons.afterClearStyles([OPACITY]);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return rootAnimation;
|
|
820
|
+
} catch (err) {
|
|
821
|
+
throw err;
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* The scale of the back button during the animation
|
|
827
|
+
* is computed based on the scale of the large title
|
|
828
|
+
* and vice versa. However, we need to account for slight
|
|
829
|
+
* variations in the size of the large title due to
|
|
830
|
+
* padding and font weight. This value should be used to subtract
|
|
831
|
+
* a small amount from the large title height when computing scales
|
|
832
|
+
* to get more accurate scale results.
|
|
833
|
+
*/
|
|
834
|
+
const LARGE_TITLE_SIZE_OFFSET = 10;
|