Модальное окно — это эмуляция диалогового окна браузера, появляющегося поверх страницы в ответ на действия пользователя и блокирующего доступ к основному содержимому страницы.
Раньше в Контуре этот компонент назывался Лайтбоксом, но мы переименовали его, чтобы не было разночтений. В профессиональном сообществе «модальное окно» более распространенный и устоявшийся термин.
Когда использовать
Используйте модальное окно для второстепенного содержимого страниц, которое требуется только в некоторых случаях, или для того, чтобы сосредоточить внимание пользователя на совершаемом действии. Как правило, это настройки, создание новых документов, заполнение небольших форм, пошаговый мастер. Например, для ввода адреса — при клике по ссылке открывается модальное окно.
Не используйте модальное окно для больших форм. Большие формы — это формы которые не помещаются в два экрана монитора. Если важно оставить контекст страницы для такой формы, возможно вам подойдет сайдпейдж.
Описание работы
Модальное окно появляется поверх страницы, затемняя ее. Это помогает сфокусировать внимание пользователя на конкретных действиях, не теряя при этом общий контекст.
Модальное окно может состоять из:
«Шапки» — заголовка и крестика,
контент-зоны,
«подвала» — или «терминальной зоны» с кнопками основных действий.
Ни один из этих пунктов, кроме крестика, не является обязательным.
Заголовок
Если модальное окно предназначено для выполнения одной конкретной задачи, она должна быть отражена в заголовке. Заголовок должен быть кратким (1-3 слова) и являться названием процесса или объекта.
Если модальное окно носит информационный характер, заголовок должен емко передавать суть сообщения:
В модальных окнах с подтверждением заголовком является вопрос:
Действия
Чаще всего в модальном окне есть минимум два терминальных (закрывающих окно) действия:
Черная кнопка — действие по умолчанию, сохраняет проделанную работу или подтверждает вызванную ранее команду. Такую кнопку нельзя называть «ОК».
Кнопка отмены — закрывает модальное окно не выполняя основное действие, называется «Отменить», не должна называться «Отмена».
Кнопки и ссылки расположены слева направо на специальной плашке отделенной чертой. Чем правее кнопка — тем менее важное и менее частотное действие. Черта отделяет кнопки, делает их более заметными и помогает ориентироваться в интерфейсе. Используйте эти приемы и в обычных формах.
Закрытие модального окна
В правом верхнем углу модального окна всегда должна быть кнопка закрытия в виде крестика. Действие кнопки аналогично действию ссылки «Отменить». Также модальное окно должно закрываться по нажатию клавиши Esc на клавиатуре.
Модальные окна, содержащие формы или другие контролы, позволяющие внести изменения, не должны закрываться при клике за пределами окна, особенно если случайный клик может привести к потере введеных данных.
Модальные окна, несущие чисто информационную функцию, могут закрываться при клике за пределами окна.
Если было изменение данных, всегда уточняйте у пользователя перед закрытием модального окна — сохранить данные или уйти без сохранения. Больше советов по работе с данными форм в гайде сохранение данных.
Не делайте модальные окна исчезающие по таймауту, есть вероятность, что пользователь не увидит их, или не успеет разобрать, что было написано в этом окне.
Размер и расположение
По горизонтали модальное окно позиционируется по центру. По вертикали — по центру и на 10% смещенным к верхней части окна браузера.
Если размер модального окна по вертикали приближается к размеру окна браузера, чтобы модальное окно не прилипало к верхней границе, оно позиционируется с отступом 40 px от верхнего края.
Размер модального окна должен соответствовать его содержимому — внутри окна не должно быть пустых областей.
В то же время, если контент требует большого экранного пространства — модальное окно должно иметь максимально возможный размер.
Например, если внутри длинный список — модальное окно должно открываться на максимально возможную высоту, а скролл работать как описано ниже. Если интерактивная карта — максимальную высоту и ширину.
В случае, если высота модального окна при работе с ним может сильно меняться, такое окно лучше располагать с фиксированным отступом от верхней границы экрана, чтобы, например, при переключении между вкладками, окно не меняло свое положение.
Прокрутка
Избегайте ситуаций, когда контента внутри модального окна становится больше высоты экрана минимально поддерживаемого разрешения. Подумайте над тем, чтобы вместо модального окна сделать отдельную страницу или сайдпейдж.
Если высота контента внутри модального окна больше высоты окна браузера, окно открывается на всю высоту окна браузера и позиционируется с отступом 40 px от верхнего края страницы. Полоса прокрутки должна появляться у всей страницы, а не внутри модального окна. Контент страницы под затемнением не должен скроллиться.
Во время прокрутки модальное окно занимает всю высоту окна без отступов, а после прокрутки до нижнего края — позиционируется с отступом 40 px от нижнего края страницы.
Залипающие панели
Если важно чтобы в любом положении прокрутки были доступны элементы управления модальным окном в футере, или был виден заголовок, заголовок и футер могут залипать. При этом окно так же открывается с отступом от верхнего края окна браузера, и прокручивается до появления отступа снизу.
Выбор режима в котором работает модальное окно — с залипаниями или в обычном, и то, какие части должны залипать остается на усмотрение дизайнера.
Следите за тем, чтобы контент страницы или модального окна не скакали при открытии и закрытии окна, или открытии новых модальных окон. Такое случается при появлении или исчезновении полосы прокрутки — с полосой прокрутки страница становится у́же.
Наложение модальных окон
Если нужно вызвать из модального окна еще одно модальное окно, при открытии нового окна затемнение не должно множиться. Затемнение должно быть одно, все открытые ранее модальные окна должны быть помещены под него.
Это исключительный случай, старайтесь избегать вложенности модальных окон.
Мини-модалка
Используется для диалоговых окон, в которых мало контента и нет дополнительных контролов.
Мини-модалка может содержать одну или две кнопки в одну строку, две кнопки в две строки, либо три кнопки в три строки. В последнем случае допускается отделять третью кнопку отступом, если ее смысл сильно отличается от остальных.
У мини-модалки всегда должна быть иконка. Если подобрать иконку по смыслу не получается, используйте дефолтную.
В некоторых случаях полезно красить иконку.
Фокус и работа с клавиатурой
Если модальное окно содержит поля ввода, при открытии окна на десктопе ставьте фокус в первое поле автоматически. На мобильных устройствах, где фокус открывает виртуальную клавиатуру такое поведение лучше отключать.
Если это поле заполнено, и с большой вероятностью будет изменено, можно выделить значение, чтобы при вводе первого символа старое значение было стерто.
При открытом модальном окне нажатие Tab не переводит фокус на элементы страницы, фокус переходит только по элементам модального окна включая крестик.
Клавиша Esc закрывает модальное окно.
Дизайн, размеры и отступы
Модальное окно с терминальной плашкой:
Дополнительные кнопки и ссылки в терминальной плашке:
Модальное окно без терминальной плашки:
Нельзя использовать модальное окно с кнопками основных действий не отделенные чертой (без терминальной плашки), если в нем присутствуют поля не выровненные по левому краю, или текст выровненный по центру:
Модальное окно без заголовка:
Если модальное окно не содержит контента, не следует использовать терминальную плашку:
Информационное модальное окно — окно без кнопок:
Как сделать модальное окно на сайте
Привет друзья, в этому году меня несколько раз просили объяснить, как сделать модальное окно на сайте. Кто-то, изучая азы веб-разработки неизбежно приходил к ситуации, когда часть контент необходимо отобразить в модальном окне, кто-то, работая с формами, хочет показать ее не в открытом виде, а в pop-up при клике на кнопку.
Несмотря на то, что на блоге уже были статьи, косвенно связанные с реализацией этого эффекта, вопросы все равно остаются и я решил сделать несколько статей, связанных с этой темой. Сегодня разберемся просто с модальными окнами, а в следующий раз сделаем пульсирующую кнопку в углу экрана, как у сервисов обратного звонка и еще просили урок с анимацией блика на кнопке.
Все это выйдет друг за другом. Конечно, это статьи, ориентированные на новичков, и мастодонтам они будут не очень интересны, но тем, кто использует блог как справочник, возможно, пригодятся эти наработки, чтобы быстро реализовывать тот или иной эффект, не описывая одно и тоже каждый раз в новом проекте и сэкономят немного времени. Приступим.
Чтобы статья получилась максимально полной, будем придерживаться следующего плана и сделаем:
модальное окно с использованием jQuery.
Простое модальное окно с использованием чистого js.
Конечно же я покажу как вызывать несколько модальных окно на станице, так как это тоже один из частых вопросов, возникающих у новичков. Разберем возможность закрытия окна при клике вне области контента, решим проблему вертикальной прокрути и другие нюансы, которые возникнут в ходе работы или о которых сообщите вы в комментариях.
Структура проекта для понимания какие файлы и где находятся.
Начнем с разметки. Для создания модального окна я обычно пользуюсь такой структурой.
Где «modal» — это затемненный фон, а «modal__content» — область контента. При этом, когда срабатывает триггер, вызывающий окно, например нажатие кнопки, то к «modal» добавляется класс «modal_active», который дает понять, что модальное окно находится в открытом состоянии. Добавляя или удаляя «modal_active» мы будем открывать или скрывать модальное окно манипулирую стилями.
Большинству читающих нужна будет форма в модальном окне. Можно использовать эту.
Я, для примера, просто выведу текст:
Smartlanding
тут.
Создал простейшую шапку сайта с кнопкой обратного звонка, которая должна вызвать модальное окно. Классический пример любого лендинга.
Давайте добавим немного стилей и приведем в порядок открытое состояние модалки. Вот, что получилось у меня.
Обратите внимание, что в правом углу контентной области модального окна появилась кнопка с изображением крестика. Файл лежит в папке «img», рядом с папкой «js» и «css».
Тут нас больше всего интересует класс «.modal». Как я и говорил, это само модальное окно с полупрозрачным фоном. Давайте подробнее разберем что там за стили.
Первым делом «вырвем» из потока блок с модальным окном при помощи фиксированного позиционирования. Затем, при помощи свойств «top», «left», «right», «bottom» растянем модальное окно по ширине на весь экран. Z-index — нужен для того, чтобы расположить окно над всеми элементами. Если у кого-то другого элемента значение этого свойства будет выше, то модальное окно не перекроет его.
Окно прекрасно смотрится, но есть одно «но!». Помните я говорил, что активное состояния модального окна, это когда у нас есть класс «modal_active», а без этого класса, окно закрыто? Сейчас мы доведем до ума эту концепцию. Суть в том, чтобы при «modal» сделать окно скрытым, а при «modal_active» — показать.
Давайте создадим этот класс в стилях и настроим отображение окна только при его наличии.
Для показа/скрытия окна будем пользоваться «visibility», «opacity», «position» и свойство «transition» для плавности анимации появления.
Если еще подробнее, то сначала зададим нулевую прозрачность, а при помощи абсолютного позиционирования и свойства «top» уберем модальное окно за пределы видимости. Дополнительно, для надежности, при помощи свойства «visibility» скроем разметку окна.
В момент, когда появится класс «modal_active» инвертируем «visibility» и «opacity», а также поменять «position» и положение в свойстве «top».
Звучит страшнее, чем кажется, на сам деле все довольно просто.
Попробуйте добавить вручную и убедиться, что окно успешно появляется.
Пол дела сделали, осталось добавлять этот класс модальному окну при клике по кнопке. Тут подход и разделится. Первый вариант сделаем с использованием jQuery, а второй без него на чистом js.
Но прежде нужно подключить файл скриптов к нашей html страничке. Делается это так:
Smartlanding
тут.
Вызов модального окна при помощи jQuery
Исходник модального окна с использованием jQuery
Делаем модальные окна для сайта. Заботимся об удобстве и доступности
Я занимаюсь вёрсткой и программированием сайтов. Почти в каждом макете, который я верстал, были модальные окна. Обычно это формы заказа звонка в лендингах, уведомления о завершении каких-то процессов, или сообщения об ошибках.
Вёрстка таких окон сначала кажется простой задачей. Модальные окна можно сделать даже без помощи JS только лишь с помощью CSS, но на практике они оказываются неудобными, и из-за маленьких недочетов модальные окна раздражают посетителей сайта.
В итоге было задумано сделать собственное простое решение.
Вообще говоря, есть несколько готовых скриптов, JavaScript библиотек, реализующих функционал модальных окон, например:
Arctic Modal,
jquery-modal,
iziModal,
Micromodal.js,
tingle.js,
Bootstrap Modal (из библиотеки Bootstrap) и др.
(в статье не рассматриваем решения на базе Frontend-фреймворков)
Несколькими из них я пользовался сам, но почти у всех находил какие-то недостатки. Некоторые из них требуют подключения библиотеки jQuery, которая есть не на всех проектах. Для разработки своего решения, нужно сначала определиться с требованиями.
Что мы ждём от модальных окон? Отвечая на этот вопрос, я основывался на докладе «Знакомьтесь, модальное окно» Анны Селезнёвой, а так-же на относительно старой статье NikoX «arcticModal — jQuery-плагин для модальных окон».
Итак, чтобы нам хотелось видеть?
Окна должны открываться как можно быстрее, без тормозов браузера, с возможностью анимировать открытие и закрытие.
Под окном должен быть оверлей. Клик/тап по оверлею должен закрывать окно.
Страница под окном не должна прокручиваться.
Окон может быть несколько. Открытие одного определенного окна должно осуществляться кликом на любой элемент страницы с data-атрибутом, который мы выберем.
Окно может быть длинным – прокручиваемым.
Желательно поработать над доступностью, а также с переносом фокуса внутрь окна и обратно.
Должно работать на IE11+
Дисклеймер: Прежде чем мы рассмотрим подробности, сразу дам ссылку на готовый код получившейся библиотеки (HystModal) на GitHub, а также ссылку на демо+документацию.
Начнём с разметки.
1. Разметка HTML и CSS
1.1. Каркас модальных окон
Как открыть окно быстро? Самое простое решение: разместить всю разметку модального окна сразу в HTML странице. Затем скрывать/показывать это окно при помощи переключения классов CSS.
Набросаем такую разметку HTML (я назвал этот скрипт «hystmodal»):
Текст модального окошка.
Итак, разместим перед закрывающим тегом
наш блок с окном ( .hystmodal ). Он будет фоном. Удобно указать уникальный атрибут id (например #myModal ) каждому окну (ведь их у нас может быть несколько).
Сделаем так, чтобы .hystmodal растягивался на всё окно браузера и закрывал собой содержимое страницы. Чтобы этого добиться, установим фиксированное позиционирование в CSS и приравняем свойства top, bottom, left и right к нулю.
.hystmodal < position: fixed; top: 0; bottom: 0; right: 0; left: 0; overflow: hidden; overflow-y: auto; -webkit-overflow-scrolling: touch; display: flex; flex-flow: column nowrap; justify-content: center; /* см. ниже */ align-items: center; z-index: 99; /* Чтобы окно не прилипало к границе браузера установим отступы */ padding:30px 0; >
В этом коде сделаны ещё две вещи:
Так как мы хотим центрировать окно внутри страницы, превращаем .hystmodal в flex-контейнер с выравниваем его потомков по центру по вертикали и горизонтали.
Окно может быть больше высоты экрана браузера, поэтому мы устанавливаем overflow-y: auto , чтобы при переполнении возникала полоса прокрутки. Также, для сенсорных экранов (в основном для Safari) нам стоит установить свойство -webkit-overflow-scrolling: touch , чтобы сенсорная прокрутка работала именно на этом блоке а не на странице.
Теперь установим стили для самого окна.
.hystmodal__window < background: #fff; /* Установим по умолчанию ширину 600px но она будет не больше ширины браузера */ width: 600px; max-width: 100%; /* Заготовка для будущих анимаций */ transition: transform 0.15s ease 0s, opacity 0.15s ease 0s; transform: scale(1); >
Кажется возникли сложности.
Проблема №1. Если высота окна больше высоты окна браузера, то контент окна будет обрезан сверху.
Это возникает из-за свойства justify-content: center . Оно позволяет нам удобно выровнять потомков по основной оси (по вертикали), но если потомок становится больше родителя то часть его становится недоступной даже при прокручиваемом контейнере. Подробнее можно посмотреть на stackoverflow. Решение – установить justify-content: flex-start , а потомку установить margin:auto . Это выровняет его по центру.
Проблема №2. В ie-11 если высота окна больше высоты окна браузера, то фон окна обрезается.
Решение: мы можем установить flex-shrink:0 потомку – тогда обрезки не происходит.
Проблема №3. В браузерах кроме Chrome нет отступа от нижней границы окна (т.е. padding-bottom не сработал).
Сложно сказать баг это браузеров или наоборот соответствует спецификации, но решения два:
установить псевдоэлемент ::after после потомка и дать ему высоту вместо padding
обернуть элемент в дополнительный блок и дать отступы уже ему.
Воспользуемся вторым методом. Добавим обертку .hystmodal__wrap . Так мы заодно обойдём и проблему №1, а вместо padding у родителя установим margin-top и margin-top у самого .hystmodal__window .
Наш итоговый html:
Заголовок модального окна
Текст модального окна .
Ещё текст модального окна .
В код также добавлены некоторые aria и role атрибуты для обеспечения доступности.
Сейчас наше окно всегда видно. Когда говорят о скрытии элементов, первое что приходит на ум это переключать свойство display со значения none до нашего значения flex.
Но этот подход нас не устроит, ведь свойство display не анимируется. Все переходы дочерних элементов, указанные в свойстве transition, работать не будут.
Нам поможет другое свойство visibility:hidden . Оно скроет окно визуально, хотя и зарезервирует под него место. А так как все будущие окна на странице имеют фиксированное позиционирование – они будут полностью скрыты и не повлияют на остальную страницу. Кроме того, на элементы с visibility:hidden нельзя установить фокус с клавиатуры, а от скрин-ридеров мы уже скрыли окна с помощью атрибута aria-hidden=»true» .
В лучшем случае, нам нужен отдельный html-элемент в качестве оверлея. Можно использовать и имеющийся элемент модального окна .hystmodal в качестве оверлея, но тогда анимация на этом элементе (например переход opacity) будет затрагивать и внутренние элементы. В итоге, не получится анимировать разные свойства для окна и оверлея отдельно.
Просто разместим элемент .hystmodal__shadow прямо перед закрывающим . В будущем, сделаем так, чтобы этот элемент создавался автоматически из js при инициализации библиотеки.
Когда модальное окна открывается, мы хотим, чтобы страница под ним не прокручивалась. Самый простой способ этого добиться — повесить overflow:hidden для body или html, когда окно открывается. Однако с этим есть проблема:
Проблема №4. В браузере Safari на iOS страница будет прокручиваться, даже если на тег html или body повешен overflow:hidden . Решается двумя способами, либо блокированием событий прокрутки (touchmove, touchend или touchsart) из js вида:
Однако при этом блокируется и прокрутка самого модального окна, а также все прокручиваемые блоки внутри окна, если они будут. Требуется дополнительные проверки селекторов из js, что приводит к усложнению кода, поэтому рассмотрим другой вариант.
ps: можно конечно применить библиотеку scroll-lock, в которую заложено это решение, но в статье было решено воспользоваться другим вариантом.
Другое решение – основано частично на CSS. Пусть когда окно открывается, на элемент будет добавляться класс .hystmodal__opened :
.hystmodal__opened
Благодаря position:fixed , окно не будет прокручиваться даже в safari, однако здесь тоже не всё гладко:
Проблема №5. При открытии/закрытии окна страница прокручивается в начало. Действительно, это происходит из-за изменения свойства position, текущая прокрутка окна сбрасывается.
Для решения, нам нужно написать следующий JS (упрощенно):
При открытии:
// Находим тег html и сохраняем его let html = document.documentElement; //сохраним текущую прокрутку: let scrollPosition = window.pageYOffset; //установим свойство top у html равное прокрутке html.style.top = -scrollPosition + "px"; html.classList.add("hystmodal__opened");
При закрытии:
html.classList.remove("hystmodal__opened"); //прокручиваем окно туда где оно было window.scrollTo(0, scrollPosition); html.style.top = "";
Отлично, приступим к JavaScript коду.
2. Код JavaScript
2.2 Каркас библиотеки
Нам нужна совместимость со старыми браузерами включая IE11 поэтому нам нужно выбрать из 2 вариантов кодинга:
Разрабатывать на старом стандарте ES5, и использовать только те фичи, которые поддерживают все браузеры.
Применить современный синтаксис ES6, но подключить транспайлер Babel, который автоматически преобразует код для всех браузеров и встроит необходимые полифилы. Было принято решение использовать второй вариант, с прицелом на будущее. Приступим.
Основа нашей библиотеки единственный класс HystModal . Ниже я приведу скелет кода с комментариями, а потом добавим остальной функционал.
class HystModal < /** * При создании экземпляра класса, мы передаём в него * js-объект с настройками. Он становится доступен * в конструкторе класса в виде переменной props */ constructor(props)< /** * Для удобства некоторые свойства можно не передавать * Мы должны заполнить их начальными значениями * Это можно сделать применив метод Object.assign */ let defaultConfig = < linkAttributeName: 'data-hystmodal', // . здесь остальные свойства >this.config = Object.assign(defaultConfig, props); // сразу вызываем метод инициализации this.init(); > /** * В свойство _shadow будет заложен div с визуальной * подложкой. Оно сделано статическим, т.к. при создании * нескольких экземпляров класса, эта подложка нужна только * одна */ static _shadow = false; init() < /** * Создаём триггеры состояния, полезные переменные и.т.д. */ this.isOpened = false; // открыто ли окно this.openedWindow = false; //ссылка на открытый .hystmodal this._modalBlock = false; //ссылка на открытый .hystmodal__window this.starter = false, //ссылка на элемент "открыватель" текущего окна // (он нужен для возвращения фокуса на него) this._nextWindows = false; //ссылка на .hystmodal который нужно открыть this._scrollPosition = 0; //текущая прокрутка (см. выше) /** * . остальное */ // Создаём только одну подложку и вставляем её в конец body if(!HystModal._shadow)< HystModal._shadow = document.createElement('div'); HystModal._shadow.classList.add('hystmodal__shadow'); document.body.appendChild(HystModal._shadow); >//Запускаем метод для обработки событий см. ниже. this.eventsFeeler(); > eventsFeeler() < /** * Нужно обработать открытие окон по клику на элементы с data-атрибутом * который мы установили в конфигурации - this.config.linkAttributeName * * Здесь мы используем делегирование события клика, чтобы обойтись одним * лишь обработчиком события на элементе html * */ document.addEventListener("click", function (e) < /** * Определяем попал ли клик на элемент, * который открывает окно */ const clickedlink = e.target.closest("[" + this.config.linkAttributeName + "]"); /** Если действительно клик был на * элементе открытия окна, находим * подходящее окно, заполняем свойства * _nextWindows и _starter и вызываем * метод open (см. ниже) */ if (clickedlink) < e.preventDefault(); this.starter = clickedlink; let targetSelector = this.starter.getAttribute(this.config.linkAttributeName); this._nextWindows = document.querySelector(targetSelector); this.open(); return; >/** Если событие вызвано на элементе * с data-атрибутом data-hystclose, * значит вызовем метод закрытия окна */ if (e.target.closest('[data-hystclose]')) < this.close(); return; >>.bind(this)); /** По стандарту, в обработчике события в this * помещается селектор на котором события обрабатываются. * Поэтому нам нужно вручную установить this на наш * экземпляр класса, который мы пишем с помощью .bind(). */ //обработаем клавишу escape и tab window.addEventListener("keydown", function (e) < //закрытие окна по escape if (e.which == 27 && this.isOpened) < e.preventDefault(); this.close(); return; >/** Вызовем метод для управления фокусом по Tab * и всю ответственность переложим на него * (создадим его позже) */ if (e.which == 9 && this.isOpened) < this.focusCatcher(e); return; >>.bind(this)); > open(selector) < this.openedWindow = this._nextWindows; this._modalBlock = this.openedWindow.querySelector('.hystmodal__window'); /** Вызываем метод управления скроллом * он будет блокировать/разблокировать * страницу в зависимости от свойства this.isOpened */ this._bodyScrollControl(); HystModal._shadow.classList.add("hystmodal__shadow--show"); this.openedWindow.classList.add("hystmodal--active"); this.openedWindow.setAttribute('aria-hidden', 'false'); this.focusContol(); //вызываем метод перевода фокуса (см. ниже) this.isOpened = true; >close() < /** * Метод закрытия текущего окна. Код упрощён * подробнее в статье далее. */ if (!this.isOpened) < return; >this.openedWindow.classList.remove("hystmodal--active"); HystModal._shadow.classList.remove("hystmodal__shadow--show"); this.openedWindow.setAttribute('aria-hidden', 'true'); //возвращаем фокус на элемент которым открылось окно this.focusContol(); //возвращаем скролл this._bodyScrollControl(); this.isOpened = false; > _bodyScrollControl() < let html = document.documentElement; if (this.isOpened === true) < //разблокировка страницы html.classList.remove("hystmodal__opened"); html.style.marginRight = ""; window.scrollTo(0, this._scrollPosition); html.style.top = ""; return; >//блокировка страницы this._scrollPosition = window.pageYOffset; html.style.top = -this._scrollPosition + "px"; html.classList.add("hystmodal__opened"); > >
Итак, мы описали класс HystModal . Чтобы всё работало, нужно всего лишь подключить наш скрипт и создать экземпляр класса:
const myModal = new HystModal(< linkAttributeName: 'data-hystmodal', >);
Проблема №6: если в браузере есть фиксированный скроллбар (который влияет на ширину страницы), то при открытии/закрытии окна происходит сдвиг контента, когда полоса прокрутки то появляется то пропадает.
Действительно – скроллбар пропадает и контент страницы перераспределяется. Чтобы решить эту проблему, можно добавить отступ справа к тегу html, равный ширине скроллбара когда он пропадает.
Однако ширина скроллбара может быть разной в разных браузерах и операционных системах. Кроме того, скроллбара может не быть на коротких страницах или он может быть плавающим (например, в Chrome на Android). Поэтому ширину нужно вычислять динамически из скрипта.
Дополним метод _bodyScrollControl()
//при открытии окна let marginSize = window.innerWidth - html.clientWidth; //ширина скроллбара равна разнице ширины окна и ширины документа (селектора html) if (marginSize) < html.style.marginRight = marginSize + "px"; >//при закрытии окна html.style.marginRight = "";
Почему код метода close() упрощён? Дело в том, что просто убирая классы CSS у элементов, мы не можем анимировать закрытие окна.
Проблема №7. При закрытии окна, свойство visibility:hidden применяется сразу и не даёт возможности анимировать закрытие окна.
Причина этого известна: свойство visibility:hidden не анимируется. Конечно, можно обойтись без анимации, но, если она нужна, сделаем следующее.
Создадим дополнительный CSS-класс .hystmodal—moved почти такой-же как и .hystmodal—active
.hystmodal--moved
Затем при закрытии сначала добавим этот класс к окну и повесим обработчик события «transitionend» на модальном окне. Затем удалим класс `.hystmodal—active , таким образом вызывая css-переход. Как только переход завершится, сработает обработчик события «transitionend», в котором сделаем всё остальное и удалим сам обработчик события.
Вы заметили, что мы создали ещё один метод _closeAfterTransition() и перенесли основную логику закрытия туда. Это нужно, чтобы удалить обработчик события transitionend после закрытия окна, ведь в метод removeEventListener необходимо передать именно ту функцию, которую мы привязывали.
Кроме того, если анимация не будет нужна, можно просто вызвать this._closeAfterTransition() не вешая его на событие.
Как мы помним, внутри addEventListener, this будет указывать на селектор где происходит событие, а не на наш экземпляр класса, поэтому в конструкторе нужно добавить ещё одну строчку для жёсткой привязки метода к this.
Нам нужно обработать ещё одно событие – закрытие окна по клику на элемент подложки .hystmodal__wrap . Мы можем повесить обработчик клика на документ для делегирования события как при открытии и проверить что событие произошло на .hystmodal__wrap примерно так:
Это будет работать, но есть один малозаметный недостаток.
Проблема №8. Если кнопку мыши нажать внутри окна, а отпустить за его пределами (над подложкой), окно закрывается.
Представьте, что в окне форма. Вы выделяете текст в поле ввода и случайно двигаете мышь чуть дальше, курсор заходит на подложку и вдруг окно закрывается, всё вдруг пропадает из виду. Не хотелось бы, чтобы так было.
Окно закрывается потому что по спецификации, если нажатие и отпускание мыши были на разных элементах, то событие click сработает на самом ближайшем общем для них элементе, а у нас это как раз .hystmodal__wrap .
Мы могли бы решить это изменением html, добавляя ещё один div сразу после .hystmodal__window и размещая его визуально под окном. Но нам бы не хотелось добавлять лишний пустой div ещё сильнее усложняя разметку.
Мы можем разбить наш addEventListener на два отдельных обработчика: для событий mousedown и mouseup и будем проверять чтобы оба события происходили именно на .hystmodal__wrap . Добавим новые обработчики событий в наш метод eventsFeeler()
document.addEventListener('mousedown', function (e) < /** * Проверяем было ли нажатие над .hystmodal__wrap, * и отмечаем это в свойстве this._overlayChecker */ if (!e.target.classList.contains('hystmodal__wrap')) return; this._overlayChecker = true; >.bind(this)); document.addEventListener('mouseup', function (e) < /** * Проверяем было ли отпускание мыши над .hystmodal__wrap, * и если нажатие тоже было на нём, то закрываем окно * и обнуляем this._overlayChecker в любом случае */ if (this._overlayChecker && e.target.classList.contains('hystmodal__wrap')) < e.preventDefault(); !this._overlayChecker; this.close(); return; >this._overlayChecker = false; >.bind(this));
2.3 Управление фокусом
У нас заготовлено два метода для управления фокусом: focusContol() для переноса фокуса внутрь окна и обратно при его закрытии, а также focusCatcher(event) для блокирования ухода фокуса из окна.
Решения для фокуса были реализованы аналогично js-библиотеке «Micromodal» (Indrashish Ghosh). А именно:
1. В служебный массив сохраним все css селекторы на которых может быть установлен фокус (свойство помещаем в init()):
//внутри метода init или конструктора this._focusElements = [ 'a[href]', 'area[href]', 'input:not([disabled]):not([type="hidden"]):not([aria-hidden])', 'select:not([disabled]):not([aria-hidden])', 'textarea:not([disabled]):not([aria-hidden])', 'button:not([disabled]):not([aria-hidden])', 'iframe', 'object', 'embed', '[contenteditable]', '[tabindex]:not([tabindex^="-"])' ];
2. В методе focusContol() находим первый такой селектор в окне и устанавливаем на него фокус, если окно открывается. Если же окно закрывается – то переводим фокус на this.starter :
focusContol() < /** Метод переносит фокус с элемента открывающего окно * в само окно, и обратно, когда окно закрывается * см. далее в тексте. */ const nodes = this.openedWindow.querySelectorAll(this._focusElements); if (this.isOpened && this.starter) < this.starter.focus(); >else < if (nodes.length) nodes[0].focus(); >>
3. В методе focusCatcher() находим в окне и превращаем в массив коллекцию всех элементов на которых может быть фокус. И проверяем, если фокус должен был выйти на пределы окна, то вместо этого устанавливаем фокус снова на первый или последний элемент (ведь фокус можно переключать как по Tab так и по Shift+Tab в обратную сторону).
Результирующий код метода focusCatcher:
focusCatcher(e) < /** Метод не позволяет фокусу перейти вне окна при нажатии TAB * элементы в окне фокусируются по кругу. */ // Находим все элементы на которые можно сфокусироваться const nodes = this.openedWindow.querySelectorAll(this._focusElements); //преобразуем в массив const nodesArray = Array.prototype.slice.call(nodes); //если фокуса нет в окне, то вставляем фокус на первый элемент if (!this.openedWindow.contains(document.activeElement)) < nodesArray[0].focus(); e.preventDefault(); >else < const focusedItemIndex = nodesArray.indexOf(document.activeElement) if (e.shiftKey && focusedItemIndex === 0) < //перенос фокуса на последний элемент nodesArray[nodesArray.length - 1].focus(); e.preventDefault(); >if (!e.shiftKey && focusedItemIndex === nodesArray.length - 1) < //перерос фокуса на первый элемент nodesArray[0].focus(); e.preventDefault(); >> >
По сути мы реализовали все необходимое для успешного создания модальных окон, осталось ещё несколько дел:
Проблема №9. В IE11 не работают методы Element.closest() и Object.assign() .
Для поддержки Element.closest, воспользуемся полифилами для closest и matches от MDN.
Можно их вставить просто так, но так как у нас проект всё равно собирается webpack, то удобно воспользоваться пакетом element-closest-polyfill который просто вставляет этот код.
Для поддержки Object.assign , можно воспользоваться уже babel-плагином @babel/plugin-transform-object-assign
3. Заключение и ссылки
Повторяя начало статьи, всё изложенное выше, я оформил в маленькую библиотеку hystModal под MIT-лицензией. Вышло 3 кБ кода при загрузке с gzip. Ещё написал для неё подробную документацию на русском и английском языке.
Что вошло ещё в библиотеку hystModal, чего не было в статье:
Настройки (вкл/выкл управление фокусом, варианты закрытия, ожидание анимации закрытия)
Коллбеки (функции вызывающиеся перед открытием окна и после его закрытия (в них передаётся объект модального окна))
Добавлен запрет на какие-либо действия пока анимация закрытия окна не завершится, а также ожидание анимации закрытия текущего окна перед открытием нового (если окно открывается из другого окна).
Оформление кнопки-крестика закрытия в CSS
Минификация CSS и JS плагинами Webpack.
Если вам будет интересна эта библиотека, буду рад звёздочке в GitHub, или напишите в Issues о найденных багах. (Особенно большие проблемы, наверное, в грамматике английской версии документации, так как мои знания языка пока на начальном уровне. Связаться со мной также можно в Instagram
Модальное окно на чистом CSS и JS
Всем привет! Я в веб-разработке не так давно. Сейчас я пишу свой сайт, который будет выступать в качестве моего портфолио и, возможно, даже целого проекта. При добавлении модального окна для авторизации на свой сайт, я подумал, а что будет, если публиковать подобные модульные вещи, чтобы любой человек мог их скопировать и не думать о них, а использовать в разработке. То есть создать готовый модуль для встраивания. Мне, как разработчику было бы удобно использовать готовое решение, тем более написанное мною, да и делиться опытом — дело приятное 🙂
NB
сам сайт скрыт для сохранения интеллектуальной собственности 🙂
А теперь пошаговое создание:
Здесь представлена несложная разметка для модального окна.
Перед разъяснением элементов и их стилей, мы укажем скрытие ползунка прокрутки для body, которое нам будет мешать:
/* убираем нижний ползунок прокрутки */ body
Давайте поясню, что есть что:
modalBackground — это фон, расположенный за модальным окном. Через него мы будем видеть наш сайт < background: rgba(0, 0, 0, 0.8); >, в то время как основной фокус будет на модальном окне. Для корректного расположения, чтобы наш фон покрывал весь сайт — мы делаем его с положением < position: fixed >и размерами на весь экран < width: 100%; height : 100%; >. А для наложения поверх других окон или элементов сайта — < z-index: 1 >. Стоит отметить, что изначально мы не можем видеть наше окно < display: none >. Для отображения возможности нажатия на фон — делаем указатель через
modalActive — непосредственно наше модальное окно. Его размеры < width: 350px; height : 495px; >я подбирал исходя из удобства верстки для маленьких экранов, так что размеры можно указать свои. Мы позиционируем наше модальное окно по центру < position: absolute; top: calc(50% - 250px); left: calc(50% - 175px); >. Делаем рамки закругления в 10 пикселей < border-radius: 10px >. Курсор для удобства делаем стандартным < cursor: default >. Свойства background-color и padding в данном случае ни на что не влияют, они используются для фона нашего окошечка внутри и для внутренних отступов нашего окошечка соответственно. Поэтому их можно ставить на свой вкус и цвет 🙂
/* позиционирование самого модального окна */ .modalActive
modalClose — это наш крестик. Я использовал два варианта выхода из модального окна, кому как удобнее — через нажатие на фон, либо через нажатие на крестик (этот вариант предпочтительнее для мобильных устройств из-за маленького экрана, по сравнению с монитором ноутбука/компьютера/планшета). Для крестика соответственно используется тег modalCross с указанием в img пути к картинке-крестика. Свойство font-family используется для выбора семейства шрифта, здесь оно не играет роли (на ваш вкус и цвет).
modalWindow — внутри него располагается содержимое нашего окна, для удобной верстки внутреннего содержимого устанавливаем относительное позиционирование относительно нашего модального окна < position: relative >. Какое оно будет — зависит только от вас 🙂
/* делаем позиционирование внутренних элементов относительно модального окна */ .modalWindow
Теперь приступим к части оживления нашего окна с помощью жабаскрипта JS !
Прежде всего нам надо получить все элементы, с которыми нам предстоит работать:
Стоит отметить, что при использовании метода getElementsByClassName мы будем получать массив всех элементов. Поэтому для корректного обращения к конкретному элементу — мы указываем индекс получаемого элемента. А так как наши массивы состоят из одного элемента — индекс во всех случаях равен [0].
Переменная modalTrigger — содержит наш триггер, при нажатии на который у нас будет появляться наше модальное окно.
Если наш сайт длинный и у нас появляется ползунок прокрутки — мы получаем его длину и записываем в scrollbarWidth. Давайте тут поподробнее. Мы изначально получаем ширину видимой части сайта через свойство clientWidth. Ширина ползунка прокрутки — разность между шириной окна внутри и шириной отображаемого содержимого (то, что под ползунком — не является отображаемым содержимым). Для корректности мы преобразовываем полученные значения через функции parseInt.
Далее мы привязываем необходимые элементы к переменным в соответствии с названием их классов, для тега body мы используем метод getElementsByTagName с указанием индекса получаемого элемента, то есть [0]. Смысл такой же, как и при getElementsByClassName. Я заметил простую вещь — если читаешь название методов, то понимаешь что они тебе дают :). В нашем случаем мы получаем множество элементов (окончание s в слове Elements), поэтому мы и работаем с массивом. А в методе getElementById мы получаем один элемент (о чем свидетельствует отсутствие окончания s в слове Element).
После инициализации рабочих переменных — мы корректируем положение body при появлении ползунка прокрутки, то есть наш ползунок будет накладываться поверх нашего сайта, а не сдвигать его, как это обычно бывает (можете проверить сами):
// функция для корректировки положения body при появлении ползунка прокрутки function bodyMargin() < bodyElementHTML.style.marginRight = "-" + scrollbarWidth + "px"; >// при длинной странице - корректируем сразу bodyMargin();
Механизм корректировки прост: в случае появления ползунка прокрутки — мы делаем отступ body через свойство margin-right на отрицательную величину ширины ползунка прокрутки. При первой загрузки страницы мы вызываем эту функцию, чтобы наше содержимое сразу же позиционировалось корректно, несмотря на наличие ползунка прокрутки. Вообще, позиционирование с учетом ползунка прокрутки — дело каждого, мне не хотелось бы, чтобы мой сайт скакал влево вправо 🙂
Создадим обработчик события нажатия на наш триггер:
// событие нажатия на триггер открытия модального окна modalTrigger.addEventListener("click", function () < // делаем модальное окно видимым modalBackground.style.display = "block"; // если размер экрана больше 1366 пикселей (т.е. на мониторе может появиться ползунок) if (windowInnerWidth >= 1366) < bodyMargin(); >// позиционируем наше окно по середине, где 175 - половина ширины модального окна modalActive.style.left = "calc(50% - " + (175 - scrollbarWidth / 2) + "px)"; >);
И в завершение создадим обработчик закрытия нашего окна при нажатии на крестик или на область за модальным окном:
// нажатие на крестик закрытия модального окна modalClose.addEventListener("click", function () < modalBackground.style.display = "none"; if (windowInnerWidth >= 1366) < bodyMargin(); >>); // закрытие модального окна на зону вне окна, т.е. на фон modalBackground.addEventListener("click", function (event) < if (event.target === modalBackground) < modalBackground.style.display = "none"; if (windowInnerWidth >= 1366) < bodyMargin(); >> >);
Вот и всё модальное окно. Если у Вас есть какие-нибудь замечания, похвала, критика, советы, любой фидбек — буду рад прочитать и внести корректировки! Всех обнял-приподнял 🙂