WebExtensionsでPopupを作るときのサイズに関するへんな挙動 (+自作WebExtensions紹介)

WebExtensions(ブラウザ拡張機能)では、
ツールバーのボタンを押したときにポップアップウィンドウを開くことができる。

<img title="こういうやつ" alt="ポップアップ" width="400" src="https://qiita-image-store.s3.amazonaws.com/0/60243/a3deec4f-2949-1408-80eb-fc5b8e416e19.gif" />

ChromeとFirefoxの両方で使えるWebExtensionsを作ったとき、
Popupのサイズ周りでいくつかつまずいて、力づくで直した内容をメモしておく。
作った拡張機能の宣伝もあるよ！

※環境
Chrome: macOS 65.0.3325.162(64bit)
Firefox: macOS 59.0.1(64bit)

# 1. Firefoxのスクロールバー幅問題

Popupするウィンドウのサイズは、PopupするHTMLのbodyタグのwidthやheightなどにcssで指定する。

```css
body {
  width: 400px;
}
```

この場合、ウィンドウ幅は400px固定で、高さはコンテンツの高さに合わせられるが、
最大値(600px)を超えているとスクロールバーが表示される。
ここで、スクロールバー幅に関する挙動がChromeとFirefoxで異なる。

## Chrome: 普通

Chromeの場合、ポップアップウィンドウの幅はコンテンツの幅＋スクロールバー幅になる。
さきほどのCSSだと、スクロールバー抜きサイズの`window.innerWidth`は`400(px)`を返し、
スクロールバーありサイズの`document.body.clientWidth`は`415(px)`を返す。
実際のウィンドウも415pxであり、スクロールバーがあるとウィンドウが少し大きくなることになる。

## Firefox: 変？

Firefoxの場合、ポップアップウィンドウの幅はコンテンツの幅になる。
スクロールバー抜きサイズの`window.innerWidth`が`400(px)`になるのはいいとして、
スクロールバーありサイズの`document.body.clientWidth`も`400(px)`を返し、
ウィンドウの幅も400pxとなる。

ただし、この状態でも幅15pxのスクロールバーは表示されているため、
(`window.innerWidth`の値に反して)実際の表示幅は`400 - 15 = 385px`になる。
一方でコンテンツの幅は先のCSSで400pxと指定しているため、コンテンツがはみ出す。
結果として、意図しない横方向へのスクロールバーが表示されてしまう。
(ちなみに`overflow-x: hidden`で横スクロールバーを消したら右端が切れる。)

<img width="300" title="ぎゃー" alt="横スクロールバーが表示される" src="https://qiita-image-store.s3.amazonaws.com/0/60243/85c143d5-3515-ae4e-5c8a-fae407e1ee24.png">


これは困る。

## 解決方法

widthの代わりになんとなくmax-widthとmin-widthを指定してみたら、
ChromeでもFirefoxでも同じCSSで横幅が収まり上手くいった。

```css
body {
  max-width: 415px;
  min-width: 380px;
}
```

ちゃんとmin-widthも書かないとダメである。
この場合、Chromeでは表示幅415pxでウィンドウ幅430pxになり、
Firefoxでは表示幅400pxでウィンドウ幅415pxになる。
微妙に横幅が異なってしまうが、そこはあまり気にしてなかったので問題なし。
なんで直るかはよくわからないが、動けばいいのさ...


# 2. Chromeの動的Viewのリサイズ問題

Reactなどを使ってPopupのViewを生成するとき、
コンテンツが描画されるに従ってウィンドウのサイズは動的に変更される。
ただ、このサイズ変更の挙動がChromeでは変になることがある。

## Firefox: 普通

Firefoxではコンテンツのサイズに応じてウィンドウがリサイズされる。普通。


## Chrome: 変？

Chromeでもコンテンツのサイズに応じてウィンドウがリサイズされる...のだが、
結構な頻度で正常にリサイズされずに何かこうグチャっとなるときがある。

<img alt="Popupオープン失敗" title="グチャッ" width="400" src="https://qiita-image-store.s3.amazonaws.com/0/60243/6c817867-18b8-4d4e-3fc0-55cadfa6f1e7.gif">


