🐐

PWAにWindow Controls Overlayでかっこいいタイトルバーをつけようとしたら、フルスクリーン表示ではまった話

2023/09/24に公開

Yagiful.comのyagiです。このほどウェブアプリのSoqAlbumをPWAとしてマイクロソフトストアに公開しました。ストア公開に際してマイクロソフトは、PWAがよりOS環境に溶け込むデザインであること("window controls overlay"や"edge side panel"に対応していること、等)を推奨していて、YagifulでもSoqAlbumを改良して、window controls overlayに新たに対応したタイトルバーを導入しました。今回は、この作業で得た経験を、SoqAlbumの宣伝がてら書き記したいと思います。

https://apps.microsoft.com/store/detail/soqalbum/9NJ7M52CDDRG

PWAとは何か、Window Controls Overlayとは何か?

PWA(Progressive Web Application)とは通常ブラウザ内でしか機能できないウェブアプリを、普通のアプリ同様にOSにインストールして、デスクトップのアイコンクリックで起動できるようにしたものです。Window controls overlayとはPWAとして起動したアプリのタイトルバー部分を、さらに詳細にデザインすることができるAPIです。

次のスクリーンショットを見比べてください。Window controls overlayに対応することで、いわゆる「ウェブアプリっぽさ」を大きく低減させることができます。

edgeで開いたSoqAlbum

PWAとして開いたSoqAlbum

PWAでwindow controls overlayを有効にしたSoqAlbum
上から順に、SoqAlbumをedgeで開いた場合、PWAとして開いた場合、PWA+Window Controls Overlayで開いた場合。PWAの"^"の上下でWindow Controls Overlayを使うかどうかを切り替えることができます。

Window Controls Overlayをどう使う ⇒ 結論: CSS環境変数だけを信じてデザインする

ではどうやってwindow controls overlayに対応するかということなのですが、私が行き着いた結論を先に言ってしまうと 「CSS環境変数だけを信じて、通常アプリとPWAアプリの両方に対応したタイトルバーデザインをCSSだけを使って作る」 ということになります。

逆に言うと 「DOM操作を使ってPWA専用のタイトルバーを作るな」 ということになります。

SoqAlbumはvue(tify)で作られていて、当初は私も、PWA専用のデザインを

<div v-if="isPWA"> <!-- isPWAはPWAであるかどうかを判定するvueのリアクティブ変数 -->
	PWA専用のタイトルバーデザイン
</div>

というような形で作り込もうとしたのですが、これがことごとく失敗しましたよ、というのが今日のお話の肝となります(笑)。

以下はその理由になりますが、ちょっと突っ込んだ話となっていますので、まずはwindow controls overlayについて大まかに知りたいという方は、直接こちらに進んでください。

理由1: display-mode: standalone でPWA判定するのは危険

さて、以下でDOM操作に失敗した理由を説明していきたいと思います。

DOM操作でタイトルバーを作り分けるには、現在のブラウザ表示がPWAかどうか判定する(上記で言うところの「isPWA」という変数を定義する)必要があります。ネットで検索すると

window.matchMedia('(display-mode: standalone)').matches 

で、PWAかどうか判定できると書いてあるページがかなりあるのですが、 これは危険です。

小さめの問題から述べますと、manifestのdisplayメンバーとして設定できるのは'standalone'だけではなく、'fullscreen', 'minimal-ui', 'browser'等もあります。加えてアプリをwindow controls overlayに対応させる場合、display_overrideメンバーに'window-controls-overlay'を設定しますので、display-modeの候補がさらに増えます。あくまでwindow.matchMediaで判定すると言うなら、可能性があるものすべてに対応する必要があります。

ただ、これらすべてに対応したとしても実はPWA判定として不十分です。もっとも大きな問題は 「通常表示かPWA表示かに関わらず、F11やrequestFullscreenでフルスクリーン表示された場合、display-modeは常にfullscreenになる」 からです。

