mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Issue number: Resolves #23942 --------- <!-- 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 current behavior? <!-- Please describe the current behavior that you are modifying. --> When an overlay is created (inserted in the DOM), but not presented, the scroll gesture is prevented. This behavior comes from the `connectedCallback` of `ion-backdrop`, where the gesture is prevented as soon as the backdrop is inserted in the DOM. This means in situations where a developer creates an overlay, but does not present it immediately, the user cannot scroll. This is not desired. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Scroll blocking behavior tied to the gesture has been removed from `ion-backdrop` and implemented into the overlays directly. - When an overlay is presented, scroll blocking is enabled on the `body` element (the user cannot scroll on the main content). - When the last presented overlay is dismissed, scroll blocking is disabled on the `body` element (the user can scroll on the main content). ## Does this introduce a breaking change? - [x] Yes - [ ] No `ion-backdrop` no longer prevents scrolling on the main content when the backdrop is either inserted into the DOM or removed from the DOM. Developers using Ionic overlays do not need to migrate their implementations. Developers with custom overlays using `ion-backdrop` internally can either use Ionic's gesture controller to disable scrolling when their overlay is presented/dismissed or can manually add the `backdrop-no-scroll` Ionic global class to the `body` element. <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> ---------
248 lines
5.3 KiB
TypeScript
248 lines
5.3 KiB
TypeScript
class GestureController {
|
|
private gestureId = 0;
|
|
private requestedStart = new Map<number, number>();
|
|
private disabledGestures = new Map<string, Set<number>>();
|
|
private disabledScroll = new Set<number>();
|
|
private capturedId?: number;
|
|
|
|
/**
|
|
* Creates a gesture delegate based on the GestureConfig passed
|
|
*/
|
|
createGesture(config: GestureConfig): GestureDelegate {
|
|
return new GestureDelegate(this, this.newID(), config.name, config.priority ?? 0, !!config.disableScroll);
|
|
}
|
|
|
|
/**
|
|
* Creates a blocker that will block any other gesture events from firing. Set in the ion-gesture component.
|
|
*/
|
|
createBlocker(opts: BlockerConfig = {}): BlockerDelegate {
|
|
return new BlockerDelegate(this, this.newID(), opts.disable, !!opts.disableScroll);
|
|
}
|
|
|
|
start(gestureName: string, id: number, priority: number): boolean {
|
|
if (!this.canStart(gestureName)) {
|
|
this.requestedStart.delete(id);
|
|
return false;
|
|
}
|
|
this.requestedStart.set(id, priority);
|
|
return true;
|
|
}
|
|
|
|
capture(gestureName: string, id: number, priority: number): boolean {
|
|
if (!this.start(gestureName, id, priority)) {
|
|
return false;
|
|
}
|
|
const requestedStart = this.requestedStart;
|
|
let maxPriority = -10000;
|
|
|
|
requestedStart.forEach((value) => {
|
|
maxPriority = Math.max(maxPriority, value);
|
|
});
|
|
|
|
if (maxPriority === priority) {
|
|
this.capturedId = id;
|
|
requestedStart.clear();
|
|
|
|
const event = new CustomEvent('ionGestureCaptured', { detail: { gestureName } });
|
|
document.dispatchEvent(event);
|
|
return true;
|
|
}
|
|
requestedStart.delete(id);
|
|
|
|
return false;
|
|
}
|
|
|
|
release(id: number) {
|
|
this.requestedStart.delete(id);
|
|
|
|
if (this.capturedId === id) {
|
|
this.capturedId = undefined;
|
|
}
|
|
}
|
|
|
|
disableGesture(gestureName: string, id: number) {
|
|
let set = this.disabledGestures.get(gestureName);
|
|
if (set === undefined) {
|
|
set = new Set<number>();
|
|
this.disabledGestures.set(gestureName, set);
|
|
}
|
|
set.add(id);
|
|
}
|
|
|
|
enableGesture(gestureName: string, id: number) {
|
|
const set = this.disabledGestures.get(gestureName);
|
|
if (set !== undefined) {
|
|
set.delete(id);
|
|
}
|
|
}
|
|
|
|
disableScroll(id: number) {
|
|
this.disabledScroll.add(id);
|
|
if (this.disabledScroll.size === 1) {
|
|
document.body.classList.add(BACKDROP_NO_SCROLL);
|
|
}
|
|
}
|
|
|
|
enableScroll(id: number) {
|
|
this.disabledScroll.delete(id);
|
|
if (this.disabledScroll.size === 0) {
|
|
document.body.classList.remove(BACKDROP_NO_SCROLL);
|
|
}
|
|
}
|
|
|
|
canStart(gestureName: string): boolean {
|
|
if (this.capturedId !== undefined) {
|
|
// a gesture already captured
|
|
return false;
|
|
}
|
|
|
|
if (this.isDisabled(gestureName)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
isCaptured(): boolean {
|
|
return this.capturedId !== undefined;
|
|
}
|
|
|
|
isScrollDisabled(): boolean {
|
|
return this.disabledScroll.size > 0;
|
|
}
|
|
|
|
isDisabled(gestureName: string): boolean {
|
|
const disabled = this.disabledGestures.get(gestureName);
|
|
if (disabled && disabled.size > 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private newID(): number {
|
|
this.gestureId++;
|
|
return this.gestureId;
|
|
}
|
|
}
|
|
|
|
class GestureDelegate {
|
|
private ctrl?: GestureController;
|
|
private priority: number;
|
|
|
|
constructor(
|
|
ctrl: GestureController,
|
|
private id: number,
|
|
private name: string,
|
|
priority: number,
|
|
private disableScroll: boolean
|
|
) {
|
|
this.priority = priority * 1000000 + id;
|
|
this.ctrl = ctrl;
|
|
}
|
|
|
|
canStart(): boolean {
|
|
if (!this.ctrl) {
|
|
return false;
|
|
}
|
|
|
|
return this.ctrl.canStart(this.name);
|
|
}
|
|
|
|
start(): boolean {
|
|
if (!this.ctrl) {
|
|
return false;
|
|
}
|
|
|
|
return this.ctrl.start(this.name, this.id, this.priority);
|
|
}
|
|
|
|
capture(): boolean {
|
|
if (!this.ctrl) {
|
|
return false;
|
|
}
|
|
|
|
const captured = this.ctrl.capture(this.name, this.id, this.priority);
|
|
if (captured && this.disableScroll) {
|
|
this.ctrl.disableScroll(this.id);
|
|
}
|
|
|
|
return captured;
|
|
}
|
|
|
|
release() {
|
|
if (this.ctrl) {
|
|
this.ctrl.release(this.id);
|
|
|
|
if (this.disableScroll) {
|
|
this.ctrl.enableScroll(this.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.release();
|
|
this.ctrl = undefined;
|
|
}
|
|
}
|
|
|
|
class BlockerDelegate {
|
|
private ctrl?: GestureController;
|
|
|
|
constructor(
|
|
ctrl: GestureController,
|
|
private id: number,
|
|
private disable: string[] | undefined,
|
|
private disableScroll: boolean
|
|
) {
|
|
this.ctrl = ctrl;
|
|
}
|
|
|
|
block() {
|
|
if (!this.ctrl) {
|
|
return;
|
|
}
|
|
if (this.disable) {
|
|
for (const gesture of this.disable) {
|
|
this.ctrl.disableGesture(gesture, this.id);
|
|
}
|
|
}
|
|
|
|
if (this.disableScroll) {
|
|
this.ctrl.disableScroll(this.id);
|
|
}
|
|
}
|
|
|
|
unblock() {
|
|
if (!this.ctrl) {
|
|
return;
|
|
}
|
|
if (this.disable) {
|
|
for (const gesture of this.disable) {
|
|
this.ctrl.enableGesture(gesture, this.id);
|
|
}
|
|
}
|
|
if (this.disableScroll) {
|
|
this.ctrl.enableScroll(this.id);
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.unblock();
|
|
this.ctrl = undefined;
|
|
}
|
|
}
|
|
|
|
export interface GestureConfig {
|
|
name: string;
|
|
priority?: number;
|
|
disableScroll?: boolean;
|
|
}
|
|
|
|
export interface BlockerConfig {
|
|
disable?: string[];
|
|
disableScroll?: boolean;
|
|
}
|
|
|
|
export const BACKDROP_NO_SCROLL = 'backdrop-no-scroll';
|
|
export const GESTURE_CONTROLLER = new GestureController();
|