Объясняется на практическом примере

Порталы предоставляют первоклассную возможность отображать дочерние элементы в узле DOM, который существует вне иерархии DOM родительского компонента, как указано в официальной документации React.js.

Порталы полезны, когда мы хотим визуализировать компоненты, но у нашего родителя есть скрытое переполнение или определенная ширина и высота, модальные окна — идеальный пример, поэтому мы собираемся создать модальное окно с нуля и применить хорошие методы доступности для пользователей.

Вы можете увидеть полный код примера здесь, в этом репозитории GitHub.

Сначала мы собираемся создать компонент, который будет называться src/Components/Modal/index.js:

export const Modal = (props) => {
  let { children, close, ...rest } = props;
  if (!children) {
    children = <p>This is a example modal</p>;
  }
  return (
      <div id="modal-dialog" {...rest}>
        <div className="flex flex-col justify-center items-center">
          {children}
          <button onClick={close}>
            Close this modal
          </button>
        </div>
      </div>
  );
};

В файле src/styles.css у нас будет следующий код:

@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;500&display=swap");
* {
  font-size: 62.5%;
  font-family: "Roboto";
  margin: 0;
  padding: 0;
}

#App {
  overflow: hidden;
  height: 20vh;
  background-color: #ccc;
}

#App > h1 {
  font-size: 2rem;
}

div#modal-dialog {
  background-color: rgba(0, 0, 0, 0.8);
  position: fixed;
  z-index: 999;
  height: 100vh;
  width: 100vw;
  top: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}

div#modal-dialog > div {
  background-color: #f5f5f5;
  padding: 2rem;
  border-radius: 1.2rem;
}

p {
  margin: 1.4rem 0;
  font-size: 1.5rem;
}

button {
  padding: 1rem;
  border-radius: 1rem;
  border: none;
  background-color: #9b59b6;
  color: #fff;
  cursor: pointer;
  transition: all 0.3s ease-in-out;
}

button:hover {
  background-color: #8e44ad;
}

.flex {
  display: flex;
}

.flex-col {
  flex-direction: column;
}

.flex-row {
  flex-direction: row;
}

.justify-center {
  justify-content: center;
}

.items-center {
  align-items: center;
}

Здесь у нас будет несколько стилей для нашего модального окна, а также мы определили несколько стандартных классов для нашего приложения.

Теперь в модальном окне мы получим несколько реквизитов, таких как дочерние элементы, закрыть (функция для закрытия модального окна) и остальные реквизиты, которые у нас могут быть, у нас также есть фиксированный элемент, который является кнопкой для закройте модальное окно, и там мы передадим функцию закрытия по событию клика.

Мы продолжим создавать div в нашем файле index.html, который будет родственным элементом родительского div нашего приложения, и файл будет выглядеть следующим образом:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <div id="modals"></div>
  </body>
</html>

В этот div мы поместим идентификатор «modals», в который модальный компонент будет вставлен благодаря порталам.

Это приносит нам пользу, так как на наш компонент не влияют стили нашего родителя, у которого скрыто переполнение, а высота и ширина определены, поскольку он не будет отображаться правильно.

Теперь мы продолжим создавать src/App.js:

import { useState } from "react";
import ReactDOM from "react-dom";
import { Modal } from "./Components/Modal";
import "./styles.css";
const domElement = document.getElementById("modals");
export default function App() {
  const [stateModal, setStateModal] = useState(false);
  const openModal = () => setStateModal(true);
  const closeModal = () => setStateModal(false);
  return (
    <div id="App" className="flex flex-col justify-center items-center">
      <h1>Portals Example</h1>
      <div className="flex flex-col items-center justify-center">
        <p>This is a div with a defined height and overflow hidden</p>
        <button onClick={openModal}>
          Open modal
        </button>
      </div>
      {stateModal &&
        ReactDOM.createPortal(
          <Modal close={closeModal}>
            <p>Modal from App.js</p>
          </Modal>,
          domElement
        )}
    </div>
  );
}

Сначала у нас есть импорт, а в строке 6 у нас есть ссылка на div#modal, получающий его с помощью

const domElement = document.getElementById("modals"); //Reference to div#modals for create portal

Нам нужно сохранить это в переменной, так как она понадобится нам для создания портала.

Затем у нас есть состояние openModal, чтобы знать, открыто ли модальное окно или закрыто, у нас также есть соответствующие функции для открытия и закрытия модального окна.

У нас есть кнопка для открытия модального окна, ниже этого у нас есть самая важная вещь, которая является условной: когда состояние модального окна равно true, мы будем использовать функцию ReactDOM createPortal, и в качестве первого параметра мы передадим элемент, который мы хотим отобразить. и как вторым параметром мы будем передавать ссылку на div, куда мы собираемся внедрить указанный компонент, чтобы у нас было что-то вроде этого:

{stateModal &&
  ReactDOM.createPortal(
  <Modal close={closeModal}>
      <p>Modal from App.js</p>
  </Mode>,
  domElement
)}

Имея это, мы сможем увидеть, как модальное окно будет отображаться внутри div#modals, которое находится за пределами родительского контейнера нашего приложения, и все это благодаря порталам, и поэтому у нас не было проблем с нашими стилями или даже с модальными элементами. отделен от дома.

Улучшение нашей доступности с помощью передового опыта

