ModalCustom
ModalCustom — низкоуровневая версия Modal, которая не диктует структуру содержимого. Вы сами компонуете шапку, тело и футер из субкомпонентов ModalCustom.Header, .Body, .Footer или собственной разметки.
Используйте ModalCustom, когда стандартной шапки из Modal недостаточно — например, нужна своя раскладка заголовка с несколькими действиями, кастомный футер с группами кнопок или нестандартный порядок секций.
Когда использовать
- Стандартная шапка / футер из
Modalне подходят — нужна своя разметка. - Сложная раскладка нескольких секций внутри одного окна.
- Кастомные слоты (например, фиксированный поиск между шапкой и телом).
Во всех остальных случаях предпочтительнее Modal — он дешевле в поддержке и даёт консистентные отступы.
Установка
pnpm add @cloud-ru/ds-modal
import { ModalCustom } from '@cloud-ru/ds-modal'
Примеры использования
Ручная композиция
tsx
import { Button } from '@cloud-ru/ds-button';
import { ModalCustom } from '@cloud-ru/ds-modal';
import { useState } from 'react';
export function CustomComposition() {
const [open, setOpen] = useState(false);
const close = () => setOpen(false);
return (
<>
<Button label='Открыть' appearance='primary' view='filled' onClick={() => setOpen(true)} />
<ModalCustom open={open} onClose={close} width='m'>
<ModalCustom.Header title='Ручная композиция' subtitle='Header, Body и Footer собираются вручную.' />
<ModalCustom.Body
content={
<div style={{ padding: 24 }}>
<p>В теле может быть любая разметка — скролл включается автоматически.</p>
<p>Это нужно, когда пресетной структуры Modal недостаточно.</p>
</div>
}
/>
<ModalCustom.Footer>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<Button label='Закрыть' appearance='neutral' view='outline' onClick={close} />
<Button label='Подтвердить' appearance='primary' view='filled' onClick={close} />
</div>
</ModalCustom.Footer>
</ModalCustom>
</>
);
}Props
Types
Props
ModalCustomProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | — | yes | Содержимое окна (композиция Header/Body/Footer) |
className | string | — | no | CSS-класс окна |
closeOnPopstate | boolean | — | no | Закрытие при навигации по истории |
container | ModalContainer | — | no | Явный DOM-контейнер для `createPortal`; иначе `usePortalContext()` или `document.body`. |
data-test-id | string | — | no | |
heightAuto | boolean | — | no | Растягивать по высоте в пределах контейнера |
mode | "aggressive" | "forced" | "regular" | MODE.Regular | no | Режим закрытия: Regular — overlay/Esc/кнопка; Aggressive — только кнопка; Forced — без кнопки и overlay/Esc. |
onClose | () => void | — | yes | Колбэк закрытия |
open | boolean | — | yes | Управление состоянием показан/не показан |
rootClassName | string | — | no | CSS-класс корневого слоя портала |
safeArea | boolean | true | no | Резервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch). |
showBackdrop | boolean | true | no | Отображение тёмной подложки за sheet'ом. При `false` фон не затемняется и click-outside не закрывает sheet (нет backdrop-узла, по которому ловится клик). |
snapPoints | SnapPoint[] | — | no | Массив фиксированных позиций sheet'а от меньшей к большей. По дефолту `undefined` — sheet `height: auto` с одним snap'ом по высоте контента. Пример: `[0.5, 1]` — sheet открывается на половину экрана, drag вверх раскрывает до full-viewport; drag вниз ниже `0.5` ведёт к закрытию. Контракт массива (движок не сортирует и не дедуплицирует — порядок и различимость на стороне потребителя): - строго по возрастанию: индекс `0` — самая компактная позиция, последний — top / expanded; - значения должны резолвиться в различные высоты (`['50%', 0.5]` на типичном вьюпорте дадут одну высоту → дубль-индекс будет недостижим свайпом); - `'fit-content'` имеет смысл только как ЕДИНСТВЕННЫЙ snap (без `snapPoints`); внутри массива фиксированных позиций его «контентная» высота не определена. |
swipeEnabled | boolean | true | no | Включает swipe-down для закрытия / swipe-up для раскрытия на следующий snap-point. При `swipeEnabled=false` snap-point по-прежнему можно переключить через controlled `snapIndex` prop'ом. |
width | "l" | "m" | "s" | — | no | Размер окна |