From 1b21e0748a3db243c4bb2e13d4bd9ad2482a73af Mon Sep 17 00:00:00 2001 From: Bernardo Cardoso <32780808+BenOsodrac@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:21:11 +0000 Subject: [PATCH] fix(core): add fallback handler for hardware back button when no router is present (#30878) Issue number: internal --------- ## What is the new behavior? This pull request enhances the hardware back button functionality to ensure a consistent user experience, especially when no custom handlers are registered. The main improvement is the addition of a fallback handler that triggers the default browser back navigation when no other handlers are present. **Hardware Back Button Improvements:** * Added a fallback handler in `startHardwareBackButton` that navigates back in browser history (`win?.history.back()`) if no custom handlers are registered, ensuring the hardware back button always performs a meaningful action. * Introduced a constant `FALLBACK_BACK_BUTTON_PRIORITY` with a value of `-1` to manage the priority of the fallback handler. **Code Consistency:** * Moved the import of `win` from `@utils/browser` to group it with other imports for consistency. ## Does this introduce a breaking change? - [ ] Yes - [x] No ## Other information --------- Co-authored-by: ShaneK --- core/src/utils/hardware-back-button.ts | 18 +++++++- .../utils/test/hardware-back-button.spec.ts | 41 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/core/src/utils/hardware-back-button.ts b/core/src/utils/hardware-back-button.ts index 2c79e3cfaa..25b9423bf8 100644 --- a/core/src/utils/hardware-back-button.ts +++ b/core/src/utils/hardware-back-button.ts @@ -1,5 +1,5 @@ -import { win } from '@utils/browser'; import type { CloseWatcher } from '@utils/browser'; +import { win } from '@utils/browser'; import { printIonError } from '@utils/logging'; import { config } from '../global/config'; @@ -69,6 +69,21 @@ export const startHardwareBackButton = () => { }); doc.dispatchEvent(ev); + /** + * If no handlers have been registered, fall back to the default + * behavior of navigating back in history. This ensures the hardware + * back button works even when no router or custom handler is present. + */ + if (handlers.length === 0) { + handlers.push({ + priority: FALLBACK_BACK_BUTTON_PRIORITY, + handler: () => { + win?.history.back(); + }, + id: index++, + }); + } + const executeAction = async (handlerRegister: HandlerRegister | undefined) => { try { if (handlerRegister?.handler) { @@ -138,3 +153,4 @@ export const startHardwareBackButton = () => { export const OVERLAY_BACK_BUTTON_PRIORITY = 100; export const MENU_BACK_BUTTON_PRIORITY = 99; // 1 less than overlay priority since menu is displayed behind overlays +const FALLBACK_BACK_BUTTON_PRIORITY = -1; // Fallback when no other handlers are registered diff --git a/core/src/utils/test/hardware-back-button.spec.ts b/core/src/utils/test/hardware-back-button.spec.ts index 87f27d407c..bd0885a068 100644 --- a/core/src/utils/test/hardware-back-button.spec.ts +++ b/core/src/utils/test/hardware-back-button.spec.ts @@ -54,6 +54,47 @@ describe('Hardware Back Button', () => { dispatchBackButtonEvent(); expect(cbSpyTwo).toHaveBeenCalled(); }); + + it('should fall back to history.back() when no handlers are registered', () => { + const historyBackSpy = jest.fn(); + const originalBack = win?.history?.back; + if (win?.history) { + win.history.back = historyBackSpy; + } + + // Don't register any ionBackButton handlers + dispatchBackButtonEvent(); + + expect(historyBackSpy).toHaveBeenCalled(); + + // Restore original + if (win?.history && originalBack) { + win.history.back = originalBack; + } + }); + + it('should not call history.back() when a handler is registered', () => { + const historyBackSpy = jest.fn(); + const originalBack = win?.history?.back; + if (win?.history) { + win.history.back = historyBackSpy; + } + + const cbSpy = jest.fn(); + document.addEventListener('ionBackButton', (ev) => { + (ev as BackButtonEvent).detail.register(0, cbSpy); + }); + + dispatchBackButtonEvent(); + + expect(cbSpy).toHaveBeenCalled(); + expect(historyBackSpy).not.toHaveBeenCalled(); + + // Restore original + if (win?.history && originalBack) { + win.history.back = originalBack; + } + }); }); describe('Experimental Close Watcher', () => {