154 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			154 lines
		
	
	
		
			3.9 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import type { PropsWithChildren } from 'react';
 | 
						|
import { useCallback, useState, useRef } from 'react';
 | 
						|
 | 
						|
import classNames from 'classnames';
 | 
						|
 | 
						|
import type { Placement, State as PopperState } from '@popperjs/core';
 | 
						|
import Overlay from 'react-overlays/Overlay';
 | 
						|
 | 
						|
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
 | 
						|
import type { SelectItem } from 'mastodon/components/dropdown_selector';
 | 
						|
import { DropdownSelector } from 'mastodon/components/dropdown_selector';
 | 
						|
import { Icon } from 'mastodon/components/icon';
 | 
						|
 | 
						|
interface DropdownProps {
 | 
						|
  value: string;
 | 
						|
  options: SelectItem[];
 | 
						|
  disabled?: boolean;
 | 
						|
  onChange: (value: string) => void;
 | 
						|
  placement?: Placement;
 | 
						|
}
 | 
						|
 | 
						|
const Dropdown: React.FC<DropdownProps> = ({
 | 
						|
  value,
 | 
						|
  options,
 | 
						|
  disabled,
 | 
						|
  onChange,
 | 
						|
  placement: initialPlacement = 'bottom-end',
 | 
						|
}) => {
 | 
						|
  const activeElementRef = useRef<Element | null>(null);
 | 
						|
  const containerRef = useRef(null);
 | 
						|
  const [isOpen, setOpen] = useState<boolean>(false);
 | 
						|
  const [placement, setPlacement] = useState<Placement>(initialPlacement);
 | 
						|
 | 
						|
  const handleToggle = useCallback(() => {
 | 
						|
    if (
 | 
						|
      isOpen &&
 | 
						|
      activeElementRef.current &&
 | 
						|
      activeElementRef.current instanceof HTMLElement
 | 
						|
    ) {
 | 
						|
      activeElementRef.current.focus({ preventScroll: true });
 | 
						|
    }
 | 
						|
 | 
						|
    setOpen(!isOpen);
 | 
						|
  }, [isOpen, setOpen]);
 | 
						|
 | 
						|
  const handleMouseDown = useCallback(() => {
 | 
						|
    if (!isOpen) activeElementRef.current = document.activeElement;
 | 
						|
  }, [isOpen]);
 | 
						|
 | 
						|
  const handleKeyDown = useCallback(
 | 
						|
    (e: React.KeyboardEvent) => {
 | 
						|
      switch (e.key) {
 | 
						|
        case ' ':
 | 
						|
        case 'Enter':
 | 
						|
          if (!isOpen) activeElementRef.current = document.activeElement;
 | 
						|
          break;
 | 
						|
      }
 | 
						|
    },
 | 
						|
    [isOpen],
 | 
						|
  );
 | 
						|
 | 
						|
  const handleClose = useCallback(() => {
 | 
						|
    if (
 | 
						|
      isOpen &&
 | 
						|
      activeElementRef.current &&
 | 
						|
      activeElementRef.current instanceof HTMLElement
 | 
						|
    )
 | 
						|
      activeElementRef.current.focus({ preventScroll: true });
 | 
						|
    setOpen(false);
 | 
						|
  }, [isOpen]);
 | 
						|
 | 
						|
  const handleOverlayEnter = useCallback(
 | 
						|
    (state: Partial<PopperState>) => {
 | 
						|
      if (state.placement) setPlacement(state.placement);
 | 
						|
    },
 | 
						|
    [setPlacement],
 | 
						|
  );
 | 
						|
 | 
						|
  const valueOption = options.find((item) => item.value === value);
 | 
						|
 | 
						|
  return (
 | 
						|
    <div ref={containerRef}>
 | 
						|
      <button
 | 
						|
        type='button'
 | 
						|
        onClick={handleToggle}
 | 
						|
        onMouseDown={handleMouseDown}
 | 
						|
        onKeyDown={handleKeyDown}
 | 
						|
        disabled={disabled}
 | 
						|
        className={classNames('dropdown-button', { active: isOpen })}
 | 
						|
      >
 | 
						|
        <span className='dropdown-button__label'>{valueOption?.text}</span>
 | 
						|
        <Icon id='down' icon={ArrowDropDownIcon} />
 | 
						|
      </button>
 | 
						|
 | 
						|
      <Overlay
 | 
						|
        show={isOpen}
 | 
						|
        offset={[5, 5]}
 | 
						|
        placement={placement}
 | 
						|
        flip
 | 
						|
        target={containerRef}
 | 
						|
        popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}
 | 
						|
      >
 | 
						|
        {({ props, placement }) => (
 | 
						|
          <div {...props}>
 | 
						|
            <div
 | 
						|
              className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}
 | 
						|
            >
 | 
						|
              <DropdownSelector
 | 
						|
                items={options}
 | 
						|
                value={value}
 | 
						|
                onClose={handleClose}
 | 
						|
                onChange={onChange}
 | 
						|
                classNamePrefix='privacy-dropdown'
 | 
						|
              />
 | 
						|
            </div>
 | 
						|
          </div>
 | 
						|
        )}
 | 
						|
      </Overlay>
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
interface Props {
 | 
						|
  value: string;
 | 
						|
  options: SelectItem[];
 | 
						|
  disabled?: boolean;
 | 
						|
  onChange: (value: string) => void;
 | 
						|
}
 | 
						|
 | 
						|
export const SelectWithLabel: React.FC<PropsWithChildren<Props>> = ({
 | 
						|
  value,
 | 
						|
  options,
 | 
						|
  disabled,
 | 
						|
  children,
 | 
						|
  onChange,
 | 
						|
}) => {
 | 
						|
  return (
 | 
						|
    <label className='app-form__toggle'>
 | 
						|
      <div className='app-form__toggle__label'>{children}</div>
 | 
						|
 | 
						|
      <div className='app-form__toggle__toggle'>
 | 
						|
        <div>
 | 
						|
          <Dropdown
 | 
						|
            value={value}
 | 
						|
            onChange={onChange}
 | 
						|
            disabled={disabled}
 | 
						|
            options={options}
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    </label>
 | 
						|
  );
 | 
						|
};
 |