iOS 10 Safari: 防止固定覆盖层后的滚动,并保持滚动位置。

44
我无法在显示固定位置的覆盖层时阻止主体内容滚动。类似的问题已经被问过很多次,但是之前有效的所有技术在iOS 10的Safari上似乎都不起作用。这似乎是最近的问题。
一些注意事项:
- 如果我将`html`和`body`都设置为`overflow: hidden`,我可以禁用滚动,但是这会使主体内容滚动到顶部。 - 如果覆盖层中的内容足够长以至于可以滚动,那么主页面内容的滚动将被正确禁用。如果覆盖层中的内容不足以引起滚动,则可以滚动主页面内容。 - 我在https://blog.christoffer.online/2015-06-10-six-things-i-learnt-about-ios-rubberband-overflow-scrolling/中包含了一个禁用`touchmove`的JavaScript函数,当覆盖层显示时。这之前是有效的,但现在不再起作用。
以下是完整的HTML源代码:
<!doctype html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
    <style type="text/css">
        html, body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
        body {
            font-family: arial;
        }
        #overlay {
            display: none;
            position: fixed;
            z-index: 9999;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            overflow: scroll;
            color: #fff;
            background: rgba(0, 0, 0, 0.5);
        }
        #overlay span {
            position: absolute;
            display: block;
            right: 10px;
            top: 10px;
            font-weight: bold;
            font-size: 44px;
            cursor: pointer;
        }
        #overlay p {
            display: block;
            padding: 100px;
            font-size: 36px;
        }
        #page {
            width: 100%;
            height: 100%;
        }
        a {
            font-weight: bold;
            color: blue;
        }
    </style>
    <script>
        $(function() {
            $('a').click(function(e) {
                e.preventDefault();
                $('body').css('overflow', 'hidden');
                $('#page').addClass('disable-scrolling'); // for touchmove technique below

                $('#overlay').fadeIn();
            });
            $('#overlay span').click(function() {
                $('body').css('overflow', 'auto');
                $('#page').removeClass('disable-scrolling'); // for touchmove technique below

                $('#overlay').fadeOut();
            });
        });

        /* Technique from http://blog.christoffer.me/six-things-i-learnt-about-ios-safaris-rubber-band-scrolling/ */
        document.ontouchmove = function ( event ) {
            var isTouchMoveAllowed = true, target = event.target;
            while ( target !== null ) {
                if ( target.classList && target.classList.contains( 'disable-scrolling' ) ) {
                    isTouchMoveAllowed = false;
                    break;
                }
                target = target.parentNode;
            }
            if ( !isTouchMoveAllowed ) {
                event.preventDefault();
            }
        };
    </script>
</head>

<body>
    <div id="overlay">
        <span>&times;</span>
        <p>fixed popover</p>
    </div>

    <div id="page">
        <strong>this is the top</strong><br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        lots of scrollable content<br>
        asdfasdf<br>
        <br>
        <div><a href="#">Show Popover</a></div>
        <br>
        <br>

    </div>

</body>

</html>
13个回答

70

#overlay元素中添加-webkit-overflow-scrolling: touch;

然后在body标签的末尾添加以下JavaScript代码:

(function () {
  var _overlay = document.getElementById('overlay');
  var _clientY = null; // remember Y position on touch start

  _overlay.addEventListener('touchstart', function (event) {
    if (event.targetTouches.length === 1) {
      // detect single touch
      _clientY = event.targetTouches[0].clientY;
    }
  }, false);

  _overlay.addEventListener('touchmove', function (event) {
    if (event.targetTouches.length === 1) {
      // detect single touch
      disableRubberBand(event);
    }
  }, false);

  function disableRubberBand(event) {
    var clientY = event.targetTouches[0].clientY - _clientY;

    if (_overlay.scrollTop === 0 && clientY > 0) {
      // element is at the top of its scroll
      event.preventDefault();
    }

    if (isOverlayTotallyScrolled() && clientY < 0) {
      //element is at the top of its scroll
      event.preventDefault();
    }
  }

  function isOverlayTotallyScrolled() {
    // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
    return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight;
  }
}())

