From a9df64d96cf11a6f145388efc9c79566d6c5b38d Mon Sep 17 00:00:00 2001 From: AgreSanGit <112690169+AgreSanGit@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:13:44 +0100 Subject: [PATCH] Modal: Optimize content scroll listening in modal (#3163) Co-authored-by: Mark Drastrup Co-authored-by: Jakob Engelbrecht --- .../modal-wrapper.component.html | 2 +- .../modal-wrapper.component.spec.ts | 77 +++++++++++++++++++ .../modal-wrapper/modal-wrapper.component.ts | 52 +++++++------ .../modal-wrapper.testbuilder.ts | 46 +++++++++++ 4 files changed, 154 insertions(+), 23 deletions(-) diff --git a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.html b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.html index b9916e02aa..c7468a89e8 100644 --- a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.html +++ b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.html @@ -28,7 +28,7 @@ - + diff --git a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.spec.ts b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.spec.ts index 449a7554e2..445eb6598d 100644 --- a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.spec.ts +++ b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.spec.ts @@ -12,6 +12,7 @@ import { ButtonComponent } from '@kirbydesign/designsystem/button'; import { PageModule } from '@kirbydesign/designsystem/page'; import { CanDismissHelper, ModalWrapperComponent } from '@kirbydesign/designsystem/modal'; import { + DummyContentEmbeddedComponent, DynamicFooterEmbeddedComponent, DynamicPageProgressEmbeddedComponent, InputEmbeddedComponent, @@ -21,6 +22,8 @@ import { TitleEmbeddedComponent, } from './modal-wrapper.testbuilder'; +const { getColor } = DesignTokenHelper; + describe('ModalWrapperComponent', () => { const createComponent = createComponentFactory({ component: ModalWrapperComponent, @@ -648,4 +651,78 @@ describe('ModalWrapperComponent', () => { }); }); }); + + describe('listenForScroll', () => { + afterEach(() => { + TestHelper.resetTestWindow(); + }); + it('should set scrollEventsEnabled to be false when opened on desktop', async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.desktop); + + modalWrapperTestBuilder = new ModalWrapperTestBuilder(createComponent); + spectator = modalWrapperTestBuilder + .flavor('modal') + .title('test') + .component(TitleEmbeddedComponent) + .build(); + + expect(spectator.component.scrollEventsEnabled).toBeFalse(); + }); + + it('should set scrollEventsEnabled to be true when opened on a phone', async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.phone); + + modalWrapperTestBuilder = new ModalWrapperTestBuilder(createComponent); + spectator = modalWrapperTestBuilder + .flavor('modal') + .title('test') + .component(TitleEmbeddedComponent) + .build(); + + expect(spectator.component.scrollEventsEnabled).toBeTrue(); + }); + + it('should set scrollEventsEnabled to be true when resizing from desktop to phone', async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.desktop); + modalWrapperTestBuilder = new ModalWrapperTestBuilder(createComponent); + spectator = modalWrapperTestBuilder + .flavor('modal') + .title('test') + .component(TitleEmbeddedComponent) + .build(); + expect(spectator.component.scrollEventsEnabled).toBeFalse(); + + await TestHelper.resizeTestWindow(TestHelper.screensize.phone); + await TestHelper.whenTrue(() => spectator.component.scrollEventsEnabled); + + expect(spectator.component.scrollEventsEnabled).toBeTrue(); + }); + + it('should set border-bottom-color on ion-toolbar when scrolling on phone to the bottom or past offset', async () => { + await TestHelper.resizeTestWindow(TestHelper.screensize.phone); + modalWrapperTestBuilder = new ModalWrapperTestBuilder(createComponent); + spectator = modalWrapperTestBuilder + .flavor('modal') + .component(DummyContentEmbeddedComponent) + .build(); + await spectator.fixture.whenStable(); + const MinimumScrollableContentHeight = '500px'; + const ionContentElement = spectator.query('ion-content') as HTMLElement; + ionContentElement.style.minHeight = MinimumScrollableContentHeight; + spectator.detectChanges(); + + spectator.component.scrollToBottom(); + await spectator.fixture.whenStable(); + spectator.detectChanges(); + await TestHelper.whenTrue(() => spectator.component.isContentScrolled); + const ionToolbarInScrolled = document.querySelector( + 'ion-header.content-scrolled ion-toolbar' + ) as HTMLElement; + ionToolbarInScrolled.style.transition = 'none'; + + expect(ionToolbarInScrolled).toHaveComputedStyle({ + 'border-bottom-color': getColor('medium'), + }); + }); + }); }); diff --git a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.ts b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.ts index aa6808059b..2e19da0196 100644 --- a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.ts +++ b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.component.ts @@ -133,12 +133,10 @@ export class ModalWrapperComponent } return this._intersectionObserver; } - + scrollEventsEnabled: boolean; isContentScrolled: boolean; - private contentScrolled$: Observable; private destroy$: Subject = new Subject(); - @HostBinding('class.drawer') get _isDrawer() { return this.config.flavor === 'drawer'; @@ -170,6 +168,7 @@ export class ModalWrapperComponent this.initializeModalRoute(); this.listenForIonModalDidPresent(); this.listenForIonModalWillDismiss(); + this.listenForScroll(); this.initializeResizeModalToModalWrapper(); this.componentPropsInjector = Injector.create({ providers: [{ provide: COMPONENT_PROPS, useValue: this.config.componentProps }], @@ -181,8 +180,6 @@ export class ModalWrapperComponent if (this.toolbarButtonsQuery) { this.toolbarButtons = this.toolbarButtonsQuery.map((buttonRef) => buttonRef.nativeElement); } - - this.initializeContentScrollListening(); } private _currentFooter: HTMLElement | null = null; @@ -343,24 +340,35 @@ export class ModalWrapperComponent } } - /* - * Runs scroll subscription outside zone to avoid excessive amount of CD cycles - * when ionScroll emits. - */ - private initializeContentScrollListening() { - this.zone.runOutsideAngular(() => { - this.contentScrolled$ = this.ionContent.ionScroll.pipe( - debounceTime(contentScrollDebounceTimeInMS), - map((event) => event.detail), - takeUntil(this.destroy$) - ); + private listenForScroll() { + const query = `(min-width: ${DesignTokenHelper.breakpoints.medium})`; + const mediaQuery = this.windowRef.nativeWindow.matchMedia(query); + const enableScrollEvents = (listOrEvent: MediaQueryList | MediaQueryListEvent) => { + const isDesktop = listOrEvent.matches; + this.scrollEventsEnabled = !isDesktop; + }; - this.contentScrolled$.subscribe((scrollInfo: ScrollDetail) => { - if (scrollInfo.scrollTop > contentScrolledOffsetInPixels !== this.isContentScrolled) { - this.isContentScrolled = !this.isContentScrolled; - this.changeDetector.detectChanges(); - } - }); + enableScrollEvents(mediaQuery); + mediaQuery.onchange = enableScrollEvents; + + // Runs scroll subscription outside zone to avoid excessive amount of CD cycles + // when ionScroll emits. + this.zone.runOutsideAngular(() => { + // Always subscribe as ionScroll only emits when scrollEventsEnabled is true + this.ionContent.ionScroll + .pipe( + debounceTime(contentScrollDebounceTimeInMS), + map((event) => event.detail), + takeUntil(this.destroy$) + ) + .subscribe((scrollInfo: ScrollDetail) => { + const contentScrolledPastOffset = scrollInfo.scrollTop > contentScrolledOffsetInPixels; + + if (contentScrolledPastOffset !== this.isContentScrolled) { + this.isContentScrolled = contentScrolledPastOffset; + this.changeDetector.detectChanges(); + } + }); }); } diff --git a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.testbuilder.ts b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.testbuilder.ts index d121920fbf..012f4c1618 100644 --- a/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.testbuilder.ts +++ b/libs/designsystem/modal/src/modal-wrapper/modal-wrapper.testbuilder.ts @@ -159,3 +159,49 @@ export class TitleEmbeddedComponent { this._title = title; } } + +@Component({ + template: ` +

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci animi aperiam deserunt + dolore error esse laborum magni natus nihil optio perferendis placeat, quae sed, sequi sunt + totam voluptatem! Dicta, quaerat! +

+

+ Aut, dignissimos dolorum ducimus et rem reprehenderit rerum sunt ut! Ad aliquid beatae cum + esse et eveniet facere natus numquam obcaecati qui quia quisquam quo repellat repudiandae sit, + soluta voluptatibus! +

+

+ Aspernatur dolore enim incidunt libero molestiae nostrum quasi? Accusamus aut culpa dolores + doloribus laborum nesciunt voluptates! Consectetur cumque doloremque eius esse et excepturi + hic, inventore mollitia nisi, reiciendis, tempora unde! +

+

+ Blanditiis, cupiditate distinctio earum illo impedit laborum velit veritatis. Accusamus + adipisci alias aperiam, assumenda corporis culpa cum debitis exercitationem impedit laborum + possimus quam qui repellat, saepe similique sint soluta. Unde. +

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Adipisci animi aperiam deserunt + dolore error esse laborum magni natus nihil optio perferendis placeat, quae sed, sequi sunt + totam voluptatem! Dicta, quaerat! +

+

+ Aut, dignissimos dolorum ducimus et rem reprehenderit rerum sunt ut! Ad aliquid beatae cum + esse et eveniet facere natus numquam obcaecati qui quia quisquam quo repellat repudiandae sit, + soluta voluptatibus! +

+

+ Aspernatur dolore enim incidunt libero molestiae nostrum quasi? Accusamus aut culpa dolores + doloribus laborum nesciunt voluptates! Consectetur cumque doloremque eius esse et excepturi + hic, inventore mollitia nisi, reiciendis, tempora unde! +

+

+ Blanditiis, cupiditate distinctio earum illo impedit laborum velit veritatis. Accusamus + adipisci alias aperiam, assumenda corporis culpa cum debitis exercitationem impedit laborum + possimus quam qui repellat, saepe similique sint soluta. Unde. +

+ `, +}) +export class DummyContentEmbeddedComponent {}