みかづきブログ・カスタム

基本的にはちょちょいのほいです。

iOSでページをスクロールしている最中にscrollToを使ってページのスクロール位置を変更すると次のtouchmoveの発生で元の位置に戻る 👆

f:id:kimizuka:20210207131317p:plain

github.com

こちらのissue に対応するために調査しました。
こんなことは普通はしないと思うのですが、iOSにてページスクロール中にscrollToを使うと、

❶ ユーザーがページのスクロールを開始する
❷ widow.scrollToでスクロール位置を変更する
❸ (指定箇所までページがスクロールする)
❹ ユーザーがtouchendを発生させないままページをスクロールする
❺ (widow.scrollTo発生前の本来の位置までスクロール位置が戻る) 👈

という挙動になるっぽいです。(iOS14.4にて確認)
つまり、touchendが発生するまでは、touchstartの位置が起点になるようです。

ちなみにAndroidのChromeの場合、

❶ ユーザーがページのスクロールを開始する
❷ widow.scrollToでスクロール位置を変更する
❸ (指定箇所までページがスクロールする)
❹ ユーザーがtouchendを発生させないままページをスクロールする
❺ (widow.scrollTo発生後の位置からスクロールする) 👈

という挙動になりました。

今回はAndroidの挙動をiOSでも再現するためにいろいろ探ってみました。
結論だけ先に書くと、windowのスクロールをAndroidと同じ挙動にするのは無理でした。

❌ touchendをdispatchする

setTimeout(() => {
  window.scrollTo(0, 100);
  document.body.dispatchEvent(new Event('touchend'));
}, 1000);

こんなイメージです。
自作のtouchendを発火させてみたのですが、残念ながら何も起こりませんでした。

❌ widow.scrollToと同時にtouch-actionをnoneにする

setTimeout(() => {
  window.scrollTo(0, 100);
  document.body.style.touchAction = 'none';
  setTimeout(() => {
    document.body.style.touchAction = 'auto';
  }, 0);
}, 1000);

こんなイメージです。
一瞬、touch-actionをnoneにしてスクロールを止め、autoに戻すという作戦です。
が。そもそも、autoに戻さなくても、touchendが発生するまではスクロールが止まりませんでした。

つまり、

❶ ユーザーがページのスクロールを開始する
❷ widow.scrollToでスクロール位置を変更し、touch-actionをnoneにする
❸ (指定箇所までページがスクロールする)
❹ ユーザーがtouchendを発生させないままページをスクロールする
❺ (widow.scrollTo発生後の位置からスクロールする)
❻ ユーザーが指を一度離すとスクロールが不可になる

という感じで、一度touchendが発生しないとtouch-actionの変更が有効にならないようです。

❌ widow.scrollToと同時にtouchmoveのpriventDefaultを実行する

setTimeout(() => {
  window.scrollTo(0, 100);
  document.body.addEventListener('touchmove', handleTouchMove, {
    passive: false
  });
  setTimeout(() => {
    document.body.removeEventListener('touchmove', handleTouchMove);
  }, 0);
}, 1000);

function handleTouchMove(evt) {
  evt.preventDefault();
}

こんなイメージです。
一瞬、touchmoveのpriventDefaultを実行する関数をaddしてスクロールを止め、removeするという作戦です。
これも、touch-actionのときと結果は同じで、ユーザーが一度指を離すまではpriventDefaultが有効になりませんでした。

⭕️ 画面と同じ大きさのoverflow: scrollの要素を作り、widow.scrollToと同時にremoveChildする

こうなったら一度window自体をremoveするしかないと思ったのですが、そんなことはできません。
なので、

❶ windowと同じ大きさのoverflow: scrollの要素をつくり、position: fixedで配置(以下、overflowと呼ぶ)
❷ window.scrollToを使わず、overflowのscrollTopを操作
❸ scrollTopを操作したいタイミングでoverflowを一度removeChildし、appendChildしなおす
❹ ❸のappendChildの際にscrollTopを代入

という流れで、一瞬チラつきはしますがほぼほぼAndroidの挙動と同様になりました。長い戦いでした。