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


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 type { Coords, DropdownProps } from './types';

const MENU_GAP = 4;

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((current) => !current);
  };

  const handleClose = () => {
    setIsOpenMenu(false);
    setCoords(null);
  };

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

    const calcMenuPosition = () => {
      const triggerElement = triggerRef.current;
      const menuElement = menuRef.current;

      if (!triggerElement || !menuElement) {
        return;
      }

      const triggerRect = triggerElement.getBoundingClientRect();
      const anchorRect = anchorRef?.current?.getBoundingClientRect() ?? triggerRect;
      const menuRect = menuElement.getBoundingClientRect();

      let top = 0;
      let left = 0;

      if (position === 'bottom') {
        top = triggerRect.bottom + MENU_GAP;
        left = triggerRect.right - menuRect.width;
      }

      if (position === 'right') {
        top = anchorRect.top;
        left = triggerRect.right + MENU_GAP;
      }

      left = Math.max(MENU_GAP, Math.min(left, window.innerWidth - menuRect.width - MENU_GAP));
      top = Math.max(MENU_GAP, Math.min(top, window.innerHeight - menuRect.height - MENU_GAP));

      setCoords({ top, left });
    };

    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 handlePointerDown = (event: PointerEvent) => {
      const target = event.target as Node;

      const isInsideRoot = rootRef.current?.contains(target);
      const isInsideMenu = menuRef.current?.contains(target);

      if (!isInsideRoot && !isInsideMenu) {
        handleClose();
      }
    };

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

    document.addEventListener('pointerdown', handlePointerDown, true);
    document.addEventListener('keydown', handleKeyDown);

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

  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 {
  position: fixed;
  width: var(--dropdown-width, $menu-width);
  padding: var(--space-0250) 0;
  border-radius: var(--space-0250);
  background-color: var(--neutral-2);
  font-family: var(--font-family);
  pointer-events: auto;
}