﻿---
description: Компонент реализующий модальное окно.
---

<Overview group="modals">

# ModalPage [tag:component]

Компонент реализующий модальное окно. Позволяет показать дополнительный контент,
удерживая пользователя в текущем контексте страницы/приложения.

</Overview>

<Playground hide>

```jsx
<PlatformProvider value="vkcom">
  <ModalPage
    open={open}
    onClose={() => {}}
    disableModalOverlay
    header={
      <ModalPageHeader
        before={
          <PanelHeaderBack label="Назад" />
        }
      >
        Информация о пользователе
      </ModalPageHeader>
    }
  >
    <Group>
      <Cell>
        <InfoRow header="Дата рождения">5 ноября 1994</InfoRow>
      </Cell>
      <Cell>
        <InfoRow header="Родной город">Санкт-Петербург</InfoRow>
      </Cell>
      <Cell>
        <InfoRow header="Место работы">Команда ВКонтакте</InfoRow>
      </Cell>
    </Group>
  </ModalPage>
</PlatformProvider>
```

</Playground>

{/* @example-description: Базовая `ModalPage` с заголовком и содержимым профиля, управляемая через состояние `open`. */}
<Playground>

```jsx
const [open, setOpen] = React.useState(false);

return (
  <>
    <Button onClick={() => setOpen(true)}>Открыть</Button>
    <ModalPage
      open={open}
      onClose={() => setOpen(false)}
      header={
        <ModalPageHeader
          before={
            <PanelHeaderBack label="Назад" onClick={() => setOpen(false)} />
          }
        >
          Информация о пользователе
        </ModalPageHeader>
      }
    >
      <Group>
        <Cell>
          <InfoRow header="Дата рождения">5 ноября 1994</InfoRow>
        </Cell>
        <Cell>
          <InfoRow header="Родной город">Санкт-Петербург</InfoRow>
        </Cell>
        <Cell>
          <InfoRow header="Место работы">Команда ВКонтакте</InfoRow>
        </Cell>
      </Group>
    </ModalPage>
  </>
);
```

</Playground>

## Область применения

- Формы для редактирования.
- Детали профиля пользователя.
- Разделы настроек, которые должны удерживать пользователя в текущем контексте приложения.

## Анатомия

`ModalPage` состоит из композиции нескольких компонентов.

**Опциональные компоненты, передающиеся пользователем**:

- [ModalPageHeader](/components/modal-page-header) – передаётся через параметр `header`;
- `ModalPageFooter` – передаётся через параметр `footer`.

**Обязательные компоненты, задающиеся библиотекой**:

- `ModalOutlet` – обёртка с фиксированной позицией для перекрытия других элементов в **DOM**;
- `ModalOverlay` – интерактивный задний фон;
- `ModalPageInternal` – инкапсулирует в себе всю логику поведения;
- `ModalPageContent` – оборачивает переданный `children` в [CustomScrollView](/components/custom-scroll-view).

## Поведение

Параметр `open` задает состояние показа/скрытия `ModalPage`. По умолчанию компонент скрыт.

При состоянии `open={false}` компонент размонтируется из DOM. Чтобы отключить это поведение, необходимо задать параметр
`keepMounted`.

Смена состояния сопровождается анимацией содержимого `ModalPage`, а также `ModalOverlay`. За сменой состояния можно следить
через события:

- `onOpen`
- `onOpened`
- `onClose` – передаёт в аргумент причину закрытия (см. описание в таблице с API)
- `onClosed`

Отличие `onOpened`/`onClosed` от `onOpen`/`onClose` в том, что они вызываются после завершения анимации.

Далее поведение зависит от разрешения экрана.

### Настольный режим

При разрешении `>= 768px` компонент `ModalPage` имеет вид **диалогового окна**.

При `open={true}`:

- параметр `size` будет ограничивать максимальную ширину окна;
- навигация через `Tab` и `Shift` + `Tab` будет зациклена на содержимом `ModalPage`.

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

По умолчанию, пользователь может закрыть **диалоговое окно** с помощью:

- нажатия на кнопку закрытия, что вызовет событие `onClose` с аргументом `click-close-button`;
- нажатия на `ModalOverlay`, что вызовет событие `onClose` с аргументом `click-overlay`;
- нажатия на `Esc`, что вызовет событие `onClose` с аргументом `escape-key`.

### Мобильный режим

При разрешении `<= 767px` компонент `ModalPage` имеет вид панели, выдвигающейся снизу вверх (далее **bottom sheet**).

> При передаче `hasCustomPanelHeaderAfter` в [ConfigProvider](/components/config-provider) модальная страница
> сверху будет смещена на высоту [PanelHeader](/components/panel-header), чтобы кнопка закрытия модальной
> страницы не перекрывалась пользовательским "плавающим" элементом (например, панелью управления
> webview).

При `open={true}` навигация через `Tab` и `Shift` + `Tab` будет зациклена на содержимом `ModalPage` (допустимо, т.к. к устройству
может быть подключена клавиатура).

По умолчанию, пользователь может закрыть **bottom sheet** с помощью:

