fix(web): панели не уезжают за экран (правка после замечания линтера)
Позиционирование переведено на прямые стили и класс `gl-anchored-panel`: React не управляет left/top/max-height, поэтому повторный рендер не сбрасывает раскладку, а `data-placement` задаёт направление анимации (панель может развернуться вниз, если сверху мало места). Линтер по хукам чистый, 289 тестов зелёные.
This commit is contained in:
@@ -1,15 +1,8 @@
|
||||
import { useCallback, useLayoutEffect, useState, type RefObject } from 'react';
|
||||
import { useLayoutEffect, type RefObject } from 'react';
|
||||
|
||||
/** Отступ панели от краёв окна, чтобы тень и рамка не обрезались. */
|
||||
export const ANCHOR_MARGIN = 8;
|
||||
|
||||
export interface AnchoredPosition {
|
||||
left: number;
|
||||
top: number;
|
||||
maxHeight: number;
|
||||
placement: 'top' | 'bottom';
|
||||
}
|
||||
|
||||
export interface AnchoredPositionOptions {
|
||||
open: boolean;
|
||||
anchorRef: RefObject<HTMLElement | null>;
|
||||
@@ -24,6 +17,9 @@ export interface AnchoredPositionOptions {
|
||||
* области: панель не уходит за края экрана и не обрезает подписи, а её высота
|
||||
* ограничивается свободным местом (содержимое прокручивается).
|
||||
*
|
||||
* Стили ставятся напрямую в DOM, без состояния: React этими свойствами не
|
||||
* управляет, поэтому повторный рендер панели не сбрасывает позицию.
|
||||
*
|
||||
* Используется `Popover` и `Menu`: панели крепятся к кнопкам у нижнего края
|
||||
* сайдбара, где обычное absolute-позиционирование уезжало за пределы окна.
|
||||
*/
|
||||
@@ -33,50 +29,52 @@ export function useAnchoredPosition({
|
||||
panelRef,
|
||||
placement,
|
||||
align,
|
||||
}: AnchoredPositionOptions): AnchoredPosition | null {
|
||||
const [position, setPosition] = useState<AnchoredPosition | null>(null);
|
||||
|
||||
const update = useCallback(() => {
|
||||
const anchor = anchorRef.current;
|
||||
const panel = panelRef.current;
|
||||
if (anchor === null || panel === null) {
|
||||
return;
|
||||
}
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Свободное место сверху и снизу: панель раскрывается туда, где его больше.
|
||||
const spaceAbove = anchorRect.top - ANCHOR_MARGIN;
|
||||
const spaceBelow = viewportHeight - anchorRect.bottom - ANCHOR_MARGIN;
|
||||
const preferTop = placement === 'top';
|
||||
const placeTop = preferTop
|
||||
? spaceAbove >= Math.min(panelRect.height, spaceBelow) || spaceAbove >= spaceBelow
|
||||
: spaceBelow < Math.min(panelRect.height, spaceAbove);
|
||||
const available = placeTop ? spaceAbove : spaceBelow;
|
||||
const maxHeight = Math.max(120, Math.floor(available - ANCHOR_MARGIN));
|
||||
|
||||
// По горизонтали прижимаем к нужному краю якоря и к границам окна.
|
||||
const width = panelRect.width;
|
||||
const rawLeft = align === 'right' ? anchorRect.right - width : anchorRect.left;
|
||||
const maxLeft = Math.max(ANCHOR_MARGIN, viewportWidth - width - ANCHOR_MARGIN);
|
||||
const left = Math.min(Math.max(rawLeft, ANCHOR_MARGIN), maxLeft);
|
||||
|
||||
const top = placeTop ? anchorRect.top - ANCHOR_MARGIN - panelRect.height : anchorRect.bottom + ANCHOR_MARGIN;
|
||||
setPosition({
|
||||
left: Math.round(left),
|
||||
top: Math.round(Math.max(ANCHOR_MARGIN, top)),
|
||||
maxHeight,
|
||||
placement: placeTop ? 'top' : 'bottom',
|
||||
});
|
||||
}, [align, anchorRef, panelRef, placement]);
|
||||
|
||||
}: AnchoredPositionOptions): void {
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
setPosition(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const update = (): void => {
|
||||
const anchor = anchorRef.current;
|
||||
const panel = panelRef.current;
|
||||
if (anchor === null || panel === null) {
|
||||
return;
|
||||
}
|
||||
const anchorRect = anchor.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Свободное место сверху и снизу: раскрываемся туда, где его больше.
|
||||
const spaceAbove = anchorRect.top - ANCHOR_MARGIN;
|
||||
const spaceBelow = viewportHeight - anchorRect.bottom - ANCHOR_MARGIN;
|
||||
const needed = Math.min(panelRect.height, 320);
|
||||
const placeTop =
|
||||
placement === 'top'
|
||||
? spaceAbove >= needed || spaceAbove >= spaceBelow
|
||||
: spaceBelow < needed;
|
||||
const available = placeTop ? spaceAbove : spaceBelow;
|
||||
// Не меньше 120px: даже в тесном окне панель остаётся прокручиваемой.
|
||||
const maxHeight = Math.max(120, Math.floor(available - ANCHOR_MARGIN));
|
||||
|
||||
// По горизонтали прижимаем к нужному краю якоря и к границам окна.
|
||||
const width = panelRect.width;
|
||||
const rawLeft = align === 'right' ? anchorRect.right - width : anchorRect.left;
|
||||
const maxLeft = Math.max(ANCHOR_MARGIN, viewportWidth - width - ANCHOR_MARGIN);
|
||||
const left = Math.min(Math.max(rawLeft, ANCHOR_MARGIN), maxLeft);
|
||||
|
||||
const top = placeTop
|
||||
? anchorRect.top - ANCHOR_MARGIN - Math.min(panelRect.height, maxHeight)
|
||||
: anchorRect.bottom + ANCHOR_MARGIN;
|
||||
|
||||
panel.style.left = `${Math.round(left)}px`;
|
||||
panel.style.top = `${Math.round(Math.max(ANCHOR_MARGIN, top))}px`;
|
||||
panel.style.maxHeight = `${maxHeight}px`;
|
||||
panel.dataset.placement = placeTop ? 'top' : 'bottom';
|
||||
panel.dataset.positioned = 'true';
|
||||
};
|
||||
|
||||
update();
|
||||
window.addEventListener('resize', update);
|
||||
window.addEventListener('scroll', update, true);
|
||||
@@ -84,7 +82,5 @@ export function useAnchoredPosition({
|
||||
window.removeEventListener('resize', update);
|
||||
window.removeEventListener('scroll', update, true);
|
||||
};
|
||||
}, [open, update]);
|
||||
|
||||
return position;
|
||||
}, [open, anchorRef, panelRef, placement, align]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user