diff --git a/core/src/components/backdrop/backdrop.tsx b/core/src/components/backdrop/backdrop.tsx index 1e566a4bc8..7d85b485cc 100644 --- a/core/src/components/backdrop/backdrop.tsx +++ b/core/src/components/backdrop/backdrop.tsx @@ -1,6 +1,5 @@ import type { ComponentInterface, EventEmitter } from '@stencil/core'; import { Component, Event, Host, Listen, Prop, h } from '@stencil/core'; -import { GESTURE_CONTROLLER } from '@utils/gesture'; import { getIonMode } from '../../global/ionic-global'; @@ -13,10 +12,6 @@ import { getIonMode } from '../../global/ionic-global'; shadow: true, }) export class Backdrop implements ComponentInterface { - private blocker = GESTURE_CONTROLLER.createBlocker({ - disableScroll: true, - }); - /** * If `true`, the backdrop will be visible. */ @@ -37,16 +32,6 @@ export class Backdrop implements ComponentInterface { */ @Event() ionBackdropTap!: EventEmitter; - connectedCallback() { - if (this.stopPropagation) { - this.blocker.block(); - } - } - - disconnectedCallback() { - this.blocker.unblock(); - } - @Listen('click', { passive: false, capture: true }) protected onMouseDown(ev: TouchEvent) { this.emitTap(ev); diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 4925051837..45a03fad95 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -52,7 +52,7 @@ export class Menu implements ComponentInterface, MenuI { width!: number; _isOpen = false; - backdropEl?: HTMLElement; + backdropEl?: HTMLIonBackdropElement; menuInnerEl?: HTMLElement; contentEl?: HTMLElement; lastFocus?: HTMLElement; diff --git a/core/src/utils/gesture/gesture-controller.ts b/core/src/utils/gesture/gesture-controller.ts index 1faad19e5c..32f381008b 100644 --- a/core/src/utils/gesture/gesture-controller.ts +++ b/core/src/utils/gesture/gesture-controller.ts @@ -243,5 +243,5 @@ export interface BlockerConfig { disableScroll?: boolean; } -const BACKDROP_NO_SCROLL = 'backdrop-no-scroll'; +export const BACKDROP_NO_SCROLL = 'backdrop-no-scroll'; export const GESTURE_CONTROLLER = new GestureController(); diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index 07d8fc8c1d..3b452091d2 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -20,6 +20,7 @@ import type { } from '../interface'; import { CoreDelegate } from './framework-delegate'; +import { BACKDROP_NO_SCROLL } from './gesture/gesture-controller'; import { OVERLAY_BACK_BUTTON_PRIORITY } from './hardware-back-button'; import { addEventListener, componentOnReady, focusElement, getElementRoot, removeEventListener } from './helpers'; import { printIonWarning } from './logging'; @@ -471,6 +472,8 @@ export const present = async ( setRootAriaHidden(true); + document.body.classList.add(BACKDROP_NO_SCROLL); + overlay.presented = true; overlay.willPresent.emit(); overlay.willPresentShorthand?.emit(); @@ -549,12 +552,15 @@ export const dismiss = async ( return false; } + const lastOverlay = doc !== undefined && getPresentedOverlays(doc).length === 1; + /** * If this is the last visible overlay then * we want to re-add the root to the accessibility tree. */ - if (doc !== undefined && getPresentedOverlays(doc).length === 1) { + if (lastOverlay) { setRootAriaHidden(false); + document.body.classList.remove(BACKDROP_NO_SCROLL); } overlay.presented = false; @@ -574,6 +580,7 @@ export const dismiss = async ( if (role !== GESTURE) { await overlayAnimation(overlay, animationBuilder, overlay.el, opts); } + overlay.didDismiss.emit({ data, role }); overlay.didDismissShorthand?.emit({ data, role }); diff --git a/core/src/utils/test/overlays/overlays-scroll-blocking.spec.ts b/core/src/utils/test/overlays/overlays-scroll-blocking.spec.ts new file mode 100644 index 0000000000..6de327cd6d --- /dev/null +++ b/core/src/utils/test/overlays/overlays-scroll-blocking.spec.ts @@ -0,0 +1,88 @@ +import { newSpecPage } from '@stencil/core/testing'; + +import { Modal } from '../../../components/modal/modal'; + +describe('overlays: scroll blocking', () => { + it('should not block scroll when the overlay is created', async () => { + const page = await newSpecPage({ + components: [Modal], + html: ` + + `, + }); + + const body = page.doc.querySelector('body')!; + + expect(body).not.toHaveClass('backdrop-no-scroll'); + }); + + it('should block scroll when the overlay is presented', async () => { + const page = await newSpecPage({ + components: [Modal], + html: ` + + `, + }); + + const modal = page.body.querySelector('ion-modal')!; + const body = page.doc.querySelector('body')!; + + await modal.present(); + + expect(body).toHaveClass('backdrop-no-scroll'); + + await modal.dismiss(); + + expect(body).not.toHaveClass('backdrop-no-scroll'); + }); + + it('should not block scroll when the overlay is dismissed', async () => { + const page = await newSpecPage({ + components: [Modal], + html: ` + + `, + }); + + const modal = page.body.querySelector('ion-modal')!; + const body = page.doc.querySelector('body')!; + + await modal.present(); + + expect(body).toHaveClass('backdrop-no-scroll'); + + await modal.dismiss(); + + expect(body).not.toHaveClass('backdrop-no-scroll'); + }); + + it('should not enable scroll until last overlay is dismissed', async () => { + const page = await newSpecPage({ + components: [Modal], + html: ` + + + `, + }); + + const modalOne = page.body.querySelector('#one')!; + const modalTwo = page.body.querySelector('#two')!; + const body = page.doc.querySelector('body')!; + + await modalOne.present(); + + expect(body).toHaveClass('backdrop-no-scroll'); + + await modalTwo.present(); + + expect(body).toHaveClass('backdrop-no-scroll'); + + await modalOne.dismiss(); + + expect(body).toHaveClass('backdrop-no-scroll'); + + await modalTwo.dismiss(); + + expect(body).not.toHaveClass('backdrop-no-scroll'); + }); +});