mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-11-01 01:18:27 +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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
// --------------------------------------------------
|
||||
.header-collapse-condense {
|
||||
@ -65,8 +74,6 @@
|
||||
* since it needs to blend in with the header above it.
|
||||
*/
|
||||
.header-collapse-condense ion-toolbar {
|
||||
--background: var(--ion-background-color, #fff);
|
||||
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@ -93,6 +100,28 @@
|
||||
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-buttons.buttons-collapse {
|
||||
opacity: 0;
|
||||
|
||||
@ -18,34 +18,51 @@ const focusController = createFocusController();
|
||||
|
||||
// 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> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
writeTask(() => {
|
||||
beforeTransition(opts);
|
||||
runTransition(opts).then(
|
||||
(result) => {
|
||||
if (result.animation) {
|
||||
result.animation.destroy();
|
||||
const transitioningInactiveHeader = getIosIonHeader(opts);
|
||||
beforeTransition(opts, transitioningInactiveHeader);
|
||||
runTransition(opts)
|
||||
.then(
|
||||
(result) => {
|
||||
if (result.animation) {
|
||||
result.animation.destroy();
|
||||
}
|
||||
afterTransition(opts);
|
||||
resolve(result);
|
||||
},
|
||||
(error) => {
|
||||
afterTransition(opts);
|
||||
reject(error);
|
||||
}
|
||||
afterTransition(opts);
|
||||
resolve(result);
|
||||
},
|
||||
(error) => {
|
||||
afterTransition(opts);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
)
|
||||
.finally(() => {
|
||||
// Ensure that the header is restored to its original state.
|
||||
setHeaderTransitionClass(transitioningInactiveHeader, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const beforeTransition = (opts: TransitionOptions) => {
|
||||
const beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => {
|
||||
const enteringEl = opts.enteringEl;
|
||||
const leavingEl = opts.leavingEl;
|
||||
|
||||
focusController.saveViewFocus(leavingEl);
|
||||
|
||||
setZIndex(enteringEl, leavingEl, opts.direction);
|
||||
// Prevent flickering of the header by adding a class.
|
||||
setHeaderTransitionClass(transitioningInactiveHeader, true);
|
||||
|
||||
if (opts.showGoBack) {
|
||||
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) => {
|
||||
if (element.classList.contains('ion-page')) {
|
||||
return element;
|
||||
@ -291,6 +342,32 @@ export const getIonPageElement = (element: HTMLElement) => {
|
||||
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 {
|
||||
progressCallback?: (ani: Animation | undefined) => void;
|
||||
baseEl: any;
|
||||
|
||||
Reference in New Issue
Block a user