- нажатия на `ModalOverlay`, что вызовет событие `onClose` с аргументом `click-overlay`;
- нажатия на `Esc`, что вызовет событие `onClose` с аргументом `escape-key` (допустимо, т.к. к устройству может быть подключена
  клавиатура);
- определенного взаимодействие через тач или указатель мыши, что вызовет событие `onClose` с аргументом `swipe-down`
  (см. **Взаимодействие через тач или указатель мыши**)

#### Взаимодействие через тач или указатель мыши

> `<ConfigProvider platform="vkcom" />`
>
> При `platform === 'vkcom` описанный в этом разделе функционал полностью игнорируется, т.к. по историческим причинам
> `platform="vkcom"` форсирует условие, что сейчас используется десктопный режим. Высота **bottom sheet** будет определяться за счёт
> контента.

Для **bottom sheet** можно задавать на какой процент высоты экрана изначально должна открываться панель – этот процент задаётся
через параметр `settlingHeight`. Минимальное значение `25`, а максимальное `100`. По умолчанию используется `50`.

От `settlingHeight` зависит дальнейшее поведение **bottom sheet**:

- при `settlingHeight <= 90` у **bottom sheet** появляется возможность через перетаскивание раскрывать высоту до `100` и схлопывать
  обратно до переданного `settlingHeight`;
- при `settlingHeight > 90` значение приводится к `100` и, соответственно, **bottom sheet** всегда раскрывается на `100`.

Если необходимо, чтобы высота **bottom sheet** зависела от его контента, то стоит использовать параметр `dynamicContentHeight`,
вместо `settlingHeight`.

В обеих случаях перетаскивание **bottom sheet** изменяет прозрачность `ModalOverlay` – при перетаскивание вниз фон становится
светлее. При `settlingHeight` процент прозрачности высчитывается относительно переданного значения в `settlingHeight`.

Пользователь через перетаскивание может вызвать закрытие **bottom sheet** по следующим условиям:

- `onClose('swipe-down')` вызовется сразу же, если **bottom sheet** достиг высоты `0`, независимо закончил ли пользователь
  взаимодействие с ним или нет;
- `onClose('swipe-down')` вызовется, если после окончания взаимодействия с **bottom sheet** его высота будет:

  - _при `settlingHeight`_: меньше половины переданного значения в `settlingHeight` (пример, если `settlingHeight={50}`, то условием
    закрытие будет
    `height < 50 / 2`);
  - _при `dynamicContentHeight`_: меньше половины контента;

- `onClose('swipe-down')` вызовется при сильном смахивании вниз;
  - _при `settlingHeight`_: добавляется условии, что **bottom sheet** должен находится на высоте равной переданному в
    `settlingHeight`;

При следующих условиях возможность перетаскивания замораживается, чтобы не блокировать пользователя:

- при условии `disableContentPanningGesture={false}`, есть взаимодействие с контентом `ModalPageContent` при `scrollTop !== 0`;
- есть взаимодействие с горизонтальным скроллом;
- есть взаимодействие с текстовым полем;
- есть взаимодействие с выделенным текстом;
- есть взаимодействие с элементом вызвавшим `event.stopPropagation()` на `onTouchStart` или `onMouseDown`.
- есть взаимодействие с элементом с **data**-атрибутом `data-vkui-block-sheet-behavior`
  (в частности, используется при `disableContentPanningGesture={true}`);

## Хук `useModalManager`

Для более удобного управления модальными окнами вы можете использовать менеджер в виде хука [`useModalManager`](/components/use-modal-manager)

## Доступность (a11y)

`ModalPage` является модальным окном (`role="dialog"`), а значит у него обязательно должно быть имя — его краткое название. Благодаря этому пользователи вспомогательных технологий знают, что это за элемент и какое у него содержимое.

Задать имя можно с помощью следующих способов:

- используя свойство `title`;
- используя свойство `aria-label`;
- используя свойство `aria-labelledby`;
- используя внутри компонент [ModalPageHeader](/components/modal-page-header). Этот компонент сам свяжется с `ModalPage` через контекст c помощью `aria-labelledby`.

## FAQ

### Как поменять максимальную ширину контента?

Используйте параметр `size`.

### Как задать фиксированную высоту контента?

Используйте параметр `height`. Высоту можно задать как числом (`height={300}`) так и в процентах (`height={'80%'}`).

> В мобильной версии на высоту по умолчанию также влияет значение `settlingHeight`. Чтобы отключить его влияние, достаточно задать
> `settlingHeight={100}`.

### Как правильно применять параметр `autoFocus`?

К сожалению, из-за особенностей **React**, `autoFocus` ломает CSS анимацию.

Чтобы исправить проблему, следует отказаться от `autoFocus` и выставлять фокус вручную при событии `onOpened`.

{/* @example-description: Ручной фокус в поле ввода `ModalPage` после завершения анимации открытия. */}
<Playground>

