Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/button-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo/e2e/screenshot.spec.ts-snapshots/button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions demo/src/app/index/pages/button/button.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<ion-back-button defaultHref="/main/index"></ion-back-button>
</ion-buttons>
<ion-title>button</ion-title>
<ion-buttons slot="end">
<ion-button (click)="navigateTo()">Push</ion-button>
</ion-buttons>
</ion-toolbar>
<ion-toolbar>
<ion-buttons slot="end">
Expand Down
8 changes: 7 additions & 1 deletion demo/src/app/index/pages/button/button.page.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, inject, OnInit } from '@angular/core';

import { FormsModule } from '@angular/forms';
import {
Expand All @@ -17,6 +17,7 @@ import {
IonText,
IonTitle,
IonToolbar,
NavController,
} from '@demo/ionic';

@Component({
Expand Down Expand Up @@ -44,7 +45,12 @@ import {
],
})
export class ButtonPage implements OnInit {
readonly navCtrl = inject(NavController);
constructor() {}

ngOnInit() {}

navigateTo() {
return this.navCtrl.navigateForward('/main/index/action-sheet');
}
}
4 changes: 2 additions & 2 deletions src/styles/utils/translucent.scss
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ ion-header.ios:not(.ios26-disabled).header-collapse-condense-inactive:not(.heade
transform: translateY(8px);
}

ion-header.ios:not(.ios26-disabled):not(.header-transitioning).header-translucent::before,
ion-header.ios:not(.ios26-disabled):not(.header-transitioning).header-translucent::after,
ion-header.ios:not(.ios26-disabled).header-translucent::before,
ion-header.ios:not(.ios26-disabled).header-translucent::after,
ion-content.ios:not(.ios26-disabled).content-fullscreen::part(background)::before {
content: '';
position: absolute;
Expand Down
152 changes: 142 additions & 10 deletions src/transition/ios.transition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,109 @@ export const shadow = <T extends Element>(el: T): ShadowRoot | T => {
return el.shadowRoot || el;
};

const animateFixedBackButton = (
root: Animation,
navEl: HTMLElement,
page: HTMLElement,
entering: boolean,
interactive: boolean,
otherPage?: HTMLElement,
) => {
const button = page.querySelector<HTMLIonBackButtonElement>(
':scope > ion-header.header-translucent:not(.ios26-disabled) ion-back-button:not(.ios26-disabled)',
);
if (!button || button.offsetWidth === 0) {
return;
}

const otherButton = otherPage?.querySelector<HTMLIonBackButtonElement>(
':scope > ion-header.header-translucent:not(.ios26-disabled) ion-back-button:not(.ios26-disabled)',
);
const persistent = !!otherButton && otherButton.offsetWidth > 0;
const buttons = [button, ...(persistent ? [otherButton!] : [])].map((element) => ({
element,
visibility: element.style.visibility,
}));
const rect = button.getBoundingClientRect();
const width = button.offsetWidth;
const height = button.offsetHeight;
Comment on lines +41 to +43

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 固定配置は事前計測した座標を使い続ける

複製位置はアニメーション構築時に一度だけ計測されます。開始前にビューポートが変化する環境を対応対象とするなら、位置ずれを確認してください。

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const clone = getClonedElement<HTMLIonBackButtonElement>('ion-back-button');
if (!clone) {
return;
}
const cloneParent = clone.parentNode!;
const cloneNextSibling = clone.nextSibling;
const cloneStyle = clone.getAttribute('style');
clone.icon = button.icon;
clone.text = button.text;
Comment on lines +51 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 固定バックボタンの複製が元の外観を引き継がない

独自の colormode を持つバックボタンでも、複製にはアイコンと文字しか反映されません。遷移中だけ既定値または以前の値で描画され、外観が切り替わります。

Learn more

実ボタンは遷移中に非表示となり、共有の ion-back-button.ion-cloned-element が代わりに表示されます。この共有要素は以前の利用時のプロパティを保持し得ます。既存の バックボタン複製処理textmodeiconcolordisabled をすべて同期していますが、新処理は二つしか同期しません。

Example: color="danger" のバックボタンへ進むと、実ボタンが隠れている間だけ複製が既定色になります。直前に別の複製処理が走っていれば、そのボタンの色が残る場合もあります。

Recommended fix: 表示前に、既存処理と同じく modecolordisabled も元のバックボタンから複製してください。

Suggested change
clone.icon = button.icon;
clone.text = button.text;
clone.icon = button.icon;
clone.text = button.text;
clone.mode = button.mode;
clone.color = button.color;
clone.disabled = button.disabled;
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

clone.mode = button.mode;
clone.color = button.color;
clone.disabled = button.disabled;
const icon = shadow(clone).querySelector('ion-icon');
const animation = createAnimation().addElement(clone);
const fadeStart = !entering && interactive ? 0.8 : 0.4;
const fadeEnd = entering ? 0.96 : interactive ? 1 : 0.95;
if (persistent) {
animation.fromTo('transform', 'scale(1)', 'scale(1)').fromTo('opacity', 1, 1);
} else if (entering) {
animation.keyframes([
{ offset: 0, transform: 'scale(1.2)', opacity: 0 },
{ offset: fadeStart, transform: 'scale(1.2)', opacity: 0 },
{ offset: 0.65, transform: 'scale(1.12)', opacity: 0.15 },
{ offset: 0.9, transform: 'scale(1.02)', opacity: 0.75 },
{ offset: fadeEnd, transform: 'scale(1)', opacity: 1 },
{ offset: 1, transform: 'scale(1)', opacity: 1 },
]);
} else {
animation.keyframes([
{ offset: 0, transform: 'scale(1)', opacity: 1 },
{ offset: fadeStart, transform: 'scale(1)', opacity: 1 },
{ offset: fadeEnd, transform: 'scale(1.2)', opacity: 0 },
{ offset: 1, transform: 'scale(1.2)', opacity: 0 },
]);
}
if (icon && !persistent) {
const from = entering ? 'blur(4px)' : 'blur(0px)';
const to = entering ? 'blur(0px)' : 'blur(4px)';
animation.addAnimation(
createAnimation()
.addElement(icon)
.keyframes([
{ offset: 0, filter: from },
{ offset: fadeStart, filter: from },
{ offset: fadeEnd, filter: to },
{ offset: 1, filter: to },
]),
);
}
root.beforeAddWrite(() => {
Object.assign(clone.style, {
position: 'fixed',
left: `${rect.left + (rect.width - width) / 2}px`,
top: `${rect.top + (rect.height - height) / 2}px`,
width: `${width}px`,
height: `${height}px`,
margin: '0',
pointerEvents: 'none',
visibility: 'visible',
display: getComputedStyle(button).display,
zIndex: '1000',
});
navEl.appendChild(clone);
buttons.forEach(({ element }) => (element.style.visibility = 'hidden'));
});
root.afterAddWrite(() => {
buttons.forEach(({ element, visibility }) => (element.style.visibility = visibility));
cloneParent.insertBefore(clone, cloneNextSibling);
if (cloneStyle === null) {
clone.removeAttribute('style');
} else {
clone.setAttribute('style', cloneStyle);
}
});
root.addAnimation(animation);
};

const getLargeTitle = (refEl: any) => {
const tabs = refEl.tagName === 'ION-TABS' ? refEl : refEl.querySelector('ion-tabs');
const query = 'ion-content ion-header:not(.header-collapse-condense-inactive) ion-title.title-large';
Expand Down Expand Up @@ -519,14 +622,27 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
.fill('both')
.beforeRemoveClass('ion-page-invisible');

const topPage = backDirection ? leavingEl : enteringEl;
if (topPage?.querySelector(':scope > ion-header.header-translucent:not(.ios26-disabled)')) {
const shadow = topPage.style.boxShadow;
rootAnimation.beforeAddWrite(() => {
topPage.style.boxShadow = `${isRTL ? 4 : -4}px 0 24px rgba(0, 0, 0, 0.04)`;
});
rootAnimation.afterAddWrite(() => {
topPage.style.boxShadow = shadow;
});
}

// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
if (leavingEl && navEl !== null && navEl !== undefined) {
const navDecorAnimation = createAnimation();
navDecorAnimation.addElement(navEl);
rootAnimation.addAnimation(navDecorAnimation);
}

if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) {
if (enteringEl.querySelector(':scope > ion-header.header-translucent:not(.ios26-disabled)')) {
enteringContentAnimation.addElement(enteringEl);
} else if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) {
enteringContentAnimation.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')!); // REVIEW
} else {
enteringContentAnimation.addElement(contentEl!); // REVIEW
Expand All @@ -545,7 +661,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
enteringContentAnimation.beforeClearStyles([OPACITY]).fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
}

if (contentEl) {
if (contentEl && !enteringEl.querySelector(':scope > ion-header.header-translucent:not(.ios26-disabled)')) {
const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect');
if (enteringTransitionEffectEl) {
const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover');
Expand Down Expand Up @@ -575,10 +691,23 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
}
}

if (topPage) {
animateFixedBackButton(
rootAnimation,
navEl,
topPage,
!backDirection,
opts.progressCallback !== undefined,
backDirection ? enteringEl : leavingEl,
);
}

const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-condense');

const { forward, backward } = createLargeTitleTransition(rootAnimation, isRTL, backDirection, enteringEl, leavingEl);
enteringToolBarEls.forEach((enteringToolBarEl) => {
if (enteringToolBarEl.closest('ion-header')?.matches('.header-translucent:not(.ios26-disabled)')) {
return;
}
const enteringToolBar = createAnimation();
enteringToolBar.addElement(enteringToolBarEl);
rootAnimation.addAnimation(enteringToolBar);
Expand Down Expand Up @@ -647,11 +776,9 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
}

// forward direction, entering page has a back button
if (!forward) {
enteringBackButton.fromTo(OPACITY, 0.01, 1);
}
enteringBackButton.fromTo(OPACITY, 0.01, 1);

if (backButtonEl && !forward) {
if (backButtonEl) {
const enteringBackBtnText = createAnimation();
enteringBackBtnText
.addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW
Expand All @@ -669,7 +796,9 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar');
const leavingHeaderEls = leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *');

if (!leavingContentEl && leavingToolBarEls.length === 0 && leavingHeaderEls.length === 0) {
if (leavingEl.querySelector(':scope > ion-header.header-translucent:not(.ios26-disabled)')) {
leavingContent.addElement(leavingEl);
} else if (!leavingContentEl && leavingToolBarEls.length === 0 && leavingHeaderEls.length === 0) {
leavingContent.addElement(leavingEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')!); // REVIEW
} else {
leavingContent.addElement(leavingContentEl!); // REVIEW
Expand All @@ -695,7 +824,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
leavingContent.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`).fromTo(OPACITY, 1, OFF_OPACITY);
}

if (leavingContentEl) {
if (leavingContentEl && !leavingEl.querySelector(':scope > ion-header.header-translucent:not(.ios26-disabled)')) {
const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect');

if (leavingTransitionEffectEl) {
Expand Down Expand Up @@ -727,6 +856,9 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
}

leavingToolBarEls.forEach((leavingToolBarEl) => {
if (leavingToolBarEl.closest('ion-header')?.matches('.header-translucent:not(.ios26-disabled)')) {
return;
}
const leavingToolBar = createAnimation();
leavingToolBar.addElement(leavingToolBarEl);

Expand Down Expand Up @@ -789,7 +921,7 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
leavingToolBarBg.fromTo('transform', 'translateX(0px)', isRTL ? 'translateX(-100%)' : 'translateX(100%)');
}

if (backButtonEl && !backward) {
if (backButtonEl) {
const leavingBackBtnText = createAnimation();
leavingBackBtnText
.addElement(shadow(backButtonEl).querySelector('.button-text')!) // REVIEW
Expand Down
Loading