スクロール有りモーダルウィンドウを全デバイスで綺麗に実装したかった


※JavaScript(プログラミング全般)経験が浅いので、コードの質などご容赦くださいm(_ _)m

モーダルウィンドウを作る機会があったので、そこで得た知見などを今後に生かしていきたいと思い今回記事にまとめたいと思います。

## デモ
モーダル内でスクロールする画面の実装を目指しました。

![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/212222/e5600cfd-b8ea-6953-daff-216d29e618c9.gif)

## コード

html

```html
<body>
  <div class="container">
    <div class="modal">
      <h2>モーダル1</h2>
      <a class="modal__open">open</a>
      <div class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル1</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
    <div class="modal">
      <h2>モーダル2</h2>
      <a class="modal__open">open</a>
      <div class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル2</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
    ・
    ・
    ・
  </div>
</body>
```

css

```css
body {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  color: #333;
}

a {
  text-decoration: none;
}

.container {
  padding: 30px 20px;
}

.modal {
  margin-bottom: 50px;
}

.modal__contents {
  display: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: #ccccccd9;
  overflow-y: scroll;
}

.modal__inner {
  padding: 60px 20px;
}

.modal__inner p:last-of-type {
  margin-bottom: 30px;
}

.modal__open,
.modal__close {
  cursor: pointer;
  border: 1px solid #333;
  border-radius: 5px;
  padding: 10px;
}
```

js

```js
const Modal = function(modal) {
  this.window = window;
  this.body = document.body;
  this.modal = modal;
  this.contents = this.modal.querySelector(`.modal__contents`);
  this.open = this.modal.querySelector(`.modal__open`);
  this.close = this.modal.querySelector(`.modal__close`);
}

Modal.prototype = {
  init: function(){
    this.handleEvent();
  },
  handleEvent: function() {
    if (this.open !== undefined ) {
      this.open.addEventListener("click", (e) => {
        e.preventDefault();
        this.show();
      })
    }
    if (this.close !== undefined ) {
      this.close.addEventListener("click", (e) => {
        e.preventDefault();
        this.hide();
      })
    }
  },
  show: function() {
    // openボタンをクリックした際の処理
    this.contents.style.display = "block";
  },
  hide: function() {
    // closeボタンをクリックした際の処理
    this.contents.style.display = "none";
  }
}

document.querySelectorAll(".modal").forEach(function(modal) {
  new Modal(modal).init();
})
```

## 課題点1
モーダル内のコンテンツを最下部までスクロールし終わった後、後ろのコンテンツもスクロールしてしまう。

=> body要素に対し`overflow: hidden;`を当ててあげれば解決

```js
  show: function() {
    this.contents.style.display = "block";
    // 追加
    this.body.style.overflow = "hidden";
  },
  hide: function() {
    this.contents.style.display = "none";
    // 追加
    this.body.removeAttribute('style');
  }
```

やったこれで実装できた！！
・・・と思っていました。


## 課題点2
しかしIOS/safariで確認すると全く止まっていません。。。
調べてみると上記ブラウザでは`overflow: hidden;`では止まらないようです。

=> `body`に`position: fixed;`をつけて無理やり止める

```js
  show: function() {
    this.contents.style.display = "block";
    // 変更
    this.body.style.position = "fixed";
    this.body.style.top = "0";
  },
```

固定できた！
・・・と思っていました。


## 課題点3
これだとモーダルを開いた時に最上部まで戻ってしまう。
=> モーダルを開いた時点の位置を変数に格納し、閉じた時にその値までスクロールの値を戻す

```js
  handleEvent: function() {
    // 追加
    let scroll_y;
    if (this.open !== undefined ) {
      this.open.addEventListener("click", (e) => {
        e.preventDefault();
        // 現在のスクロール位置を格納
        scroll_y = this.window.scrollY;
        this.show(scroll_y);
      })
    }
    if (this.close !== undefined ) {
      this.close.addEventListener("click", (e) => {
        e.preventDefault();
        this.hide(scroll_y);
      })
    }
  },
  show: function(scroll_y) {
    this.contents.style.display = "block";
    this.body.style.position = "fixed";
    // 変更
    this.body.style.top = `${-1 * scroll_y}px`;
  },
  hide: function(scroll_y) {
    this.contents.style.display = "none";
    this.body.removeAttribute('style');
    // 閉じた際開いた位置までスクロール
    this.window.scrollTo(0, scroll_y);
  },
```

よしできた！
・・・と思っていました。


## 課題点4
モーダルが開いた際に上部のアドレスバーが大きくなりかくついて見えてしまうし、横画面の時開いた位置に戻らない・・・。
気になる！
一度気になってしまうとずっと気になってしまう。。。

=> `"touchmove"`の挙動を制御してみる

調べてみると`addEventListener`で`"touchmove"`に`preventDefault`を指定してあげればスクロールしなくなるらしい！

