【令和版】window.close() でタブが閉じない時の解決法

開発者「すいません、`window.close()`が動かないんですけど…」

ワイ「コンソールになんか書いてないですか？」

> Scripts may close only the windows that were opened by them.

開発者「ってでてます。」

ワイ「そこに書いてある通りですね。」

開発者「すみません、英語わかんなくて…」

ワイ「」

---

なぜ閉じないのかわからない人も、どうすればいいかと相談される人も、`window.close()`で消耗するのはおしまいにしましょう。

もう聞かれた時にURLを投げればいいだけにしておきたいので、この記事を残しておきます。

さて、まずはなぜ解決できないのかを知っていくために、先ほどの会話の中で出てきた英文の日本語訳を確認します。

![Google翻訳：スクリプトは、スクリプトによって開かれたウィンドウのみを閉じることができます。](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142709/b8ce786d-7bd9-0144-2938-c55b503a8142.png)

直訳なのでわかりづらいですが、要は__JavaScriptで閉じることができるウィンドウは、JavaScriptで開かれたウィンドウのみ__であると書いてあります。

しかしながら、時々JavaScriptで開いてないウィンドウも`window.close()`で閉じることができる気がします。それはなぜでしょうか？

一旦冷静になって仕様を確認してみましょう。

## What is `window.close()`

> A browsing context is script-closable if it is an auxiliary browsing context that was created by a script (as opposed to by an action of the user), or if it is a top-level browsing context whose session history contains only one Document.

参考文献：<https://html.spec.whatwg.org/multipage/window-object.html#dom-window-close>

ざっくり要約すると、`window.close()`で閉じることができるのは次のパターンです。

- JavaScriptによって開かれた場合
- 履歴が1つしかないトップレベルブラウジングコンテキストの場合

それぞれ1つずつ見ていきます。

### JavaScriptで開かれた場合

`window.open()`[^1]によって開かれた場合がこれに該当します。

[^1]: <https://developer.mozilla.org/ja/docs/Web/API/Window/open><br><https://html.spec.whatwg.org/multipage/window-object.html#dom-open>

ちなみに、`window.open()`でも、`features`に`noopener`が指定されていると「JavaScriptで開かれた場合」に__該当しない__振る舞いになります。
逆に、`a[target="_blank"]`でも`[rel="opener"]`を付与すれば、「JavaScriptで開かれた場合」に__該当する振る舞い__になります[^2]。

[^2]: Google Chrome 89、Firefox 86、Edge 89 で動作確認済み。Safari（14）ではhistoryの挙動が異なります。

こうした実際の動きをみると、「JavaScriptによって開かれた」というよりも、`opener`[^3]が存在する場合、と言った方が正しいかもしれませんね。

[^3]: <https://developer.mozilla.org/ja/docs/Web/API/Window/opener>


### 履歴が1つしかないトップレベルブラウジングコンテキストの場合

2つの要素が関係しているので、ここも1つずつ確認していきましょう。

#### 履歴が1つしかない

`window.history.length`が`1`のときを指します。

これは、タブ（ブラウジングコンテキスト）が開かれてから、どこページにも遷移していないということを表します。つまり、そのブラウジングコンテキストで「戻る」「進む」がどちらか１方向でもできる状態だと、履歴の長さは2以上になります。

```js
if (window.history.length === 1) {
  // It is a browsing context whose session history contains only one Document.
}
```

#### トップレベルブラウジングコンテキスト

- `window` - 現在のブラウジングコンテキストの`WindowProxy`
- `window.parent` - 親ブラウジングコンテキストの`WindowProxy`
- `window.top` - トップレベルブラウジングコンテキストの`WindowProxy`

`iframe`などでブラウジングコンテキストがネストしているとき、自身のブラウジングコンテキストがトップレベルかを確認するには、次のような方法で確認できます[^4]。

[^4]: https://html.spec.whatwg.org/multipage/browsers.html#navigating-nested-browsing-contexts-in-the-dom

```js
if (window === window.top) {
  // It is a top-level browsing context
}
```

逆に**特別なことをしていなければ、常にトップレベルブラウジングコンテキスト**のはずです。


## どちらの状況も満たせない時にウィンドウを閉じるにはどうしたらいいの？

残念ですが閉じることができません[^5]。

[^5]: 通常のJavaScriptの範疇を超えない場合に限る（ブラウザ拡張を利用するなどはその限りではない）

そういう場合は、そもそも閉じるボタンの実装を見送って「このタブを閉じてください」など、ユーザ自身にページを閉じるよう促す文言を掲載したりするしかないでしょう。

もし、多くのケースで閉じるボタンが有効ではあるものの、特定のケースでのみ前述の条件を満たせないような場合には、代替テキストを表示する方法もあります。

```html
<button type="button" id="close">このボタンを押すとページが閉じます</button>

<script>
const closeBtn = document.getElementById('close');

closeBtn.addEventListener('click', function () {
  window.close();

  // `window.closed`を参照することでページが閉じられているかを確認できます。
  if (!window.closed) {
    this.textContent = '閉じるのに失敗しました';
  }
});
</script>
```

あるいは、もともと閉じることができるかを判定して、「閉じるボタン」と「ブラウザのUIで閉じるよう促す文言」を出し分けるのもいいかもしれません。

```html
<p id="close">ブラウザの×ボタンを押して、このページを閉じてください。</p>

<script>
const placeholder = document.getElementById('close');

if (
  window.opener ||
  (
    window === window.top &&
    history.length <= 1
  )
) {
  const closeBtn = document.createElement('button');

  closeBtn.type = 'button';
  closeBtn.textContent = 'このボタンを押してページを閉じます';
  closeBtn.addEventListener('click', function () {
    window.close();
  });
  placeholder.replaceWith(closeBtn);
}
</script>
```

## テスト用スペース

動いているのが確認したいという方向けに`window.close()`の動きをテストするためのページを用意してみました。

https://heppokofrontend.dev/webdev/trial/javascript/window.close?utm_source=qiita&utm_medium=social&utm_campaign=article

このページでは次の状態の違い、確認できます。

- トップレベルブラウジングコンテキストかどうか
- openerの存在
- 履歴の長さ
- Script closable（`window.close()`が利用可能か）


## 終わりに

わたしたちが`window.close()`閉じない問題を本当の意味で解決する方法は、そもそももっと上流工程を見直して`window.close()`が不要な導線設計になるようにするほかありません。

可能であれば、導線設計の話の部分にも食い込んで提案するようにしていきたいですね😔