atomic-modal-scrollbar-inside.md 5.0 KB

Модалка со скроллбаром внутри контента

Контекст: В проекте не было переиспользуемого компонента 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>

Последствия:

  • При высоком контенте (форма редактирования, список полей) скроллбар появляется на body/оверлее — пользователь видит скроллбар за пределами модалки
  • Модалки без max-h + overflow-y-auto могут выйти за границы экрана
  • Дублирование кода: 7 мест с идентичной логикой оверлея + 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 Новый скролл

Не затронуто

  • PlaceCardModal (MapView.tsx) — не модалка, а bottom panel
  • MapPicker — полноэкранный компонент

Принцип

Скроллбар должен быть частью модального контента, а не оверлея. Пользователь должен понимать границы модалки, и скроллбар внутри них это подчёркивает.

Связанные заметки

  • [[decision-modal-component-extraction]] — почему выбрали компонент, а не починку на месте
  • [[frontend-api-client]]
  • [[architecture-overview]]

Источник

Задача 2026-07-04: «Если в модалке нужен скроллбар, то он должен быть внутри модалки».

Теги

#frontend #react #modal #ux #refactoring #DRY