const NO_ENTER_ANIM_CLASS = '--no-enter-anim'; const NO_LEAVE_ANIM_CLASS = '--no-leave-anim'; // Элементы, а не счётчик: класс вешается прямо на DOM. Уходящий компонент управлять // своим `:class` уже не может — Vue гасит его реактивный эффект в unmount, до конца // leave-транзишена, поэтому ре-рендер по реактивному флагу не случится. const overlays = new Set(); function syncLeaveAnimation() { const chained = overlays.size > 1; overlays.forEach((el) => el.classList.toggle(NO_LEAVE_ANIM_CLASS, chained)); } /** * Стек открытых оверлеев (попапы и мобильные шиты дропдауна). Нужен, чтобы анимация * появления играла только у первого оверлея, а анимация закрытия — только у последнего: * при переходе попап→попап уходящий и приходящий диалоги ехали бы навстречу друг другу * и картинка скакала бы. */ export const useOverlayStack = () => { /** * Решение по анимации появления принимается один раз и дальше не меняется: состав * стека успеет измениться до того, как браузер прочитает transition уходящего. */ function registerOverlay(el?: HTMLElement | null) { if (!import.meta.client || !el) return; overlays.forEach((item) => { if (!item.isConnected) overlays.delete(item); }); el.classList.toggle(NO_ENTER_ANIM_CLASS, overlays.size > 0); overlays.add(el); syncLeaveAnimation(); } /** * Выпуск отложен на кадр: следующий оверлей монтируется позже unmount-хука уходящего * (а ленивый — ещё и через микротаск). Убрать элемент из стека сразу — уходящий не * успеет узнать, что ему на смену открывают другой попап, и проиграет закрытие. */ function releaseOverlay(el?: HTMLElement | null) { if (!import.meta.client || !el) return; const release = () => { overlays.delete(el); syncLeaveAnimation(); }; if (typeof requestAnimationFrame === 'function') { requestAnimationFrame(release); } else { release(); } } return { registerOverlay, releaseOverlay }; };