```jsx
const [open, setOpen] = React.useState(false);

const inputRef = React.useRef(null);

const handleOpen = React.useCallback(() => {
  if (inputRef.current) {
    inputRef.current.focus();
  }
}, []);

return (
  <>
    <Button onClick={() => setOpen(true)}>Открыть</Button>
    <ModalPage
      id="modal-with-auto-focus"
      open={open}
      onOpened={handleOpen}
      onClose={() => setOpen(false)}
    >
      <Div>
        <Input getRootRef={inputRef} />
      </Div>
    </ModalPage>
  </>
);
```

</Playground>

## Свойства и методы [#api]

| Свойство | Тип | По умолчанию | Описание |
| --- | --- | --- | --- |
| `disableCloseAnimation` | `boolean` | `-` | Отключает анимацию закрытия модалки. |
| `disableContentPanningGesture` | `boolean` | `-` | Отключает раскрытие и закрытие панели в мобильном виде. |
| `disableFocusTrap` | `boolean` | `-` | Позволяет отключить захват фокуса.  Нужно использовать, когда поверх одной модалки открывается другая, чтобы два `FocusTrap` не конфликтовали. |
| `disableModalOverlay` | `boolean` | `-` | Отключает отображение и взаимодействие с фоном модалки. |
| `disableOpenAnimation` | `boolean` | `-` | Отключает анимацию открытия модалки. |
| `dynamicContentHeight` | `boolean` | `-` | Если высота контента в модальной странице может поменяться, нужно установить это свойство. |
| `footer` | `ReactNode` | `-` | Подвал модальной страницы, `<ModalPageFooter />`. |
| `getModalContentRef` | `Ref<HTMLDivElement>` | `-` | Возвращает DOM-элемент содержимого модального окна. |
| `getRootRef` | `Ref<HTMLDivElement>` | `-` |  |
| `header` | `ReactNode` | `-` | Шапка модальной страницы, `<ModalPageHeader />`. |
| `height` | `string \| number` | `-` | Задаёт модальному окну фиксированную высоту. Можно передать числовое значение в пикселях, а можно строкой, в том числе и в процентах "50%". В мобильной версии 'settlingHeight' будет считаться относительно заданного height. |
| `hideCloseButton` | `boolean` | `-` | Скрывает кнопку закрытия (актуально для iOS, так как можно отрисовать кнопку закрытия внутри модалки). |
| `id` | `string` | `-` |  |
| `keepMounted` | `boolean` | `false` | Сохранять ли компонент в DOM при `open={false}`. |
| `modalContentTestId` | `string` | `-` | `data-testid` для содержимого модального окна. |
| `modalDismissButtonLabel` | `string` | `-` | Текст для скринридера. |
| `modalDismissButtonTestId` | `string` | `-` | `data-testid` для кнопки закрытия. |
| `modalOverlayTestId` | `string` | `-` | `data-testid` для оверлея. |
| `nav` | `string` | `-` | Уникальный идентификатор навигационного элемента (вместо id) |
| `noFocusToDialog` | `boolean` | `-` | Отключает фокус на интерактивный элемент после открытия модалки.  > Важно установить фокус после открытия в любое место модалки используя событие `onOpened`, иначе не будет работать закрытие по нажатию `Esc`. |
| `onClose` | `((reason: ModalPageCloseReason, event?: UIEvent<HTMLElement, UIEvent>) => void) \| undefined` | `-` | Будет вызвано при начале закрытия модалки. |
| `onClosed` | `VoidFunction` | `-` | Будет вызвано при окончательном закрытии модалки. |
| `onOpen` | `VoidFunction` | `-` | Будет вызвано при начале открытия модалки. |
| `onOpened` | `VoidFunction` | `-` | Будет вызвано при окончательном открытии модалки. |
| `open` | `boolean` | `false` | Состояние видимости. |
| `outsideButtons` | `ReactNode` | `-` | Управляющие элементы под кнопкой закрытия.  Доступно только в `compact`-режиме. Рекомендуется размещать иконки размера 20, обернутые в ModalOutsideButton. |
| `preventClose` | `boolean` | `-` | Позволяет отключить возможность закрытия модальной страницы (смахивание, клавиша `ESC`, нажатие на подложку).  ⚠️ ВНИМАНИЕ: использование этой опции негативно сказывается на пользовательском опыте. |
| `restoreFocus` | `boolean \| (() => boolean \| HTMLElement)` | `true` | Управление поведением возврата фокуса при закрытии всплывающего окна. |
| `settlingHeight` | `number` | `50` | Процент, на который изначально будет открыта модальная страница.  > ⚠️ Следует использовать следующие значения: `25`, `50`, `75`, `100`. > При передаче `< 25` значение приведётся к `25`, при передаче `> 75` значение приведётся к `75`.  Игнорируется при включении `dynamicContentHeight`. |
| `size` | `LiteralUnion<"s" \| "auto" \| "m" \| "l" \| "max-content" \| "min-content" \| "fit-content", string \| number>` | `s` | Задаёт контенту максимальную ширину на десктопе. |
| `style` | `Omit<CSSProperties, "height" \| "maxHeight" \| "maxWidth">` | `-` | Дополнительные стили. |

