##背景
PC や Android など、iOS 以外のデバイスで、ページ全体をスクロール無効にし、それ以外の要素は有効にしたい場合は、以下の CSS を追加すれば良いです。
(中略)
しかし、iOS では、この方法だとうまくいきません。
##実現したいこと
ページ全体をスクロール禁止にした上で、スクロール可能な要素についてのみ内部の(子要素を含む)どこをタッチしてもスクロールする。
overflow:scrollに設定されていても子要素が少なくスクロール可能状態になっていない要素や、スクロール可能状態だが一番下までスクロールされきっている要素はきちんとpreventDefaultしないと上位要素をスクロールしようとしてしまうことに注意する。
(諸事情によりwindowやbodyではなく、自分で作成した一番上位の要素をrootElementとして、これにイベントを設定する仕様になっている)
実現方法
- 再帰的に親要素を辿ってスクロール可能要素があるか確認する関数を作成
const check = (elem: HTMLElement,rootID:string): boolean => {
elem.scrollBy({ top: 1 });
if (elem?.id == rootID) {
return false;
} else if (elem.scrollTop) {
if(elem.scrollTop>1)elem.scrollBy({ top: -1 });
return true;
} else if(elem.parentElement){
return check(elem.parentElement);
}else{
return false;
}
};
- スクロール可能な要素がなければpreventDefaultする関数を作成
const preventScroll(rootElement:HtmlElement) => {
rootElement.addEventListener(
'touchmove',
e => {
const elem = e.target as HTMLElement;
if (!(elem && check(elem,rootElement.id)) && e.cancelable) e.preventDefault();
},
{ passive: false }
);
});
```
- スクロールを禁止したい要素に適用(ここでは仮称globalContainerに適用)
```typescript
const root=document.getElementById("globalContainer");
if(root) preventScroll(root)
```
##備考
check()内で一度スクロールを試みた後、スクロール可能な要素と判定された場合最初にスクロールを試みた分を戻しているが、
```typescript
const check = (elem: HTMLElement,rootID:string): boolean => {
elem.scrollBy({ top: 1 });
(中略)
else if (elem.scrollTop) {
if(elem.scrollTop > 1) elem.scrollBy({ top: -1 });
return true;
}
```
これによりスクロール可能要素が最後までスクロールしきった状態にならず、スクロールしきった要素を引っ張って画面全体がスクロールされる仕様に対処している。
##まとめ
力押しの判定方法で目標を実現した。
また、e.targetをHTMLElementにキャストしている部分や備考で示した箇所の記述など、確実に正しい記述である自信がない部分も多いため、指摘歓迎。
## 参考
- [iOS でページ全体はスクロールを無効にし、個別の要素(textarea など)では有効にする方法](https://qiita.com/noraworld/items/2834f2e6f064e6f6d41a)