/** * Workaround для бага позиционирования Vuetify connected-location-strategy. * * Когда активатор `v-select`/`v-autocomplete` находится внутри * `position: fixed`-контейнера (кастомный дровер, диалог), Vuetify включает * ветку `activatorFixed` (`isFixedPosition()` → true). Её `getIntrinsicSize()` * вычитает `el.style.left` из измеренной геометрии оверлея; на переходном * кадре, когда контент ещё отрисован в нулевой позиции, а инлайновый * `style.left` уже не нулевой, `contentBox.x` становится отрицательным и * стратегия аккумулирует смещение — меню уезжает на кратное X активатора * (за край экрана). * * Обычно гонку сглаживают пересчёты, размазанные по анимации открытия. Под * `prefers-reduced-motion: reduce` (умолчание Windows Server) анимации нет — * один пересчёт на «плохом» кадре остаётся финальным. * * Фикс: дождаться, пока контент оверлея отрисован и геометрически стабилен, * затем один раз послать `resize` — Vuetify пересчитает позицию по уже * устоявшейся геометрии и поставит меню корректно. Безопасно при motion ON * (пересчёт по стабильной геометрии идемпотентен) и для не-fixed контейнеров. * * Привязывать к `@update:menu` нужного `v-autocomplete`/`v-select`. */ // Ядро: дождаться, пока геометрия последнего открытого меню устаканится, и один // раз послать resize — Vuetify пересчитает позицию по уже стабильной геометрии. function scheduleStabilize(): void { if (typeof window === 'undefined') return; let prevLeft = Number.NaN; let stableFrames = 0; let totalFrames = 0; const tick = (): void => { // Последний открытый overlay-menu (на случай вложенных оверлеев). const menus = document.querySelectorAll('.v-overlay.v-menu .v-overlay__content'); const el = menus[menus.length - 1]; if (el && el.getBoundingClientRect().width > 0) { const left = Math.round(el.getBoundingClientRect().left); stableFrames = left === prevLeft ? stableFrames + 1 : 0; prevLeft = left; // 3 кадра без движения = геометрия устоялась → один чистый пересчёт. if (stableFrames >= 3) { window.dispatchEvent(new Event('resize')); return; } } // Предохранитель ~1.5 c: не зацикливаться, если оверлей не появился. if (++totalFrames < 90) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } let installed = false; /** * Глобально включает обход бага позиционирования меню Vuetify: один * `MutationObserver` ловит появление любого `.v-overlay.v-menu` в DOM и * запускает стабилизацию позиции. Вешать один раз при запуске приложения — * покрывает все `v-select`/`v-autocomplete`/`v-menu`, текущие и будущие, без * ручной разметки в шаблонах. * * Идемпотентна (повторный вызов — noop). SSR-safe. Возвращает teardown * (отключить наблюдатель — нужно тестам и на случай явной остановки). */ export function installMenuRepositionFix(): () => void { const noop = (): void => {}; if (installed) return noop; if ( typeof window === 'undefined' || typeof document === 'undefined' || typeof MutationObserver === 'undefined' || !document.body ) { return noop; } installed = true; const observer = new MutationObserver((mutations) => { for (const m of mutations) { for (const node of m.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.matches('.v-overlay.v-menu') || node.querySelector('.v-overlay.v-menu')) { scheduleStabilize(); return; // одного запуска на пачку мутаций достаточно } } } }); observer.observe(document.body, { childList: true, subtree: true }); return () => { observer.disconnect(); installed = false; }; }