Стек модальных окон

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


Использование useModal и useOverlay

Для создания модального окна используется хук useModal, который устанавливает необходимые ARIA-атрибуты и фокусную ловушку (focus trap). Он гарантирует, что при открытом модальном окне фокус клавиатуры не выйдет за пределы окна.

import { useModal } from '@react-aria/overlays';
import { useRef } from 'react';

function Modal({ children }) {
  let ref = useRef();
  let { modalProps } = useModal();
  
  return (
    <div {...modalProps} ref={ref}>
      {children}
    </div>
  );
}

Для управления визуальной частью и позиционированием применяется useOverlay. Он обеспечивает корректное закрытие модального окна при клике вне области, нажатии клавиши Escape и совместим с анимацией появления/исчезновения.

import { useOverlay, usePreventScroll } from '@react-aria/overlays';

function Overlay({ isOpen, onClose, children }) {
  let ref = useRef();
  usePreventScroll(); // предотвращает прокрутку фона
  let { overlayProps } = useOverlay({ isOpen, onClose, isDismissable: true }, ref);
  
  return (
    <div {...overlayProps} ref={ref}>
      {children}
    </div>
  );
}

Управление состоянием с useOverlayTriggerState

Для более сложных сценариев, где требуется открывать/закрывать несколько окон или управлять их вложенностью, используется useOverlayTriggerState. Он обеспечивает централизованное управление состоянием модального окна.

import { useOverlayTriggerState } from '@react-stately/overlays';

function App() {
  let state = useOverlayTriggerState({});

  return (
    <>
      <button onCl ick={state.open}>Открыть модальное окно</button>
      {state.isOpen && <Modal onCl ose={state.close}>Содержимое окна</Modal>}
    </>
  );
}

Ключевое преимущество — синхронизация состояния UI с фокусом и закрытием через клавиши или клики вне окна.


Фокусная ловушка и управление клавиатурой

React Aria автоматически применяет focus trap, который блокирует перемещение фокуса за пределы активного модального окна. Это достигается через комбинацию useModal и useOverlay, но при необходимости можно управлять фокусом вручную с помощью useFocusRing и useFocus.

import { useFocusRing } from '@react-aria/focus';

function FocusableButton(props) {
  let { isFocusVisible, focusProps } = useFocusRing();
  return (
    <button {...props} {...focusProps} style={{ outline: isFocusVisible ? '2px solid blue' : 'none' }}>
      {props.children}
    </button>
  );
}

Вложенные модальные окна

Для создания стека модальных окон важно учитывать правильное управление порядком фокуса и слойностью (z-index). React Aria автоматически обеспечивает правильное распределение фокуса через внутренний стек оверлеев.

function NestedModals() {
  let parentState = useOverlayTriggerState({});
  let childState = useOverlayTriggerState({});

  return (
    <>
      <button onCl ick={parentState.open}>Открыть родительское окно</button>
      {parentState.isOpen && (
        <Modal onCl ose={parentState.close}>
          Родительское окно
          <button onCl ick={childState.open}>Открыть дочернее окно</button>
          {childState.isOpen && <Modal onCl ose={childState.close}>Дочернее окно</Modal>}
        </Modal>
      )}
    </>
  );
}

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


Предотвращение прокрутки фона

React Aria предоставляет хук usePreventScroll, который блокирует прокрутку контента за модальным окном. Этот подход особенно важен для мобильных устройств и сложных интерфейсов с длинными списками.

import { usePreventScroll } from '@react-aria/overlays';

function ScrollBlockingModal({ children }) {
  usePreventScroll();
  return <div>{children}</div>;
}

Интеграция с анимациями

Хук useOverlay совместим с библиотеками анимации, такими как Framer Motion. Для плавного появления/исчезновения модального окна достаточно использовать состояние isOpen и контролировать рендер с анимацией.

import { motion, AnimatePresence } from 'framer-motion';

function AnimatedModal({ isOpen, onClose, children }) {
  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
          <Modal onCl ose={onClose}>{children}</Modal>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Ключевые рекомендации

  • Всегда использовать useModal и useOverlay вместе для обеспечения доступности.
  • Управление состоянием лучше централизовать через useOverlayTriggerState.
  • Для вложенных модальных окон следует следить за правильным порядком фокуса и слоев.
  • Не забывать блокировать прокрутку фонового контента с usePreventScroll.
  • Фокусные индикаторы должны быть видимыми для клавиатурной навигации через useFocusRing.

React Aria превращает управление модальными окнами в структурированную и доступную систему, где отдельные хуки отвечают за состояние, фокус, доступность и визуализацию. Такой подход позволяет строить как простые модальные диалоги, так и сложные интерфейсы со стеком окон, полностью соответствующие стандартам WCAG.