[JavaScript, ReactJS] Как мы решили проблемы с z-index

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
27-Апр-2021 11:30

Привет хабр!Буквально недавно на работе я получил баг с z-index, я его по быстрому пофиксил и получил еще два бага. Я как то не придавал этой проблеме значения, и тут мой коллега Дмитрий Рокало ревьювил мой очередной пул реквест и пришел ко мне с идеей, как покончить войну с z-index в нашем проекте. И как раз в тот же день, я слушал подкаст веб стандарты и там обсуждали статью по работе с z-index. И решение, которое предлагают в статье, показалось мне крайне нелепым по сравнению с тем, что предложил мне Дима. Поэтому я решил спонтанно записать это видео и написать статью. Возможно это решение кому-то будет полезным (Данная статья является расшифровкой видео).Обрисуем ситуациюДавайте рассмотрим пример. У нас при клике на иконку открывается попап или модальное окно или назовите его еще как угодно. Сейчас речь не про нейминг, но мы в проекте у себя называем это попапом. Этот попап всегда находится над основным контентом, поэтому мы дали всем попапам z-index: 100, и это сработало.
В этом попапе есть список пользователей, и напротив каждого пользователя, есть иконка открытия меню. При нажатии на нее выпадает меню. Такое отображение меню мы реализовали через поповер. По факту, внутри поповера может быть все что угодно, это просто контейнер. Опять же этот поповер находится даже поверх попапа, поэтому мы решили всем поповерам присвоить z-index: 1000. И это так же прекрасно сработало.
.popup {
  z-index: 100;
}
.popover {
  z-index: 10000;
}
В некоторых проектах, чтобы управлять z-index в одном месте, мы создавали отдельный файл с scss переменными.
z-index-popup: 100;
z-index-popover: 1000;
Баги, конечно, возникали, но их было фиксить достаточно просто, когда в одном файле видишь всю картину проекта.Последствия такого подходаИ спустя какое-то время у нас на проекте появился новый поповер. Когда нажимаешь на аватарку другого пользователя, открывается поповер с более подробной информацией о нем и возможностью заблокировать этого пользователя.
Если мы нажимаем на кнопку заблокировать пользователя, тогда появляется дополнительный попап, который спрашивает: "а вы уверены, что хотите заблокировать пользователя?"
И тут появился тот самый баг, который вы видите на экране. z-index попала 100, а z-index поповера 1000. Конечно же  я сразу подшаманил, чтобы все работало, но это походило скорее на костыль.РешениеВ основе нашего решения лежит использование порталов. Давайте вспомним, что это такое:
Так сложилось, что когда мы создавали свой UiKit мы решили, что попап и поповер мы будем вставлять в проект через портал. Это было сделано для того, чтобы случайно какой-нибудь overflow: hidden не обрезал какую-либо важную часть. Я думаю многие сталкивались с этой проблемой.
Компонент <Portal>Сам компонент <Portal> выглядит следующим образом. При первом рендере мы создаем <div> и храним его в state.  Далее с помощью createPortal мы кладем children внутрь только что созданного <div>. И в useEffect все тот же <div> уже начиненный каким-то контентом помещаем в конец body. И при анмаунте компонента все тот же <div> удаляется из body. Компонент достаточно простой.
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
const Portal = ({ children }) => {
  const [container] = useState(() => document.createElement('div'));
  useEffect(() => {
    document.body.appendChild(container);
    return () => {
      document.body.removeChild(container);
    };
  }, []);
  return ReactDOM.createPortal(children, container);
};
export default Portal;
Один из ключевых моментов здесь, это то что мы помещаем каждый новый портал именно в конец какого-то контейнера в нашем случае это body.Суть этой особенности, если вставить несколько <div> подряд с одинаковым z-index. В таком случае <div>, который является последним всегда будет поверх предыдущих.Компонент <Popup>Остается только использовать этот компонент <Portal> в UiKit компоненте <Popup>. Здесь мы оборачиваем  весь контент компонентом <Portal>. А внутри вставляем <div>. У которого position: fixed на весь экран и z-index: 1.
const Popup = ({ children, onClose, isOpened }) => {
  if (!isOpened) {
    return null;
  }
  return (
    <Portal>
      <div className="popup" role="dialog">
        <div
          className="overlay"
          role="button"
          tabIndex={0}
          onClick={onClose}
        />
        <div className="content">{children}</div>
      </div>
    </Portal>
  );
};
Компонент <Popover>Точно тоже самое мы делаем с компонентом <Popover>. Точно так же оборачиваем весь контент в <Portal>, далее оборачиваем в обработчик <ClickOutside> для обработки клика вне поповера и уже идет сам контейнер <Popper> от библиотеки react-popper. Который навешивает инлайн стилями position: absolute на наш <div>,  и остается добавить ему только z-index: 1.
const Popover = ({ onClose, reference, placement, children }) => {
  const popperRef = useRef();
  return (
    <Portal>
      <ClickOutside reference={popperRef.current} onClickOutside={onClose}>
        <Popper
          innerRef={popperRef}
          referenceElement={reference}
          placement={placement}
        >
          {({ ref, style }) => (
            <div ref={ref} style={style} className="popover">
              {children}
            </div>
          )}
        </Popper>
      </ClickOutside>
    </Portal>
  );
};
Вот и вся реализацияПроверим результатПример 1Перейдем к первому примеру с иконкой. Мы нажимаем на иконку - открывается попап. Он как мы знаем добавился в конец body и имеет z-index: 1, поэтому показывается поверх остального контента. Далее мы открываем меню пользователя, которое отображается в поповере и он точно так же, как и попап добавляется в конец body и т.к. позиция в DOM дереве у поповера ниже, поэтому он показывается поверх попапа.
Пример 2С другой стороны, рассмотрим снова пример с аватаркой. Кликнем по аватарке, появится поповер и в конец body он так же добавился.  Нажимаем кнопку заблокировать пользователя и видим попап уже не под поповером, а над поповером. Это произошло, потому что теперь попап в конце  DOM дерева и поэтому у него позиция выше. И такой фокус работает при любом количестве разных типов компонентов вставляемых через <Portal>.
ПодытожимСуть данного подхода очень простая: какой элемент последний появился на экране, тот и показывается поверх всего. А если вам вдруг в каком то кейсе такая логика не подходит. Вы можете просто присвоить z-index: 2. Хотя я сомневаюсь, что вам это понадобится. По крайней мере в нашем достаточно сложном проекте с 30+ попапов и столько же поповеров, вроде бы закрыло все кейсы. По крайней мере, пока никто не жалуется)
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_reactjs, #_react, #_portal, #_zindex, #_popup, #_popover, #_javascript, #_reactjs
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 15:44
Часовой пояс: UTC + 5