c7e015a9ac
Старый per-instance экспорт больше не используется (заменён глобальным installMenuRepositionFix). Старый тест-файл удалён - механизм покрыт installMenuRepositionFix.spec.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
5.1 KiB
TypeScript
99 lines
5.1 KiB
TypeScript
/**
|
|
* 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<HTMLElement>('.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;
|
|
};
|
|
}
|