Контекст: В проекте не было переиспользуемого компонента Modal. Каждая модалка создавалась ad-hoc — дублирование разметки (fixed inset-0 z-50 оверлей + stopPropagation + backdrop). При этом скроллбар либо отсутствовал (риск выхода контента за viewport), либо был на оверлее, а не на внутреннем контенте модалки.
// ❌ EditModal: scroll на ОВЕРЛЕЕ, а не на контенте
<div className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto ..." onClick={onClose}>
<div className="w-full max-w-2xl ..." onClick={(e) => e.stopPropagation()}>
{/* контент без скролла */}
</div>
</div>
// ❌ LoginModal, RegisterModal, ReworkModal: вообще без overflow
<div className="fixed inset-0 z-50 flex items-center justify-center ..." onClick={onClose}>
<form ... onClick={(e) => e.stopPropagation()}>
{/* может вылезти за viewport */}
</form>
</div>
Последствия:
max-h + overflow-y-auto могут выйти за границы экранаstopPropagationЕдиный компонент Modal с max-h-[90vh] overflow-y-auto на внутреннем контейнере:
export default function Modal({ onClose, children, size = 'md', className = '' }: ModalProps) {
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
onClick={onClose}
>
<div
className={`w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto rounded-xl bg-[#1e293b] shadow-xl ${className}`}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
)
}
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 |
Новый скролл |
MapView.tsx) — не модалка, а bottom panelСкроллбар должен быть частью модального контента, а не оверлея. Пользователь должен понимать границы модалки, и скроллбар внутри них это подчёркивает.
Задача 2026-07-04: «Если в модалке нужен скроллбар, то он должен быть внутри модалки».
#frontend #react #modal #ux #refactoring #DRY