中安拓也のブログ

プログラミングについて書くブログ。 Twitterやってます @l08084

【Ionic v5】Alert/Modal同士でz-indexを交換する

環境

TypeScriptベースのフレームワークであるAngularと、iOS/AndroidのハイブリッドモバイルアプリケーションのフレームワークであるIonicを使用しています。

  • "rxjs": "~6.6.0",
  • "@angular/fire": "^6.1.4"
  • "firebase": "^8.3.1"
  • "semver": "^7.3.5"

ionic infoコマンドの実行結果

$ ionic info

Ionic:

   Ionic CLI                     : 6.11.8 (/usr/local/lib/node_modules/@ionic/cli)
   Ionic Framework               : @ionic/angular 5.6.0
   @angular-devkit/build-angular : 0.1101.4
   @angular-devkit/schematics    : 11.1.4
   @angular/cli                  : 11.1.4
   @ionic/angular-toolkit        : 3.1.0

Cordova:

   Cordova CLI       : 8.0.0
   Cordova Platforms : none
   Cordova Plugins   : no whitelisted plugins (1 plugins total)

Utility:

   cordova-res : not installed
   native-run  : not installed

System:

   ios-deploy : 1.9.2
   ios-sim    : 6.1.2
   NodeJS     : v12.13.1 (/usr/local/bin/node)
   npm        : 6.14.12
   OS         : macOS Catalina
   Xcode      : Xcode 12.0.1 Build version 12A7300

Alert/Modal同士でz-indexを交換する

Ionicのアラート/モーダルはz-indexを交換することができるため、アラート/モーダル同士の奥行きの位置を入れ替えることができます。

  /**
   * アラート/モーダルの奥行きの位置を交換する
   *
   * @private
   * @memberof VersionCheckService
   */
  private swapPositions(): void {
    [this.versionUpAlert.style.zIndex, this.maintenanceAlert.style.zIndex] = [
      this.maintenanceAlert.style.zIndex,
      this.versionUpAlert.style.zIndex,
    ];
  }

例えば、下記の画像では、バージョンアップのアラートとメンテナンスのアラートが重なって表示されていて、あとに表示されたバージョンアップのアラートが前面に表示されています。

f:id:l08084:20210411161642p:plain
バージョンアップのアラートが前面に表示されている

この状態で先ほど作成したswapPositions()を呼び出すと、アラートの位置が交換されて、メンテナンスのアラートが前面に表示されます。(隠れて見えませんが背後にバージョンアップのアラートが表示されています)

f:id:l08084:20210411163147p:plain
メンテナンスのアラートが前に来る

該当コードの全体像は下記となります。

import { Injectable } from '@angular/core';
import { AngularFireDatabase } from '@angular/fire/database';
import { Observable } from 'rxjs';
import { Maintenance } from '../model/maintenance.model';
import { Version } from '../model/version.model';
import * as semver from 'semver';
import { AlertController } from '@ionic/angular';
@Injectable({
  providedIn: 'root',
})
export class VersionCheckService {
  // このアプリのバージョン
  private readonly appVersion = '1.0.0';

  private maintenance$: Observable<Maintenance>;
  private version$: Observable<Version>;
  private maintenanceAlert: HTMLIonAlertElement;
  private versionUpAlert: HTMLIonAlertElement;

  constructor(
    private db: AngularFireDatabase,
    private alertController: AlertController
  ) {}

  /**
   * 初期設定
   *
   * @memberof VersionCheckService
   */
  public initSetting(): void {
    // Realtime Databaseからデータを取得
    this.maintenance$ = this.db
      .object<Maintenance>('maintenance')
      .valueChanges();
    this.version$ = this.db.object<Version>('version').valueChanges();

    this.maintenance$.subscribe(
      async (maintenance: Maintenance) =>
        await this.checkMaintenance(maintenance)
    );
    this.version$.subscribe(
      async (version: Version) =>
        await this.checkVersion(this.appVersion, version)
    );
  }

  /**
   * メンテナンスポップアップを表示する。
   *
   * @private
   * @param {Maintenance} maintenance
   * @returns {Promise<void>}
   * @memberof VersionCheckService
   */
  private async checkMaintenance(maintenance: Maintenance): Promise<void> {
    if (!maintenance) {
      return;
    }

    if (!maintenance.maintenanceFlg) {
      // メンテナンスフラグがOFFだったら処理を中断する
      if (this.maintenanceAlert) {
        // メンテナンスメッセージが開かれている場合は閉じる
        await this.maintenanceAlert.dismiss();
        this.maintenanceAlert = undefined;
      }
      return;
    }

    // メンテナンスメッセージを表示する
    this.maintenanceAlert = await this.alertController.create({
      header: maintenance.title,
      message: maintenance.message,
      backdropDismiss: false, // 背景をクリックしても閉じない
    });
    await this.maintenanceAlert.present();
  }

  /**
   * 強制バージョンアップメッセージを表示する。
   *
   * @private
   * @param {string} appVersion
   * @param {Version} version
   * @returns
   * @memberof VersionCheckService
   */
  private async checkVersion(appVersion: string, version: Version) {
    if (!version || !version.minimumVersion) {
      return;
    }

    if (semver.gte(appVersion, version.minimumVersion)) {
      // 最低バージョンよりもアプリのバージョンが高かったら処理を中断する
      if (this.versionUpAlert) {
        // 強制バージョンアップメッセージが開かれている場合は閉じる
        await this.versionUpAlert.dismiss();
        this.versionUpAlert = undefined;
      }
      return;
    }

    // 強制バージョンアップメッセージを表示する
    this.versionUpAlert = await this.alertController.create({
      header: version.title,
      message: version.message,
      backdropDismiss: false, // 背景をクリックしても閉じない
    });
    await this.versionUpAlert.present();
  }

  /**
   * アラート/モーダルの奥行きの位置を交換する
   *
   * @private
   * @memberof VersionCheckService
   */
  private swapPositions(): void {
    [this.versionUpAlert.style.zIndex, this.maintenanceAlert.style.zIndex] = [
      this.maintenanceAlert.style.zIndex,
      this.versionUpAlert.style.zIndex,
    ];
  }
}

Alert/Modalのz-indexを参照できるのはなぜか?

そもそも、なぜIonicのアラート/モーダルで、z-indexを参照・更新できるのか?という点について説明します。

Ionicのアラートの型はHTMLIonAlertElement、モーダルの型はHTMLIonModalElementに設定されています。

HTMLIonAlertElementHTMLIonModalElementHTMLElementを継承しているため、HTMLElement.styleによって、z-index含むスタイル関連のメソッドやプロパティの情報を取得することができる、というわけです。

おわりに

IonicのコンポーネントってStencilで書かれているWeb Componentsなんですね......知らなかった。コントリビュートしやすそう

参考サイト

ElementCSSInlineStyle.style - Web API | MDN

Stencilを使ってWebComponentを作ってみる - Qiita

Web Components | MDN

Using custom elements - Web Components | MDN

型付きコンポーネント - Stencil

ionic-framework/alert.tsx at 8e0e5da7407adecb7471b3a6b0ac059337761355 · ionic-team/ionic-framework · GitHub

ionic-framework/core/src/components/modal at 8e0e5da7407adecb7471b3a6b0ac059337761355 · ionic-team/ionic-framework · GitHub