ただこれを実装するならば、メイン部分とモーダルの中身を分けないといけないと思い、
`contaeiner`と`modal__contents`を兄弟関係に修正しました。

```html
<div class="container">
  <!-- スクロール止めたい -->
  <a href="#modal1" class="modal__open">open</a>
</div>
<div id="modal1" class="modal__contents">
  <!-- スクロールしたい -->
</div>
```

このように外に出せばいいはず。
コンテンツをidで紐ずける為プロパティも修正。

```js
const Modal = function(modal) {
  this.window = window;
  this.body = document.body;
  // 追加
  this.container = document.querySelector(`.container`);
  this.modal = modal;
  this.open = this.modal.querySelector(`.modal__open`);
  // 修正
  this.contents = document.querySelector(this.open.getAttribute('href'));
  // 修正
  this.close = this.contents.querySelector(`.modal__close`);
}
```

`preventDefault`は無名関数で実行すると`removeEventListener`が効かないので関数を定義します。

```js
  hide: function(scroll_y) {
    this.contents.style.display = "none";
    this.body.removeAttribute('style');
    this.window.scrollTo(0, scroll_y);
  },
  // 追加
  scrollControll: function(e) {
    e.preventDefault();
  }

```

モーダルを開いた時にイベントリスナーを登録。
※`{passive: false}`は必須です

```js
  show: function(scroll_y) {
    // 追加
    this.container.addEventListener("touchmove", this.scrollControll, { passive: false});
    this.contents.style.display = "block";
    this.body.style.position = "fixed";
    this.body.style.top = `${-1 * scroll_y}px`;
  },
  hide: function(scroll_y) {
    // 追加
    this.container.removeEventListener("touchmove", this.scrollControll, { passive: false});
    this.contents.style.display = "none";
    this.body.removeAttribute('style');
    this.window.scrollTo(0, scroll_y);
  },
```

確かにスクロールはしない。
けどモーダル部分もスクロールしない・・・、これはだめだ・・・。
僕なりにそのあと色々と調べて見たのですが、指定要素だけスクロールを止めることは今の知識量では無理でした(T T)

## 結果

IOS/safariで
・かくついてもいいからどうしても背景固定がしたい！
=> `position: fixed;`を使って対応

・スクロールしちゃうのは仕方がない！
=> `overflow: hidden`を使って対応

・スクロールがないモーダルの場合
=> イベントリスナーで`"touchmove"`のデフォルトの挙動をoffにして対応しましょう！

僕は最終的に無理くりpositionで固定するより素直な実装の方が良い気がしたので、「IOS/safariはスクロールしても仕方がない」としました。

もし他にいい方法があれば教えていただきたく思いますm(_ _)m

途中ごちゃつきましたので、最後系のhtmlとjsのソースをもう一度記載致します(`overflow: hidden`)。
拙い文章を最後まで読んでいただきありがとうございました。

html

```html
  <div class="container">
    <div class="modal">
      <h2>モーダル1</h2>
      <a class="modal__open">open</a>
      <div class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル1</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
    <div class="modal">
      <h2>モーダル2</h2>
      <a class="modal__open">open</a>
      <div class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル2</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
    <div class="modal">
      <h2>モーダル3</h2>
      <a class="modal__open">open</a>
      <div class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル3</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
    <div class="modal">
      <h2>モーダル4</h2>
      <a class="modal__open">open</a>
      <div class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル4</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
    <div class="modal">
      <h2>モーダル5</h2>
      <a class="modal__open">open</a>
      <div id="mdoal5" class="modal__contents">
        <div class="modal__inner">
          <h3>モーダル5</h3>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <p>テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト</p>
          <a class="modal__close">close</a>
        </div>
      </div>
    </div>
  </div>
```

js

```js
const Modal = function(modal) {
  this.window = window;
  this.body = document.body;
  this.modal = modal;
  this.contents = this.modal.querySelector(`.modal__contents`);
  this.open = this.modal.querySelector(`.modal__open`);
  this.close = this.modal.querySelector(`.modal__close`);
}

Modal.prototype = {
  init: function(){
    this.handleEvent();
  },
  handleEvent: function() {
    if (this.open !== undefined ) {
      this.open.addEventListener("click", (e) => {
        e.preventDefault();
        this.show();
      })
    }
    if (this.close !== undefined ) {
      this.close.addEventListener("click", (e) => {
        e.preventDefault();
        this.hide();
      })
    }
  },
  show: function(scroll_y) {
    this.contents.style.display = "block";
    this.body.style.overflow = "hidden";
  },
  hide: function(scroll_y) {
    this.contents.style.display = "none";
    this.body.removeAttribute('style');
  },
}

document.querySelectorAll(".modal").forEach(function(modal) {
  new Modal(modal).init();
})
```