ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • @radix-ui 훑어보기 - accordion
    오픈소스 2024. 3. 24. 19:08

    @radix-ui/accorian

     

    primitives/packages/react/accordion/src/Accordion.tsx at main · radix-ui/primitives

    Radix Primitives is an open-source UI component library for building high-quality, accessible design systems and web apps. Maintained by @workos. - radix-ui/primitives

    github.com

    // 사용하는 법
    import * as Accordion from '@radix-ui/react-accordion';
    import React from 'react';
    import classNames from 'classnames';
    import { ChevronDownIcon } from '@radix-ui/react-icons';
    
    
    export Default () => (
      <Accordion.Root>
        <Accordion.Item>
          <Accordion.Header>
            <Accordion.Trigger />
          </Accordion.Header>
          <Accordion.Content />
        </Accordion.Item>
      </Accordion.Root>
    );
    
    const AccordionDemo = () => (
      <Accordion.Root
        className="bg-mauve6 w-[300px] rounded-md shadow-[0_2px_10px] shadow-black/5"
        type="single"
        defaultValue="item-1"
        collapsible
      >
        <AccordionItem value="item-1">
          <AccordionTrigger>Is it accessible?</AccordionTrigger>
          <AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
        </AccordionItem>
    
        <AccordionItem value="item-2">
          <AccordionTrigger>Is it unstyled?</AccordionTrigger>
          <AccordionContent>
            Yes. It's unstyled by default, giving you freedom over the look and feel.
          </AccordionContent>
        </AccordionItem>
    
        <AccordionItem value="item-3">
          <AccordionTrigger>Can it be animated?</AccordionTrigger>
          <AccordionContent>
            Yes! You can animate the Accordion with CSS or JavaScript.
          </AccordionContent>
        </AccordionItem>
      </Accordion.Root>
    );
    
    const AccordionItem = React.forwardRef(({ children, className, ...props }, forwardedRef) => (
      <Accordion.Item
        className={classNames(
          'focus-within:shadow-mauve12 mt-px overflow-hidden first:mt-0 first:rounded-t last:rounded-b focus-within:relative focus-within:z-10 focus-within:shadow-[0_0_0_2px]',
          className
        )}
        {...props}
        ref={forwardedRef}
      >
        {children}
      </Accordion.Item>
    ));
    
    const AccordionTrigger = React.forwardRef(({ children, className, ...props }, forwardedRef) => (
      <Accordion.Header className="flex">
        <Accordion.Trigger
          className={classNames(
            'text-violet11 shadow-mauve6 hover:bg-mauve2 group flex h-[45px] flex-1 cursor-default items-center justify-between bg-white px-5 text-[15px] leading-none shadow-[0_1px_0] outline-none',
            className
          )}
          {...props}
          ref={forwardedRef}
        >
          {children}
          <ChevronDownIcon
            className="text-violet10 ease-[cubic-bezier(0.87,_0,_0.13,_1)] transition-transform duration-300 group-data-[state=open]:rotate-180"
            aria-hidden
          />
        </Accordion.Trigger>
      </Accordion.Header>
    ));
    
    const AccordionContent = React.forwardRef(({ children, className, ...props }, forwardedRef) => (
      <Accordion.Content
        className={classNames(
          'text-mauve11 bg-mauve2 data-[state=open]:animate-slideDown data-[state=closed]:animate-slideUp overflow-hidden text-[15px]',
          className
        )}
        {...props}
        ref={forwardedRef}
      >
        <div className="py-[15px] px-5">{children}</div>
      </Accordion.Content>
    ));
    
    export default AccordionDemo;
    // Accordion에서 export 될 요소들
    
    const Root = Accordion;
    const Item = AccordionItem;
    const Header = AccordionHeader;
    const Trigger = AccordionTrigger;
    const Content = AccordionContent;
    
    export {
      createAccordionScope,
      //
      Accordion,
      AccordionItem,
      AccordionHeader,
      AccordionTrigger,
      AccordionContent,
      //
      Root,
      Item,
      Header,
      Trigger,
      Content,
    };
    export type {
      AccordionSingleProps,
      AccordionMultipleProps,
      AccordionItemProps,
      AccordionHeaderProps,
      AccordionTriggerProps,
      AccordionContentProps,
    };
    import { createCollection } from '@radix-ui/react-collection';
    // 컴포넌트 내에서 자식 요소들의 컬렉션을 관리하기 위한 hook & utility제공
    
    type AccordionTriggerElement = React.ElementRef<typeof CollapsiblePrimitive.Trigger>;
    // 아코디언 트리거 타입은 CollapsiblePrimitve의 트리거 컴포넌트의 요소 참조.
    
    const ACCORDION_NAME = 'Accordion';
    const ACCORDION_KEYS = ['Home', 'End', 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'];
    
    const [Collection, useCollection, createCollectionScope] = createCollection<AccordionTriggerElement>(ACCORDION_NAME);
    
    // createCollection 함수의 반환값
    // 제네릭으로 받는 ACCORDIAN_NAME을 통해 개발자 도구에서 식별하기 쉽게 함
    // Collection : 컬렉션의 구현체 - 컴포넌트 트리에서 이 컬렉션을 사용하여 자식 요소들을 감싸기 가능
    // useCollection: 컬렉션 내의 항목에 대한 정보 가져오는 훅. eg) 특정 항목이 포커스 중인지 
    // createCollectionScrope : 컬렉션의 범위를 정의하는데 사용. 컬렉션을 여러 레벨에 걸쳐 분리할 때 유용

     

    import { createContextScope } from '@radix-ui/react-context';
    import { createCollapsibleScope } from '@radix-ui/react-collapsible';
    
    const [createAccordionContext, createAccordionScope] = createContextScope(ACCORDION_NAME, [
      createCollectionScope,
      createCollapsibleScope,
    ]);
    
    // createContextScope : 데이터를 전역 관리 - [createCollectionScope, createCollapsibleScope]는 Accordion 컴포넌트가 컬렉션과 접힘(콜랩서블) 기능을 포함하도록 설정
    //    createAccodionContext: 컨텍스트 생성
    //    createAccodionScope : 해당 컨텍스트의 스코프 정의
    
    const useCollapsibleScope = createCollapsibleScope();
    //	createCollapsibleScope(): 접히는 기능을 가진 컴포넌트를 위한 컨텍스트 스코프 생성 
    //  useCollapsibleScope: 컴포넌트 내에서 해당 스코프를 사용하여 관련 컨텍스트를 쉽게 접근하고 사용
    type AccordionImplElement = React.ElementRef<typeof Primitive.div>;
    
    type AccordionImplSingleElement = AccordionImplElement
    type AccordionImplMultipleElement = AccordionImplElement;
    
    type AccordionElement = AccordionImplMultipleElement | AccordionImplSingleElement;
    import { Primitive } from '@radix-ui/react-primitive';
    // Primitive는 Radix UI가 제공하는 기본적인 UI 구성 요소를 포함하는 객체
    import type * as Radix from '@radix-ui/react-primitive';
    
    type PrimitiveDivProps = Radix.ComponentPropsWithoutRef<typeof Primitive.div>;
    
    interface AccordionImplProps extends PrimitiveDivProps {
      /**
       * Whether or not an accordion is disabled from user interaction.
       *
       * @defaultValue false
       */
      disabled?: boolean;
      /**
       * The layout in which the Accordion operates.
       * @default vertical
       */
      orientation?: React.AriaAttributes['aria-orientation'];
      /**
       * The language read direction.
       */
      dir?: Direction;
    }
    
    
    interface AccordionImplSingleProps extends AccordionImplProps {
      /**
       * The controlled stateful value of the accordion item whose content is expanded.
       */
      value?: string;
      /**
       * The value of the item whose content is expanded when the accordion is initially rendered. Use
       * `defaultValue` if you do not need to control the state of an accordion.
       */
      defaultValue?: string;
      /**
       * The callback that fires when the state of the accordion changes.
       */
      onValueChange?(value: string): void;
      /**
       * Whether an accordion item can be collapsed after it has been opened.
       * @default false
       */
      collapsible?: boolean;
    }
    
    interface AccordionImplMultipleProps extends AccordionImplProps {
      /**
       * The controlled stateful value of the accordion items whose contents are expanded.
       */
      value?: string[];
      /**
       * The value of the items whose contents are expanded when the accordion is initially rendered. Use
       * `defaultValue` if you do not need to control the state of an accordion.
       */
      defaultValue?: string[];
      /**
       * The callback that fires when the state of the accordion changes.
       */
      onValueChange?(value: string[]): void;
    }
    
    
    interface AccordionSingleProps extends AccordionImplSingleProps {
      type: 'single';
    }
    
    interface AccordionMultipleProps extends AccordionImplMultipleProps {
      type: 'multiple';
    }
    // Accordion.Root
    
    import type { Scope } from '@radix-ui/react-context';
    // Scope : 같은 페이지 내에서 여러 개의 독립적인 아포넌트 컴포넌트가 있을 때 각 아코디언의 상태를 분리하기 위함
    type ScopedProps<P> = P & { __scopeAccordion?: Scope };
    
    // AccordionElement: ref로 전달 될 수 있는 요소의 타입
    // AccordionSingleProps | AccordionMultipleProps : 아코디언 컴포넌트가 받을 수 있는 속성의 타입
    
    const Accordion = React.forwardRef<AccordionElement, AccordionSingleProps | AccordionMultipleProps>(
      (props: ScopedProps<AccordionSingleProps | AccordionMultipleProps>, forwardedRef) => {
        const { type, ...accordionProps } = props;
        const singleProps = accordionProps as AccordionImplSingleProps;
        const multipleProps = accordionProps as AccordionImplMultipleProps;
        return (
       //Collection 컴포넌트는 위에서 언급한 createCollection 함수의 반환값
          <Collection.Provider scope={props.__scopeAccordion}>
            {type === 'multiple' ? (
              <AccordionImplMultiple {...multipleProps} ref={forwardedRef} />
            ) : (
              <AccordionImplSingle {...singleProps} ref={forwardedRef} />
            )}
          </Collection.Provider>
        );
      }
    );
    
    // const ACCORDION_NAME = 'Accordion';
    Accordion.displayName = ACCORDION_NAME;

     

    //propTypes는 컴포넌트에 전달되는 props의 유효성을 검사하기 위함(리액트의 내장 기능)-개발 모드에서 사용
    
    Accordion.propTypes = {
      type(props) {
        const value = props.value || props.defaultValue;
        // 싱글이나 멀티인지 검사
        if (props.type && !['single', 'multiple'].includes(props.type)) {
          return new Error(
            'Invalid prop `type` supplied to `Accordion`. Expected one of `single | multiple`.'
          );
        }
        //멀티인데 스트링 값 들어온건 아닌지
        if (props.type === 'multiple' && typeof value === 'string') {
          return new Error(
            'Invalid prop `type` supplied to `Accordion`. Expected `single` when `defaultValue` or `value` is type `string`.'
          );
        }
        //싱글인데 배열 들어온건 아닌지
        if (props.type === 'single' && Array.isArray(value)) {
          return new Error(
            'Invalid prop `type` supplied to `Accordion`. Expected `multiple` when `defaultValue` or `value` is type `string[]`.'
          );
        }
        return null;
      },
    };
    type AccordionValueContextValue = {
      value: string[];	// 현재 열린 아코디언 항목의 식별자 저장
      onItemOpen(value: string): void;
      onItemClose(value: string): void;
    };
    
    
    // createAccordionContext는 위에서 createContextScope로 반환 된 것
    const [AccordionValueProvider, useAccordionValueContext] = createAccordionContext<AccordionValueContextValue>(ACCORDION_NAME);
    // AccordionValueProvider: 컨텍스트의 값을 제공하는 컴포넌트- 하위 컴포넌트들은 useAccordionValueContext 훅을 통해 이 값을 접근할 수 있음.
    // useAccordionValueContext: 컨텍스트의 값을 소비하는 훅- 컴포넌트 내에서 이 훅을 사용하면, 현재 컨텍스트의 값을 가져올 수 있음
    
    const [AccordionCollapsibleProvider, useAccordionCollapsibleContext] = createAccordionContext(
      ACCORDION_NAME,
      { collapsible: false }
    );
    // 아코디언의 접힘(collapsible) 관련 컨텍스트를 생성
    import { useControllableState } from '@radix-ui/react-use-controllable-state';
    // useControllableState : 컨트롤 가능 상태 관리 - 부모 컴포넌트로부터 상태를 받아 사용할 수도 있는 상태
    // ==> controlled(부모에 의해 받은 props로 직접 관리) / uncontrolled(부모는 초기 값만 제공) 모드를 쉽게 전환
    
    const AccordionImplSingle = React.forwardRef<AccordionImplSingleElement, AccordionImplSingleProps>(
      (props: ScopedProps<AccordionImplSingleProps>, forwardedRef) => {
        const {
          value: valueProp,
          defaultValue,
          onValueChange = () => {},
          collapsible = false,
          ...accordionSingleProps
        } = props; //SingleProps에 대한 것은 위에 있음
    
        const [value, setValue] = useControllableState({
          prop: valueProp,
          defaultProp: defaultValue,
          onChange: onValueChange,
        });
    
        return (
          <AccordionValueProvider
            scope={props.__scopeAccordion}
            value={value ? [value] : []}
            onItemOpen={setValue}
            onItemClose={React.useCallback(() => collapsible && setValue(''), [collapsible, setValue])}
          >
            <AccordionCollapsibleProvider scope={props.__scopeAccordion} collapsible={collapsible}>
              <AccordionImpl {...accordionSingleProps} ref={forwardedRef} />
            </AccordionCollapsibleProvider>
          </AccordionValueProvider>
        );
      }
    );
    const AccordionImplMultiple = React.forwardRef<
      AccordionImplMultipleElement,
      AccordionImplMultipleProps
    >((props: ScopedProps<AccordionImplMultipleProps>, forwardedRef) => {
      const {
        value: valueProp,
        defaultValue,
        onValueChange = () => {},
        ...accordionMultipleProps
      } = props;
    
      const [value = [], setValue] = useControllableState({
        prop: valueProp,
        defaultProp: defaultValue,
        onChange: onValueChange,
      });
    
      const handleItemOpen = React.useCallback(
        (itemValue: string) => setValue((prevValue = []) => [...prevValue, itemValue]),
        [setValue]
      );
    
      const handleItemClose = React.useCallback(
        (itemValue: string) =>
          setValue((prevValue = []) => prevValue.filter((value) => value !== itemValue)),
        [setValue]
      );
    
      return (
        <AccordionValueProvider
          scope={props.__scopeAccordion}
          value={value}
          onItemOpen={handleItemOpen}
          onItemClose={handleItemClose}
        >
          <AccordionCollapsibleProvider scope={props.__scopeAccordion} collapsible={true}>
            <AccordionImpl {...accordionMultipleProps} ref={forwardedRef} />
          </AccordionCollapsibleProvider>
        </AccordionValueProvider>
      );
    });

     

    const AccordionImpl = React.forwardRef<AccordionImplElement, AccordionImplProps>(
      (props: ScopedProps<AccordionImplProps>, forwardedRef) => {
        const { __scopeAccordion, disabled, dir, orientation = 'vertical', ...accordionProps } = props;
        const accordionRef = React.useRef<AccordionImplElement>(null);
        const composedRefs = useComposedRefs(accordionRef, forwardedRef);
        const getItems = useCollection(__scopeAccordion);
        const direction = useDirection(dir);
        const isDirectionLTR = direction === 'ltr';
    
        const handleKeyDown = composeEventHandlers(props.onKeyDown, (event) => {
          if (!ACCORDION_KEYS.includes(event.key)) return;
          const target = event.target as HTMLElement;
          const triggerCollection = getItems().filter((item) => !item.ref.current?.disabled);
          const triggerIndex = triggerCollection.findIndex((item) => item.ref.current === target);
          const triggerCount = triggerCollection.length;
    
          if (triggerIndex === -1) return;
    
          // Prevents page scroll while user is navigating
          event.preventDefault();
    
          let nextIndex = triggerIndex;
          const homeIndex = 0;
          const endIndex = triggerCount - 1;
    
          const moveNext = () => {
            nextIndex = triggerIndex + 1;
            if (nextIndex > endIndex) {
              nextIndex = homeIndex;
            }
          };
    
          const movePrev = () => {
            nextIndex = triggerIndex - 1;
            if (nextIndex < homeIndex) {
              nextIndex = endIndex;
            }
          };
    
          switch (event.key) {
            case 'Home':
              nextIndex = homeIndex;
              break;
            case 'End':
              nextIndex = endIndex;
              break;
            case 'ArrowRight':
              if (orientation === 'horizontal') {
                if (isDirectionLTR) {
                  moveNext();
                } else {
                  movePrev();
                }
              }
              break;
            case 'ArrowDown':
              if (orientation === 'vertical') {
                moveNext();
              }
              break;
            case 'ArrowLeft':
              if (orientation === 'horizontal') {
                if (isDirectionLTR) {
                  movePrev();
                } else {
                  moveNext();
                }
              }
              break;
            case 'ArrowUp':
              if (orientation === 'vertical') {
                movePrev();
              }
              break;
          }
    
          const clampedIndex = nextIndex % triggerCount;
          triggerCollection[clampedIndex].ref.current?.focus();
        });
    
        return (
          <AccordionImplProvider
            scope={__scopeAccordion}
            disabled={disabled}
            direction={dir}
            orientation={orientation}
          >
            <Collection.Slot scope={__scopeAccordion}>
              <Primitive.div
                {...accordionProps}
                data-orientation={orientation}
                ref={composedRefs}
                onKeyDown={disabled ? undefined : handleKeyDown}
              />
            </Collection.Slot>
          </AccordionImplProvider>
        );
      }
    );
    /* -------------------------------------------------------------------------------------------------
     * AccordionItem
     * -----------------------------------------------------------------------------------------------*/
    
    const ITEM_NAME = 'AccordionItem';
    
    type AccordionItemContextValue = { open?: boolean; disabled?: boolean; triggerId: string };
    const [AccordionItemProvider, useAccordionItemContext] =
      createAccordionContext<AccordionItemContextValue>(ITEM_NAME);
    
    type AccordionItemElement = React.ElementRef<typeof CollapsiblePrimitive.Root>;
    type CollapsibleProps = Radix.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>;
    interface AccordionItemProps
      extends Omit<CollapsibleProps, 'open' | 'defaultOpen' | 'onOpenChange'> {
      /**
       * Whether or not an accordion item is disabled from user interaction.
       *
       * @defaultValue false
       */
      disabled?: boolean;
      /**
       * A string value for the accordion item. All items within an accordion should use a unique value.
       */
      value: string;
    }
    
    /**
     * `AccordionItem` contains all of the parts of a collapsible section inside of an `Accordion`.
     */
    const AccordionItem = React.forwardRef<AccordionItemElement, AccordionItemProps>(
      (props: ScopedProps<AccordionItemProps>, forwardedRef) => {
        const { __scopeAccordion, value, ...accordionItemProps } = props;
        const accordionContext = useAccordionContext(ITEM_NAME, __scopeAccordion);
        const valueContext = useAccordionValueContext(ITEM_NAME, __scopeAccordion);
        const collapsibleScope = useCollapsibleScope(__scopeAccordion);
        const triggerId = useId();
        const open = (value && valueContext.value.includes(value)) || false;
        const disabled = accordionContext.disabled || props.disabled;
    
        return (
          <AccordionItemProvider
            scope={__scopeAccordion}
            open={open}
            disabled={disabled}
            triggerId={triggerId}
          >
            <CollapsiblePrimitive.Root
              data-orientation={accordionContext.orientation}
              data-state={getState(open)}
              {...collapsibleScope}
              {...accordionItemProps}
              ref={forwardedRef}
              disabled={disabled}
              open={open}
              onOpenChange={(open) => {
                if (open) {
                  valueContext.onItemOpen(value);
                } else {
                  valueContext.onItemClose(value);
                }
              }}
            />
          </AccordionItemProvider>
        );
      }
    );
    
    AccordionItem.displayName = ITEM_NAME;
    /* -------------------------------------------------------------------------------------------------
     * AccordionHeader
     * -----------------------------------------------------------------------------------------------*/
    
    const HEADER_NAME = 'AccordionHeader';
    
    type AccordionHeaderElement = React.ElementRef<typeof Primitive.h3>;
    type PrimitiveHeading3Props = Radix.ComponentPropsWithoutRef<typeof Primitive.h3>;
    interface AccordionHeaderProps extends PrimitiveHeading3Props {}
    
    /**
     * `AccordionHeader` contains the content for the parts of an `AccordionItem` that will be visible
     * whether or not its content is collapsed.
     */
    const AccordionHeader = React.forwardRef<AccordionHeaderElement, AccordionHeaderProps>(
      (props: ScopedProps<AccordionHeaderProps>, forwardedRef) => {
        const { __scopeAccordion, ...headerProps } = props;
        const accordionContext = useAccordionContext(ACCORDION_NAME, __scopeAccordion);
        const itemContext = useAccordionItemContext(HEADER_NAME, __scopeAccordion);
        return (
          <Primitive.h3
            data-orientation={accordionContext.orientation}
            data-state={getState(itemContext.open)}
            data-disabled={itemContext.disabled ? '' : undefined}
            {...headerProps}
            ref={forwardedRef}
          />
        );
      }
    );
    
    AccordionHeader.displayName = HEADER_NAME;
    /* -------------------------------------------------------------------------------------------------
     * AccordionTrigger
     * -----------------------------------------------------------------------------------------------*/
    
    const TRIGGER_NAME = 'AccordionTrigger';
    
    type AccordionTriggerElement = React.ElementRef<typeof CollapsiblePrimitive.Trigger>;
    type CollapsibleTriggerProps = Radix.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>;
    interface AccordionTriggerProps extends CollapsibleTriggerProps {}
    
    /**
     * `AccordionTrigger` is the trigger that toggles the collapsed state of an `AccordionItem`. It
     * should always be nested inside of an `AccordionHeader`.
     */
    const AccordionTrigger = React.forwardRef<AccordionTriggerElement, AccordionTriggerProps>(
      (props: ScopedProps<AccordionTriggerProps>, forwardedRef) => {
        const { __scopeAccordion, ...triggerProps } = props;
        const accordionContext = useAccordionContext(ACCORDION_NAME, __scopeAccordion);
        const itemContext = useAccordionItemContext(TRIGGER_NAME, __scopeAccordion);
        const collapsibleContext = useAccordionCollapsibleContext(TRIGGER_NAME, __scopeAccordion);
        const collapsibleScope = useCollapsibleScope(__scopeAccordion);
        return (
          <Collection.ItemSlot scope={__scopeAccordion}>
            <CollapsiblePrimitive.Trigger
              aria-disabled={(itemContext.open && !collapsibleContext.collapsible) || undefined}
              data-orientation={accordionContext.orientation}
              id={itemContext.triggerId}
              {...collapsibleScope}
              {...triggerProps}
              ref={forwardedRef}
            />
          </Collection.ItemSlot>
        );
      }
    );
    
    AccordionTrigger.displayName = TRIGGER_NAME;
    /* -------------------------------------------------------------------------------------------------
     * AccordionContent
     * -----------------------------------------------------------------------------------------------*/
    
    const CONTENT_NAME = 'AccordionContent';
    
    type AccordionContentElement = React.ElementRef<typeof CollapsiblePrimitive.Content>;
    type CollapsibleContentProps = Radix.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>;
    interface AccordionContentProps extends CollapsibleContentProps {}
    
    /**
     * `AccordionContent` contains the collapsible content for an `AccordionItem`.
     */
    const AccordionContent = React.forwardRef<AccordionContentElement, AccordionContentProps>(
      (props: ScopedProps<AccordionContentProps>, forwardedRef) => {
        const { __scopeAccordion, ...contentProps } = props;
        const accordionContext = useAccordionContext(ACCORDION_NAME, __scopeAccordion);
        const itemContext = useAccordionItemContext(CONTENT_NAME, __scopeAccordion);
        const collapsibleScope = useCollapsibleScope(__scopeAccordion);
        return (
          <CollapsiblePrimitive.Content
            role="region"
            aria-labelledby={itemContext.triggerId}
            data-orientation={accordionContext.orientation}
            {...collapsibleScope}
            {...contentProps}
            ref={forwardedRef}
            style={{
              ['--radix-accordion-content-height' as any]: 'var(--radix-collapsible-content-height)',
              ['--radix-accordion-content-width' as any]: 'var(--radix-collapsible-content-width)',
              ...props.style,
            }}
          />
        );
      }
    );
    
    AccordionContent.displayName = CONTENT_NAME;

     

    '오픈소스' 카테고리의 다른 글

    [npm] package.json 옵션 정리 *main, module, exports, type, types  (0) 2024.01.10
    npm에 배포해보기  (1) 2024.01.09
Designed by Tistory.