Files
portal/app/resources/js/utils/menuRepositionFix.ts
T
Дмитрий c7e015a9ac
Accessibility (Pa11y live) / a11y (push) Waiting to run
SAST — Semgrep / Semgrep SAST scan (push) Waiting to run
refactor(fe): убрать мёртвый repositionMenuAfterOpen - ядро внутреннее
Старый per-instance экспорт больше не используется (заменён глобальным
installMenuRepositionFix). Старый тест-файл удалён - механизм покрыт
installMenuRepositionFix.spec.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 10:30:06 +03:00

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;
};
}