Skip to content

Commit

Permalink
[feat] FAQ 컴포넌트 (#255)
Browse files Browse the repository at this point in the history
* chore: storybook emotion 설정

- storybookjs/storybook#7540

* feat: svg wrapper component

* feat: arrow icon

* feat: typo theme

* feat: emotion theme

* feat: emotion faq example

* feat: emotion theme

* feat: emotion faq example

* feat: typo theme

* chore: vscode typescript 버전 세팅

* chore: rename FAQ -> FAQItem

* feat: create FAQList

* feat: create FAQList story

* feat: li list style none

* chore: mock 데이터 위치 수정
  • Loading branch information
kimyouknow authored Sep 10, 2023
1 parent 5905a6d commit 9947e15
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 38 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"editor.formatOnSave": true
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
59 changes: 53 additions & 6 deletions src/components/FAQ/FAQ.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { useState } from 'react';
import { Meta } from '@storybook/react';

import { FAQList } from '~/components/FAQ/FAQList';

import { FAQ } from './index';

const meta: Meta<typeof FAQ> = {
Expand All @@ -8,22 +12,65 @@ const meta: Meta<typeof FAQ> = {

export default meta;

export const List = {
render: () => <FAQList FAQList={MOCK_FAQ_LIST} />,
};

export const Interaction = {
render: () => {
const [isOpen, setIsOpen] = useState(false);

const onClickOpenButton = () => {
setIsOpen(prev => !prev);
};

return (
<FAQ.Item
isOpen={isOpen}
onClickOpenButton={onClickOpenButton}
title={MOCK_FAQ_LIST[0].title}
description={MOCK_FAQ_LIST[0].description}
/>
);
},
};

export const Close = {
render: () => (
<FAQ
<FAQ.Item
isOpen={false}
title="직장인도 참여 가능한가요?"
description="디프만 모집은 직업, 연령 등 자격 요건에 제한을 두지 않습니다. 그러나 팀원들과 작업 속도, 일정을 맞추려면 더 많은 개인 시간을 할애할 필요가 있습니다. 따라서 회사 업무와 프로젝트를 병행할 수 있을지 충분히 고려하여 지원부탁드립니다."
onClickOpenButton={() => {}}
title={MOCK_FAQ_LIST[0].title}
description={MOCK_FAQ_LIST[0].description}
/>
),
};

export const Open = {
render: () => (
<FAQ
<FAQ.Item
isOpen={true}
title="직장인도 참여 가능한가요?"
description="디프만 모집은 직업, 연령 등 자격 요건에 제한을 두지 않습니다. 그러나 팀원들과 작업 속도, 일정을 맞추려면 더 많은 개인 시간을 할애할 필요가 있습니다. 따라서 회사 업무와 프로젝트를 병행할 수 있을지 충분히 고려하여 지원부탁드립니다."
onClickOpenButton={() => {}}
title={MOCK_FAQ_LIST[0].title}
description={MOCK_FAQ_LIST[0].description}
/>
),
};

const MOCK_FAQ_LIST = [
{
title: '직장인도 참여 가능한가요?',
description:
'디프만 모집은 직업, 연령 등 자격 요건에 제한을 두지 않습니다. 그러나 팀원들과 작업 속도, 일정을 맞추려면 더 많은 개인 시간을 할애할 필요가 있습니다. 따라서 회사 업무와 프로젝트를 병행할 수 있을지 충분히 고려하여 지원부탁드립니다.',
},
{
title: '직장인도 참여 가능한가요??',
description:
'디프만 모집은 직업, 연령 등 자격 요건에 제한을 두지 않습니다. 그러나 팀원들과 작업 속도, 일정을 맞추려면 더 많은 개인 시간을 할애할 필요가 있습니다. 따라서 회사 업무와 프로젝트를 병행할 수 있을지 충분히 고려하여 지원부탁드립니다.',
},
{
title: '직장인도 참여 가능한가요???',
description:
'디프만 모집은 직업, 연령 등 자격 요건에 제한을 두지 않습니다. 그러나 팀원들과 작업 속도, 일정을 맞추려면 더 많은 개인 시간을 할애할 필요가 있습니다. 따라서 회사 업무와 프로젝트를 병행할 수 있을지 충분히 고려하여 지원부탁드립니다.',
},
];
29 changes: 0 additions & 29 deletions src/components/FAQ/FAQ.tsx

This file was deleted.

86 changes: 86 additions & 0 deletions src/components/FAQ/FAQItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { css, Theme } from '@emotion/react';
import { motion, Variants } from 'framer-motion';

import { ArrowIcon } from '~/components/Icons';
import { theme } from '~/styles/theme';

interface FAQItemProps {
isOpen: boolean;
onClickOpenButton: () => void;
title: string;
description: string;
}

export function FAQItem({ isOpen, onClickOpenButton, title, description }: FAQItemProps) {
return (
<li onClick={onClickOpenButton}>
<motion.div
css={theme => headerCss(theme, isOpen)}
animate={isOpen ? 'open' : 'closed'}
variants={headerVariants}
transition={{ duration: 0.3, ease: 'easeOut' }}
>
<h3>{title}</h3>
<motion.div variants={arrowIconVariants} transition={{ duration: 0.3, ease: 'easeOut' }}>
<ArrowIcon
direction={isOpen ? 'up' : 'down'}
css={theme => arrowIconCss(theme, isOpen)}
/>
</motion.div>
</motion.div>

<motion.div
css={bodyCss}
initial={{ opacity: 0, height: 0 }}
animate={isOpen ? 'open' : 'closed'}
variants={bodyVariants}
transition={{ duration: 0.3, height: 0, ease: 'easeOut' }}
>
<p>{description}</p>
</motion.div>
</li>
);
}

const headerVariants: Variants = {
open: { backgroundColor: theme.colors.blue400 },
closed: { backgroundColor: theme.colors.black400 },
};

const bodyVariants: Variants = {
open: { opacity: 1, height: 'fit-content' },
closed: { opacity: 0, height: 0 },
};

const arrowIconVariants: Variants = {
open: { stroke: theme.colors.black800 },
closed: { stroke: theme.colors.blue400 },
};

const headerCss = (theme: Theme, isOpen: boolean) => css`
background-color: ${isOpen ? theme.colors.blue400 : theme.colors.black400};
display: flex;
justify-content: space-between;
align-items: center;
padding: 25px 30px;
> h3 {
color: ${isOpen ? theme.colors.black800 : theme.colors.white};
text-align: center;
${theme.typos.pretendard.subTitle2}
}
`;

const arrowIconCss = (theme: Theme, isOpen: boolean) => css`
> path {
stroke: ${isOpen ? theme.colors.black800 : theme.colors.blue400};
}
`;

const bodyCss = (theme: Theme) => css`
background-color: ${theme.colors.black800};
> p {
padding: 40px;
color: ${theme.colors.white};
${theme.typos.pretendard.body1};
}
`;
36 changes: 36 additions & 0 deletions src/components/FAQ/FAQList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from 'react';

import { FAQItem } from '~/components/FAQ/FAQItem';

type FAQListType = { title: string; description: string };

interface FAQListProps {
FAQList: FAQListType[];
}

const DEFAULT_OPEN = 0;
const CLOSE = -1;

export function FAQList({ FAQList }: FAQListProps) {
const [activeIndex, setActiveIndex] = useState(DEFAULT_OPEN);

const onClickActiveFaq = (idx: number) => {
/**
* 열려 있는 아이템 다시 클릭하면 닫히게 하기
*/
setActiveIndex(prev => (prev === idx ? CLOSE : idx));
};

return (
<ul>
{FAQList.map((item, index) => (
<FAQItem
key={item.title}
isOpen={activeIndex === index}
onClickOpenButton={() => onClickActiveFaq(index)}
{...item}
/>
))}
</ul>
);
}
11 changes: 10 additions & 1 deletion src/components/FAQ/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export { FAQ } from './FAQ';
import { FAQItem } from '~/components/FAQ/FAQItem';
import { FAQList } from '~/components/FAQ/FAQList';

export const FAQ = Object.assign(
{},
{
List: FAQList,
Item: FAQItem,
}
);
3 changes: 2 additions & 1 deletion src/styles/resetCss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export const resetCss = css`
line-height: 1;
}
ol,
ul {
ul,
li {
list-style: none;
}
blockquote,
Expand Down

0 comments on commit 9947e15

Please sign in to comment.