Примечание.
При работе с порталами помните, что управление фокусом клавиатуры очень важно. Что касается диалогов, убедитесь, что каждый может взаимодействовать с ними, следуя Практикам создания модальных окон WAI-ARIA .

Программное управление фокусом.

Наши приложения React постоянно изменяют модель HTML DOM во время выполнения, что иногда приводит к потере фокуса клавиатуры или установке на неожиданный элемент. Чтобы исправить это, нам нужно программно переместить фокус клавиатуры в правильном направлении. Например, сброс фокуса клавиатуры на кнопку, которая открывала модальное окно после того, как это модальное окно было закрыто.

Затем мы собираемся улучшить наши компоненты, чтобы не было ошибок.

Что произойдет, если по какой-то причине у вас есть модальный режим для удаления чего-либо, и когда модальный режим открывается, фокус отправляется на кнопку подтверждения, это плохое управление доступностью, потому что его можно непреднамеренно активировать вводом с клавиатуры, так что это всегда лучше оставить фокус на действии закрытия модала и вернуть его на кнопку, которая активировала модал, чтобы он не затерялся в каком-то несуществующем его элементе.

Для этого мы должны заблокировать прокрутку, а также предотвратить уход фокуса с нашего компонента, мы будем использовать 2 зависимости, через которые мы собираемся установить:

npm i no-scroll focus-trap-react

Мы собираемся улучшить наш модальный компонент, перенаправив фокус на кнопку отмены, и мы сделаем это благодаря хуку React useRef.

src/Components/Modal/index.js:

import noScroll from "no-scroll";
import { useEffect, useRef } from "react";
import FocusTrap from "focus-trap-react";
export const Modal = (props) => {
  let { children, openButtonRef, close, ...rest } = props;
  if (!children) {
    children = <p>This is a example modal</p>;
  }

  let buttonRef = useRef();

  useEffect(() => {
    buttonRef ? buttonRef.current.focus() : null;
    noScroll.on();
    return () => {
      openButtonRef ? openButtonRef.current.focus() : null;
      noScroll.off();
    };
  }, []);

  return (
    <FocusTrap>
      <div id="modal-dialog" {...rest}>
        <div className="flex flex-col justify-center items-center">
          {children}
          <button ref={buttonRef} onClick={close}>
            Close this modal
          </button>
        </div>
      </div>
    </FocusTrap>
  );
};

Сначала мы импортируем наши новые зависимости:

import FocusTrap from "focus-trap-react";
import noScroll from "no-scroll";

Затем мы создаем ссылку, которую будем использовать в нашей кнопке let buttonRef = useRef();
, и делаем ссылку следующим образом с нашей модальной кнопкой закрытия <button ref={buttonRef} onClick={close}>Close this modal</button>

Мы также добавим новое свойство, которое является ссылкой на нашу кнопку для открытия нашего модального окна, чтобы вернуть фокус, когда это модальное окно закрыто: let { children, openButtonRef, close, ...rest } = props;

С помощью useRef мы будем знать, когда этот модал будет отображаться, что будет указывать на то, что он открыт, мы проверим, есть ли ссылки на кнопку закрытия, если ссылка есть, мы сфокусируем ее с помощью openButtonRef ? openButtonRef.current.focus() : null;, а также заблокируем прокрутку. к нашему приложению с noScroll.off()
и, что наиболее важно, когда этот компонент будет размонтирован, мы вернем фокус на кнопку, которая открыла модальное окно, и мы снова разблокируем прокрутку с помощью следующего кода

openButtonRef ? openButtonRef.current.focus() : null; 
noScroll.off();

Для которых useEffect будет следующим:

useEffect(() => {
     buttonRef ? buttonRef.current.focus() : null;
     noScroll.on();
     return() => {
       openButtonRef ? openButtonRef.current.focus() : null;
       noScroll.off();
     };
   }, []);

Наконец, мы обернем наш модальный компонент компонентом:

<FocusTrap>
{......}
</FocusTrap>

В нашем компоненте src/App.js мы собираемся создать ссылку на нашу кнопку открытия и передать ее нашему модальному модулю, чтобы наш файл выглядел так:

import { useRef, useState } from "react";
import ReactDOM from "react-dom";
import { Modal } from "./Components/Modal";
import "./styles.css";

const domElement = document.getElementById("modals");

export default function App() {
  const [stateModal, setStateModal] = useState(false);

  let openButtonRef = useRef();

  const openModal = () => setStateModal(true);
  const closeModal = () => setStateModal(false);

  return (
    <div id="App" className="flex flex-col justify-center items-center">
      <h1>Portals Example</h1>
      <div className="flex flex-col items-center justify-center">
        <p>This is a div with a defined height and overflow hidden</p>
        <button ref={openButtonRef} onClick={openModal}>
          open modal
        </button>
      </div>
      {stateModal &&
        ReactDOM.createPortal(
          <Modal close={closeModal} openButtonRef={openButtonRef}>
            <p>Modal from App.js</p>
          </Mode>,
          domElement
        )}
    </div>
  );
}

Таким образом, мы применили хорошие методы доступности, прокрутка будет заблокирована, а также фокус будет ограничен только нашим модальным окном, которое мы можем протестировать с помощью кнопки Tab, в этом примере мы узнали о реагирующих порталах и создать модальное окно с хорошим практики.

Теперь осталось только попрактиковаться и продолжить изучение того, что мы можем улучшить в этом модальном компоненте.

Скажите, в каком другом примере вы бы использовали реагирующие порталы?