最悪の事態は、ユーザーがアプリ画面(もしくはブラウザ画面)をフルスクリーンにしたままリロードしてしまった場合で、この場合、PWAであるかどうかに関わらず、display-modeは当然fullscreenでリロードされます。このような可能性があるのでdisplay-modeのみでPWA判定することは危険(というか不可能)なのです。

理由2: fullscreenはwindow controls overlayの鬼門

fullscreenはPWAの判定を難しくすると言いましたが、PWAでwindow controls overlayを使う場合は、fullscreenで更なる困難にぶちあたります。

下のスクリーンショットはマイクロソフトがwindow controls overlayの利用デモとして提供しているアプリ"1DIV"(こちら)をwindowsでフルスクリーン表示したときのタイトルバーを表しています。

マイクロソフト製window controls overlay利用のデモPWA
マイクロソフト製、window controls overlay利用のデモプログラム"1DIV"をWindows 10上のedge(117)でインストール。Window controls overlayを有効にして、F11でフルスクリーン表示にした時のスクリーンショット。赤く囲んだ部分(デッドゾーン)は、クローズボタンなどがあった名残で、HTMLへのクリックを受け付けない。

問題の部分を赤で囲みました。フルスクリーン表示なのだからクローズボタン等があった場所はなくなるべきだと思いますが、そうなってません。赤い部分はフルスクリーン表示のトップ部分ですから、空いているならリンクやボタンでも置いて有効利用したくなるエリアなのですが、困ったことに、ここにクリッカブルなエレメントを置いてクリックしても反応は返ってきません。 つまり赤い部分は、クローズボタンが表示されないにも関わらず、あたかもクローズボタンがまだそこにあるかのような応答をします。赤で囲った部分は、まさに デッドゾーンです。

疑問に思われる方は、"1DIV"(こちら)をブラウザ経由でwindowsにインストールして、F11でフルスクリーン表示してみてください。アプリ画面のさまざまな場所を右クリックして表示されるメニューを比べてみると、デッドゾーンを右クリックした場合だけ表示内容が変わるのが分かります。

仮に何らかの方法で、display: fullscreenの状態でもPWAかどうかの判定ができる(isPWAが定義できる)と仮定します。その場合PWA専用にデッドゾーンを避けるようなDOM操作をjavascriptで書きたくなる人もいるかもしれませんが、 やっかいなことにmacの場合(なんやかんやで)このデッドゾーンは現れません。 そもそもこのデッドゾーンは、将来のブラウザリリースで解消される可能性が高いのではないかとも私は思っています。

一方 CSSの環境変数はフルスクリーン表示でデッドゾーンが存在した場合でも、デッドゾーンを避けるように正しく(?)値が変化します。 "1DIV"の上部のデザインがうまくデッドゾーンを避けられているのは、"1DIV"の上部のデザインがCSSの環境変数に基づいているからです。

私自身、vueのDOM操作を使いたくていろいろやってはみたのですが、複雑な諸条件を加味していくと不毛に複雑なコードになる(例えばresizeやnavigator.windowControlsOverlayのgeometrychangeをlistenしてdisplay変化を検知して、複数のdisplay状態それぞれに対応するコードを各ページに書いておいたものを切り替える、とか?)ことにうんざりしまして…、結局 「あてなるのはCSS環境変数だけ」 と思いこむことにして、DOM操作のことは忘れることにしました(笑)。

具体例: マイクロソフト製、window controls overlayのデモプログラム("1DIV")を解説する

ここでは、CSS環境変数利用の具体例として(先にも述べました)マイクロソフトがwindow controls overlay利用のお手本として提供しているデモプログラム"1DIV"(こちら)について、解説して行きたいと思います。

1DIVでwindow controls overlayを無効

