Skip to content

Commit

Permalink
Modal: Optimize content scroll listening in modal (#3163)
Browse files Browse the repository at this point in the history
Co-authored-by: Mark Drastrup <mark.drastrup@gmail.com>
Co-authored-by: Jakob Engelbrecht <jakob@basher.dk>
  • Loading branch information
3 people authored Dec 18, 2023
1 parent 0f14952 commit a9df64d
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
</ion-toolbar>
</ion-header>

<ion-content [scrollEvents]="true">
<ion-content [scrollEvents]="scrollEventsEnabled">
<ion-header *ngIf="_hasCollapsibleTitle" collapse="condense">
<ion-toolbar>
<span class="kirby-text-xlarge" #contentTitle></span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -21,6 +22,8 @@ import {
TitleEmbeddedComponent,
} from './modal-wrapper.testbuilder';

const { getColor } = DesignTokenHelper;

describe('ModalWrapperComponent', () => {
const createComponent = createComponentFactory({
component: ModalWrapperComponent,
Expand Down Expand Up @@ -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'),
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,10 @@ export class ModalWrapperComponent
}
return this._intersectionObserver;
}

scrollEventsEnabled: boolean;
isContentScrolled: boolean;
private contentScrolled$: Observable<ScrollDetail>;

private destroy$: Subject<void> = new Subject<void>();

@HostBinding('class.drawer')
get _isDrawer() {
return this.config.flavor === 'drawer';
Expand Down Expand Up @@ -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 }],
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
});
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,49 @@ export class TitleEmbeddedComponent {
this._title = title;
}
}

@Component({
template: `
<p>
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!
</p>
<p>
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!
</p>
<p>
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!
</p>
<p>
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.
</p>
<p>
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!
</p>
<p>
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!
</p>
<p>
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!
</p>
<p>
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.
</p>
`,
})
export class DummyContentEmbeddedComponent {}

0 comments on commit a9df64d

Please sign in to comment.