@rdlabo/ionic-theme-ios27 1.2.0-1 → 1.2.0-beta.pr226.shaa2e125f11160
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/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -12
- package/dist/sheets-of-glass/animations.d.ts +13 -4
- package/dist/sheets-of-glass/animations.d.ts.map +1 -1
- package/dist/sheets-of-glass/animations.js +45 -32
- package/dist/sheets-of-glass/index.d.ts +1 -1
- package/dist/sheets-of-glass/index.d.ts.map +1 -1
- package/dist/sheets-of-glass/index.js +40 -19
- package/docs/iphone-duo-with-original-theme.md +17 -3
- package/docs/migration.md +3 -3
- package/docs/native-ui-shell.md +1 -1
- package/package.json +1 -1
- package/src/index.ts +40 -9
- package/src/sheets-of-glass/animations.ts +54 -33
- package/src/sheets-of-glass/index.ts +54 -15
- package/src/styles/vertical-bars.scss +14 -0
package/src/index.ts
CHANGED
|
@@ -17,24 +17,55 @@ export const registerTabBarEffect = (targetElement: HTMLElement): registeredEffe
|
|
|
17
17
|
const reducedMotion = win.matchMedia('(prefers-reduced-motion: reduce)');
|
|
18
18
|
const verticalBarsRoot = targetElement.closest('ion-app');
|
|
19
19
|
let effect: registeredEffect | undefined;
|
|
20
|
+
let placement: 'disabled' | 'horizontal' | 'left' | 'right' | undefined;
|
|
20
21
|
const update = () => {
|
|
21
|
-
effect?.destroy();
|
|
22
22
|
const isVertical =
|
|
23
|
-
!!verticalBarsRoot?.classList.contains('ios-theme-vertical-bars') &&
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
!!verticalBarsRoot?.classList.contains('ios-theme-vertical-bars') &&
|
|
24
|
+
targetElement.matches('ion-tabs > ion-tab-bar:not(.ios-theme-disabled, .ios26-disabled)') &&
|
|
25
|
+
!targetElement.closest('ion-menu, ion-modal, ion-popover');
|
|
26
|
+
const disabled = reducedMotion.matches || !targetElement.matches('.ios:not(.ios-theme-disabled, .ios26-disabled)');
|
|
27
|
+
const nextPlacement = disabled
|
|
28
|
+
? 'disabled'
|
|
29
|
+
: isVertical
|
|
30
|
+
? verticalBarsRoot?.classList.contains('ios-theme-vertical-bars-left')
|
|
31
|
+
? 'left'
|
|
32
|
+
: 'right'
|
|
33
|
+
: 'horizontal';
|
|
34
|
+
// Gesture/activation classes also change during interaction. Rebuild only when
|
|
35
|
+
// eligibility or placement changes, retaining cancellation when the rail changes sides.
|
|
36
|
+
if (placement === nextPlacement) return;
|
|
37
|
+
placement = nextPlacement;
|
|
38
|
+
effect?.destroy();
|
|
39
|
+
effect = disabled
|
|
40
|
+
? undefined
|
|
41
|
+
: registerEffect(
|
|
42
|
+
targetElement,
|
|
43
|
+
'ion-tab-button',
|
|
44
|
+
'tab-selected',
|
|
45
|
+
{
|
|
28
46
|
small: 'scale(1.1, 1)',
|
|
29
47
|
medium: 'scale(1.2)',
|
|
30
48
|
large: 'scale(1.15, 1.4)',
|
|
31
49
|
xlarge: 'scale(1.3)',
|
|
32
|
-
}
|
|
50
|
+
},
|
|
51
|
+
isVertical,
|
|
52
|
+
);
|
|
33
53
|
};
|
|
34
54
|
update();
|
|
35
55
|
reducedMotion.addEventListener('change', update);
|
|
36
|
-
const placementObserver = new win.MutationObserver(
|
|
37
|
-
|
|
56
|
+
const placementObserver = new win.MutationObserver((records) => {
|
|
57
|
+
if (
|
|
58
|
+
records.some((record) =>
|
|
59
|
+
record.type === 'attributes'
|
|
60
|
+
? record.target === targetElement || record.target === verticalBarsRoot
|
|
61
|
+
: [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some((node) => node.contains(targetElement)),
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
update();
|
|
65
|
+
});
|
|
66
|
+
if (verticalBarsRoot)
|
|
67
|
+
placementObserver.observe(verticalBarsRoot, { attributes: true, attributeFilter: ['class'], childList: true, subtree: true });
|
|
68
|
+
placementObserver.observe(targetElement, { attributes: true, attributeFilter: ['class'] });
|
|
38
69
|
return {
|
|
39
70
|
destroy: () => {
|
|
40
71
|
reducedMotion.removeEventListener('change', update);
|
|
@@ -3,6 +3,21 @@ import { AnimationPosition, EffectScales } from './interfaces';
|
|
|
3
3
|
import { createAnimation, GestureDetail } from '@ionic/core';
|
|
4
4
|
import { getStep } from '../utils';
|
|
5
5
|
|
|
6
|
+
// Work in main/cross-axis coordinates so native horizontal samples also drive the rail.
|
|
7
|
+
export const animationRect = (element: Element, vertical = false) => {
|
|
8
|
+
const rect = element.getBoundingClientRect();
|
|
9
|
+
return vertical
|
|
10
|
+
? { left: rect.top, top: rect.left, right: rect.bottom, width: rect.height, height: rect.width, x: rect.y, y: rect.x }
|
|
11
|
+
: rect;
|
|
12
|
+
};
|
|
13
|
+
const translate = (x: number, y: number, vertical: boolean) => `translate3d(${vertical ? y : x}px, ${vertical ? x : y}px, 0)`;
|
|
14
|
+
const stretchTransform = (x: number, y: number, vertical: boolean) => `scale(${vertical ? y : x}, ${vertical ? x : y})`;
|
|
15
|
+
const mainSize = (element: HTMLElement, vertical: boolean) => (vertical ? element.offsetHeight : element.offsetWidth);
|
|
16
|
+
const axisScale = (transform: string, vertical: boolean) => {
|
|
17
|
+
const matrix = new DOMMatrixReadOnly(transform);
|
|
18
|
+
return vertical ? { a: matrix.d, d: matrix.a } : matrix;
|
|
19
|
+
};
|
|
20
|
+
|
|
6
21
|
export const getScaleAnimation = (effectElement: Element): Animation => {
|
|
7
22
|
return createAnimation().addElement(effectElement.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!).easing('ease-out');
|
|
8
23
|
};
|
|
@@ -46,21 +61,27 @@ const tabBarTapFrames = [
|
|
|
46
61
|
];
|
|
47
62
|
|
|
48
63
|
// UITabBarController presentation-layer samples: horizontal stretch precedes vertical stretch.
|
|
49
|
-
export const createTabBarPressAnimation = (
|
|
50
|
-
|
|
64
|
+
export const createTabBarPressAnimation = (
|
|
65
|
+
effect: HTMLElement,
|
|
66
|
+
from: HTMLElement,
|
|
67
|
+
to: HTMLElement,
|
|
68
|
+
bar: HTMLElement,
|
|
69
|
+
vertical = false,
|
|
70
|
+
): Animation => {
|
|
71
|
+
const box = animationRect(bar, vertical);
|
|
51
72
|
const center = box.left + box.width / 2;
|
|
52
|
-
const left = center - bar
|
|
53
|
-
const fromBox = from
|
|
54
|
-
const toBox = to
|
|
55
|
-
const parentScale = box.width / bar
|
|
56
|
-
const buttonScale =
|
|
73
|
+
const left = center - mainSize(bar, vertical) / 2;
|
|
74
|
+
const fromBox = animationRect(from, vertical);
|
|
75
|
+
const toBox = animationRect(to, vertical);
|
|
76
|
+
const parentScale = box.width / mainSize(bar, vertical);
|
|
77
|
+
const buttonScale = axisScale(getComputedStyle(from).transform, vertical);
|
|
57
78
|
const width = fromBox.width / parentScale / buttonScale.a;
|
|
58
79
|
const height = fromBox.height / parentScale / buttonScale.d;
|
|
59
80
|
const start = (fromBox.left + fromBox.width / 2 - box.left) / parentScale;
|
|
60
81
|
const end = (toBox.left + toBox.width / 2 - box.left) / parentScale;
|
|
61
82
|
const selected = from === to;
|
|
62
83
|
const frames = tabBarPressFrames.map(([time, position, extraWidth, extraHeight, expansion]) => {
|
|
63
|
-
const scale = 1 + (14.14 / bar
|
|
84
|
+
const scale = 1 + (14.14 / mainSize(bar, vertical)) * expansion;
|
|
64
85
|
// A selected item expands in place, without the transfer's sideways deformation.
|
|
65
86
|
const press = 1 - Math.exp(-18 * time) * (1 + 18 * time);
|
|
66
87
|
return {
|
|
@@ -73,26 +94,24 @@ export const createTabBarPressAnimation = (effect: HTMLElement, from: HTMLElemen
|
|
|
73
94
|
const move = createAnimation()
|
|
74
95
|
.addElement(effect)
|
|
75
96
|
.beforeStyles({
|
|
76
|
-
width: `${width}px`,
|
|
77
|
-
height: `${height}px`,
|
|
97
|
+
width: `${vertical ? height : width}px`,
|
|
98
|
+
height: `${vertical ? width : height}px`,
|
|
78
99
|
display: 'block',
|
|
79
100
|
opacity: '1',
|
|
80
101
|
})
|
|
81
|
-
.keyframes(
|
|
82
|
-
frames.map(({ offset, x }) => ({ offset, transform: `translate3d(${x}px, ${box.top + box.height / 2 - height / 2}px, 0)` })),
|
|
83
|
-
);
|
|
102
|
+
.keyframes(frames.map(({ offset, x }) => ({ offset, transform: translate(x, box.top + box.height / 2 - height / 2, vertical) })));
|
|
84
103
|
const stretch = getScaleAnimation(effect)
|
|
85
104
|
.easing('linear')
|
|
86
|
-
.keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform:
|
|
105
|
+
.keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform: stretchTransform(sx, sy, vertical) })));
|
|
87
106
|
return createAnimation().duration(900).easing('linear').addAnimation([move, stretch]);
|
|
88
107
|
};
|
|
89
108
|
|
|
90
109
|
// Drag samples: faster movement stretches sideways, then rebounds vertically at rest.
|
|
91
|
-
export const createTabBarDragAnimation = (effect: HTMLElement, bar: HTMLElement, velocity: number): Animation => {
|
|
110
|
+
export const createTabBarDragAnimation = (effect: HTMLElement, bar: HTMLElement, velocity: number, vertical = false): Animation => {
|
|
92
111
|
const native = effect.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!;
|
|
93
|
-
const current =
|
|
94
|
-
const { width, height } = effect
|
|
95
|
-
const scale = 1 + 14.14 / bar
|
|
112
|
+
const current = axisScale(native.style.transform || getComputedStyle(native).transform, vertical);
|
|
113
|
+
const { width, height } = animationRect(effect, vertical);
|
|
114
|
+
const scale = 1 + 14.14 / mainSize(bar, vertical);
|
|
96
115
|
const restX = ((width + 16) * scale) / width;
|
|
97
116
|
const restY = ((height + 16) * scale) / height;
|
|
98
117
|
// Approximate the 100/600pt/s UIKit samples; keep deformation bounded for fast swipes.
|
|
@@ -102,10 +121,10 @@ export const createTabBarDragAnimation = (effect: HTMLElement, bar: HTMLElement,
|
|
|
102
121
|
.duration(500)
|
|
103
122
|
.easing('linear')
|
|
104
123
|
.keyframes([
|
|
105
|
-
{ offset: 0, transform:
|
|
106
|
-
{ offset: 0.2, transform:
|
|
107
|
-
{ offset: 0.44, transform:
|
|
108
|
-
{ offset: 1, transform:
|
|
124
|
+
{ offset: 0, transform: stretchTransform(current.a, current.d, vertical) },
|
|
125
|
+
{ offset: 0.2, transform: stretchTransform(restX + stretch / width, restY - stretch / height, vertical) },
|
|
126
|
+
{ offset: 0.44, transform: stretchTransform(restX - rebound / width, restY + rebound / height, vertical) },
|
|
127
|
+
{ offset: 1, transform: stretchTransform(restX, restY, vertical) },
|
|
109
128
|
]);
|
|
110
129
|
};
|
|
111
130
|
|
|
@@ -114,10 +133,11 @@ export const createTabBarReleaseAnimation = (
|
|
|
114
133
|
effectElement: Element,
|
|
115
134
|
target: HTMLElement,
|
|
116
135
|
tap?: { elapsed: number; distance: number },
|
|
136
|
+
vertical = false,
|
|
117
137
|
): Animation => {
|
|
118
138
|
const tapElapsed = tap?.elapsed;
|
|
119
139
|
const native = effectElement.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!;
|
|
120
|
-
const scale =
|
|
140
|
+
const scale = axisScale(native.style.transform || getComputedStyle(native).transform, vertical);
|
|
121
141
|
const samples = [
|
|
122
142
|
[0, 1],
|
|
123
143
|
[0.033, 0.85],
|
|
@@ -131,11 +151,11 @@ export const createTabBarReleaseAnimation = (
|
|
|
131
151
|
[0.45, 0],
|
|
132
152
|
];
|
|
133
153
|
const bar = target.parentElement!;
|
|
134
|
-
const box = bar
|
|
135
|
-
const current = effectElement
|
|
136
|
-
const targetBox = target
|
|
154
|
+
const box = animationRect(bar, vertical);
|
|
155
|
+
const current = animationRect(effectElement, vertical);
|
|
156
|
+
const targetBox = animationRect(target, vertical);
|
|
137
157
|
const center = box.left + box.width / 2;
|
|
138
|
-
const x = center + (targetBox.left + targetBox.width / 2 - center) / (box.width / bar
|
|
158
|
+
const x = center + (targetBox.left + targetBox.width / 2 - center) / (box.width / mainSize(bar, vertical)) - current.width / 2;
|
|
139
159
|
const y = box.top + box.height / 2 - current.height / 2;
|
|
140
160
|
const elapsed = (tapElapsed ?? 0) / 1000;
|
|
141
161
|
const duration = tapElapsed === undefined ? 450 : 800 - tapElapsed;
|
|
@@ -159,7 +179,7 @@ export const createTabBarReleaseAnimation = (
|
|
|
159
179
|
...tabBarTapFrames.slice(nextIndex).map(([time, position, width, height]) => ({
|
|
160
180
|
offset: (time - elapsed) / (0.8 - elapsed),
|
|
161
181
|
x: x + ((current.x - x) * (1 - position)) / Math.max(0.1, 1 - progress),
|
|
162
|
-
y,
|
|
182
|
+
y: y + ((current.y - y) * (1 - position)) / Math.max(0.1, 1 - progress),
|
|
163
183
|
sx: 1 + (width * (time >= 0.367 ? 1 + 0.75 * extraDistance : 1)) / current.width,
|
|
164
184
|
sy: 1 + (height * (time >= 0.367 ? 1 + 0.5 * extraDistance : 1)) / current.height,
|
|
165
185
|
})),
|
|
@@ -169,7 +189,7 @@ export const createTabBarReleaseAnimation = (
|
|
|
169
189
|
.addElement(native)
|
|
170
190
|
.duration(duration)
|
|
171
191
|
.easing('linear')
|
|
172
|
-
.keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform:
|
|
192
|
+
.keyframes(frames.map(({ offset, sx, sy }) => ({ offset, transform: stretchTransform(sx, sy, vertical) })));
|
|
173
193
|
const move = createAnimation()
|
|
174
194
|
.addElement(effectElement)
|
|
175
195
|
.duration(duration)
|
|
@@ -177,7 +197,7 @@ export const createTabBarReleaseAnimation = (
|
|
|
177
197
|
.keyframes(
|
|
178
198
|
frames.map(({ offset, x, y }) => ({
|
|
179
199
|
offset,
|
|
180
|
-
transform:
|
|
200
|
+
transform: translate(x, y, vertical),
|
|
181
201
|
})),
|
|
182
202
|
);
|
|
183
203
|
// Blend the reflective lens into the selected surface during the final 200ms.
|
|
@@ -204,7 +224,7 @@ export const createTabBarReleaseAnimation = (
|
|
|
204
224
|
if (tapElapsed !== undefined) {
|
|
205
225
|
// A short UIKit tap keeps expanding after lift-off, then settles independently of the lens.
|
|
206
226
|
// Width deltas from the 50ms tap; preserve the rendered size for an uninterrupted handoff.
|
|
207
|
-
const width = bar
|
|
227
|
+
const width = mainSize(bar, vertical);
|
|
208
228
|
const expansion = box.width - width;
|
|
209
229
|
const peak = Math.max(8.65, expansion);
|
|
210
230
|
const platter = createAnimation()
|
|
@@ -282,6 +302,7 @@ export const createMoveAnimation = (
|
|
|
282
302
|
detail: GestureDetail,
|
|
283
303
|
tabSelectedElement: Element,
|
|
284
304
|
animationPosition: AnimationPosition,
|
|
305
|
+
vertical = false,
|
|
285
306
|
): Animation => {
|
|
286
307
|
return createAnimation()
|
|
287
308
|
.duration(500)
|
|
@@ -295,8 +316,8 @@ export const createMoveAnimation = (
|
|
|
295
316
|
})
|
|
296
317
|
.fromTo(
|
|
297
318
|
'transform',
|
|
298
|
-
|
|
299
|
-
|
|
319
|
+
translate(animationPosition.minPositionX, animationPosition.positionY, vertical),
|
|
320
|
+
translate(animationPosition.maxPositionX, animationPosition.positionY, vertical),
|
|
300
321
|
)
|
|
301
322
|
.progressStep(getStep(detail.currentX, animationPosition));
|
|
302
323
|
};
|
|
@@ -4,6 +4,7 @@ import type { Animation, Gesture, GestureDetail } from '@ionic/core';
|
|
|
4
4
|
import { changeSelectedElement, cloneElement, getStep } from '../utils';
|
|
5
5
|
import { isNativeUIShell } from '../native-integration';
|
|
6
6
|
import {
|
|
7
|
+
animationRect,
|
|
7
8
|
createMoveAnimation,
|
|
8
9
|
createPreMoveAnimation,
|
|
9
10
|
createTabBarDragAnimation,
|
|
@@ -21,6 +22,7 @@ export const registerEffect = (
|
|
|
21
22
|
effectTagName: string,
|
|
22
23
|
selectedClassName: string,
|
|
23
24
|
scales: EffectScales,
|
|
25
|
+
vertical = false,
|
|
24
26
|
): registeredEffect | undefined => {
|
|
25
27
|
if (!targetElement.classList.contains('ios')) {
|
|
26
28
|
return undefined;
|
|
@@ -42,6 +44,7 @@ export const registerEffect = (
|
|
|
42
44
|
let destroyed = false;
|
|
43
45
|
const originalTabBarScale = targetElement.style.getPropertyValue('--ios27-tab-bar-scale');
|
|
44
46
|
const effectElement = cloneElement(effectTagName, false);
|
|
47
|
+
if (vertical) effectElement.classList.add('ios27-vertical-tab-effect');
|
|
45
48
|
const stopTabPress = () => {
|
|
46
49
|
if (!tabPressAnimation) return;
|
|
47
50
|
const native = effectElement.shadowRoot!.querySelector<HTMLElement>('[part="native"]')!;
|
|
@@ -122,17 +125,30 @@ export const registerEffect = (
|
|
|
122
125
|
};
|
|
123
126
|
targetElement.addEventListener('pointerdown', onPointerDown);
|
|
124
127
|
|
|
128
|
+
const axisDetail = (detail: GestureDetail): GestureDetail =>
|
|
129
|
+
vertical
|
|
130
|
+
? {
|
|
131
|
+
...detail,
|
|
132
|
+
currentX: detail.currentY,
|
|
133
|
+
currentY: detail.currentX,
|
|
134
|
+
startX: detail.startY,
|
|
135
|
+
startY: detail.startX,
|
|
136
|
+
velocityX: detail.velocityY,
|
|
137
|
+
velocityY: detail.velocityX,
|
|
138
|
+
}
|
|
139
|
+
: detail;
|
|
125
140
|
const createAnimationGesture = () => {
|
|
126
141
|
targetElement.classList.add(GESTURE_NAME);
|
|
127
142
|
gesture = createGesture({
|
|
128
143
|
el: targetElement,
|
|
129
144
|
threshold: 0,
|
|
145
|
+
direction: vertical ? 'y' : 'x',
|
|
130
146
|
gestureName: `${GESTURE_NAME}_${effectTagName}_${crypto.randomUUID()}`,
|
|
131
|
-
onStart: (event) => onStartGesture(event),
|
|
132
|
-
onMove: (event) => onMoveGesture(event),
|
|
147
|
+
onStart: (event) => onStartGesture(axisDetail(event)),
|
|
148
|
+
onMove: (event) => onMoveGesture(axisDetail(event)),
|
|
133
149
|
onEnd: (detail) => {
|
|
134
150
|
// Ionic coalesces moves; consume the release coordinate before committing a drag.
|
|
135
|
-
if (effectTagName === 'ion-tab-button' && tabDragging) onMoveGesture(detail);
|
|
151
|
+
if (effectTagName === 'ion-tab-button' && tabDragging) onMoveGesture(axisDetail(detail));
|
|
136
152
|
onEndGesture().then();
|
|
137
153
|
},
|
|
138
154
|
});
|
|
@@ -160,34 +176,48 @@ export const registerEffect = (
|
|
|
160
176
|
if (isNativeUIShell(targetElement)) return false;
|
|
161
177
|
currentTouchedElement = ((detail.event.target as HTMLElement).closest(effectTagName) as HTMLElement) || undefined;
|
|
162
178
|
const tabSelectedElement = targetElement.querySelector(`${effectTagName}.${selectedClassName}`);
|
|
163
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
currentTouchedElement === undefined ||
|
|
181
|
+
currentTouchedElement.parentElement !== targetElement ||
|
|
182
|
+
(currentTouchedElement as HTMLIonTabButtonElement).disabled ||
|
|
183
|
+
tabSelectedElement === null
|
|
184
|
+
) {
|
|
185
|
+
currentTouchedElement = undefined;
|
|
164
186
|
return false;
|
|
165
187
|
}
|
|
166
188
|
selectedElementBeforeGesture = tabSelectedElement as HTMLElement;
|
|
167
189
|
if (effectTagName === 'ion-tab-button') {
|
|
168
190
|
// UIKit expands the platter by 14.14pt at both two- and four-tab widths.
|
|
169
|
-
targetElement.style.setProperty(
|
|
191
|
+
targetElement.style.setProperty(
|
|
192
|
+
'--ios27-tab-bar-scale',
|
|
193
|
+
String(1 + 14.14 / (vertical ? targetElement.offsetHeight : targetElement.offsetWidth)),
|
|
194
|
+
);
|
|
170
195
|
}
|
|
171
196
|
animationPosition = {
|
|
172
|
-
minPositionX: targetElement
|
|
173
|
-
maxPositionX:
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
minPositionX: animationRect(targetElement, vertical).left,
|
|
198
|
+
maxPositionX:
|
|
199
|
+
animationRect(targetElement, vertical).right - (vertical ? tabSelectedElement.clientHeight : tabSelectedElement.clientWidth),
|
|
200
|
+
width: vertical ? tabSelectedElement.clientHeight : tabSelectedElement.clientWidth,
|
|
201
|
+
positionY: animationRect(tabSelectedElement, vertical).top,
|
|
176
202
|
};
|
|
177
203
|
targetElement.classList.add(ANIMATED_NAME);
|
|
178
204
|
changeSelectedElement(targetElement, currentTouchedElement, effectTagName, selectedClassName);
|
|
179
205
|
|
|
180
206
|
if (effectTagName === 'ion-tab-button') {
|
|
181
|
-
moveAnimation = createMoveAnimation(effectElement, detail, tabSelectedElement, animationPosition);
|
|
207
|
+
moveAnimation = createMoveAnimation(effectElement, detail, tabSelectedElement, animationPosition, vertical);
|
|
182
208
|
moveAnimation.progressStart(
|
|
183
209
|
true,
|
|
184
|
-
getStep(
|
|
210
|
+
getStep(
|
|
211
|
+
animationRect(tabSelectedElement, vertical).left + animationRect(tabSelectedElement, vertical).width / 2,
|
|
212
|
+
animationPosition,
|
|
213
|
+
),
|
|
185
214
|
);
|
|
186
215
|
tabPressAnimation = createTabBarPressAnimation(
|
|
187
216
|
effectElement,
|
|
188
217
|
tabSelectedElement as HTMLElement,
|
|
189
218
|
currentTouchedElement,
|
|
190
219
|
targetElement,
|
|
220
|
+
vertical,
|
|
191
221
|
);
|
|
192
222
|
tabPressStartedAt = performance.now();
|
|
193
223
|
void tabPressAnimation.play();
|
|
@@ -225,7 +255,7 @@ export const registerEffect = (
|
|
|
225
255
|
if (Math.abs(detail.currentX - detail.startX) < 3 && !tabDragging) return;
|
|
226
256
|
tabDragging = true;
|
|
227
257
|
stopTabPress();
|
|
228
|
-
tabPressAnimation = createTabBarDragAnimation(effectElement, targetElement, detail.velocityX);
|
|
258
|
+
tabPressAnimation = createTabBarDragAnimation(effectElement, targetElement, detail.velocityX, vertical);
|
|
229
259
|
void tabPressAnimation.play();
|
|
230
260
|
} else if (scaleAnimationPromise === undefined) {
|
|
231
261
|
if (Math.abs(detail.velocityX) > maxVelocity) {
|
|
@@ -249,11 +279,19 @@ export const registerEffect = (
|
|
|
249
279
|
}
|
|
250
280
|
|
|
251
281
|
const currentX = detail.currentX;
|
|
252
|
-
const previousY = targetElement
|
|
253
|
-
const nextEl = (targetElement.getRootNode() as Document | ShadowRoot).elementFromPoint(
|
|
282
|
+
const previousY = animationRect(targetElement, vertical).top + animationRect(targetElement, vertical).height / 2;
|
|
283
|
+
const nextEl = (targetElement.getRootNode() as Document | ShadowRoot).elementFromPoint(
|
|
284
|
+
vertical ? previousY : currentX,
|
|
285
|
+
vertical ? currentX : previousY,
|
|
286
|
+
);
|
|
254
287
|
const latestTouchedElement = (nextEl?.closest(effectTagName) as HTMLElement) || undefined;
|
|
255
288
|
|
|
256
|
-
if (
|
|
289
|
+
if (
|
|
290
|
+
latestTouchedElement &&
|
|
291
|
+
latestTouchedElement.parentElement === targetElement &&
|
|
292
|
+
!(latestTouchedElement as HTMLIonTabButtonElement).disabled &&
|
|
293
|
+
currentTouchedElement !== latestTouchedElement
|
|
294
|
+
) {
|
|
257
295
|
currentTouchedElement = latestTouchedElement;
|
|
258
296
|
changeSelectedElement(targetElement, currentTouchedElement, effectTagName, selectedClassName);
|
|
259
297
|
}
|
|
@@ -317,6 +355,7 @@ export const registerEffect = (
|
|
|
317
355
|
distance: Math.abs(tabs.indexOf(currentTouchedElement) - tabs.indexOf(selectedElementBeforeGesture!)),
|
|
318
356
|
}
|
|
319
357
|
: undefined,
|
|
358
|
+
vertical,
|
|
320
359
|
);
|
|
321
360
|
currentTouchedElement.classList.remove('ion-activated');
|
|
322
361
|
releaseAnimation = releasing;
|
|
@@ -152,6 +152,10 @@ ion-app.ios-theme-vertical-bars
|
|
|
152
152
|
top: auto;
|
|
153
153
|
bottom: calc(var(--ion-safe-area-bottom, 0px) + var(--ios-theme-side-tab-bar-gap));
|
|
154
154
|
|
|
155
|
+
&.ios27-enable-gesture {
|
|
156
|
+
touch-action: pan-x pinch-zoom;
|
|
157
|
+
}
|
|
158
|
+
|
|
155
159
|
> ion-tab-button:not(.ion-cloned-element) {
|
|
156
160
|
display: block;
|
|
157
161
|
flex: none;
|
|
@@ -162,6 +166,7 @@ ion-app.ios-theme-vertical-bars
|
|
|
162
166
|
border-radius: 23px;
|
|
163
167
|
&::part(native) {
|
|
164
168
|
justify-content: center;
|
|
169
|
+
min-height: 52px;
|
|
165
170
|
}
|
|
166
171
|
ion-label {
|
|
167
172
|
position: absolute;
|
|
@@ -340,3 +345,12 @@ ion-app.ios-theme-vertical-bars.ios-theme-vertical-bars-left {
|
|
|
340
345
|
left: max(4px, calc((var(--ios-theme-vertical-bars-safe-area-left-resolved) - 46px) / 2));
|
|
341
346
|
}
|
|
342
347
|
}
|
|
348
|
+
|
|
349
|
+
// The lens is attached to body, outside the rail selectors.
|
|
350
|
+
ion-tab-button.ios.ion-cloned-element.ios27-vertical-tab-effect {
|
|
351
|
+
border-radius: 23px;
|
|
352
|
+
&::part(native) {
|
|
353
|
+
min-height: 0;
|
|
354
|
+
height: 100%;
|
|
355
|
+
}
|
|
356
|
+
}
|