mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-02 02:35:20 +08:00
fix(header): prevent flickering during iOS page transitions (#30705)
Issue number: resolves #25326 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> The header flickers upon page transition when on iOS mode and using a condensed header: **Entering Page Two (P1 → P2):** When navigating to Page Two, which has a collapsing header (intended to be hidden until scroll), the header briefly flashes into view. This happens because the header is initially rendered with full `opacity: 1` before the component's logic can apply `opacity: 0` to hide it, causing a visible flicker. **Navigating Back (P2 → P1):** When navigating back, Page One's header briefly bleeds through the top of Page Two. Although Page Two is on top (`z−index: 100`), its collapsing header is set to `opacity: 0`. This transparency allows Page One header (`z−index: 99`) to become visible underneath, as the transparent area cannot block the content below it. The header flickers upon page transition when on iOS mode and using a fade header: **Entering Page Two (P1 → P2):** When navigating to Page Two, which has a fade header (should not have a background on load), the header background briefly flashes into view. This happens because the header is initially rendered with full `opacity: 1` before the component's logic can apply `opacity: 0` to hide it, causing a visible flicker. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Added a transition-specific class that is applied to the condensed ion-header element to override its default transparency. This guarantees the header to act as an opaque block during the page transition, eliminating visual flickering caused by early `opacity: 0` or the header underneath bleeding through. - Added a transition-specific class that is applied to the fade ion-header element to override its default opaque background. This guarantees the header to act as a transparent block during the page transition, eliminating visual flickering caused by default `opacity: 1`. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev build: `8.7.6-dev.11759524961.1cff6814`
This commit is contained in:
@ -39,6 +39,15 @@
|
|||||||
--opacity-scale: inherit;
|
--opacity-scale: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override styles applied during the page transition to prevent
|
||||||
|
* header flickering.
|
||||||
|
*/
|
||||||
|
.header-collapse-fade.header-transitioning ion-toolbar {
|
||||||
|
--background: transparent;
|
||||||
|
--border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
// iOS Header - Collapse Condense
|
// iOS Header - Collapse Condense
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
.header-collapse-condense {
|
.header-collapse-condense {
|
||||||
@ -65,8 +74,6 @@
|
|||||||
* since it needs to blend in with the header above it.
|
* since it needs to blend in with the header above it.
|
||||||
*/
|
*/
|
||||||
.header-collapse-condense ion-toolbar {
|
.header-collapse-condense ion-toolbar {
|
||||||
--background: var(--ion-background-color, #fff);
|
|
||||||
|
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +100,28 @@
|
|||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Large title toolbar should just use the content background
|
||||||
|
* since it needs to blend in with the header above it.
|
||||||
|
*/
|
||||||
|
.header-collapse-condense ion-toolbar,
|
||||||
|
/**
|
||||||
|
* Override styles applied during the page transition to prevent
|
||||||
|
* header flickering.
|
||||||
|
*/
|
||||||
|
.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
|
||||||
|
--background: var(--ion-background-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override styles applied during the page transition to prevent
|
||||||
|
* header flickering.
|
||||||
|
*/
|
||||||
|
.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
|
||||||
|
--border-style: none;
|
||||||
|
--opacity-scale: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
|
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
|
||||||
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
|
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@ -18,34 +18,51 @@ const focusController = createFocusController();
|
|||||||
|
|
||||||
// TODO(FW-2832): types
|
// TODO(FW-2832): types
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the main page transition.
|
||||||
|
* It also manages the lifecycle of header visibility (if any)
|
||||||
|
* to prevent visual flickering in iOS. The flickering only
|
||||||
|
* occurs for a condensed header that is placed above the content.
|
||||||
|
*
|
||||||
|
* @param opts Options for the transition.
|
||||||
|
* @returns A promise that resolves when the transition is complete.
|
||||||
|
*/
|
||||||
export const transition = (opts: TransitionOptions): Promise<TransitionResult> => {
|
export const transition = (opts: TransitionOptions): Promise<TransitionResult> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
writeTask(() => {
|
writeTask(() => {
|
||||||
beforeTransition(opts);
|
const transitioningInactiveHeader = getIosIonHeader(opts);
|
||||||
runTransition(opts).then(
|
beforeTransition(opts, transitioningInactiveHeader);
|
||||||
(result) => {
|
runTransition(opts)
|
||||||
if (result.animation) {
|
.then(
|
||||||
result.animation.destroy();
|
(result) => {
|
||||||
|
if (result.animation) {
|
||||||
|
result.animation.destroy();
|
||||||
|
}
|
||||||
|
afterTransition(opts);
|
||||||
|
resolve(result);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
afterTransition(opts);
|
||||||
|
reject(error);
|
||||||
}
|
}
|
||||||
afterTransition(opts);
|
)
|
||||||
resolve(result);
|
.finally(() => {
|
||||||
},
|
// Ensure that the header is restored to its original state.
|
||||||
(error) => {
|
setHeaderTransitionClass(transitioningInactiveHeader, false);
|
||||||
afterTransition(opts);
|
});
|
||||||
reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const beforeTransition = (opts: TransitionOptions) => {
|
const beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => {
|
||||||
const enteringEl = opts.enteringEl;
|
const enteringEl = opts.enteringEl;
|
||||||
const leavingEl = opts.leavingEl;
|
const leavingEl = opts.leavingEl;
|
||||||
|
|
||||||
focusController.saveViewFocus(leavingEl);
|
focusController.saveViewFocus(leavingEl);
|
||||||
|
|
||||||
setZIndex(enteringEl, leavingEl, opts.direction);
|
setZIndex(enteringEl, leavingEl, opts.direction);
|
||||||
|
// Prevent flickering of the header by adding a class.
|
||||||
|
setHeaderTransitionClass(transitioningInactiveHeader, true);
|
||||||
|
|
||||||
if (opts.showGoBack) {
|
if (opts.showGoBack) {
|
||||||
enteringEl.classList.add('can-go-back');
|
enteringEl.classList.add('can-go-back');
|
||||||
@ -278,6 +295,40 @@ const setZIndex = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a class to ensure that the header (if any)
|
||||||
|
* does not flicker during the transition. By adding the
|
||||||
|
* transitioning class, we ensure that the header has
|
||||||
|
* the necessary styles to prevent the following flickers:
|
||||||
|
* 1. When entering a page with a condensed header, the
|
||||||
|
* header should never be visible. However,
|
||||||
|
* it briefly renders the background color while
|
||||||
|
* the transition is occurring.
|
||||||
|
* 2. When leaving a page with a condensed header, the
|
||||||
|
* header has an opacity of 0 and the pages
|
||||||
|
* have a z-index which causes the entering page to
|
||||||
|
* briefly show it's content underneath the leaving page.
|
||||||
|
* 3. When entering a page or leaving a page with a fade
|
||||||
|
* header, the header should not have a background color.
|
||||||
|
* However, it briefly shows the background color while
|
||||||
|
* the transition is occurring.
|
||||||
|
*
|
||||||
|
* @param header The header element to modify.
|
||||||
|
* @param isTransitioning Whether the transition is occurring.
|
||||||
|
*/
|
||||||
|
const setHeaderTransitionClass = (header: HTMLElement | null, isTransitioning: boolean) => {
|
||||||
|
if (!header) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitionClass = 'header-transitioning';
|
||||||
|
if (isTransitioning) {
|
||||||
|
header.classList.add(transitionClass);
|
||||||
|
} else {
|
||||||
|
header.classList.remove(transitionClass);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const getIonPageElement = (element: HTMLElement) => {
|
export const getIonPageElement = (element: HTMLElement) => {
|
||||||
if (element.classList.contains('ion-page')) {
|
if (element.classList.contains('ion-page')) {
|
||||||
return element;
|
return element;
|
||||||
@ -291,6 +342,32 @@ export const getIonPageElement = (element: HTMLElement) => {
|
|||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the ion-header element from a page based on the
|
||||||
|
* direction of the transition.
|
||||||
|
*
|
||||||
|
* @param opts Options for the transition.
|
||||||
|
* @returns The ion-header element or null if not found or not in 'ios' mode.
|
||||||
|
*/
|
||||||
|
const getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => {
|
||||||
|
const enteringEl = opts.enteringEl;
|
||||||
|
const leavingEl = opts.leavingEl;
|
||||||
|
const direction = opts.direction;
|
||||||
|
const mode = opts.mode;
|
||||||
|
|
||||||
|
if (mode !== 'ios') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = direction === 'back' ? leavingEl : enteringEl;
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element.querySelector('ion-header');
|
||||||
|
};
|
||||||
|
|
||||||
export interface TransitionOptions extends NavOptions {
|
export interface TransitionOptions extends NavOptions {
|
||||||
progressCallback?: (ani: Animation | undefined) => void;
|
progressCallback?: (ani: Animation | undefined) => void;
|
||||||
baseEl: any;
|
baseEl: any;
|
||||||
|
|||||||
Reference in New Issue
Block a user