From 8c22485d4975b1510bf83223147acf9a3acc2b09 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 2 Sep 2026 15:17:32 -0300 Subject: [PATCH] feat(modal): enhance modal behavior and viewport handling for iOS devices --- .../modal-host/modal-host.component.spec.ts | 8 +++ .../modal-host/modal-host.component.ts | 71 +++++++++++++++++++ .../modal-shell/modal-shell.component.html | 1 + .../modal-shell/modal-shell.component.scss | 5 +- .../modal-shell/modal-shell.component.ts | 23 ++++++ src/index.html | 23 +++--- 6 files changed, 120 insertions(+), 11 deletions(-) diff --git a/src/app/shared/components/modal-host/modal-host.component.spec.ts b/src/app/shared/components/modal-host/modal-host.component.spec.ts index 4946a52..9f8f583 100644 --- a/src/app/shared/components/modal-host/modal-host.component.spec.ts +++ b/src/app/shared/components/modal-host/modal-host.component.spec.ts @@ -51,6 +51,10 @@ describe('ModalHostComponent', () => { afterEach(() => { doc.body.style.overflow = ''; doc.body.style.paddingRight = ''; + doc.body.style.position = ''; + doc.body.style.top = ''; + doc.body.style.left = ''; + doc.body.style.width = ''; TestBed.resetTestingModule(); }); @@ -99,6 +103,10 @@ describe('ModalHostComponent', () => { expect(service.activeModal()).toBeNull(); expect(doc.body.style.overflow).toBe(''); expect(doc.body.style.paddingRight).toBe(''); + expect(doc.body.style.position).toBe(''); + expect(doc.body.style.top).toBe(''); + expect(doc.body.style.left).toBe(''); + expect(doc.body.style.width).toBe(''); }); it('closes on backdrop click when enabled', () => { diff --git a/src/app/shared/components/modal-host/modal-host.component.ts b/src/app/shared/components/modal-host/modal-host.component.ts index a85e7e9..137fa33 100644 --- a/src/app/shared/components/modal-host/modal-host.component.ts +++ b/src/app/shared/components/modal-host/modal-host.component.ts @@ -55,11 +55,23 @@ export class ModalHostComponent { const body = this.document.body; const previousOverflow = body.style.overflow; const previousPaddingRight = body.style.paddingRight; + const previousPosition = body.style.position; + const previousTop = body.style.top; + const previousLeft = body.style.left; + const previousWidth = body.style.width; const view = this.document.defaultView; + const scrollX = view?.scrollX ?? 0; + const scrollY = view?.scrollY ?? 0; const scrollbarWidth = view ? Math.max(0, view.innerWidth - this.document.documentElement.clientWidth) : 0; + const activeElement = this.document.activeElement; + + if (activeElement instanceof HTMLElement && activeElement !== body) { + activeElement.blur(); + } + if (scrollbarWidth > 0 && view) { const currentPaddingRight = Number.parseFloat(view.getComputedStyle(body).paddingRight) || 0; @@ -68,6 +80,47 @@ export class ModalHostComponent { body.style.overflow = 'hidden'; + const isIos = view + ? /iPad|iPhone|iPod/.test(view.navigator.userAgent) || + (view.navigator.platform === 'MacIntel' && view.navigator.maxTouchPoints > 1) + : false; + + if (isIos) { + body.style.position = 'fixed'; + body.style.top = `${-scrollY}px`; + body.style.left = `${-scrollX}px`; + body.style.width = '100%'; + } + + let viewportFrame: number | undefined; + const viewportSyncTimers: number[] = []; + const syncVisualViewport = () => { + if (!view) { + return; + } + + if (viewportFrame !== undefined) { + view.cancelAnimationFrame(viewportFrame); + } + + viewportFrame = view.requestAnimationFrame(() => { + viewportFrame = undefined; + this.modalShell()?.setVisualViewport(view.visualViewport, view.scrollX, view.scrollY); + }); + }; + + view?.visualViewport?.addEventListener('resize', syncVisualViewport); + view?.visualViewport?.addEventListener('scroll', syncVisualViewport); + view?.addEventListener('orientationchange', syncVisualViewport); + syncVisualViewport(); + + if (view) { + viewportSyncTimers.push( + view.setTimeout(syncVisualViewport, 100), + view.setTimeout(syncVisualViewport, 300), + ); + } + const onKeyDown = (event: KeyboardEvent) => { if (event.key !== 'Escape' || !this.activeModal()?.config.closeOnEscape) { return; @@ -82,8 +135,26 @@ export class ModalHostComponent { onCleanup(() => { this.document.removeEventListener('keydown', onKeyDown); + view?.visualViewport?.removeEventListener('resize', syncVisualViewport); + view?.visualViewport?.removeEventListener('scroll', syncVisualViewport); + view?.removeEventListener('orientationchange', syncVisualViewport); + + if (viewportFrame !== undefined) { + view?.cancelAnimationFrame(viewportFrame); + } + + viewportSyncTimers.forEach((timer) => view?.clearTimeout(timer)); + body.style.overflow = previousOverflow; body.style.paddingRight = previousPaddingRight; + body.style.position = previousPosition; + body.style.top = previousTop; + body.style.left = previousLeft; + body.style.width = previousWidth; + + if (isIos) { + view?.scrollTo(scrollX, scrollY); + } }); }); } diff --git a/src/app/shared/components/modal-shell/modal-shell.component.html b/src/app/shared/components/modal-shell/modal-shell.component.html index 4a11c7d..c5e1fd8 100644 --- a/src/app/shared/components/modal-shell/modal-shell.component.html +++ b/src/app/shared/components/modal-shell/modal-shell.component.html @@ -1,4 +1,5 @@