一度閉じて開き直せば直ることもあるが、直らないこともあるし手間だし見た目が嫌。

## 解決方法

描画完了後にもう一度、コンテンツの高さを強制的に変更してやればウィンドウ再計算処理が走ってくれる。
ただし、(本来の)コンテンツのサイズよりも大きい値を指定しなければリサイズしてくれない。

格闘した末、いろいろ諦めてガチャガチャやったら直った。
ソースも不格好だし、見た目にもワチャワチャやってるのが目に見えて気持ち悪いが、
とりあえず表示はされるのでまあ良いやと妥協している。

```js
// 描画完了後に呼ばれる関数
const dirtyHeightFix = async () => {
  const appElement = document.getElementById('app'); // 全体のコンテナ(Reactのroot)

  // 高さを適当にゴリゴリいじってサイズを直す
  // スクリプト上の描画処理がレンダリングにいつ反映されるかわからんので数回やる(キモい)
  for (let i = 0; i < 3; i++) {
    const height = appElement.getBoundingClientRect().height;
    document.body.style.height = `${height + 1}px`;
    await new Promise((r) => setTimeout(r, 100)); // 100ms待つ
  }
  document.body.style.height = '';

  // スクロールバーが出る高さならこれでは直らないので、
  // さらにガガっといじったら何か直る
  if (appElement.getBoundingClientRect().height > 600) {
    appElement.style.height = '600px';
    appElement.style.overflowY = 'hidden';
    await new Promise((r) => setTimeout(r, 100)); // 100ms待つ
    appElement.style.height = '';
    appElement.style.overflowY = '';
  }
}
```
<img alt="Popupオープン成功" title="わちゃわちゃ" width="400" src="https://qiita-image-store.s3.amazonaws.com/0/60243/e779e422-111c-67c4-b170-566860aa8181.gif">

変な表示状態でもJavaScriptからは正常なウィンドウサイズに見えているようなので、
正常に表示されているかを判別できず、どっちにしろこの処理は行う必要がある。
クソ強引なのでもうちょいスマートな方法がありそう。

以上で終わり。ここからは宣伝。

# 作ったもの

社内Wiki内を行ったり来たりの反復横跳びが面倒すぎたので、
サイトドメインごとに独自のブックマークを管理できるブラウザ拡張機能を作った。
自分専用のサイト内リンクのように使える。

<img title="べんり！！！！" width="500" alt="拡張機能のスクショ" src="https://qiita-image-store.s3.amazonaws.com/0/60243/508f9d9e-e2ca-46ff-d769-4594e3630770.gif">

* [In-Site Bookmark - Chrome ウェブストア](https://chrome.google.com/webstore/detail/in-site-bookmark/mpleiaoobkcpaedlgmpnlohojkbjgfbj?hl=ja)
* [In-Site Bookmark – Firefox 向けアドオン](https://addons.mozilla.org/ja/firefox/addon/in-site-bookmark/)


同じ要領で見てるサイトドメインの表示履歴を見るやつも作った。

* [In-Site History - Chrome ウェブストア](https://chrome.google.com/webstore/detail/in-site-history/jhcjfcakhkchbcchegiaifdcdadnopie?hl=ja)
* [In-Site History – Firefox 向けアドオン](https://addons.mozilla.org/ja/firefox/addon/in-site-history/)

一応ソースはこちら。

* [tkrkt/in-site-extensions](https://github.com/tkrkt/in-site-extensions)

ちなみに実装としては、
FirefoxとChromeのextensionを同時に作れる[extension-boilerplate](https://github.com/EmailThis/extension-boilerplate)をベースに、
PopupとBackgroundでぐるっとFluxを回せるという[react-chrome-redux](https://github.com/tshaddix/react-chrome-redux)を混ぜ込んだ
[extension-boilerplate-react-redux](https://github.com/sangwonl/extension-boilerplate-react-redux)というのがあったので、
それをTypeScript化した[extension-boilerplate-react-redux-typescript](https://github.com/tkrkt/extension-boilerplate-react-redux-typescript)というのを作って、
そいつにRedux-SagaやらTypeScript-FSAやらを入れて使っている。

良かったら使ってみてね。