Accordion

A collapsible content structure for organizing information efficiently.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Iusto accusantium impedit corporis magnam esse pariatur libero sint mollitia veniam, sed quos hic voluptatibus molestias porro ab. Possimus ratione commodi dolorem.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Iusto accusantium impedit corporis magnam esse pariatur libero sint mollitia veniam, sed quos hic voluptatibus molestias porro ab. Possimus ratione commodi dolorem.
Lorem ipsum dolor sit amet consectetur adipisicing elit. Iusto accusantium impedit corporis magnam esse pariatur libero sint mollitia veniam, sed quos hic voluptatibus molestias porro ab. Possimus ratione commodi dolorem.

import React, { useEffect, useMemo, useState } from "react";
import { cva } from "class-variance-authority";
import { FiChevronDown } from "react-icons/fi";

type AccordionProps = {
  children: React.ReactNode;
};

type AccordionTriggerProps = {
  children: React.ReactNode;
  showContent?: boolean;
  setShowContent?: React.Dispatch<React.SetStateAction<boolean>>;
};

type AccordionContentProps = {
  children: React.ReactNode;
  showContent?: boolean;
  contentHeight?: string;
  contentRef?: React.RefObject<HTMLDivElement>;
};

type AccordionItemProps = {
  children: React.ReactNode;
  value: string;
};

const accordionVariants = cva("border-black bg-orange-500/80 p-5 font-bold", {
  variants: {
    variant: {
      primary: "bg-orange-500/80",
      secondary: "bg-pink-500/80",
      light: "bg-slate-50/80",
      dark: "bg-zinc-900/80",
      blue: "bg-blue-500/80",
      yellow: "bg-[#f7cb46]/80",
      green: "bg-green-500/80",
      red: "bg-red-500/80",
    },
  },
  defaultVariants: {
    variant: "primary",
  },
});

const Accordion: React.FC<AccordionProps> & {
  Item: React.FC<
    AccordionItemProps & AccordionContentProps & AccordionTriggerProps
  >;
  Trigger: React.FC<AccordionTriggerProps>;
  Content: React.FC<AccordionContentProps>;
} = ({ children }) => {
  const [contentStates, setContentStates] = useState<{
    [key: string]: boolean;
  }>({});
  const [contentHeights, setContentHeights] = useState<{
    [key: string]: string;
  }>({});
  const contentRefs: { [key: string]: React.RefObject<HTMLDivElement> } =
    useMemo(() => ({}), []);

  useEffect(() => {
    const updatedContentStates: { [key: string]: boolean } = {};
    const updatedContentHeights: { [key: string]: string } = {};

    Object.keys(contentRefs).forEach((key) => {
      if (contentRefs[key]?.current) {
        updatedContentHeights[key] =
          (contentRefs[key]?.current?.scrollHeight || 0) + "px";
        updatedContentStates[key] = contentStates[key] || false;
      }
    });

    setContentHeights(updatedContentHeights);
    setContentStates(updatedContentStates);
  }, [contentRefs]);

  const handleToggleContent = (key: string) => {
    setContentStates((prevContentStates) => {
      const newContentStates: { [key: string]: boolean } = {};
      Object.keys(prevContentStates).forEach((contentKey) => {
        if (contentKey === key) {
          newContentStates[contentKey] = !prevContentStates[contentKey];
        } else {
          newContentStates[contentKey] = false;
        }
      });

      return newContentStates;
    });
  };

  return (
    <div className="w-full max-w-[500px] border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
      {React.Children.map(children, (child) => {
        if (React.isValidElement(child)) {
          if (child.type === Accordion.Item) {
            const { children, value } = child.props as AccordionItemProps;
            contentRefs[value] = React.createRef<HTMLDivElement>();

            return React.cloneElement(
              child as React.ReactElement<
                AccordionItemProps &
                  AccordionContentProps &
                  AccordionTriggerProps
              >,
              {
                key: value,
                contentRef: contentRefs[value],
                showContent: contentStates[value] || false,
                contentHeight: contentHeights[value] || "0px",
                setShowContent: () => handleToggleContent(value),
              },
            );
          }
        }
        return null;
      })}
    </div>
  );
};

Accordion.Item = function AccordionItem({
  children,
  value,
  showContent,
  contentHeight,
  contentRef,
  setShowContent,
}) {
  return (
    <div>
      {React.Children.map(children, (child) => {
        if (React.isValidElement(child)) {
          if (child.type === Accordion.Trigger) {
            return React.cloneElement(
              child as React.ReactElement<AccordionTriggerProps>,
              {
                showContent,
                setShowContent,
              },
            );
          } else if (child.type === Accordion.Content) {
            return React.cloneElement(
              child as React.ReactElement<AccordionContentProps>,
              {
                showContent,
                contentHeight,
                contentRef,
              },
            );
          }
        }
        return null;
      })}
    </div>
  );
};

Accordion.Trigger = function AccordionTrigger({
  children,
  showContent,
  setShowContent,
}) {
  return (
    <button
      role="button"
      aria-expanded={showContent}
      className={`flex w-full items-center justify-between border-b border-black ${accordionVariants()}`}
      onClick={() => {
        if (setShowContent) {
          setShowContent(!showContent);
        }
      }}
    >
      {children}
      <FiChevronDown
        style={{ transform: `rotate(${showContent ? "180deg" : "0"})` }}
        className="ml-4 min-h-[24px] min-w-[24px] transition-transform ease-in-out"
      />
    </button>
  );
};

Accordion.Content = function AccordionContent({
  children,
  showContent,
  contentHeight,
  contentRef,
}) {
  return (
    <div
      ref={contentRef}
      style={{
        height: showContent ? contentHeight : "0px",
        transition: "height 0.3s ease-in-out",
      }}
      className="max-h-[500px] overflow-x-hidden overflow-y-auto rounded-[5px] bg-white font-bold"
    >
      <div className="p-5">{children}</div>
    </div>
  );
};

Accordion.displayName = "Accordion";

export default Accordion;
©2024with
❤️
❤️
❤️
byIval Shamkya