Перейти до вмісту
tatetUI

Оверлеї та фокус

Спільний стек шарів, лічильник блокування прокрутки і пастка фокуса, що розуміє телепорти.

UiModal, UiDrawer і UiConfirmDialog не мають власної логіки оверлея. Уся вона в чотирьох композаблах, які вони ділять. Через це вкладені оверлеї працюють правильно — а це саме те, що ламалося у вихідних проєктах.

Проблема, яку це розв'язує

Раніше кожен компонент рахував z-index сам: модалка мала власний стек від 1001, drawer — захардкоджений z-[1000], діалог підтвердження — 99999. Наслідки:

  • drawer, відкритий із модалки, опинявся під нею;
  • два drawer'и один над одним розрулювалися лише порядком у DOM;
  • Escape закривав усі відкриті оверлеї одразу;
  • закриття внутрішнього оверлея розблоковувало прокрутку фону, поки зовнішній ще відкритий.

Шкала z-index

1000  перший оверлей
1010  другий (крок 10)
1020  третій
…
1100  випадайки UiSelect
9999  тости

Крок 10 лишає місце для внутрішніх шарів панелі — sticky-шапки, футера.

1100 для випадайок обране так, щоб перші ~10 оверлеїв лишалися нижче: селект усередині модалки має бути видимим над нею.

Тости на 9999 і з data-overlay-ignore — вони мусять лишатися клікабельними навіть під відкритою модалкою.

Шар знімається одразу, z-index — після анімації

Це найнеочевидніша частина. При закритті шар виходить зі стеку негайно, а його z-index лишається зафіксованим до кінця анімації виходу.

Якби шар чекав завершення анімації, будь-який незавершений leave (перервана анімація, фонова вкладка) лишав би мертвий запис у стеку. Тоді isTopmost брехав би, і Escape перестав би закривати видимий оверлей.

Заморожування z-index потрібне з іншого боку: без нього панель, що закривається, пірнула б під сусідній оверлей посеред анімації.

Прокрутка блокується з лічильником

position: fixed, а не overflow: hidden — на iOS Safari overflow не зупиняє «гумовий» скрол фону.

Лічильник тримачів означає, що фон розблоковується лише коли закрився останній оверлей.

Позиція прокрутки зберігається і відновлюється, а scroll-behavior примусово ставиться в auto — інакше повернення на місце анімувалося б.

Пастка фокуса розуміє телепорти

Головний нюанс. UiSelect рендерить випадайку прямо в <body> — тобто формально поза панеллю модалки.

Тому «фон» визначається не як «усе поза контейнером», а як «те, що ця пастка сама позначила inert у момент активації». Елементи, які з'явилися пізніше — та сама випадайка — лишаються клікабельними й фокусованими.

Вкладеність працює за принципом LIFO: коли поверх drawer'а відкривається модалка, її пастка позначає inert і корінь drawer'а теж, а при закритті відновлює рівно те, що змінила сама.

Різні дефолти closeOnBackdrop

UiModaltrue, UiDrawerfalse. Це не непослідовність: модалка зазвичай інформаційна, і випадкове закриття нічого не коштує. Drawer майже завжди містить форму, де це втрата введеного.

Обидва вимагають, щоб натискання і почалося, і закінчилося на фоні. Без цього виділення тексту зсередини панелі з відпусканням мишки над фоном закриває вікно разом із незбереженою формою.