1
代码最好保存在外部文件中,以便可以进行缓存。 - Rolf
1
我想同样的技巧也适用于Firefox for iOS,它似乎有相同的问题? - Anthony Kong
4
当出现遮罩层时,我必须复制所有JS代码来禁用后台的滚动条吗?我希望有一种更优雅的解决方案。 - victor
1
如果需要阻塞动量,我刚刚添加了自己的解决方案,受 @BohdanDidukh 的影响 :) -- https://dev59.com/PFgR5IYBdhLWcg3wM7Hh#57566116 - Aidin
1
今晚看了很多帖子,这个方法出奇的好用。 设备:iPhone-11-Pro Max 操作系统:15.6.1 浏览器:Safari - Llama D'Attore
显示剩余8条评论

13

1
是的,不幸的是,在移动版Safari上这并不起作用。我一直没有找到任何真正解决移动版Safari滚动问题的方法。 - Jeremy Gottfried
1
这个包在我的情况下阻止了 IOS 设备上固定位置的模态框滚动。 - Liam McArthur
这个软件包对我来说非常宝贵。@Will的工作太棒了! - Hirbod

6

我曾长时间尝试寻找一种简洁的解决方案,现在似乎最好的方法是在中设置pointer-events: none;,并显式地在要允许滚动的项目上设置pointer-events: auto;


3
尝试在iOS 12上使用,但没有效果 :/ - t0vana
@t0vana,我们在iOS 12上已经将其用于生产环境,并且它对我们有效。 - Firas Dib
1
我唯一能让它工作的方法是为所有站点内容设置一个包装元素,该元素设置为 position: absolute; width: 100%; height: 100%; overflow: scroll; 并在该包装元素上设置 pointer-events: none。在 body 上设置它不起作用。我认为这是最好的方法,因为苹果似乎会破坏任何其他技术。 - Gavin
谢谢!这在我的移动Safari上起作用了。如果我将pointer-events: none添加到body和包装元素,然后将pointer-events: auto添加到我想要滚动的元素,就可以了。 - Jeremy Gottfried
在iOS 13上无法正常运行 即使对每个元素都添加pointer-events: none;也不能禁用滚动 - Monfa.red
pointer-events: none 在 iOS 15 上似乎无法锁定滚动,你也是这样吗? - Fred K

4

Bohdan解决方案很好。然而,它不能捕获/阻塞动量——即当用户不在页面顶部时,但接近页面顶部(比如,scrollTop 为5px),突然进行大幅下拉操作时的情况!Bohand的解决方案捕获了touchmove事件,但由于-webkit-overflow-scrolling是基于动量的,动量本身可能会导致额外的滚动,在我的情况下会隐藏标题栏,非常令人恼火。

为什么会这样?

事实上,-webkit-overflow-scrolling: touch是一个双重用途属性。

  1. 好处是它提供了橡皮筋平滑滚动效果,这在iOS设备上的自定义overflow:scrolling容器元素中几乎是必需的。
  2. 不需要的目的是这种“超出滚动”。这有点合理,因为它都是关于平稳而不是突然停止! :)

阻止惯性滚动的解决方案

我为自己想出的解决方案是从Bohdan的解决方案改编而来,但我不是阻止touchmove事件,而是更改上述的CSS属性。

只需在挂载/渲染时将具有overflow:scroll(和-webkit-overflow-scrolling:touch)的元素传递给此函数即可。

此函数的返回值应在销毁/销毁之前调用。

const disableOverscroll = function(el: HTMLElement) {
    function _onScroll() {
        const isOverscroll = (el.scrollTop < 0) || (el.scrollTop > el.scrollHeight - el.clientHeight);
        el.style.webkitOverflowScrolling = (isOverscroll) ? "auto" : "touch";
        //or we could have: el.style.overflow = (isOverscroll) ? "hidden" : "auto";
    }

    function _listen() {
        el.addEventListener("scroll", _onScroll, true);
    }

    function _unlisten() {
        el.removeEventListener("scroll", _onScroll);
    }

    _listen();
    return _unlisten();
}

快速简短的解决方案

或者,如果您不关心unlisten(这是不建议的),一个更短的答案是:

el = document.getElementById("overlay");
el.addEventListener("scroll", function {
    const isOverscroll = (el.scrollTop < 0) || (el.scrollTop > el.scrollHeight - el.clientHeight);
    el.style.webkitOverflowScrolling = (isOverscroll) ? "auto" : "touch";
}, true);

