JS30. Задание 26 Stripe Follow Along Dropdown
Демо: https://js3026.github.io/
Код: https://github.com/js3026/js3026.github.io
Этот проект основывается на том, что мы узнали в задании "Follow Along Links", и применяет его к выпадающему меню. Мы пытаемся имитировать меню навигации на веб-сайте Stripe.
Нам необходимо получить размеры каждого раскрывающегося элемента и применить эти измерения к раскрывающемуся фону.
Разметка
В HTML-файле у нас есть элемент навигации, содержащий элементы div для раскрывающегося меню и неупорядоченный список.
Стили
CSS содержит два свойства, которые были для меня новыми. Первое свойство - perspective.
nav {
position: relative;
perspective: 600px;
}
Перспектива позволяет изменить перспективу просмотра 3D-элементов подробнее.
Другое свойство называется will-change
.dropdown {
...
transition: all 0.5s;
transform: translateY(100px);
will-change: transform;
}
Will-change оптимизирует анимацию, позволяя браузеру заранее узнать, какие свойства и элементы должны быть обработаны, что потенциально увеличивает производительность этой конкретной операции подробнее.
Код
Теперь давайте рассмотрим Javascript. Первое, что мы делаем, - это находим все пункты выпадающего меню:
var triggers = document.querySelectorAll(".cool > li");
Находим белый div, который будет фоном для каждого раскрывающегося списка , а также само меню - элемент nav.
var background = document.querySelector(".dropdownBackground");
var nav = document.querySelector(".top");
Затем нам нужно подключить слушателей событий к элементам списка.
triggers.forEach(trigger => trigger.addEventListener("mouseenter", handleEnter));
triggers.forEach(trigger => trigger.addEventListener("mouseleave", handleLeave));
Для каждого триггера нам понадобится два слушателя: "mouseenter" и "mouseleave".
События
mouseenter/mouseleave
срабатывают, когда курсор заходит на элемент и уходит с него, но с двумя особенностями.- Не учитываются переходы внутри элемента.
- События
mouseenter/mouseleave
не всплывают.
Курсор заходит на элемент – срабатывает
mouseenter
, а затем – неважно, куда он внутри него переходит, mouseleave
будет только тогда, когда курсор окажется за пределами элемента.Когда мышь входит в элемент списка, запускается функция handleEnter; когда мышь уходит, запускается функция «handleLeave».
Функция handleEnter добавляет класс trigger-enter целевому элементу списка.
this.classList.add("trigger-enter");
Класс trigger-enter изменит display: none; на display: block;
dropdown {
...
display: none;
}
.trigger-enter .dropdown {
display: block;
}
.trigger-enter-active .dropdown {
opacity: 1;
}
Следующий фрагмент кода определит, содержит ли элемент list класс «trigger-enter».
setTimeout(() => this.classList.contains("trigger-enter") && this.classList.add("trigger-enter-active"), 150);
Если это так, к данному элементу присоединяется класс trigger-enter-active, который добавляет непрозрачность элементу списка: opacity: 1;
Условие и последующее действие содержатся в setTimeout, который выполняется через 150 миллисекунд.
Чтобы this был равен элементу списка, мы используем стрелочную функцию setTimeout(() =>. Обычная функция устанавливает значение this равным объекту window.
Задержка в 150 миллисекунд гарантирует, что содержимое элемента списка появится на странице после того, как фон будет установлен.
Затем мы присоединяем к фону класс open, который сделает фон для раскрывающегося содержимого видимым и на странице.
background.classList.add("open");
Соответственно, функция handleLeave удаляет у списка классы trigger-enter, trigger-enter-active, а у фона класс open
function handleLeave() {
this.classList.remove("trigger-enter", "trigger-enter-active");
background.classList.remove("open");
}
Дальше будет немного сложно. Добавление и удаление классов это понятно, но нам нужно, чтобы белый фон появлялся именно там, где раскрывается наш список.
Разумеется, намного проще это было бы сделать в разметке, разместив списка внутри блока с фоном, но тогда он не будет так легко и красочно парить на странице.
Чтобы добиться такой лёгкости, придётся ещё немного поработать над размерами и расположением фона.
Внутри функции handleEnter создаём несколько переменных
var dropdown = this.querySelector(".dropdown");
var dropdownCoords = dropdown.getBoundingClientRect();
var navCoords = nav.getBoundingClientRect();
dropdown - это раскрывающийся список на момент, когда он уже открыт и имеет размеры и положение на странице.
dropdownCoords - координаты (и размеры) списка, которые можно получить, используя getBoundingClientRect
navCoords - то же самое для панели навигации
Последний набор координат будет использоваться для учёта любого содержимого, которое может находиться над навигационным элементом. Наше окно с раскрывающимся списком абсолютно спозиционировано, но сама страница абсолютного позиционирования не имеет и любой элемент, который добавляется выше навигационного меню будет смещать его вниц.
Мы компенсируем пространство, создавая новый объект coords
var coords = {
height: dropdownCoords.height,
width: dropdownCoords.width,
top: dropdownCoords.top - navCoords.top,
left: dropdownCoords.left - navCoords.left
};
Как вы можете видеть, верхнее и левое свойства были настроены для любого содержимого, которое может испортить выравнивание раскрывающегося содержимого и его фона. Теперь, когда у нас есть эти скорректированные координаты, мы можем применить их к фону:
background.style.setProperty("width", `${coords.width}px`);
background.style.setProperty("height", `${coords.height}px`);
background.style.setProperty("transform", `translate(${coords.left}px, ${coords.top}px)`);
Код полностью:
var triggers = document.querySelectorAll(".cool > li");
var background = document.querySelector(".dropdownBackground");
var nav = document.querySelector(".top");
function handleEnter() {
this.classList.add("trigger-enter");
setTimeout(() => this.classList.contains("trigger-enter") && this.classList.add("trigger-enter-active"), 150);
background.classList.add("open");
var dropdown = this.querySelector(".dropdown");
var dropdownCoords = dropdown.getBoundingClientRect();
var navCoords = nav.getBoundingClientRect();
var coords = {
height: dropdownCoords.height,
width: dropdownCoords.width,
top: dropdownCoords.top - navCoords.top,
left: dropdownCoords.left - navCoords.left
};
background.style.setProperty("width", `${coords.width}px`);
background.style.setProperty("height", `${coords.height}px`);
background.style.setProperty("transform", `translate(${coords.left}px, ${coords.top}px)`);
}
function handleLeave() {
this.classList.remove("trigger-enter", "trigger-enter-active");
background.classList.remove("open");
}
triggers.forEach(trigger => trigger.addEventListener("mouseenter", handleEnter));
triggers.forEach(trigger => trigger.addEventListener("mouseleave", handleLeave));