Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [7.7.0](https://github.com/ionic-team/ionic-framework/compare/v7.6.7...v7.7.0) (2024-01-31)


### Features

* add experimental hardware back button support in browsers ([#28705](https://github.com/ionic-team/ionic-framework/issues/28705)) ([658d1ca](https://github.com/ionic-team/ionic-framework/commit/658d1caccd530350843b85c0e24544ec27dd9eb4)), closes [#28703](https://github.com/ionic-team/ionic-framework/issues/28703)





## [7.6.7](https://github.com/ionic-team/ionic-framework/compare/v7.6.6...v7.6.7) (2024-01-31)


Expand Down
11 changes: 11 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [7.7.0](https://github.com/ionic-team/ionic-framework/compare/v7.6.7...v7.7.0) (2024-01-31)


### Features

* add experimental hardware back button support in browsers ([#28705](https://github.com/ionic-team/ionic-framework/issues/28705)) ([658d1ca](https://github.com/ionic-team/ionic-framework/commit/658d1caccd530350843b85c0e24544ec27dd9eb4)), closes [#28703](https://github.com/ionic-team/ionic-framework/issues/28703)





## [7.6.7](https://github.com/ionic-team/ionic-framework/compare/v7.6.6...v7.6.7) (2024-01-31)


Expand Down
4 changes: 2 additions & 2 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "7.6.7",
"version": "7.7.0",
"description": "Base components for Ionic",
"keywords": [
"ionic",
Expand Down
15 changes: 14 additions & 1 deletion core/src/components/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ComponentInterface } from '@stencil/core';
import { Build, Component, Element, Host, Method, h } from '@stencil/core';
import type { FocusVisibleUtility } from '@utils/focus-visible';
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
import { printIonWarning } from '@utils/logging';
import { isPlatform } from '@utils/platform';

import { config } from '../../global/config';
Expand Down Expand Up @@ -34,9 +36,20 @@ export class App implements ComponentInterface {
import('../../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
}
const hardwareBackButtonModule = await import('../../utils/hardware-back-button');
if (config.getBoolean('hardwareBackButton', isHybrid)) {
const supportsHardwareBackButtonEvents = isHybrid || shoudUseCloseWatcher();
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
hardwareBackButtonModule.startHardwareBackButton();
} else {
/**
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
* then the close watcher will not be used.
*/
if (shoudUseCloseWatcher()) {
printIonWarning(
'experimentalCloseWatcher was set to `true`, but hardwareBackButton was set to `false`. Both config options must be `true` for the Close Watcher API to be used.'
);
}

hardwareBackButtonModule.blockHardwareBackButton();
}
if (typeof (window as any) !== 'undefined') {
Expand Down
8 changes: 7 additions & 1 deletion core/src/components/menu/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core';
import { Build, Component, Element, Event, Host, Listen, Method, Prop, State, Watch, h } from '@stencil/core';
import { getTimeGivenProgression } from '@utils/animation/cubic-bezier';
import { GESTURE_CONTROLLER } from '@utils/gesture';
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';
import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes, assert, clamp, isEndSide as isEnd } from '@utils/helpers';
import { menuController } from '@utils/menu-controller';
Expand Down Expand Up @@ -321,7 +322,6 @@ export class Menu implements ComponentInterface, MenuI {
}
}

@Listen('keydown')
onKeydown(ev: KeyboardEvent) {
if (ev.key === 'Escape') {
this.close();
Expand Down Expand Up @@ -781,8 +781,14 @@ export class Menu implements ComponentInterface, MenuI {
const { type, disabled, isPaneVisible, inheritedAttributes, side } = this;
const mode = getIonMode(this);

/**
* If the Close Watcher is enabled then
* the ionBackButton listener in the menu controller
* will handle closing the menu when Escape is pressed.
*/
return (
<Host
onKeyDown={shoudUseCloseWatcher() ? null : this.onKeydown}
role="navigation"
aria-label={inheritedAttributes['aria-label'] || 'menu'}
class={{
Expand Down
21 changes: 10 additions & 11 deletions core/src/components/split-pane/split-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,16 @@ export class SplitPane implements ComponentInterface {
return;
}

if ((window as any).matchMedia) {
// Listen on media query
const callback = (q: MediaQueryList) => {
this.visible = q.matches;
};

const mediaList = window.matchMedia(mediaQuery);
mediaList.addListener(callback as any);
this.rmL = () => mediaList.removeListener(callback as any);
this.visible = mediaList.matches;
}
// Listen on media query
const callback = (q: MediaQueryList) => {
this.visible = q.matches;
};

const mediaList = window.matchMedia(mediaQuery);
// TODO FW-5869
mediaList.addListener(callback as any);
this.rmL = () => mediaList.removeListener(callback as any);
this.visible = mediaList.matches;
}

private isPane(element: HTMLElement): boolean {
Expand Down
27 changes: 26 additions & 1 deletion core/src/utils/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,32 @@ type IonicEvents = {
): void;
};

type IonicWindow = Window & IonicEvents;
export interface CloseWatcher extends EventTarget {
new (options?: CloseWatcherOptions): any;
requestClose(): void;
close(): void;
destroy(): void;

oncancel: (event: Event) => void | null;
onclose: (event: Event) => void | null;
}

interface CloseWatcherOptions {
signal: AbortSignal;
}

/**
* Experimental browser features that
* are selectively used inside of Ionic
* Since they are experimental they typically
* do not have types yet, so we can add custom ones
* here until types are available.
*/
type ExperimentalWindowFeatures = {
CloseWatcher?: CloseWatcher;
};

type IonicWindow = Window & IonicEvents & ExperimentalWindowFeatures;
type IonicDocument = Document & IonicEvents;

export const win: IonicWindow | undefined = typeof window !== 'undefined' ? window : undefined;
Expand Down
8 changes: 8 additions & 0 deletions core/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ export interface IonicConfig {
*/
platform?: PlatformConfig;

/**
* @experimental
* If `true`, the [CloseWatcher API](https://github.com/WICG/close-watcher) will be used to handle
* all Escape key and hardware back button presses to dismiss menus and overlays and to navigate.
* Note that the `hardwareBackButton` config option must also be `true`.
*/
experimentalCloseWatcher?: boolean;

// PRIVATE configs
keyboardHeight?: number;
inputShims?: boolean;
Expand Down
57 changes: 54 additions & 3 deletions core/src/utils/hardware-back-button.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import { win } from '@utils/browser';
import type { CloseWatcher } from '@utils/browser';

import { config } from '../global/config';

// TODO(FW-2832): type
type Handler = (processNextHandler: () => void) => Promise<any> | void | null;

Expand All @@ -13,6 +18,21 @@ interface HandlerRegister {
id: number;
}

/**
* CloseWatcher is a newer API that lets
* use detect the hardware back button event
* in a web browser: https://caniuse.com/?search=closewatcher
* However, not every browser supports it yet.
*
* This needs to be a function so that we can
* check the config once it has been set.
* Otherwise, this code would be evaluated the
* moment this file is evaluated which could be
* before the config is set.
*/
export const shoudUseCloseWatcher = () =>
config.get('experimentalCloseWatcher', false) && win !== undefined && 'CloseWatcher' in win;

/**
* When hardwareBackButton: false in config,
* we need to make sure we also block the default
Expand All @@ -29,9 +49,9 @@ export const blockHardwareBackButton = () => {

export const startHardwareBackButton = () => {
const doc = document;

let busy = false;
doc.addEventListener('backbutton', () => {

const backButtonCallback = () => {
if (busy) {
return;
}
Expand Down Expand Up @@ -81,7 +101,38 @@ export const startHardwareBackButton = () => {
};

processHandlers();
});
};

/**
* If the CloseWatcher is defined then
* we don't want to also listen for the native
* backbutton event otherwise we may get duplicate
* events firing.
*/
if (shoudUseCloseWatcher()) {
let watcher: CloseWatcher | undefined;

const configureWatcher = () => {
watcher?.destroy();
watcher = new win!.CloseWatcher!();

/**
* Once a close request happens
* the watcher gets destroyed.
* As a result, we need to re-configure
* the watcher so we can respond to other
* close requests.
*/
watcher!.onclose = () => {
backButtonCallback();
configureWatcher();
};
};

configureWatcher();
} else {
doc.addEventListener('backbutton', backButtonCallback);
}
};

export const OVERLAY_BACK_BUTTON_PRIORITY = 100;
Expand Down
4 changes: 2 additions & 2 deletions core/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const transitionEndAsync = (el: HTMLElement | null, expectedDuration = 0)
*/
const transitionEnd = (el: HTMLElement | null, expectedDuration = 0, callback: (ev?: TransitionEvent) => void) => {
let unRegTrans: (() => void) | undefined;
let animationTimeout: any;
let animationTimeout: number | undefined;
const opts: AddEventListenerOptions = { passive: true };
const ANIMATION_FALLBACK_TIMEOUT = 500;

Expand All @@ -45,7 +45,7 @@ const transitionEnd = (el: HTMLElement | null, expectedDuration = 0, callback: (
animationTimeout = setTimeout(onTransitionEnd, expectedDuration + ANIMATION_FALLBACK_TIMEOUT);

unRegTrans = () => {
if (animationTimeout) {
if (animationTimeout !== undefined) {
clearTimeout(animationTimeout);
animationTimeout = undefined;
}
Expand Down
38 changes: 29 additions & 9 deletions core/src/utils/overlays.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { doc } from '@utils/browser';
import type { BackButtonEvent } from '@utils/hardware-back-button';
import { shoudUseCloseWatcher } from '@utils/hardware-back-button';

import { config } from '../global/config';
import { getIonMode } from '../global/ionic-global';
Expand Down Expand Up @@ -353,20 +354,39 @@ const connectListeners = (doc: Document) => {
const lastOverlay = getPresentedOverlay(doc);
if (lastOverlay?.backdropDismiss) {
(ev as BackButtonEvent).detail.register(OVERLAY_BACK_BUTTON_PRIORITY, () => {
return lastOverlay.dismiss(undefined, BACKDROP);
/**
* Do not return this promise otherwise
* the hardware back button utility will
* be blocked until the overlay dismisses.
* This is important for a modal with canDismiss.
* If the application presents a confirmation alert
* in the "canDismiss" callback, then it will be impossible
* to use the hardware back button to dismiss the alert
* dialog because the hardware back button utility
* is blocked on waiting for the modal to dismiss.
*/
lastOverlay.dismiss(undefined, BACKDROP);
});
}
});

// handle ESC to close overlay
doc.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') {
const lastOverlay = getPresentedOverlay(doc);
if (lastOverlay?.backdropDismiss) {
lastOverlay.dismiss(undefined, BACKDROP);
/**
* Handle ESC to close overlay.
* CloseWatcher also handles pressing the Esc
* key, so if a browser supports CloseWatcher then
* this behavior will be handled via the ionBackButton
* event.
*/
if (!shoudUseCloseWatcher()) {
doc.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') {
const lastOverlay = getPresentedOverlay(doc);
if (lastOverlay?.backdropDismiss) {
lastOverlay.dismiss(undefined, BACKDROP);
}
}
}
});
});
}
}
};

Expand Down
Loading