Firefox拡張機能のポップアップでファイル選択ダイアログを開くとポップアップが閉じる問題と対処法
ブラウザ拡張機能で設定のインポート機能などを実装していると、Chromeでは問題なく動くのにFirefoxだけファイル選択ダイアログを開いた瞬間にポップアップが閉じてしまう、という現象に遭遇することがあります。一見バグのようですが、これはFirefoxのポップアップの設計に起因する仕様で、いくつか実装パターンを工夫する必要があるので整理しておきます。
何が起きるか
ブラウザ拡張機能のポップアップ(browser_action / action)にインポートボタンを置き、クリックで<input type="file">を起動する一般的な実装を考えます。
<button id="import">インポート</button><input type="file" id="file" hidden />
const button = document.querySelector<HTMLButtonElement>('#import')!const fileInput = document.querySelector<HTMLInputElement>('#file')!
button.addEventListener('click', () => { fileInput.click()})
fileInput.addEventListener('change', async () => { const file = fileInput.files?.[0] if (!file) return const text = await file.text() // 設定の取り込み処理})
これをChromeとFirefoxで実行すると、結果は次のように分岐します。
| ブラウザ | 結果 |
|---|---|
| Chrome / Edge | OSのファイル選択ダイアログが開き、選択後にchangeイベントが発火する |
| Firefox | ファイル選択ダイアログが開いた瞬間にポップアップが閉じ、changeイベントは発火しない |
ポップアップが閉じるとJavaScriptコンテキストごと破棄されるため、changeイベントを受け取れず、インポート処理は完走しません。
原因
Firefoxのポップアップは「フォーカスを失った瞬間に閉じる」というUI設計になっています。OSネイティブのファイル選択ダイアログが開くとブラウザウィンドウからフォーカスが外れる扱いになり、結果としてポップアップが自動で閉じてしまうという流れです。
これは実装ミスというよりMozilla側で長年認識されている既知の制約で、Bugzillaにもチケットが立てられています。
- Bug 1658694 - Opening input type="file" in extension Popup window will close the popup
- Bug 1292701 - Autoclose popups shouldn't close when they open a modal dialog
Bug 1658694は2020年、Bug 1292701に至っては2016年から起票されているもので、執筆時点で長期未解決のまま残っています。Bug 1658694は別チケットへの統合(duplicate)として整理されている時期もあり、追跡先は変動する可能性がある点に注意してください。いずれにせよWebExtensions polyfillで吸収できる類の問題ではないため、拡張機能側で実装の工夫が必要になります。
ちなみに同じ問題はMetaMaskも踏んでいて、最終的に「設定画面はポップアップではなく新しいタブで開く」という実装に切り替えています。
対処法
1. 新しいタブで開く(推奨)
最もシンプルかつ確実なのが、ファイル選択を伴う処理だけ別タブに切り出してしまう方法です。
function openSettingsInNewTab(): void { const url = chrome.runtime.getURL('popup.html?popup=true&tab=settings') chrome.tabs.create({ url }) window.close()}
chrome.runtime.getURLで拡張機能内のページURLを取得し、chrome.tabs.createで新しいタブを開きます。クエリパラメータでポップアップ用のビューと判別できるようにしておけば、ポップアップ用HTMLをそのまま使い回せます。
タブとして開いてしまえばフォーカスが外れたことによる自動クローズは発生しないため、<input type="file">もそのまま動作します。
2. browser.windows.create でミニウィンドウを開く
フルタブまでは大げさという場合は、windows.createで小さなポップアップ風ウィンドウを開く手もあります。
chrome.windows.create({ url: chrome.runtime.getURL('popup.html?popup=true&tab=settings'), type: 'popup', width: 400, height: 300})
type: 'popup'を指定すると、アドレスバーやタブのない独立した小ウィンドウが開きます。見た目はポップアップに近い一方で、内部的には通常のウィンドウなのでファイル選択ダイアログを開いても閉じることはありません。
ただしユーザーから見ると「ボタンを押したら別ウィンドウが開いた」という体験になるため、UI上の違和感は少なからず出ます。元のポップアップとの繋がりを示すデザインの工夫があったほうが親切です。
3. Options pageに退避させる
設定インポートのようにポップアップ前提でなくてよい操作は、そもそも拡張機能の「オプションページ」に置いてしまうのが正攻法です。
{ "options_ui": { "page": "options.html", "open_in_tab": true }}
// ポップアップ側のボタンから設定画面(オプションページ)を開くchrome.runtime.openOptionsPage()
open_in_tab: trueを指定するとオプションページがタブで開かれるため、ファイル選択ダイアログによるフォーカス移動の影響は受けません。インポート/エクスポートのようなファイル操作系UIは、設計レベルで「ポップアップではなくオプションページに置く」と整理してしまうのが、長期的には最も安定します。
4. Drag & Dropでのインポート(補助的な手段として)
ファイル選択ダイアログそのものを使わず、ドラッグ&ドロップでファイルを受け取る実装も併用候補にはなります。
const dropArea = document.querySelector<HTMLDivElement>('#drop-area')!
dropArea.addEventListener('dragover', (e) => { e.preventDefault()})
dropArea.addEventListener('drop', async (e) => { e.preventDefault() const file = e.dataTransfer?.files[0] if (!file) return const text = await file.text() // 設定の取り込み処理})
ただしこの方法はOSのファイルマネージャからドラッグ操作する都合上、ドラッグの開始時点ではブラウザ外のアプリにフォーカスが乗っています。Firefoxのバージョンや環境によってはドラッグ中にポップアップが閉じてしまう挙動も報告されているため、汎用的な回避策としては信頼しきれません。あくまで「ファイル選択ダイアログを使うボタン」と並立する補助的な選択肢として捉え、メインの導線は前述の新しいタブ・ウィンドウ・オプションページのいずれかに寄せるのが安全です。
5. Background Script + adoptNode()(非推奨)
Mozilla Discourseで紹介されているテクニックで、Background Script側にファイル入力要素を作成しadoptNode()でポップアップ側に取り込むというものです。
ただしこの手法はFirefoxのバージョンによって動作しなくなったという報告もあり、安定性は期待できません。加えて、Manifest V3ではbackgroundがService Workerとして動作するためそもそもDOMが存在せず、document.createElement()やadoptNode()を呼ぶこと自体が構造的にできません。MV3への移行を考えるとこの方法は袋小路になるため、新規実装にはあまり向きません。
実装上の工夫
Firefoxとそれ以外で挙動が分岐する以上、実装側でもある程度の判定や切り替えが必要になります。
Firefoxの検出
ランタイムで判定する場合、最も手堅いのはFirefox専用APIであるruntime.getBrowserInfoの有無を見る方法です。記事内のコード例に合わせてchrome.*名前空間で書くなら次のようになります。
const isFirefox = typeof chrome !== 'undefined' && typeof chrome.runtime?.getBrowserInfo === 'function'
WebExtensions上ではFirefoxがchrome.*とbrowser.*の両方を実装している一方、runtime.getBrowserInfoはFirefox側にしか存在しません。browserグローバル自体はwebextension-polyfillなどのポリフィルを使うとChromeでも有効になるため、「browserが存在するか」だけでは判定材料として不足するケースがあります。
ユーザーエージェントベースで判定する場合は次のような形でも問題ありません。
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox')
PlasmoやWebExtension関連のビルドツールを使っている場合は、ビルドターゲットに応じて環境変数を切り替える方式も有力です。たとえばPlasmoではprocess.env.PLASMO_BROWSERが'firefox'になります。ビルド時に値が決まるためバンドルサイズの最適化にも繋がります。
ポップアップとタブで同じHTMLを使い回す
新しいタブやウィンドウで同じHTMLを使い回す場合、URLにフラグを持たせておくと表示の切り替えが楽になります。
const params = new URLSearchParams(window.location.search)const isOpenedAsPopup = params.get('popup') === 'true'
if (isOpenedAsPopup) { document.body.classList.add('is-popup-window')}
ポップアップ用にスタイルを変更したり、閉じるボタンを表示したりといった調整が必要であれば、こうしたフラグを基準にCSSやJSで分岐させると整理しやすくなります。
UX上の配慮
「インポートだけ別画面で開く」という体験は、ユーザーから見るとやや唐突に感じられることもあります。Firefoxの場合のみボタンの挙動を切り替える設計にしつつ、ボタンに小さく外部リンク風のアイコンを添えておくと、別画面へ遷移することを暗に伝えやすくなります。
button.addEventListener('click', () => { if (isFirefox) { openSettingsInNewTab() return } fileInput.click()})
実装例: TokiDokeでの対応
実際にこのIssueの発端となったのは、自分が開発しているブラウザ拡張機能TokiDoke(Firefox版)でデータインポート機能を実装した際の挙動でした。
Chrome版では、ポップアップ内に普通に「データをインポート」ボタンを置く一般的な形で問題なく動作します。
一方Firefox版では、ボタン押下時にポップアップが閉じてしまう問題があったため、前述の対処法のうち「windows.createでミニウィンドウを開く」アプローチを採用しました。type: 'popup'の独立した小ウィンドウへ設定画面を逃がした上で、いきなり別ウィンドウが開くことへの違和感を減らすために、ボタン文言を「新しいウインドウでインポート」に変更し、外部リンク風のアイコンと共に注意書きをインラインで表示しています。
UX側の工夫としては次の3点を意識しました。
- 挙動が違うことの事前通知: 注意書きを近接配置することで、ボタンを押す前に別ウィンドウが開くことを伝える
- ボタン文言の変更: 「データをインポート」→「新しいウインドウでインポート」に変更し、操作内容を正確に説明
- アイコンによる視覚的補強:
↗の外部リンク風アイコンで、別画面に遷移するという行為を直感的に伝える
ブラウザの仕様による違いをユーザーに押し付けないように、Firefox側だけで明示的に違いを説明するUIへ切り替える形にしています。なおTokiDokeではPlasmoのビルドターゲット (PLASMO_BROWSER === 'firefox') でブラウザを判定しています。フレームワークが提供するビルド時フラグを使えば、ランタイムでのUA判定やbrowserグローバルチェックに頼らずに済むため、複数ブラウザ向けにビルドを切り分けている場合はこちらが手堅い選択肢です。
まとめ
Firefox拡張機能のポップアップでファイル選択ダイアログを開くとポップアップが閉じてしまうのは、ポップアップがフォーカス喪失で自動的に閉じる仕様に起因するものです。polyfillでは解決できないので、ポップアップの外側で処理を完結させるか、ファイル選択ダイアログ自体を回避するかの工夫が必要になります。
実装としては「新しいタブで開く」「windows.createでミニウィンドウを開く」「オプションページに退避させる」あたりが現実的で、特にインポート/エクスポートのようなファイル操作はオプションページに置くと設計が安定します。Bugzilla側のチケットは長期未解決の状態が続いているため、修正を待つよりも回避策ベースでUIを設計しておくほうが安全です。
参考リンク
- Bug 1658694 - Opening input type="file" in extension Popup window will close the popup
- Bug 1292701 - Autoclose popups shouldn't close when they open a modal dialog
- Getting file from file chooser after extension popup closed - Mozilla Discourse
- Firefox file input closes popup - MetaMask Issue #1686
- browser.windows.create() - MDN
- browser.tabs.create() - MDN
- browser.runtime.openOptionsPage() - MDN
- options_ui - MDN
- browser.runtime.getBrowserInfo() - MDN