IOS 10 Safari: предотвращение прокрутки за фиксированным оверлеем и сохранение положения прокрутки

Я не могу предотвратить прокрутку содержимого основного тела, пока отображается оверлей с фиксированным положением. Подобные вопросы задавались много раз, но все методы, которые ранее работали, похоже, не работают на Safari в iOS 10. Это похоже на недавнюю проблему.

Некоторые заметки:

  • Я могу отключить прокрутку, если для html и body установить overflow: hidden, однако это заставит содержимое тела прокручиваться вверх.
  • Если содержимое в оверлее достаточно длинное, чтобы его можно было прокручивать, прокрутка корректно отключена для содержимого главной страницы. Если содержимое в оверлее недостаточно длинное, чтобы вызвать прокрутку, вы можете прокрутить содержимое главной страницы.
  • Я включил функцию javascript из https://blog.christoffer.online/2015-06-10-six-things-i-learnt-about-ios-rubberband-overflow-scrolling/, которая отключает touchmove во время отображения наложения. Это работало ранее, но больше не работает.

Вот полный исходный код 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>

Ответ 1


добавьте -webkit-overflow-scrolling: touch; к элементу #overlay.

И добавьте, пожалуйста, этот код javascript в конце тега body:

(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;
    }
}())

Надеюсь, это поможет вам.

Ответ 2

Сочетание подхода Богдана Дидуха с моим предыдущим подходом позволило создать простой в использовании пакет npm для отключения/включения прокрутки тела.

https://github.com/willmcpo/body-scroll-lock

Подробнее о том, как работает это решение, читайте по адресу https://medium.com/jsdownunder/locking-body-scroll-for-all-devices-22def9615177.

Ответ 3

Я долго пытался найти чистое решение для этого, и мне кажется, что мне лучше всего подходит установка pointer-events: none; на теле, а затем pointer-events: auto; явно на элементе, в котором я хочу разрешить прокрутку.

Ответ 4

Простое изменение режима прокрутки переполнения на body сработало для меня:

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

Ответ 5

Для тех, кто использует 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

Ответ 6

БогданРешение выше, отлично. Тем не менее, он не улавливает/не блокирует импульс - то есть, когда пользователь не находится точно в верхней части страницы, но находится вблизи верхней части страницы (скажем, scrollTop составляет 5 пикселей) и внезапно пользователь делает внезапное массивное опускание! Решение Bohand перехватывает события touchmove, но поскольку -webkit-overflow-scrolling основано на импульсе, сам импульс может вызвать дополнительную прокрутку, которая в моем случае скрывала заголовок и действительно раздражала.

Почему это происходит?

На самом деле, -webkit-overflow-scrolling: touch - свойство двойного назначения.

  1. Хорошая цель - дать эффект плавной прокрутки с резиновой лентой, который почти необходим в пользовательских элементах контейнера overflow:scrolling на устройствах iOS.
  2. Однако нежелательная цель - это "переполнение". Что имеет смысл, если учесть, что все должно быть плавно, а не внезапно останавливаться! :)

Решение для блокирования импульса

Решение, которое я придумал для себя, было адаптировано из решения Богдана, но вместо того, чтобы блокировать события touchmove, я изменяю вышеупомянутый атрибут CSS.

Просто передайте элемент с overflow: scroll-webkit-overflow-scrolling: touch) этой функции во время монтирования/рендеринга.

Возвращаемое значение этой функции должно вызываться во время destroy/beforeDestroy.

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);

Ответ 7

Я нашел код на GitHub. Работает на Safari в iOS 10,11,12

/* ScrollClass */
class ScrollClass {
constructor () {
    this.$body = $('body');

    this.styles = {
        disabled: {
            'height': '100%',
            'overflow': 'hidden',
        },

        enabled: {
            'height': '',
            'overflow': '',
        }
    };
}

disable ($element = $(window)) {
    let disabled = false;
    let scrollTop = window.pageYOffset;

    $element
        .on('scroll.disablescroll', (event) => {
            event.preventDefault();

            this.$body.css(this.styles.disabled);

            window.scrollTo(0, scrollTop);
            return false;
        })
        .on('touchstart.disablescroll', () => {
            disabled = true;
        })
        .on('touchmove.disablescroll', (event) => {
            if (disabled) {
                event.preventDefault();
            }
        })
        .on('touchend.disablescroll', () => {
            disabled = false;
        });
}

enable ($element = $(window)) {
    $element.off('.disablescroll');

    this.$body.css(this.styles.enabled);
}
}

использовать:

Scroll = new ScrollClass();

Scroll.disable();// disable scroll for $(window)

Scroll.disable($('element'));// disable scroll for $('element')

Scroll.enable();// enable scroll for $(window)

Scroll.enable($('element'));// enable scroll for $('element')

Я надеюсь, что это поможет вам.

Ответ 8

В некоторых случаях, когда содержимое тела скрыто за наложением, вы можете сохранить текущую позицию прокрутки, используя const scrollPos = window.scrollY, а затем применить position: fixed; к телу. Когда модель закроется, снимите фиксированное положение с тела и запустите window.scrollTo(0, scrollPos), чтобы восстановить предыдущее положение.

Для меня это было самое простое решение с наименьшим количеством кода.

Ответ 9

Когда ваш оверлей открыт, вы можете добавить класс, например prevent-scroll в body, чтобы предотвратить прокрутку элементов позади вашего наложения:

body.prevent-scroll {
  position: fixed;
  overflow: hidden;
  width: 100%;
  height: 100%;
}

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

Ответ 10

Попробуйте добавить к телу максимальную высоту: 100vh;

Ответ 11

Попробуйте добавить CSS в html, это работает для меня:

html {
    overflow-x: hidden;
}