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 срабатывают, когда курсор заходит на элемент и уходит с него, но с двумя особенностями.
  1. Не учитываются переходы внутри элемента.
  2. События 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));