Skip to content

Commit

Permalink
feat: Accordion 컴포넌트 제작
Browse files Browse the repository at this point in the history
  • Loading branch information
bottlewook committed Jan 20, 2024
1 parent 17cac44 commit 31336fc
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 121 deletions.
42 changes: 22 additions & 20 deletions src/components/shared/accordion/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import { useCallback, useMemo, useState } from 'react';
import {
forwardRef, useCallback, useMemo, useState,
} from 'react';

import AccordionContext from '@contexts/AccordionContext';
import AccordionContext from '@/contexts/AccordionContext';

import AccordionBody from './body';
import AccordionHeader from './header';
import AccordionItem from './item';
import { AccordionProps } from './type/accordion.type';

function Accordion({ children }: { children: React.ReactNode | React.ReactNode[] }) {
const [activeItem, setActiveItem] = useState('');
// eslint-disable-next-line max-len
const Accordion = forwardRef<HTMLDivElement, AccordionProps>(({ defaultActiveItems = [], children, ...props }, ref) => {
const [activeItems, setActiveItems] = useState<string[]>(defaultActiveItems);

const changeActiveItem = useCallback((value: string) => {
if (activeItem !== value) setActiveItem(value);
if (activeItem === value) setActiveItem('');
}, [setActiveItem, activeItem]);
const handleSetActiveItem = useCallback((item: string) => {
if (activeItems?.includes(item)) {
setActiveItems(activeItems.filter((activeItem) => { return activeItem !== item; }));
} else {
setActiveItems([...activeItems, item]);
}
}, [activeItems]);

const values = useMemo(() => {
return {
activeItem,
changeSelectedItem: changeActiveItem,
activeItems,
setActiveItem: handleSetActiveItem,
};
}, [activeItem, changeActiveItem]);
}, [activeItems, handleSetActiveItem]);

return (
<AccordionContext.Provider value={values}>
{children}
<div ref={ref} {...props}>
{children}
</div>
</AccordionContext.Provider>
);
}

Accordion.Item = AccordionItem;
Accordion.Header = AccordionHeader;
Accordion.Body = AccordionBody;
});

export default Accordion;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.container {
width: 100%;
overflow: hidden;
transition: height 0.3s ease;

& > div[data-name="body-inner"] {
padding: 16px;
}
}
46 changes: 46 additions & 0 deletions src/components/shared/accordion/body/AccordionBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
forwardRef, useEffect, useState, useRef,
} from 'react';

import classNames from 'classnames/bind';

import { useAccordion } from '@/contexts/AccordionContext';

import { AccordionBodyProps } from '../type/accordion.type';

import styles from './AccordionBody.module.scss';

const cx = classNames.bind(styles);

const AccordionBody = forwardRef<HTMLDivElement, AccordionBodyProps>(({
itemName = '', children, className, ...props
}, ref) => {
const innerRef = useRef<HTMLDivElement>(null);

const { activeItems } = useAccordion();
const isActive = activeItems.includes(itemName);

const [currentBodyHeight, setCurrentBodyHeight] = useState<string>();

useEffect(() => {
if (innerRef.current == null) return;

setCurrentBodyHeight(isActive ? `${innerRef.current.clientHeight}px` : '0');
}, [isActive]);

return (
<div
ref={ref}
{...props}
className={cx('container', className)}
data-action-item={isActive}
style={{ height: currentBodyHeight ?? `${innerRef?.current?.clientHeight}px` }}
>
<div data-name="body-inner" ref={innerRef}>
{children}
</div>
</div>
);
});

export default AccordionBody;
12 changes: 0 additions & 12 deletions src/components/shared/accordion/body/index.module.scss

This file was deleted.

25 changes: 0 additions & 25 deletions src/components/shared/accordion/body/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 16px;
border-bottom: 1px solid var(--gray-100);
}
35 changes: 35 additions & 0 deletions src/components/shared/accordion/header/AccordionHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
forwardRef, useCallback,
} from 'react';

import classNames from 'classnames/bind';

import { useAccordion } from '@contexts/AccordionContext';

import { AccordionHeaderProps } from '../type/accordion.type';

import styles from './AccordionHeader.module.scss';

const cx = classNames.bind(styles);

const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(({
itemName = '', children, onClick, className, openIcon, closeIcon, ...props
}, ref) => {
const { setActiveItem, activeItems } = useAccordion();

const handleClick = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setActiveItem(itemName);
onClick?.(event);
}, [itemName, onClick, setActiveItem]);

const isActive = activeItems.includes(itemName);

return (
<button onClick={handleClick} ref={ref} {...props} className={cx('container', className)}>
{children}
{isActive ? closeIcon : openIcon}
</button>
);
});

export default AccordionHeader;
36 changes: 0 additions & 36 deletions src/components/shared/accordion/header/index.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.container {
width: 100%;
border-top: 1px solid var(--gray);

&:last-of-type {
border-bottom: 1px solid var(--gray);
}
}
33 changes: 33 additions & 0 deletions src/components/shared/accordion/item/AccordionItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Children, cloneElement, forwardRef, isValidElement,
} from 'react';

import classNames from 'classnames/bind';

import { AccordionItemProps } from '../type/accordion.type';

import styles from './AccordionItem.module.scss';

const cx = classNames.bind(styles);

const AccordionItem = forwardRef<HTMLDivElement, AccordionItemProps>(({
itemName, children, className, ...props
}, ref) => {
const childrenWithProps = Children.toArray(children);

const accordionItemChildren = childrenWithProps.map((child) => {
if (isValidElement(child)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
return cloneElement(child, { ...child.props, itemName });
}

return null;
});
return (
<div ref={ref} {...props} className={cx('container', className)}>
{accordionItemChildren}
</div>
);
});

export default AccordionItem;
24 changes: 0 additions & 24 deletions src/components/shared/accordion/item/index.tsx

This file was deleted.

19 changes: 19 additions & 0 deletions src/components/shared/accordion/type/accordion.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export type AccordionProps = {
defaultActiveItems?: string[]
children: React.ReactNode | React.ReactNode[]
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;

export type AccordionItemProps = {
children: React.ReactNode[]
itemName: string
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;

export type AccordionHeaderProps = {
itemName?: string
openIcon?: React.ReactNode
closeIcon?: React.ReactNode
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export type AccordionBodyProps = {
itemName?: string
} & React.HTMLAttributes<HTMLDivElement>;
8 changes: 4 additions & 4 deletions src/contexts/AccordionContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createContext, useContext } from 'react';

interface AccordionContextValue {
activeItem: string
changeSelectedItem: (item: string) => void
activeItems: string[]
setActiveItem: (item: string) => void
}

const AccordionContext = createContext<AccordionContextValue>({
activeItem: '',
changeSelectedItem: () => { },
activeItems: [],
setActiveItem: () => { },
});

export function useAccordion() {
Expand Down

0 comments on commit 31336fc

Please sign in to comment.