Skip to content

Commit 7ba939f

Browse files
authored
fix(overlays): prevent scroll gestures when the overlay is presented (#28415)
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. --> ---------
1 parent 6cd819a commit 7ba939f

File tree

5 files changed

+98
-18
lines changed

5 files changed

+98
-18
lines changed

core/src/components/backdrop/backdrop.tsx

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { ComponentInterface, EventEmitter } from '@stencil/core';
22
import { Component, Event, Host, Listen, Prop, h } from '@stencil/core';
3-
import { GESTURE_CONTROLLER } from '@utils/gesture';
43

54
import { getIonMode } from '../../global/ionic-global';
65

@@ -13,10 +12,6 @@ import { getIonMode } from '../../global/ionic-global';
1312
shadow: true,
1413
})
1514
export class Backdrop implements ComponentInterface {
16-
private blocker = GESTURE_CONTROLLER.createBlocker({
17-
disableScroll: true,
18-
});
19-
2015
/**
2116
* If `true`, the backdrop will be visible.
2217
*/
@@ -37,16 +32,6 @@ export class Backdrop implements ComponentInterface {
3732
*/
3833
@Event() ionBackdropTap!: EventEmitter<void>;
3934

40-
connectedCallback() {
41-
if (this.stopPropagation) {
42-
this.blocker.block();
43-
}
44-
}
45-
46-
disconnectedCallback() {
47-
this.blocker.unblock();
48-
}
49-
5035
@Listen('click', { passive: false, capture: true })
5136
protected onMouseDown(ev: TouchEvent) {
5237
this.emitTap(ev);

core/src/components/menu/menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export class Menu implements ComponentInterface, MenuI {
5252
width!: number;
5353
_isOpen = false;
5454

55-
backdropEl?: HTMLElement;
55+
backdropEl?: HTMLIonBackdropElement;
5656
menuInnerEl?: HTMLElement;
5757
contentEl?: HTMLElement;
5858
lastFocus?: HTMLElement;

core/src/utils/gesture/gesture-controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,5 +243,5 @@ export interface BlockerConfig {
243243
disableScroll?: boolean;
244244
}
245245

246-
const BACKDROP_NO_SCROLL = 'backdrop-no-scroll';
246+
export const BACKDROP_NO_SCROLL = 'backdrop-no-scroll';
247247
export const GESTURE_CONTROLLER = new GestureController();

core/src/utils/overlays.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
} from '../interface';
2121

2222
import { CoreDelegate } from './framework-delegate';
23+
import { BACKDROP_NO_SCROLL } from './gesture/gesture-controller';
2324
import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button';
2425
import { addEventListener, componentOnReady, focusElement, getElementRoot, removeEventListener } from './helpers';
2526
import { printIonWarning } from './logging';
@@ -471,6 +472,8 @@ export const present = async <OverlayPresentOptions>(
471472

472473
setRootAriaHidden(true);
473474

475+
document.body.classList.add(BACKDROP_NO_SCROLL);
476+
474477
overlay.presented = true;
475478
overlay.willPresent.emit();
476479
overlay.willPresentShorthand?.emit();
@@ -549,12 +552,15 @@ export const dismiss = async <OverlayDismissOptions>(
549552
return false;
550553
}
551554

555+
const lastOverlay = doc !== undefined && getPresentedOverlays(doc).length === 1;
556+
552557
/**
553558
* If this is the last visible overlay then
554559
* we want to re-add the root to the accessibility tree.
555560
*/
556-
if (doc !== undefined && getPresentedOverlays(doc).length === 1) {
561+
if (lastOverlay) {
557562
setRootAriaHidden(false);
563+
document.body.classList.remove(BACKDROP_NO_SCROLL);
558564
}
559565

560566
overlay.presented = false;
@@ -574,6 +580,7 @@ export const dismiss = async <OverlayDismissOptions>(
574580
if (role !== GESTURE) {
575581
await overlayAnimation(overlay, animationBuilder, overlay.el, opts);
576582
}
583+
577584
overlay.didDismiss.emit({ data, role });
578585
overlay.didDismissShorthand?.emit({ data, role });
579586

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { newSpecPage } from '@stencil/core/testing';
2+
3+
import { Modal } from '../../../components/modal/modal';
4+
5+
describe('overlays: scroll blocking', () => {
6+
it('should not block scroll when the overlay is created', async () => {
7+
const page = await newSpecPage({
8+
components: [Modal],
9+
html: `
10+
<ion-modal></ion-modal>
11+
`,
12+
});
13+
14+
const body = page.doc.querySelector('body')!;
15+
16+
expect(body).not.toHaveClass('backdrop-no-scroll');
17+
});
18+
19+
it('should block scroll when the overlay is presented', async () => {
20+
const page = await newSpecPage({
21+
components: [Modal],
22+
html: `
23+
<ion-modal></ion-modal>
24+
`,
25+
});
26+
27+
const modal = page.body.querySelector('ion-modal')!;
28+
const body = page.doc.querySelector('body')!;
29+
30+
await modal.present();
31+
32+
expect(body).toHaveClass('backdrop-no-scroll');
33+
34+
await modal.dismiss();
35+
36+
expect(body).not.toHaveClass('backdrop-no-scroll');
37+
});
38+
39+
it('should not block scroll when the overlay is dismissed', async () => {
40+
const page = await newSpecPage({
41+
components: [Modal],
42+
html: `
43+
<ion-modal></ion-modal>
44+
`,
45+
});
46+
47+
const modal = page.body.querySelector('ion-modal')!;
48+
const body = page.doc.querySelector('body')!;
49+
50+
await modal.present();
51+
52+
expect(body).toHaveClass('backdrop-no-scroll');
53+
54+
await modal.dismiss();
55+
56+
expect(body).not.toHaveClass('backdrop-no-scroll');
57+
});
58+
59+
it('should not enable scroll until last overlay is dismissed', async () => {
60+
const page = await newSpecPage({
61+
components: [Modal],
62+
html: `
63+
<ion-modal id="one"></ion-modal>
64+
<ion-modal id="two"></ion-modal>
65+
`,
66+
});
67+
68+
const modalOne = page.body.querySelector('#one')!;
69+
const modalTwo = page.body.querySelector('#two')!;
70+
const body = page.doc.querySelector('body')!;
71+
72+
await modalOne.present();
73+
74+
expect(body).toHaveClass('backdrop-no-scroll');
75+
76+
await modalTwo.present();
77+
78+
expect(body).toHaveClass('backdrop-no-scroll');
79+
80+
await modalOne.dismiss();
81+
82+
expect(body).toHaveClass('backdrop-no-scroll');
83+
84+
await modalTwo.dismiss();
85+
86+
expect(body).not.toHaveClass('backdrop-no-scroll');
87+
});
88+
});

0 commit comments

Comments
 (0)