更新:在 iPhone X 和 iOS 15 上无法正常工作。 :( - Aidin

3

对我来说,只需更改body上的溢出滚动行为即可:

body {
    -webkit-overflow-scrolling: touch;
}

不错,我没想到那个。 - Gavin
1
非标准:https://developer.mozilla.org/zh-CN/docs/Web/CSS/-webkit-overflow-scrolling - idiglove
1
一个平台特定问题的解决方法是否标准并不重要。(虽然这并没有帮助,我认为在iOS 13左右发生了一些变化。) - Glenn Maynard

3

我也在Safari(iOS)上遇到了同样的问题。我考虑了上面所有的解决方案,但对于这些hack并不满意。后来,我了解到了属性 touch-action。将touch-action:none添加到覆盖层中解决了我的问题。对于上述问题,在覆盖层内的中添加touch-action:none

  #overlay span {
        position: absolute;
        display: block;
        right: 10px;
        top: 10px;
        font-weight: bold;
        font-size: 44px;
        cursor: pointer;
        touch-action: none;
    }

这是适用于Safari > 13的正确答案。不幸的是,并非所有苹果设备都已更新。但是,touch-actionbody, html { position: fixed; }结合似乎暂时可行。 - Brmm
这将禁用移动设备上覆盖层的所有滚动,这并不像一个好的解决方案,或者只适用于非常特定的情况。 - Manuel

2
当您的叠加层被打开时,您可以将类似于 prevent-scroll 的类添加到 body 中,以防止叠加层后面的元素滚动:
body.prevent-scroll {
  position: fixed;
  overflow: hidden;
  width: 100%;
  height: 100%;
}

https://codepen.io/claudiojs/pen/ZKeLvq


32
很抱歉,它将不会保持滚动位置。 - aventic

1
在某些情况下,当正文内容被覆盖层隐藏时,您可以使用const scrollPos = window.scrollY存储当前滚动位置,然后将position: fixed;应用于正文。当模型关闭时,从正文中删除固定位置并运行window.scrollTo(0, scrollPos)以恢复先前的位置。对我来说,这是最简单的解决方案,需要最少的代码。

这对我来说是最简单的解决方案,因为如果有嵌套的滚动容器,其他方法会变得非常复杂。 - robocub

1

对于使用React的人,我已经成功地将@bohdan-didukh的解决方案放在一个组件的componentDidMount方法中。类似这样(链接可在移动浏览器中查看):

class Hello extends React.Component {
  componentDidMount = () => {
    var _overlay = document.getElementById('overlay');
    var _clientY = null; // remember Y position on touch start

    function isOverlayTotallyScrolled() {
        // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#Problems_and_solutions
        return _overlay.scrollHeight - _overlay.scrollTop <= _overlay.clientHeight;
    }

    function disableRubberBand(event) {
        var clientY = event.targetTouches[0].clientY - _clientY;

        if (_overlay.scrollTop === 0 && clientY > 0) {
            // element is at the top of its scroll
            event.preventDefault();
        }

        if (isOverlayTotallyScrolled() && clientY < 0) {
            //element is at the top of its scroll
            event.preventDefault();
        }
    }

    _overlay.addEventListener('touchstart', function (event) {
        if (event.targetTouches.length === 1) {
            // detect single touch
            _clientY = event.targetTouches[0].clientY;
        }
    }, false);

    _overlay.addEventListener('touchmove', function (event) {
        if (event.targetTouches.length === 1) {
            // detect single touch
            disableRubberBand(event);
        }
    }, false);
  }

  render() {
    // border and padding just to illustrate outer scrolling disabled 
    // when scrolling in overlay, and enabled when scrolling in outer
    // area
    return <div style={{ border: "1px solid red", padding: "48px" }}>
      <div id='overlay' style={{ border: "1px solid black", overflowScrolling: 'touch', WebkitOverflowScrolling: 'touch' }}>
        {[...Array(10).keys()].map(x => <p>Text</p>)}
      </div>
    </div>;
  }
}

ReactDOM.render(
  <Hello name="World" />,
  document.getElementById('container')
);

可通过手机查看:https://jsbin.com/wiholabuka

可编辑链接:https://jsbin.com/wiholabuka/edit?html,js,output


0
如果您正在使用React并且可以访问模态状态,最简单的方法是添加以下内容。
 document.ontouchmove = (e) =>{
    if(MODAL_OPEN_STATE) e.preventDefault();
 }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接