fix(core): add fallback handler for hardware back button when no router is present (#30878)

Issue number: internal

---------

<!-- 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 new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

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

<!--
  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. -->

---------

Co-authored-by: ShaneK <shane@shanessite.net>
This commit is contained in:
Bernardo Cardoso
2025-12-18 16:21:11 +00:00
committed by GitHub
parent 34bcf95481
commit 1b21e0748a
2 changed files with 58 additions and 1 deletions

View File

@@ -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

View File

@@ -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', () => {