From 1a976aa7073bf6eeecb8553f347815254bd97417 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 3 Oct 2025 10:27:00 -0700 Subject: [PATCH 1/3] fix(header): prevent flickering upon page transition on iOS --- core/src/components/header/header.ios.scss | 24 ++++- core/src/utils/transition/index.ts | 101 ++++++++++++++++++--- 2 files changed, 109 insertions(+), 16 deletions(-) diff --git a/core/src/components/header/header.ios.scss b/core/src/components/header/header.ios.scss index cda084da7c4..7a86cea7fa6 100644 --- a/core/src/components/header/header.ios.scss +++ b/core/src/components/header/header.ios.scss @@ -65,8 +65,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 +91,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; diff --git a/core/src/utils/transition/index.ts b/core/src/utils/transition/index.ts index 69789bf862d..41e2b98abc9 100644 --- a/core/src/utils/transition/index.ts +++ b/core/src/utils/transition/index.ts @@ -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 => { 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,36 @@ const setZIndex = ( } }; +/** + * Add a class to ensure that the inactive 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 + * inactive 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 + * inactive 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. + * + * @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 +338,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; From cff681400a300b98bf84e6133b4fe0b6e46fa635 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 3 Oct 2025 13:46:49 -0700 Subject: [PATCH 2/3] fix(header): prevent flickering when using fade --- core/src/components/header/header.ios.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/core/src/components/header/header.ios.scss b/core/src/components/header/header.ios.scss index 7a86cea7fa6..0c0f2007d42 100644 --- a/core/src/components/header/header.ios.scss +++ b/core/src/components/header/header.ios.scss @@ -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 { From b99e370e3b44bca29d8ebe014134468bf8f92f2d Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 6 Oct 2025 09:23:15 -0700 Subject: [PATCH 3/3] docs(header): update comment --- core/src/utils/transition/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/src/utils/transition/index.ts b/core/src/utils/transition/index.ts index 41e2b98abc9..e74e7318c82 100644 --- a/core/src/utils/transition/index.ts +++ b/core/src/utils/transition/index.ts @@ -296,18 +296,22 @@ const setZIndex = ( }; /** - * Add a class to ensure that the inactive header (if any) + * 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 - * inactive header should never be visible. However, + * 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 - * inactive header has an opacity of 0 and the pages + * 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.