Оверлеї та фокус
Спільний стек шарів, лічильник блокування прокрутки і пастка фокуса, що розуміє телепорти.
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
UiModal — true, UiDrawer — false. Це не непослідовність: модалка
зазвичай інформаційна, і випадкове закриття нічого не коштує. Drawer майже
завжди містить форму, де це втрата введеного.
Обидва вимагають, щоб натискання і почалося, і закінчилося на фоні. Без цього виділення тексту зсередини панелі з відпусканням мишки над фоном закриває вікно разом із незбереженою формою.