1DIVでwindow controls overlayを有効
"1DIV"(マイクロソフト製、window controls overlayのデモプログラム)をPWAとしてインストール、起動して、window controls overlayを無効にした場合(上)と、有効にした場合(下)のスクリーンショット。"^"の向きで有効・無効を切り替える。タイトルバーのデザインはその両方に対応できるようなものである必要があります。

CSS環境変数とは何か?

PWAのmanifestでdisplay_overrideメンバーに'window-controls-overlay'を設定した場合、タイトルバーのデザインを、「通常のデザイン」と「window controls overlayを使ったデザイン」の2通りで切り替えられるスイッチ("^"の上下)が現れます。CSSの環境変数は「window controls overlayを使ったデザイン」を選択した場合のみ有効になる変数です。

window controls overlayで利用できる環境変数

環境変数はtitlebar-area-x, titlebar-area-y, titlebar-area-width, titlebar-area-heightがありまして、それぞれ図に示した部分の長さを表す変数です。CSSとして参照する場合、

env(titlebar-area-height, 0px)

などという形で使います。ここで0pxはtitlebar-area-heightが定義されてない場合のデフォルト値で、タイトルバーの選択で「通常のデザイン」を選んだ場合とか、そもそもPWAではなく通常のブラウザページとしてページが開かれている場合、あるいはwindow controls overlayに対応していない古いブラウザではデフォルト値が使用されます。

CSS環境変数の"1DIV"での使われ方

開発ツールの「検証」で"1DIV"の上部部分のエレメントを確認すると、

<body>
	<div class="drag"></div>

と、body直下にwindow-controls-overlay用のHTMLエレメントが見つかります。ここでdragのクラス定義は

.drag {
    position: fixed;
    top: 0;
    width: 100%;
    height: env(titlebar-area-height, 0);
    -webkit-app-region: drag;
}

となります。このdivはHTML画面上部にfixされた横幅いっぱいの長方形のエレメントで高さはenv(titlebar-area-height, 0)です。 window controls overlayが有効でない場合は高さ0となり、このdivは実質存在しないエレメントとなります。

-webkit-app-region: dragの指定は、この部分がタイトルバーと同じ働きをすることを宣言するものです。 つまりwindow controls overlayが有効の時は、この長方形部分をマウスで掴んでドラッグして、ウインドウを移動すること等が可能になります。

この-webkit-app-region: dragの指定で注意しなければならないのは、この指定の有効範囲はHTMLエレメントのz-indexと関係しない、 ということです。 "1DIV"の場合、この長方形の上に"search"と書かれたinputエレメントが乗っていますが、このinputエレメントの上部は長方形と重なっており、-webkit-app-region: dragの効果が 「突き抜けて」 有効となります。つまりinputの上半分あたりはクリックしてもタイトルバーへのクリックとして扱われ、input側にフォーカスが移らない事態となります。

"1DIV"ではこの事態を避けるため、inputエレメントに次のようなCSSが指定がされています。

-webkit-app-region: no-drag;

この指定は先の長方形に指定した'-webkit-app-region: drag;'を打ち消すように働きます。これによりinputエレメントの中でクリックすれば、常にinput側にフォーカスするように修正されます。

このinputエレメントや、その左のアプリアイコンと右のnew+ボタンはheaderエレメントの中にあります。つまり

<header>
	inputやnew+ボタンなどの部品
</header>

アプリのクローズボタンとの重複を避けるためには、このheaderエレメントの位置と大きさがうまく調整される必要があります。

header {
    top: 0;
    display: flex;
    gap: var(--padding);
    justify-content: space-between;
    left: env(titlebar-area-x, 0);
    width: env(titlebar-area-width, 100%);
}

以上がheaderに指定されたcssです。左側の位置を環境変数titlebar-area-xで、headerの横幅をtitlebar-area-widthで調整していることが分かります。このデザインによりwindow controls overlayが効いている場合と、そうでない場合両方で、headerの大きさがうまく調整されることが分かるでしょう。

