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>
);
}
);