- PVSM.RU - https://www.pvsm.ru -

Модальные окна, которые мы заслужили

Начнем

В разных критериях и манифестах качества есть такой пункт, как блокировка скролла страницы, когда открыто модальное окно. Модальное окно подразумевает под собой взаимодействие только с ним.

Многие зададут вопрос: «Почему не использовать готовые решения?». Проблема в том, что этот функционал либо вообще не реализован, либо сделан очень плохо. Можно использовать самое простое и плохое решение:

.body { overflow: hidden;}

Здесь мы получаем легкое подергивание страницы. Код [1]. Стоит отметить: проблема встречается только там, где скролл занимает место на странице, например, в Safari такой проблемы нет.

Анимация проблемы(Gif)

Модальные окна, которые мы заслужили - 1

Некоторые решения на просторах npm предлагают замену скролла отступом или блоком, который стилизован под скролл (простая серая полоска), много магии, но проблему они не исправляют.

Немного подправим начальный код, и все работает:

.body { 
    position:fixed;
    overflow-y: scroll;
}

Теперь появилась другая проблема: страница прыгает вверх. Код [2]. Решение через js простое: при открытии страницы берем значение, на которое проскроллена страница, и задаем это значение свойству top тега <body> Код [3].

function getBodyScrollTop() {
    return self.pageYOffset || (document.documentElement && document.documentElement.ScrollTop) || (document.body && document.body.scrollTop);
  }

openModalButton.addEventListener('click', e => {
  e.preventDefault()
  
  body.dataset.scrollY = getBodyScrollTop() // сохраним значение скролла
  body.style.top = `-${body.dataset.scrollY}px`
  
  modal.classList.add('modal--open')
  body.classList.add('body-lock')
})

closeModalButton.addEventListener('click', e => {
  e.preventDefault()
  
  modal.classList.remove('modal--open')
  body.classList.remove('body-lock')
  window.scrollTo(0, body.dataset.scrollY) // берем значение, которое сохранили
})

Иногда на сайте может встречаться страничка, у которой мало контента и нет скролла, но при открытии модального окна наш код добавляет его. Немного доработаем скролл. Готовое решение [4].

function existVerticalScroll() {
   return document.body.offsetHeight > window.innerHeight
}

openModalButton.addEventListener('click', e => {
  e.preventDefault()
  
  body.dataset.scrollY = getBodyScrollTop()
  
  modal.classList.add('modal--open')
  
  if(existVerticalScroll()) { // новая строка
     body.classList.add('body-lock')
     body.style.top = `-${body.dataset.scrollY}px`
   }
})

closeModalButton.addEventListener('click', e => {
  e.preventDefault()
  
  modal.classList.remove('modal--open')
  
  if(existVerticalScroll()) { // новая строка
   body.classList.remove('body-lock')
   window.scrollTo(0,body.dataset.scrollY)    
  }
})

Доступность

Если о ней не подумать, будет возможное перемещение с помощью таба вне модального окна. На gif только нажатие Tab несколько раз.

Анимация проблемы(Gif)

Модальные окна, которые мы заслужили - 2

На просторах интернета нет простого решения этой проблемы, а писать свой костыль велосипед не очень рационально, когда есть библиотека focus-trap.js [5]. Здесь автор более детально раскрыл тему библиотек и их проблем [6].

// Не забудьте подключить плагин
const modalFocusTrap = createFocusTrap(".modal"); // инициализируем плагин для модального окна

openModalButton.addEventListener('click', e => {
  e.preventDefault()
  
  body.dataset.scrollY = getBodyScrollTop()
  
  modal.classList.add('modal--open')
  modalFocusTrap.activate(); // новая строка. Активируем плагин

  if(existVerticalScroll()) {
     body.classList.add('body-lock')
     body.style.top = `-${body.dataset.scrollY}px`
   }
})

closeModalButton.addEventListener('click', e => {
  e.preventDefault()
  
  modal.classList.remove('modal--open')
  modalFocusTrap.deactivate(); // новая строка. Отключаем плагин

  if(existVerticalScroll()) {
   body.classList.remove('body-lock')
   window.scrollTo(0,body.dataset.scrollY)    
  }
})

Итоги

Даже если на странице несколько модальных окон, весь код можно обернуть в одну функцию и передавать название класса модального окна.

function initModal(className) {
    // код, что мы писали выше для одной модалки
}
initModal('modalName1')
initModal('modalName2')

Проблема на первый взгляд несложная, но как много неочевидных моментов. Думаю, эта статья поможет вам делать более качественные модальные окна. Готовый код [7]

Полезные материалы

Автор: Денис

Источник [11]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/342391

Ссылки в тексте:

[1] Код: https://codepen.io/Den4ink3424/pen/MWYORaZ

[2] Код: https://codepen.io/Den4ink3424/pen/povdBOm

[3] Код: https://codepen.io/Den4ink3424/pen/MWYORLW

[4] Готовое решение: https://codepen.io/Den4ink3424/pen/NWPwZyj

[5] focus-trap.js: https://github.com/davidtheclark/focus-trap

[6] Здесь автор более детально раскрыл тему библиотек и их проблем: https://habr.com/ru/post/338130/

[7] Готовый код: https://codesandbox.io/embed/purple-tree-3rdnr?fontsize=14&hidenavigation=1&theme=dark

[8] Focusable Elements — Browser Compatibility Table: https://allyjs.io/data-tables/focusable.html

[9] Оставил для новичков, кто встретил dataset в первый раз: https://developer.mozilla.org/ru/docs/Web/API/HTMLElement/dataset

[10] О теге <dialog>: https://developer.mozilla.org/ru/docs/Web/HTML/Element/dialog

[11] Источник: https://habr.com/ru/post/483114/?utm_source=habrahabr&utm_medium=rss&utm_campaign=483114