フルスクリーンのデッドゾーン問題が、将来修正されても動くコードを考えよう。

先に述べたデッドゾーン問題ですが、私個人としてはおそらく将来のブラウザでは修正されているだろうと予想しています。この記事を書いている時点のchrome/edgeのバージョンは117です。このバージョンでchrome/edgeでフルスクリーン表示した時、env(titlebar-area-height)は0より大きな値をとりますが、将来のバージョンのブラウザではenv(titlebar-area-height)が0に修正される可能性があるということです。

"1DIV"のようにCSS環境変数だけに頼ったシンプルなデザインであれば、将来の環境変数の変化にも、自動的に対応できるのではないかと予想できます。

とにかく、せめてデッドゾーン等の問題がクリアされるまでは、突っ込んだタイトルバーデザインを作り込むのは避けるべきだと思います。デッドゾーンに過剰に対応したあまり、デッドゾーンがなくなるとおかしくなるデザインになってしまっては、将来困ります。

どうしてもDOM操作でタイトルバーをデザインしたい場合

とはいえCSSだけで複雑なデザインに対処することには限界があることは確かです。DOM操作をどうしても行いたい場合、まず、現在のページがPWAとして開かれているのか、そうではないのかが判断できること(isPWAが定義できること)が重要です。

先に述べたとおり、PWAかどうかの判断をdisplay-modeだけでやろうとするのは難しいです。いろいろネットで調べた結果、唯一簡単に解決できそうな対処法が、manifestのstart_urlを次のように設定することでした。

"start_url": "./index.html?mode=PWA",

この場合、スタート時のURLのパラメータをjavascriptで解析することでPWAかどうかを判断できます。

let parser=new URL(window.location.href);
let mode=parser.searchParams.get('mode');
let isPWA= mode === 'PWA';

などとして、スタート時のmodeパラメータを読み取ることでisPWAを定義できそうです。

ただこの方法だと、意地悪なユーザーが通常のページ表示時のURLに'mode=PWA'を追加してしまった場合、おかしな表示が出力されてしまう心配が残り。それが嫌で私自身はこの方法は採用しませんでしたが、どうしてもDOM操作をしたい場合は、このような方法を使うのがよいかなとは思いました。

まとめ

今回の話を要約すると、

  • PWAの判定法としてネットでよく語られる window.matchMedia('(display-mode: standalone)').matches には問題があること。

  • Window controls overlayを利用する場合、fullscreen表示が鬼門で、デッドゾーンのような問題が発生すること。

  • PWA専用のDOM操作はできなくはないかもしれないが、思ったより大変。とりあえず環境変数だけ使ってCSSでデザインしたほうが無難。

いずれの問題も開発中のウェブアプリ、SoqAlbumを改修する時に、私自身が遭遇し、無駄に時間を費やしてしまった問題です。同じ様にPWAやwindow controls overlayに対応する作業をこれから行う人にとって、この記事が参考になるものであれば幸いです。

なお、SoqAlbumがどのようにデッドゾーン問題を対処したかに興味がありましたら、windowsでマイクロソフトストアからSoqAlbumをインストールしてみてください(無料です)。起動して、タイトルバーの"^"をクリックしてwindow controls overlayを有効にしてください(初回の起動ではうまく切り替わらなかった記憶もあるのですが… その場合は、アプリを閉じてもう一度起動してください)。アプリの上部の「F」ボタンを押して、右下のフルスクリーンアイコンをクリックすれば、アプリがフルスクリーン表示となります。注意深く見ていただければ、良くも悪くも微妙な方法(あるいは全くやる気の感じられない方法?)で、右上のデッドゾーンをさりげなく避けてアイコン配置しているのかがお分かりいただけるかと思います。将来的にデッドゾーン問題が解決されれば、自動的にあの隙間が埋まる算段なのですが、うまくいきますかね(笑)

Discussion