Существует ли надежная и эффективная стратегия позиционирования для выравнивания выпадающего компонента? - PullRequest
2 голосов
/ 20 марта 2020

Я разрабатываю раскрывающийся компонент в React / Preact. Взаимодействие раскрывающегося списка показано ниже:

enter image description here

По очевидным причинам компонент может находиться в любом месте на странице, и любой элемент DOM-предка может создать новый контекст стека или использование overflow: hidden, я не могу использовать relative родительское и absolute дочернее комбинированное позиционирование.

Я использую Portal и визуализирую поверхность наложения непосредственно как дочерний элемент тега body когда он открыт. Вопрос здесь - как расположить оверлейное меню над элементом привязки, чтобы их центры сталкивались друг с другом?

Несколько вещей, которые следует учитывать:

  1. Это стандартный c компонент. Оверлейное меню может иметь абсолютно любой контент и, следовательно, любой размер. Размер не известен заранее. Назовите его InlineModal или InlineBox, если необходимо.
  2. Кроме того, элемент привязки , вокруг которого центрировано меню, может изменить свою позицию по многим причинам. Например, ниже содержимого, содержащего данные AJAX, которое при загрузке вызывает смещение компонента вниз. Или даже загрузка изображения по этому вопросу.
  3. Пока меню открыто, его собственный размер может изменяться из-за некоторых флажков в меню, которые добавляют или удаляют другие параметры из меню.
  4. По краям элемент должен быть отрегулирован так, чтобы он не обрезался окном браузера. Он должен пытаться вписаться в окно, даже если он пропускает выравнивание по центру.

Я могу представить несколько подходов, таких как:

  1. Использовать Rx. js interval с animationFrameScheduler (внутренне RAF - RequestAnimationFrame) и непрерывно вычислять точку фиксированного положения, например top и left.
  2. Используйте простой обработчик onResize и onScroll для изменения положения элемент. Но это не учитывает вышеупомянутый фактор, так как у меня нет способа узнать, сместился ли оригинальный якорь из-за какого-либо другого действия на странице.

CSS позиционирование на самом деле не имеет значения как элемент добавляется к body; fixed и absolute будут нормально работать.

Что-то, что я пробую с Rx. js и Popmotion:

import { action } from 'popmotion';
import { Observable, interval, animationFrameScheduler } from 'rxjs';
import { map, distinctUntilChanged } from 'rxjs/operators';

export type Coordinates = readonly [number, number];

export function stickToCenterAction(reference: HTMLElement, popper: HTMLElement) {

  return action(({ update }) => {
    const sub = stickToCenter(reference, popper)
      .subscribe(([x, y]) => update({ x, y }));

    return {
      stop: () => {
        sub.unsubscribe();
      }
    };
  });

}

function stickToCenter(reference: HTMLElement, popper: HTMLElement): Observable<Coordinates> {
  return interval(0, animationFrameScheduler).pipe(
    map(() => calculateCenter(reference, popper)),
    distinctUntilChanged(([oX, oY], [nX, nY]) => oX === nX && oY === nY ));
}


function calculateCenter(reference: HTMLElement, popper: HTMLElement): Coordinates {
  const [rCenterX, rCenterY] = findCenterForElement(reference);
  const [x, y] = findXYForGivenCenter(popper, [rCenterX, rCenterY]);

  return [x, y];
}

function findCenterForElement(reference: HTMLElement): Coordinates {
  const { left, top, width, height } = reference.getBoundingClientRect();

  const rCenterX = left + (width / 2);
  const rCenterY = top + (height / 2);

  return [rCenterX, rCenterY]
}


function findXYForGivenCenter(elm: HTMLElement, co: Coordinates): Coordinates {

  const [rCenterX, rCenterY] = co;
  const { width, height } = elm.getBoundingClientRect();

  const x = rCenterX - (width / 2);
  const y = rCenterY - (height / 2);

  return [x, y];
}

Это то, как оно потребляется в компонент:

import { styler, tween, composite } from 'popmotion';

export type InlineBoxProps = {
  anchor: (ref: Ref<any>) => ComponentChildren;
  content: (ref: Ref<any>) => ComponentChildren;

  isOpen?: boolean;
};


const scaleEffect = composite({
  opacity: tween({ from: 0, to: 1, duration: 400 }),
  scale: tween({ from: 0.75, to: 1, duration: 240 })
});


export function InlineBox(props: InlineBoxProps) {

  // ... References for anchorElm, contentElm, etc.

  useEffect(() => {

    if (isOpen && anchorElm && contentElm) {

      const elmStyle = styler(contentElm);

      const positionEffect = stickToCenterAction(anchorElm, contentElm)
        .pipe(({ x, y }: any) => ({ y, x }));

      const finalEffect = composite({ boom: scaleEffect, translate: positionEffect })
        .filter((x) => !!x.translate)
        .pipe((({ boom, translate }: any) => ({ ...boom, ...translate })));

      const anim = finalEffect
        .start((v: any) => elmStyle.set(v));

      return () => anim.stop();

    }

  }, [isOpen, anchorElm, contentElm]);

  return (
    <Fragment>
      {anchor(anchorElmRef)}
      { isOpen
        ? <PortalIntoBody>{content(contentElmRef)}</PortalIntoBody>
        : null }
    </Fragment>
  );
}

Я рассмотрел другие реализации, такие как antd , материал , et c. но они, кажется, не рассматривали вышеупомянутые возможности, и их меню на уровне дизайна весьма ограничено, чтобы иметь какой-либо общий c контент.

...