デバイスの画面幅からはみ出している要素を検出する方法（１）

レスポンシブ対応のデバッグする際、デバイスの画面幅からはみ出して横スクロールの原因となっている要素がないかJavaScriptを使ってチェックしたい。

デバイスの画面幅の値を取得するには、最初、`screen.width`や`window.innerWidth`プロパティの値から取ればいいのかなと思っていましたが、それだと実行環境によってはチェックがうまくいかないケースがあるとわかりました。

この記事（前編）は、ブラウザーのレスポンシブデザインモード(RDM)と実機のブラウザーとでデバイスの画面幅がどのように認識されるか、その違いを調べたメモです。

## レスポンシブモードと実機での表示の違い
例として、レイアウト幅1200pxのページを画面幅1024pxのiPad Proに表示した場合を取り上げます。

※サンプルのコードはこちら: https://codepen.io/kaz_hashimoto/pen/XWEXRdO

下図は左から順に、Chrome DevToolsからのデバイスモード、Safariのレスポンシブデザインモード、iOSシミュレーターを使って表示した画面です。
![fig01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/b8b9a4a1-12f2-60dd-3f09-1c207644019e.jpeg)
※画像引用：Photo by [Inspirationfeed](https://unsplash.com/@inspirationfeed?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)


デバイスモードやレスポンシブデザインモード（以下、この２つを総称してRDMと表記）で表示した場合、コンテンツが画面幅に収まらないため横スクロールが発生しています。一方、実機[^1]で表示した場合はページ全体が縮小して画面の横幅にフィットして表示されました。
[^1]: テストしたのはXcodeのiOSシミュレーターですが説明を簡単にするため本文中は「実機」とみなして表記します。

もう１つ大きな違いはメディアクエリの結果です（左端の矢印）。

| クエリ | RDM | 実機 |
|:--|:-:|:-:|
| `@media (max-width: 1024px)` | true  | false  |

ブラウザーが認識している画面の寸法がRDMと実機とで異なるようです。そこで、このとき画面やビューポートの寸法に関するプロパティの値は何が返されるのかを検証してみました。

- [screen.width](https://developer.mozilla.org/ja/docs/Web/API/Screen/width)
- [window.innerWidth](https://developer.mozilla.org/ja/docs/Web/API/Window/innerWidth)
- [visualViewport.width](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport/width)
- [visualViewport.scale](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport/scale)

以下、デバイスのタイプはすべてiPad Pro (12.9-inch)を使用し、画面の方向は縦とします。デバイスの画面サイズは1024x1366です。また、HTMLにはビューポート属性が指定されているものとします。

```html:html
<meta name="viewport" content="width=device-width, initial-scale=1">
```

## 結果
まずはデスクトップ版ブラウザー[^2]のRDMで表示した場合です。表でセルの値が1024pxを超えるものを青色の背景で示しました。
![table1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/fc7804a1-a891-287c-c9d2-8e7a3fd42ace.png)
Safariだけ`screen.width`の値が2560pxになっていますが、これは私が使用しているモニター(iMac 27inch)の画面幅の値(cssピクセル単位)を返してきたようです。
[^2]: 使用したブラウザーのバージョン（記事執筆時点）は、Chrome 105, Firefox 104, Safari 15.6.1, Edge 105です。OSはEdgeのみWindows 10、それ以外はmacOS Big Surです。

一方、`window.innerWidth`については、レイアウト幅の1200pxを返すものと、デバイスの画面幅1024pxを返すものが混在しています。

次に実機で表示した場合です。
![table2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/f6fc4460-eeaa-7ce4-4c7e-c4a7cb23c22c.png)
iOS13以上ではページがフィットして表示されましたが、iOS12ではフィットせずに画面幅から溢れて横スクロールが発生しました。

２つの表からわかるように、iOS12を除き右端のセルの値は常に1024を表示しています。この計算式を使えば、RDMでも実機でも一律にデバイスの画面幅を取得することができそうです。

```javascript:式1
visualViewport.width * visualViewport.scale
```

iOSの場合、[VisualViewport](https://developer.mozilla.org/en-US/docs/Web/API/VisualViewport)インターフェイスはiOS13からサポートされました。iOS12以前など`visualViewport`をサポートしていない古いブラウザーについては`window.innerWidth`で代用するしかないかと思います。

:::note info
MDNの説明によると、`window.innerWidth`の値は[レイアウトビューポート](https://developer.mozilla.org/ja/docs/Web/API/Window/innerWidth)の幅から取られるのが仕様なので、このサンプルの場合、1200pxを返すのが正しいと思われます。
:::

## 他のケースで検証
式1で画面幅の値が得られるかどうか他のケースで検証してみましょう。

### タブを追加した時のサイズ
iPadでタブを追加するとresizeイベントが発生します。下の動画では右側のconsoleに各プロパティの値がどう変化するかを表示しています。

<p class="codepen" data-height="610.8324584960938" data-default-tab="result" data-slug-hash="gOzrZxL" data-user="kaz_hashimoto" style="height: 610.8324584960938px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/kaz_hashimoto/pen/gOzrZxL">
  Vimeo Player API demo</a> by Kazuhiro Hashimoto (<a href="https://codepen.io/kaz_hashimoto">@kaz_hashimoto</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

consoleのログにはタイミングごとにラベルを付けました。
![table3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/39509d95-f126-5eb3-9d90-6552670d6b06.png)
`window.innerWidth`と`visualViewport.width`の値は共に最初1024pxに設定されますが、画像の読み込みが完了したタイミングで1200pxに変わります。そしてタブを追加してアクティブにした時にresizeイベントが発生し、一旦1024pxに戻った後、少し経ってから1200pxに変わる、という複雑な動きをしています。

一方、`visualViewport.width * visualViewport.scale`は常に画面幅の1024pxが得られます。

### Split View
次にiPadのSplit Viewで画面を分割した場合です。画面を縦置き・横置き両方やってみます。
![fig02_splitview.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/9449834b-4e7f-5ef8-2075-b91bfa403cfc.jpeg)
まずはSafariのRDMです。SafariのRDMはiPadのSplit View表示もシミュレートできます。
![table4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/428bd595-2a16-8d9c-b398-6343a7fdb66f.png)
`screen.width`は相変わらずモニターの画面幅の値を返しますが、`visualViewport.width * visualViewport.scale`は、Split Viewで分割された画面の幅が得られます。

次に実機でのSplit Viewです。
![table5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/469745/fcfc68d8-2624-bdc9-ad3a-577b844c5888.png)
`screen.width`はデバイスの画面幅、`window.innerWidth`および`visualViewport.width`はレイアウト幅(1200px)を返しています。こちらも同様に`visualViewport.width * visualViewport.scale`は、Split Viewで分割された画面の領域の幅が得られます。
## まとめ
後で使うため、式１を関数で定義しておきます。

`getDeviceWidth()`は、ビューポートのスケーリングの影響を取り除いた形でデバイスの画面幅の値（cssピクセル単位）を返します。
※Split Viewで画面を領域に分割している場合、この関数は領域の幅の値を返します

```javascript:javascript
function getDeviceWidth() {
  if (typeof window.visualViewport === 'undefined') {
    return window.innerWidth;
  }
  const width = visualViewport.width * visualViewport.scale;
  return Math.round(width);
}
```

[後編の記事（２）](https://qiita.com/kazhashimoto/items/19398c5a7d92acb59d65)へ続く。