Viewing File: /home/markqprx/iniasli.pro/client/ui/accordion/accordion.tsx

import React, {
  cloneElement,
  isValidElement,
  Key,
  ReactElement,
  ReactNode,
  useId,
  useRef,
} from 'react';
import clsx from 'clsx';
import {AnimatePresence, m} from 'framer-motion';
import {useControlledState} from '@react-stately/utils';
import {FocusScope, useFocusManager} from '@react-aria/focus';
import {AccordionAnimation} from '@common/ui/accordion/accordtion-animation';
import {ArrowDropDownIcon} from '@common/icons/material/ArrowDropDown';

type Props = {
  variant?: 'outline' | 'default' | 'minimal';
  children?: ReactNode;
  mode?: 'single' | 'multiple';
  expandedValues?: Key[];
  defaultExpandedValues?: Key[];
  onExpandedChange?: (key: Key[]) => void;
  className?: string;
  isLazy?: boolean;
};
export const Accordion = React.forwardRef<HTMLDivElement, Props>(
  (
    {
      variant = 'default',
      mode = 'single',
      children,
      className,
      isLazy,
      ...other
    },
    ref,
  ) => {
    const [expandedValues, setExpandedValues] = useControlledState(
      other.expandedValues,
      other.defaultExpandedValues || [],
      other.onExpandedChange,
    );

    const itemsCount = React.Children.count(children);

    return (
      <div
        className={clsx(variant === 'outline' && 'space-y-10', className)}
        ref={ref}
        role="presentation"
      >
        <AnimatePresence>
          <FocusScope>
            {React.Children.map(children, (child, index) => {
              if (!isValidElement<ClonedItemProps>(child)) return null;
              return cloneElement<ClonedItemProps>(child, {
                key: child.key || index,
                value: child.props.value || index,
                isFirst: index === 0,
                isLast: index === itemsCount - 1,
                mode,
                variant,
                expandedValues,
                setExpandedValues,
                isLazy,
              });
            })}
          </FocusScope>
        </AnimatePresence>
      </div>
    );
  },
);

interface AccordionItemProps {
  children: ReactNode;
  disabled?: boolean;
  label: ReactNode;
  description?: ReactNode;
  value?: Key;
  isFirst?: boolean;
  isLast?: boolean;
  bodyClassName?: string;
  labelClassName?: string;
  buttonPadding?: string;
  chevronPosition?: 'left' | 'right';
  startIcon?: ReactElement;
  endAppend?: ReactElement;
}
interface ClonedItemProps extends AccordionItemProps {
  variant?: 'outline' | 'default' | 'minimal';
  expandedValues: Key[];
  setExpandedValues: (keys: Key[]) => void;
  mode: 'single' | 'multiple';
  value: Key;
  isLazy?: boolean;
}
export function AccordionItem({
  children,
  label,
  disabled,
  bodyClassName,
  labelClassName,
  buttonPadding = 'py-10 pl-14 pr-10',
  startIcon,
  description,
  endAppend,
  chevronPosition = 'right',
  isFirst,
  isLast,
  ...other
}: AccordionItemProps) {
  const {expandedValues, setExpandedValues, variant, value, mode, isLazy} =
    other as ClonedItemProps;
  const ref = useRef<HTMLButtonElement>(null);
  const isExpanded = !disabled && expandedValues.includes(value);
  const wasExpandedOnce = useRef(false);
  if (isExpanded) {
    wasExpandedOnce.current = true;
  }
  const focusManager = useFocusManager();
  const id = useId();
  const buttonId = `${id}-button`;
  const panelId = `${id}-panel`;

  const onKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>) => {
    switch (e.key) {
      case 'ArrowDown':
        focusManager?.focusNext();
        break;
      case 'ArrowUp':
        focusManager?.focusPrevious();
        break;
      case 'Home':
        focusManager?.focusFirst();
        break;
      case 'End':
        focusManager?.focusLast();
        break;
    }
  };

  const toggle = () => {
    const i = expandedValues.indexOf(value);
    if (i > -1) {
      const newKeys = [...expandedValues];
      newKeys.splice(i, 1);
      setExpandedValues(newKeys);
    } else if (mode === 'single') {
      setExpandedValues([value]);
    } else {
      setExpandedValues([...expandedValues, value]);
    }
  };

  const chevron = (
    <div className={clsx(variant === 'minimal' && '')}>
      <ArrowDropDownIcon
        aria-hidden="true"
        size="md"
        className={clsx(
          disabled ? 'text-disabled' : 'text-muted',
          isExpanded && 'rotate-180 transition-transform',
        )}
      />
    </div>
  );

  return (
    <div
      className={clsx(
        variant === 'default' && 'border-b',
        variant === 'outline' && 'rounded-panel border',
        disabled && 'text-disabled',
      )}
    >
      <h3
        className={clsx(
          'flex w-full items-center justify-between text-sm',
          disabled && 'pointer-events-none',
          isFirst && variant === 'default' && 'border-t',
          isExpanded && variant !== 'minimal'
            ? 'border-b'
            : 'border-b border-b-transparent',
          variant === 'outline'
            ? isExpanded
              ? 'rounded-panel-t'
              : 'rounded-panel'
            : undefined,
        )}
      >
        <button
          disabled={disabled}
          aria-expanded={isExpanded}
          id={buttonId}
          aria-controls={panelId}
          type="button"
          ref={ref}
          onKeyDown={onKeyDown}
          onClick={() => {
            if (!disabled) {
              toggle();
            }
          }}
          className={clsx(
            'flex flex-auto items-center gap-10 text-left outline-none hover:bg-hover focus-visible:bg-primary/focus',
            buttonPadding,
          )}
        >
          {chevronPosition === 'left' && chevron}
          {startIcon &&
            cloneElement(startIcon, {
              size: 'md',
              className: clsx(
                startIcon.props.className,
                disabled ? 'text-disabled' : 'text-muted',
              ),
            })}
          <div className="flex-auto overflow-hidden overflow-ellipsis">
            <div className={labelClassName} data-testid="accordion-label">
              {label}
            </div>
            {description && (
              <div className="text-xs text-muted">{description}</div>
            )}
          </div>
          {chevronPosition === 'right' && chevron}
        </button>
        {endAppend && (
          <div className="flex-shrink-0 px-4 text-sm text-muted">
            {endAppend}
          </div>
        )}
      </h3>
      <m.div
        aria-labelledby={id}
        role="region"
        variants={AccordionAnimation.variants}
        transition={AccordionAnimation.transition}
        initial={false}
        animate={isExpanded ? 'open' : 'closed'}
      >
        <div className={clsx('p-16', bodyClassName)}>
          {!isLazy || wasExpandedOnce ? children : null}
        </div>
      </m.div>
    </div>
  );
}
Back to Directory File Manager