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

от автора

Привет, Хабр!

Буквально недавно на работе я получил баг с z-index, я его по быстрому пофиксил и получил еще два бага. Я как то не придавал этой проблеме значения, и тут мой коллега Дмитрий Рокало ревьювил мой очередной пул реквест и пришел ко мне с идеей, как покончить войну с z-index в нашем проекте. И как раз в тот же день, я слушал подкаст веб стандарты и там обсуждали статью по работе с z-index. И решение, которое предлагают в статье, показалось мне крайне нелепым по сравнению с тем, что предложил мне Дима. Поэтому я решил спонтанно записать это видео и написать статью. Возможно это решение кому-то будет полезным (Данная статья является расшифровкой видео).

Обрисуем ситуацию

Давайте рассмотрим пример. У нас при клике на иконку открывается попап или модальное окно или назовите его еще как угодно. Сейчас речь не про нейминг, но мы в проекте у себя называем это попапом. Этот попап всегда находится над основным контентом, поэтому мы дали всем попапам z-index: 100, и это сработало.

.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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *