mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
chore: sync with main
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -29,9 +49,9 @@ export const blockHardwareBackButton = () => {
|
||||
|
||||
export const startHardwareBackButton = () => {
|
||||
const doc = document;
|
||||
|
||||
let busy = false;
|
||||
doc.addEventListener('backbutton', () => {
|
||||
|
||||
const backButtonCallback = () => {
|
||||
if (busy) {
|
||||
return;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -48,37 +48,31 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item>
|
||||
<ion-label>Input Above Keyboard</ion-label>
|
||||
<ion-input id="input-above-keyboard"></ion-input>
|
||||
<ion-input label="Input Above Keyboard" id="input-above-keyboard"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Textarea Above Keyboard</ion-label>
|
||||
<ion-textarea id="textarea-above-keyboard"></ion-textarea>
|
||||
<ion-textarea label="Textarea Above Keyboard" id="textarea-above-keyboard"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<div class="spacer" style="height: 400px"></div>
|
||||
|
||||
<ion-item id="item-below-keyboard">
|
||||
<ion-label>Input Below Keyboard</ion-label>
|
||||
<ion-input id="input-below-keyboard"></ion-input>
|
||||
<ion-input label="Input Below Keyboard" id="input-below-keyboard"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Textarea Below Keyboard</ion-label>
|
||||
<ion-textarea id="textarea-below-keyboard"></ion-textarea>
|
||||
<ion-textarea label="Textarea Below Keyboard" id="textarea-below-keyboard"></ion-textarea>
|
||||
</ion-item>
|
||||
|
||||
<div class="spacer" style="height: 400px"></div>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Input Outside Viewport</ion-label>
|
||||
<ion-input id="input-outside-viewport"></ion-input>
|
||||
<ion-input label="Input Outside Viewport" id="input-outside-viewport"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Textarea Outside Viewport</ion-label>
|
||||
<ion-textarea id="textarea-outside-viewport"></ion-textarea>
|
||||
<ion-textarea label="Textarea Outside Viewport" id="textarea-outside-viewport"></ion-textarea>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-input type="text" placeholder="Enter some text..."></ion-input>
|
||||
<ion-input type="text" placeholder="Enter some text..."></ion-input>
|
||||
<ion-input aria-label="input" type="text" placeholder="Enter some text..."></ion-input>
|
||||
<ion-input aria-label="input" type="text" placeholder="Enter some text..."></ion-input>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
</ion-app>
|
||||
|
||||
@@ -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';
|
||||
@@ -359,20 +360,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -589,6 +609,12 @@ export const dismiss = async <OverlayDismissOptions>(
|
||||
overlay.didDismiss.emit({ data, role });
|
||||
overlay.didDismissShorthand?.emit({ data, role });
|
||||
|
||||
// Get a reference to all animations currently assigned to this overlay
|
||||
// Then tear them down to return the overlay to its initial visual state
|
||||
const animations = activeAnimations.get(overlay) || [];
|
||||
|
||||
animations.forEach((ani) => ani.destroy());
|
||||
|
||||
activeAnimations.delete(overlay);
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { BackButtonEvent } from '../../../src/interface';
|
||||
import { startHardwareBackButton } from '../hardware-back-button';
|
||||
import { config } from '../../global/config';
|
||||
import { win } from '@utils/browser';
|
||||
|
||||
describe('Hardware Back Button', () => {
|
||||
beforeEach(() => startHardwareBackButton());
|
||||
@@ -54,6 +56,56 @@ describe('Hardware Back Button', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Experimental Close Watcher', () => {
|
||||
test('should not use the Close Watcher API when available', () => {
|
||||
const mockAPI = mockCloseWatcher();
|
||||
|
||||
config.reset({ experimentalCloseWatcher: false });
|
||||
|
||||
startHardwareBackButton();
|
||||
|
||||
expect(mockAPI.mock.calls).toHaveLength(0);
|
||||
});
|
||||
test('should use the Close Watcher API when available', () => {
|
||||
const mockAPI = mockCloseWatcher();
|
||||
|
||||
config.reset({ experimentalCloseWatcher: true });
|
||||
|
||||
startHardwareBackButton();
|
||||
|
||||
expect(mockAPI.mock.calls).toHaveLength(1);
|
||||
});
|
||||
test('Close Watcher should dispatch ionBackButton events', () => {
|
||||
const mockAPI = mockCloseWatcher();
|
||||
|
||||
config.reset({ experimentalCloseWatcher: true });
|
||||
|
||||
startHardwareBackButton();
|
||||
|
||||
const cbSpy = jest.fn();
|
||||
document.addEventListener('ionBackButton', cbSpy);
|
||||
|
||||
// Call onclose on Ionic's instance of CloseWatcher
|
||||
mockAPI.getMockImplementation()!().onclose();
|
||||
|
||||
expect(cbSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
const mockCloseWatcher = () => {
|
||||
const mockCloseWatcher = jest.fn();
|
||||
mockCloseWatcher.mockReturnValue({
|
||||
requestClose: () => null,
|
||||
close: () => null,
|
||||
destroy: () => null,
|
||||
oncancel: () => null,
|
||||
onclose: () => null,
|
||||
});
|
||||
(win as any).CloseWatcher = mockCloseWatcher;
|
||||
|
||||
return mockCloseWatcher;
|
||||
};
|
||||
|
||||
const dispatchBackButtonEvent = () => {
|
||||
const ev = new Event('backbutton');
|
||||
document.dispatchEvent(ev);
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item>
|
||||
<ion-label>Text Input</ion-label>
|
||||
<ion-input id="root-input"></ion-input>
|
||||
<ion-input label="Text Input" id="root-input"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-button id="create" onclick="createModal()">Create a Modal</ion-button>
|
||||
@@ -66,8 +65,7 @@
|
||||
Modal Content
|
||||
|
||||
<ion-item>
|
||||
<ion-label>Text Input</ion-label>
|
||||
<ion-input class="modal-input modal-input-${id}"></ion-input>
|
||||
<ion-input label="Text Input" class="modal-input modal-input-${id}"></ion-input>
|
||||
</ion-item>
|
||||
|
||||
<ion-button id="modal-create">Create a Modal</ion-button>
|
||||
|
||||
Reference in New Issue
Block a user