Skip to content

Commit

Permalink
Merge branch 'master' into feature-kw-settings-page
Browse files Browse the repository at this point in the history
  • Loading branch information
esmeetewinkel authored Dec 31, 2024
2 parents 1f75db5 + a859eb4 commit ea8dff6
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 25 deletions.
43 changes: 43 additions & 0 deletions src/app/feature/nav-stack/nav-stack-back.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { computed, effect, Injectable } from "@angular/core";
import { NavStackService } from "./nav-stack.service";
import { App } from "@capacitor/app";
import { Capacitor, PluginListenerHandle } from "@capacitor/core";

@Injectable({ providedIn: "root" })
/**
* Utility service to help manage intercepting back button presses when working with nav stacks
* Currently just supports hardware back button press on android, used to dismiss top nav stack
*/
export class NavStackBackService {
/** Track whether navStacks are currently open or not to enable back interception */
private navStacksOpen = computed(() => this.navStackService.openNavStacks().length > 0);

private backButtonListener: PluginListenerHandle;

constructor(private navStackService: NavStackService) {
effect(async () => {
const platform = Capacitor.getPlatform();
if (platform !== "android") return;

if (this.navStacksOpen()) {
await this.addBackButtonListener();
} else {
await this.removeBackButtonListener();
}
});
}

private async addBackButtonListener() {
const listener = await App.addListener("backButton", async (e) => {
await this.navStackService.closeTopNavStack();
});
this.backButtonListener = listener;
}

private async removeBackButtonListener() {
if (this.backButtonListener) {
await this.backButtonListener.remove();
this.backButtonListener = undefined;
}
}
}
8 changes: 7 additions & 1 deletion src/app/feature/nav-stack/nav-stack.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import { TemplateComponentsModule } from "../../shared/components/template/templ
import { TemplateActionRegistry } from "src/app/shared/components/template/services/instance/template-action.registry";
import { NavStackService } from "./nav-stack.service";
import { NavStackActionFactory } from "./nav-stack.actions";
import { NavStackBackService } from "./nav-stack-back.service";

