Files
portal/app/resources/js/utils/menuRepositionFix.ts
T
Дмитрий 54451d2ea6 feat(projects): RegionsBulkDialog — subject-level regions (89 RF subjects) #1426
Bulk regions dialog reworked from federal-district bitmask to subject/region
selection, consistent with ProjectDetailsDrawer/NewProjectDialog. Full-stack:
add_regions/remove_regions on projects.regions INT[], BulkProjectActionRequest
split validation, ProjectService model-instance update. federal-districts.ts
removed (zero consumers). +menuRepositionFix util for v-autocomplete menu.
phpstan-baseline: bump actingAs ignore count 14->15 (new validation test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 03:41:46 +03:00

53 lines
3.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`.
*/
export function repositionMenuAfterOpen(open: boolean): void {
if (!open || 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);
}