Загрузка данных


import { Button } from 'exchange-elements/v2';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';

import { Portal } from '@src/components/Portal';

import styles from './index.module.scss';
import { Coords, DropdownProps } from './types';

export function Dropdown({
  children,
  selectedValue,
  position = 'bottom',
  className = '',
  buttonClassName = '',
  menuClassName = '',
  anchorRef,
  renderTrigger,
}: DropdownProps) {
  const [isOpenMenu, setIsOpenMenu] = useState(false);
  const [coords, setCoords] = useState<Coords | null>(null);

  const rootRef = useRef<HTMLDivElement>(null);
  const triggerRef = useRef<HTMLDivElement>(null);
  const menuRef = useRef<HTMLDivElement>(null);

  const handleToggle = () => setIsOpenMenu((prev) => !prev);
  const handleClose = () => setIsOpenMenu(false);

  useLayoutEffect(() => {
    if (!isOpenMenu) {
      setCoords(null);
      return;
    }

    const calcMenuPosition = () => {
      const triggerEl = triggerRef.current;
      const menuEl = menuRef.current;

      if (!triggerEl || !menuEl) return;

      const triggerRect = triggerEl.getBoundingClientRect();
      const anchorRect = anchorRef?.current?.getBoundingClientRect() ?? triggerRect;

      let topMenu = 0;
      let leftMenu = 0;

      if (position === 'bottom') {
        topMenu = triggerRect.bottom;
        leftMenu = triggerRect.left;
      }

      if (position === 'right') {
        topMenu = anchorRect.top;
        leftMenu = triggerRect.right;
      }

      setCoords({ top: topMenu, left: leftMenu });
    };

    calcMenuPosition();

    window.addEventListener('resize', calcMenuPosition);
    window.addEventListener('scroll', calcMenuPosition, true);

    return () => {
      window.removeEventListener('resize', calcMenuPosition);
      window.removeEventListener('scroll', calcMenuPosition, true);
    };
  }, [isOpenMenu, position, anchorRef]);

  useEffect(() => {
    const onPointerDown = (e: PointerEvent) => {
      const t = e.target as Node;

      const insideRoot = rootRef.current?.contains(t);
      const insideMenu = menuRef.current?.contains(t);

      if (!insideRoot && !insideMenu) handleClose();
    };

    const onKeyDown = (e: KeyboardEvent) => {
      if (e.key === 'Escape') handleClose();
    };

    document.addEventListener('pointerdown', onPointerDown, true);
    document.addEventListener('keydown', onKeyDown);

    return () => {
      document.removeEventListener('pointerdown', onPointerDown, true);
      document.removeEventListener('keydown', onKeyDown);
    };
  }, []);

  return (
    <div
      ref={rootRef}
      className={`${styles.dropdown} ${className}`}
    >
      <div ref={triggerRef}>
        {renderTrigger ? (
          renderTrigger({ isOpen: isOpenMenu, onToggle: handleToggle, onClose: handleClose })
        ) : (
          <Button
            size="sm"
            onClick={handleToggle}
            className={`${styles.button} ${isOpenMenu ? styles.pressed : ''} ${buttonClassName}`}
            label={selectedValue}
          />
        )}
      </div>

      {isOpenMenu && (
        <Portal>
          <div
            ref={menuRef}
            className={`${styles.menu} ${menuClassName}`}
            style={{
              top: coords?.top,
              left: coords?.left,
              visibility: coords ? 'visible' : 'hidden',
            }}
          >
            {children}
          </div>
        </Portal>
      )}
    </div>
  );
}



@use '../../theme/mixins' as m;

$menu-width: 200px;

.dropdown {
  position: relative;

  .button {
    @include m.buttonBase;
  }
}

.menu {
  font-family: var(--font-family);
  width: $menu-width;
  position: fixed;

  background-color: var(--neutral-2);
  border-radius: var(--space-0250);
  padding: var(--space-0250) 0;
  z-index: 999999; // todo: временное решение
}