@NgModule({
declarations: [NavStackComponent],
imports: [CommonModule, IonicModule, TemplateComponentModule, TemplateComponentsModule],
})
export class NavStackModule {
constructor(templateActionRegistry: TemplateActionRegistry, navStackService: NavStackService) {
constructor(
templateActionRegistry: TemplateActionRegistry,
navStackService: NavStackService,
// include navStackBack service to enable back button management
navStackBackService: NavStackBackService
) {
const { nav_stack } = new NavStackActionFactory(navStackService);
templateActionRegistry.register({ nav_stack });
}
Expand Down
14 changes: 7 additions & 7 deletions src/app/feature/nav-stack/nav-stack.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe("NavStackService", () => {
const modalSpy = await pushNavStack();

expect(modalSpy.present).toHaveBeenCalled();
expect(service["openNavStacks"].length).toEqual(1);
expect(service.openNavStacks().length).toEqual(1);
});

it("tracks stack index as attributes", async () => {
Expand All @@ -82,17 +82,17 @@ describe("NavStackService", () => {
const modalSpy1 = await pushNavStack();
const modalSpy2 = await pushNavStack();

expect(service["openNavStacks"].length).toEqual(2);
expect(service.openNavStacks().length).toEqual(2);
expect(modalSpy1.getAttribute("data-nav-stack-index")).toEqual("0");
expect(modalSpy2.getAttribute("data-nav-stack-index")).toEqual("1");
});

it("should remove the modal from service['openNavStacks'] on dismissal", async () => {
it("should remove the modal from service.openNavStacks on dismissal", async () => {
const modalSpy = await pushNavStack();
await service.closeTopNavStack();

expect(modalSpy.dismiss).toHaveBeenCalled();
expect(service["openNavStacks"].length).toBe(0);
expect(service.openNavStacks().length).toBe(0);
});

it("should close the top modal in the stack", async () => {
Expand All @@ -103,8 +103,8 @@ describe("NavStackService", () => {

expect(modalSpy1.dismiss).not.toHaveBeenCalled();
expect(modalSpy2.dismiss).toHaveBeenCalled();
expect(service["openNavStacks"].length).toBe(1);
expect(service["openNavStacks"][0]).toBe(modalSpy1);
expect(service.openNavStacks().length).toBe(1);
expect(service.openNavStacks()[0]).toBe(modalSpy1);
});

it("should close all modals in the stack", async () => {
Expand All @@ -115,6 +115,6 @@ describe("NavStackService", () => {

expect(modalSpy1.dismiss).toHaveBeenCalled();
expect(modalSpy2.dismiss).toHaveBeenCalled();
expect(service["openNavStacks"].length).toBe(0);
expect(service.openNavStacks().length).toBe(0);
});
});
58 changes: 41 additions & 17 deletions src/app/feature/nav-stack/nav-stack.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Injectable } from "@angular/core";
import { Injectable, signal } from "@angular/core";
import { ModalController } from "@ionic/angular";
import { INavStackConfig, NavStackComponent } from "./components/nav-stack/nav-stack.component";
import { SyncServiceBase } from "src/app/shared/services/syncService.base";
Expand All @@ -9,7 +9,8 @@ interface NavStackModal extends HTMLIonModalElement {}
providedIn: "root",
})
export class NavStackService extends SyncServiceBase {
private openNavStacks: HTMLIonModalElement[] = [];
public openNavStacks = signal<HTMLIonModalElement[]>([]);
private readonly MAX_NAV_STACKS = 10;

constructor(private modalCtrl: ModalController) {
super("navStack");
Expand All @@ -21,18 +22,22 @@ export class NavStackService extends SyncServiceBase {
* Await and remove from openNavStacks array on dismiss
*/
public async pushNavStack(navStackConfig: INavStackConfig) {
if (this.openNavStacks().length >= this.MAX_NAV_STACKS) {
console.warn(`[NAV STACK] Maximum number of nav stacks reached: ${this.MAX_NAV_STACKS}`);
return null;
}
const modal = await this.createNavStackModal(navStackConfig);
await this.presentAndTrackModal(modal);
return modal;
}

public async closeAllNavStacks() {
await Promise.all(this.openNavStacks.map(async (navStack) => await navStack.dismiss()));
public closeAllNavStacks() {
return Promise.all(this.openNavStacks().map((navStack) => navStack.dismiss()));
}

public async closeTopNavStack() {
if (this.openNavStacks.length === 0) return;
await this.closeNavStack(this.openNavStacks.length - 1);
if (this.openNavStacks().length === 0) return;
await this.closeNavStack(this.openNavStacks().length - 1);
}

/**
Expand All @@ -49,26 +54,45 @@ export class NavStackService extends SyncServiceBase {
}

private async presentAndTrackModal(modal: NavStackModal) {
const navStackIndex = this.openNavStacks.length;
const navStackIndex = this.openNavStacks().length;
modal.setAttribute("data-nav-stack-index", navStackIndex.toString());
modal.style.setProperty("--nav-stack-index", navStackIndex.toString());

// Remove array entry whenever modal is dismissed
modal.onWillDismiss().then(() => {
const index = this.getNavStackIndex(modal);
if (index === -1) return;
this.openNavStacks.splice(index, 1);
});
// Handle nav stack dismissal here (whether programmatically from service or from nav-stack component)
modal.onWillDismiss().then(() => this.handleNavStackDismissal(modal));

this.addNavStackToArray(modal);
await modal.present();
this.openNavStacks.push(modal);
}

private handleNavStackDismissal(modal: NavStackModal) {
const index = this.getNavStackIndex(modal);
if (index === -1) return;
this.removeNavStackFromArray(index);
}

private getNavStackIndex(modalElement: HTMLIonModalElement) {
return this.openNavStacks.indexOf(modalElement);
return this.openNavStacks().indexOf(modalElement);
}

/**
* Programmatically dismiss a nav-stack. Handling removing from openNavStacks array is done elsewhere
* to handle both programmatic dismissal and dismissal from nav-stack component (e.g. via close button)
*/
private async closeNavStack(index: number) {
await this.openNavStacks[index].dismiss();
this.openNavStacks.splice(index, 1);
const modal = this.openNavStacks()[index];
if (modal) {
await modal.dismiss();
}
}

private addNavStackToArray(modal: NavStackModal) {
// use array destructure to create new object
this.openNavStacks.update((stacks) => [...stacks, modal]);
}

private removeNavStackFromArray(index: number) {
// use filter instead of splice to avoid modifying original object
this.openNavStacks.update((stacks) => stacks.filter((v, i) => i !== index));
}
}

0 comments on commit ea8dff6

Please sign in to comment.