# Модалка со скроллбаром внутри контента **Контекст**: В проекте не было переиспользуемого компонента Modal. Каждая модалка создавалась ad-hoc — дублирование разметки (`fixed inset-0 z-50` оверлей + `stopPropagation` + backdrop). При этом скроллбар либо отсутствовал (риск выхода контента за viewport), либо был на оверлее, а не на внутреннем контенте модалки. ## Проблема ### До рефакторинга ```tsx // ❌ EditModal: scroll на ОВЕРЛЕЕ, а не на контенте
e.stopPropagation()}> {/* контент без скролла */}
// ❌ LoginModal, RegisterModal, ReworkModal: вообще без overflow
e.stopPropagation()}> {/* может вылезти за viewport */}
``` **Последствия**: - При высоком контенте (форма редактирования, список полей) скроллбар появляется на body/оверлее — пользователь видит скроллбар за пределами модалки - Модалки без `max-h` + `overflow-y-auto` могут выйти за границы экрана - Дублирование кода: 7 мест с идентичной логикой оверлея + `stopPropagation` ## Решение Единый компонент `Modal` с `max-h-[90vh] overflow-y-auto` на **внутреннем** контейнере: ```tsx export default function Modal({ onClose, children, size = 'md', className = '' }: ModalProps) { return (
e.stopPropagation()} > {children}
) } ``` ### Что изменилось ```mermaid graph LR A["До: 7 ad-hoc модалок"] --> B["После: 1 компонент Modal"] B --> C["max-h-[90vh] на inner div"] C --> D["overflow-y-auto на inner div"] D --> E["Скроллбар внутри модалки"] B --> F["Размеры: sm/md/lg/xl"] B --> G["className override для фона"] ``` ### Ключевое различие | | До | После | |---|---|---| | Расположение `overflow-y-auto` | На оверлее (EditModal) или отсутствует | На внутреннем контейнере | | Скроллбар | Снаружи модалки (оверлей/body) | Внутри модалки | | DRY | Нарушен — 7 копий разметки | Соблюдён — 1 компонент | | Background override | Разный в каждом | `className` prop | ## Переведённые модалки | Компонент | Файл | `size` | Примечание | |-----------|------|--------|------------| | EditModal | `app/places/my/page.tsx` | `xl` | Был главной проблемой | | LoginModal | `components/LoginModal.tsx` | `sm` | Кастомный фон через className | | RegisterModal | `components/RegisterModal.tsx` | `sm` | Кастомный фон через className | | AddLocationModal | `components/AddLocationModal.tsx` | `xl` | Упрощён, убран лишний wrapper | | ReworkModal | `app/admin/page.tsx` | `md` | Новый скролл | | CreateUserModal | `app/admin/users/page.tsx` | `sm` | Новый скролл | | Delete confirm (2 шт.) | `places/my`, `admin` | `sm` | Новый скролл | ## Не затронуто - **PlaceCardModal** (`MapView.tsx`) — не модалка, а bottom panel - **MapPicker** — полноэкранный компонент ## Принцип > Скроллбар должен быть частью модального контента, а не оверлея. Пользователь должен понимать границы модалки, и скроллбар внутри них это подчёркивает. ## Связанные заметки - [[decision-modal-component-extraction]] — почему выбрали компонент, а не починку на месте - [[frontend-api-client]] - [[architecture-overview]] ## Источник Задача 2026-07-04: «Если в модалке нужен скроллбар, то он должен быть внутри модалки». ## Теги #frontend #react #modal #ux #refactoring #DRY