PortalContext

@cloud-ru/ds-portal-context — служебный пакет: задаёт через React Context корневой DOM-узел, в который компоненты дизайн-системы рендерят порталы (Tooltip, Popover, Dropdown, Modal, Drawer и др.). По умолчанию портал монтируется в document.body; контекст нужен, когда DS работает внутри shadow DOM, iframe или встроенного приложения с собственным rooted DOM.

Когда использовать

  • DS встраивается в micro-frontend / shadow DOM — document.body недоступен или принадлежит host-приложению.
  • Необходимо поднимать stacking-контекст всех порталов в один контейнер (например, ради scoped CSS).
  • E2E или Storybook-тесты — портал нужно монтировать в фиксированный узел iframe.

В обычном SPA-приложении провайдер не требуется — компоненты по умолчанию используют document.body.

Установка

pnpm add @cloud-ru/ds-portal-context
import { PortalContextProvider } from '@cloud-ru/ds-portal-context'

Примеры использования

Кастомный root через PortalContext

Кастомный root через PortalContextПортал монтируется в указанный ref, а не в `document.body`.
Хост-компонент
tsx
import { PortalContextProvider, usePortalContext } from '@cloud-ru/ds-portal-context';
import { useMemo, useState } from 'react';
import { createPortal } from 'react-dom';

function PortalChild() {
  const root = usePortalContext();

  if (!root.current) return null;

  return createPortal(<span>Я отрендерен в кастомном root-узле через PortalContext</span>, root.current);
}

export function CustomRoot() {
  const [node, setNode] = useState<HTMLDivElement | null>(null);
  const root = useMemo(() => ({ current: node }), [node]);

  return (
    <div style={{ display: 'flex', gap: 12, flexDirection: 'column' }}>
      <PortalContextProvider root={root}>
        <span>Хост-компонент</span>
        <PortalChild />
      </PortalContextProvider>
      <div ref={setNode} data-test-id='portal-root' />
    </div>
  );
}

Каскад тем: разные корни порталов

Каскад тем: разные корни порталовДва блока со своей темой и своим корнем порталов. Тултип и поповер из тёмного блока рендерятся в тёмной теме, из светлого — в светлой, потому что каждый монтируется в DOM-узел своего блока.

Тёмный блок

Светлый блок

tsx
import { Button } from '@cloud-ru/ds-button';
import { Popover } from '@cloud-ru/ds-popover';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { COLOR_SCHEME, ColorScheme, useThemeClassnames } from '@cloud-ru/ds-theme';
import { Tooltip } from '@cloud-ru/ds-tooltip';
import { useRef } from 'react';

import styles from './CascadingThemes.module.scss';

// Тематический блок: useThemeClassnames форсит colorScheme и эмитит полный набор sn-* на свой div.
// Этот же div — корень порталов блока (PortalContextProvider root={paneRef}), поэтому тултип и
// поповер монтируются ВНУТРЬ него и наследуют тему блока через CSS-каскад токенов.
function ThemedPane({ scheme, title }: { scheme: ColorScheme; title: string }) {
  const themeClassName = useThemeClassnames({ colorScheme: scheme });
  const paneRef = useRef<HTMLDivElement>(null);

  return (
    <div ref={paneRef} className={`${styles.pane} ${themeClassName}`}>
      <PortalContextProvider root={paneRef}>
        <p className={styles.paneTitle}>{title}</p>
        <Tooltip tip='Тултип рендерится в теме своего блока' placement='top'>
          <Button label='Навести — тултип' appearance='primary' view='filled' />
        </Tooltip>
        <Popover content='Поповер — тоже в теме блока' placement='bottom' trigger='click'>
          <Button label='Кликнуть — поповер' appearance='neutral' view='outline' />
        </Popover>
      </PortalContextProvider>
    </div>
  );
}

export function CascadingThemes() {
  return (
    <div className={styles.grid}>
      <ThemedPane scheme={COLOR_SCHEME.Dark} title='Тёмный блок' />
      <ThemedPane scheme={COLOR_SCHEME.Light} title='Светлый блок' />
    </div>
  );
}

Props

Types

PropsPortalContextProviderProps
PropTypeDefaultRequiredDescription
childrenstring | number | boolean | ReactElement<any, string | JSXElementConstructor<any>> | Iterable<ReactNode> | ReactPortal | null | undefinedno
rootRefObject<HTMLElement | null>no

Types

PortalContextProviderProps

Смотри также