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

в 17:23, , рубрики: css, html, javascript, доступность, Разработка веб-сайтов, скролл

Начнем

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

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

.body { overflow: hidden;}

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

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

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

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

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

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

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

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) // берем значение, которое сохранили
})

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

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. Здесь автор более детально раскрыл тему библиотек и их проблем.

// Не забудьте подключить плагин
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')

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

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

Автор: Денис

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js