Как мы решили проблемы с z-index

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Привет хабр!

Буквально недавно на работе я получил баг с 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+ попапов и столько же поповеров, вроде бы закрыло все кейсы. По крайней мере, пока никто не жалуется)

Источник: https://habr.com/ru/post/553592/


Интересные статьи

Интересные статьи

На работе я занимаюсь поддержкой пользователей и обслуживанием коробочной версии CRM Битрикс24, в том числе и написанием бизнес-процессов. Нужно отметить, что на самом деле я не «чист...
Всем привет! В современном мире крайне важна возможность масштабировать приложение по щелчку пальцев, ведь нагрузка на приложение может сильно отличаться в разное время. Наплыв клиентов, которы...
Привет, друзья! Меня зовут Петр, я представитель малого белорусского бизнеса со штатом чуть более 20 сотрудников. В данной статье хочу поделиться негативным опытом покупки 1С-Битрикс. ...
Привет! Меня зовут Саша, я iOS-разработчик в команде, которая делает ленту ВКонтакте. Сейчас расскажу, как мы оптимизируем отображение интерфейса и обходим связанные с этим проблемы. Думаю, вы п...
Даже в голову приходит этим заниматься, если честно. Я не сторонник идей равенства и братства, я за естественное регулирование социальных процессов. Но кто-то считает это важным и даже тратит ...