Skip to content

Commit 820fa28

Browse files
authored
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`
1 parent f445856 commit 820fa28

File tree

2 files changed

+122
-16
lines changed

2 files changed

+122
-16
lines changed

core/src/components/header/header.ios.scss

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
--opacity-scale: inherit;
4040
}
4141

42+
/**
43+
* Override styles applied during the page transition to prevent
44+
* header flickering.
45+
*/
46+
.header-collapse-fade.header-transitioning ion-toolbar {
47+
--background: transparent;
48+
--border-style: none;
49+
}
50+
4251
// iOS Header - Collapse Condense
4352
// --------------------------------------------------
4453
.header-collapse-condense {
@@ -65,8 +74,6 @@
6574
* since it needs to blend in with the header above it.
6675
*/
6776
.header-collapse-condense ion-toolbar {
68-
--background: var(--ion-background-color, #fff);
69-
7077
z-index: 0;
7178
}
7279

@@ -93,6 +100,28 @@
93100
transition: all 0.2s ease-in-out;
94101
}
95102

103+
/**
104+
* Large title toolbar should just use the content background
105+
* since it needs to blend in with the header above it.
106+
*/
107+
.header-collapse-condense ion-toolbar,
108+
/**
109+
* Override styles applied during the page transition to prevent
110+
* header flickering.
111+
*/
112+
.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
113+
--background: var(--ion-background-color, #fff);
114+
}
115+
116+
/**
117+
* Override styles applied during the page transition to prevent
118+
* header flickering.
119+
*/
120+
.header-collapse-condense-inactive.header-transitioning:not(.header-collapse-condense) ion-toolbar {
121+
--border-style: none;
122+
--opacity-scale: 1;
123+
}
124+
96125
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title,
97126
.header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-buttons.buttons-collapse {
98127
opacity: 0;

core/src/utils/transition/index.ts

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,34 +18,51 @@ const focusController = createFocusController();
1818

1919
// TODO(FW-2832): types
2020

21+
/**
22+
* Executes the main page transition.
23+
* It also manages the lifecycle of header visibility (if any)
24+
* to prevent visual flickering in iOS. The flickering only
25+
* occurs for a condensed header that is placed above the content.
26+
*
27+
* @param opts Options for the transition.
28+
* @returns A promise that resolves when the transition is complete.
29+
*/
2130
export const transition = (opts: TransitionOptions): Promise<TransitionResult> => {
2231
return new Promise((resolve, reject) => {
2332
writeTask(() => {
24-
beforeTransition(opts);
25-
runTransition(opts).then(
26-
(result) => {
27-
if (result.animation) {
28-
result.animation.destroy();
33+
const transitioningInactiveHeader = getIosIonHeader(opts);
34+
beforeTransition(opts, transitioningInactiveHeader);
35+
runTransition(opts)
36+
.then(
37+
(result) => {
38+
if (result.animation) {
39+
result.animation.destroy();
40+
}
41+
afterTransition(opts);
42+
resolve(result);
43+
},
44+
(error) => {
45+
afterTransition(opts);
46+
reject(error);
2947
}
30-
afterTransition(opts);
31-
resolve(result);
32-
},
33-
(error) => {
34-
afterTransition(opts);
35-
reject(error);
36-
}
37-
);
48+
)
49+
.finally(() => {
50+
// Ensure that the header is restored to its original state.
51+
setHeaderTransitionClass(transitioningInactiveHeader, false);
52+
});
3853
});
3954
});
4055
};
4156

42-
const beforeTransition = (opts: TransitionOptions) => {
57+
const beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => {
4358
const enteringEl = opts.enteringEl;
4459
const leavingEl = opts.leavingEl;
4560

4661
focusController.saveViewFocus(leavingEl);
4762

4863
setZIndex(enteringEl, leavingEl, opts.direction);
64+
// Prevent flickering of the header by adding a class.
65+
setHeaderTransitionClass(transitioningInactiveHeader, true);
4966

5067
if (opts.showGoBack) {
5168
enteringEl.classList.add('can-go-back');
@@ -278,6 +295,40 @@ const setZIndex = (
278295
}
279296
};
280297

298+
/**
299+
* Add a class to ensure that the header (if any)
300+
* does not flicker during the transition. By adding the
301+
* transitioning class, we ensure that the header has
302+
* the necessary styles to prevent the following flickers:
303+
* 1. When entering a page with a condensed header, the
304+
* header should never be visible. However,
305+
* it briefly renders the background color while
306+
* the transition is occurring.
307+
* 2. When leaving a page with a condensed header, the
308+
* header has an opacity of 0 and the pages
309+
* have a z-index which causes the entering page to
310+
* briefly show it's content underneath the leaving page.
311+
* 3. When entering a page or leaving a page with a fade
312+
* header, the header should not have a background color.
313+
* However, it briefly shows the background color while
314+
* the transition is occurring.
315+
*
316+
* @param header The header element to modify.
317+
* @param isTransitioning Whether the transition is occurring.
318+
*/
319+
const setHeaderTransitionClass = (header: HTMLElement | null, isTransitioning: boolean) => {
320+
if (!header) {
321+
return;
322+
}
323+
324+
const transitionClass = 'header-transitioning';
325+
if (isTransitioning) {
326+
header.classList.add(transitionClass);
327+
} else {
328+
header.classList.remove(transitionClass);
329+
}
330+
};
331+
281332
export const getIonPageElement = (element: HTMLElement) => {
282333
if (element.classList.contains('ion-page')) {
283334
return element;
@@ -291,6 +342,32 @@ export const getIonPageElement = (element: HTMLElement) => {
291342
return element;
292343
};
293344

345+
/**
346+
* Retrieves the ion-header element from a page based on the
347+
* direction of the transition.
348+
*
349+
* @param opts Options for the transition.
350+
* @returns The ion-header element or null if not found or not in 'ios' mode.
351+
*/
352+
const getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => {
353+
const enteringEl = opts.enteringEl;
354+
const leavingEl = opts.leavingEl;
355+
const direction = opts.direction;
356+
const mode = opts.mode;
357+
358+
if (mode !== 'ios') {
359+
return null;
360+
}
361+
362+
const element = direction === 'back' ? leavingEl : enteringEl;
363+
364+
if (!element) {
365+
return null;
366+
}
367+
368+
return element.querySelector('ion-header');
369+
};
370+
294371
export interface TransitionOptions extends NavOptions {
295372
progressCallback?: (ani: Animation | undefined) => void;
296373
baseEl: any;

0 commit comments

Comments
 (0)