Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[디자인시스템] 3주차 변성진 #9

Open
wants to merge 3 commits into
base: seounjin
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended', 'plugin:storybook/recommended'],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
"plugin:storybook/recommended",
"plugin:storybook/recommended",
],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
'react-refresh/only-export-components': [
'warn',
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"@typescript-eslint/no-unnecessary-type-constraint": "warn",
},
}
};
1 change: 1 addition & 0 deletions .storybook/preview-body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div id="modal"></div>
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<title>Vite + React + TS</title>
</head>
<body>
<div id="modal"></div>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
7 changes: 7 additions & 0 deletions src/components/Modal/Modal.body.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";

const ModalBody = ({ children }: PropsWithChildren) => {
return <div id="modal-description">{children}</div>;
};

export default ModalBody;
8 changes: 8 additions & 0 deletions src/components/Modal/Modal.context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createContext } from "react";

export interface SelectContextTypes {
isOpen: boolean;
onClose: () => void;
}

export const ModalContext = createContext<SelectContextTypes | null>(null);
8 changes: 8 additions & 0 deletions src/components/Modal/Modal.footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PropsWithChildren } from "react";
import { Footer } from "./Modal.styles";

const ModalFooter = ({ children }: PropsWithChildren) => {
return <Footer>{children}</Footer>;
};

export default ModalFooter;
8 changes: 8 additions & 0 deletions src/components/Modal/Modal.header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { PropsWithChildren } from "react";
import { Header } from "./Modal.styles";

const ModalHeader = ({ children }: PropsWithChildren) => {
return <Header id="title-dialog">{children}</Header>;
};

export default ModalHeader;
10 changes: 10 additions & 0 deletions src/components/Modal/Modal.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useContext } from "react";
import { ModalContext } from "./Modal.context";

export const useModalContext = () => {
const context = useContext(ModalContext);
if (!context) {
throw new Error("컴포넌트에 필요한 컨텍스트가 없습니다.");
}
return context;
};
24 changes: 24 additions & 0 deletions src/components/Modal/Modal.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useModalContext } from "./Modal.hooks";
import * as S from "./Modal.styles";
import { PropsWithChildren } from "react";

interface ModalLayoutProps extends PropsWithChildren {}

const ModalLayout = ({ children }: ModalLayoutProps) => {
const { isOpen, onClose } = useModalContext();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useModal()(예시) 같은 훅을 하나 만들어서, 거기서 isOpen, onClose 같은 속성을 관리하고,
추가로 모달은 키보드 인터랙션(ex) esc를 누르면 꺼진다, overlay를 누르면 꺼진다) 같은 것도 같이 관리하면 좋을 것 같아요.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 키보드 이벤트가 나와서 참고로 말씀드리면,

운영체제마다 키보드의 event.key 값이 조금씩 다른거 아시나요?
예를들어, 맥이랑 윈도우에서 어디는 스페이스바 이벤트 키가 어디는 event.key === 'Space' 이고, 어디는 event.key=' ' 입니다 ㅎㅎ
그래서 esc를 눌러도 event.key==='Esc' 일수도, event.key==='Escape' 일 수도 있습니다!

이런 점 참고해서 키보드 인터랙션도 구현하시면 좋을 것 같아요


return isOpen ? (
<S.Layout
role="dialog"
aria-labelledby="modal-title"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<div role="dialog" aria-modal="true" aria-labelledby="dialog_label" aria-describedby="dialog_desc">
  <h2 id="dialog_label">모달 제목</h2>
  <div id="dialog_desc">모달 내용 설명입니다.</div>
</div>

보통 이렇게 id랑 짝꿍인 aria-속성이 들어가면 그에 맞는 id가 기대되는데
modal-title 에 맞는 id는 없는 것....같아요? 👀

aria-describedby="modal-description"
>
<S.Overlay aria-hidden={true} onClick={onClose} />
<S.Content role="document" tabIndex={-1}>
{children}
</S.Content>
Comment on lines +17 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tabIndex를 넣으신 이유가 있을까요??
스토리북상으로 확인했을 땐 tabIndex={-1} 이 있을 때랑, 없을 때랑 차이가 없는 것...같아요...?

</S.Layout>
) : null;
};

export default ModalLayout;
14 changes: 14 additions & 0 deletions src/components/Modal/Modal.portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PropsWithChildren } from "react";
import { createPortal } from "react-dom";

const ModalPortal = ({ children }: PropsWithChildren) => {
const container = document.getElementById("modal");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모달용 포탈을 만들어주셨군요! 👍


if (container === null) {
return null;
}

return createPortal(children, container);
};

export default ModalPortal;
70 changes: 70 additions & 0 deletions src/components/Modal/Modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Meta, StoryObj } from "@storybook/react";
import Modal, { ModalProps } from "./Modal";
import Button from "../Button/Button";
import { useState } from "react";

const meta: Meta<typeof Modal> = {
title: "Components/Modal",
component: Modal,
tags: ["autodocs"],
argTypes: {},
};

export default meta;

export const ExampleModal: StoryObj<ModalProps> = {
args: {},
render: () => {
const [isOpen, setIsOpen] = useState<boolean>(false);

const onConfirm = () => {
setIsOpen(false);
};

const onOpen = () => {
setIsOpen(true);
};

const onClose = () => {
setIsOpen(false);
};
Comment on lines +18 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보통 open, close 같은 열림 상태 관리나, 사용자가 닫을 수 있는 방법을 공통화해놓으면 사용자가 여러군데에서 사용할 수 있더라구요. (chakra에서는 useDisclosure 라는 훅으로 사용하고 있어요)


return (
<>
<Button size="md" variant="primary" color="gray" onClick={onOpen}>
모달버튼
</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>제목</Modal.Header>
<Modal.Body>
모달 컴포넌트 입니다.
<br />
모달 컴포넌트 입니다.
<br />
모달 컴포넌트 입니다.
</Modal.Body>
<Modal.Footer>
<Button
aria-label="modal open"
size="sm"
variant="primary"
color="gray"
onClick={onConfirm}
>
Open
</Button>
<Button
aria-label="modal close"
size="sm"
variant="outline"
color="gray"
onClick={onClose}
>
Close
</Button>
</Modal.Footer>
</Modal>
</>
);
},
};
48 changes: 48 additions & 0 deletions src/components/Modal/Modal.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import styled from "styled-components";
import theme from "../../styles";

export const Layout = styled.div`
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
background-color: rgb(0, 0, 0, 0.6);
`;

export const Overlay = styled.div`
position: absolute;
width: 100%;
height: 100%;
`;

export const Content = styled.div`
position: relative;
display: flex;
flex-direction: column;
gap: 20px;

width: 100%;
max-width: 380px;
padding: 20px;
border-radius: 10px;
background-color: ${theme.colors.sjWhite}};
`;

export const Header = styled.header`
font-size: 20px;
font-weight: 600;
`;

export const Footer = styled.footer`
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
`;
28 changes: 28 additions & 0 deletions src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ModalLayout from "./Modal.layout";
import ModalPortal from "./Modal.portal";
import ModalHeader from "./Modal.header";
import ModalBody from "./Modal.body";
import ModalFooter from "./Modal.footer";
import { PropsWithChildren } from "react";
import { ModalContext } from "./Modal.context";

export interface ModalProps extends PropsWithChildren {
isOpen: boolean;
onClose: () => void;
}

const Modal = ({ children, isOpen, onClose }: ModalProps) => {
return (
<ModalContext.Provider value={{ isOpen, onClose }}>
<ModalPortal>
<ModalLayout>{children}</ModalLayout>
</ModalPortal>
</ModalContext.Provider>
);
};

export default Modal;

Modal.Header = ModalHeader;
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
32 changes: 32 additions & 0 deletions src/components/Select/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeEvent, useEffect, useState } from "react";
import { useSelectContext } from "./Select.hooks";
import { StyledInput } from "./Select.styles";

const Input = () => {
const [value, setValue] = useState<string>("");
const { option, setIsOpen, onInputChange } = useSelectContext();

useEffect(() => {
setValue(option);
}, [option]);

const onChange = (event: ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
if (onInputChange) {
onInputChange(event);
}

setValue(inputValue);
};

return (
<StyledInput
value={value}
onChange={onChange}
onFocus={() => setIsOpen(true)}
onBlur={() => setIsOpen(false)}
/>
);
};

export default Input;
9 changes: 8 additions & 1 deletion src/components/Select/Option.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import * as S from "./Select.styles";
import { ReactNode } from "react";
import { useSelectContext } from "./Select.hooks";
import useId from "../../hooks/useId";

export interface OptionProps {
children?: ReactNode;
value: string;
}

const Option = ({ children, value }: OptionProps) => {
const id = useId();

const { setIsOpen, setOption, onSelect } = useSelectContext();

const handleOption = () => {
Expand All @@ -16,7 +19,11 @@ const Option = ({ children, value }: OptionProps) => {
setIsOpen(false);
};

return <S.OptionItem onMouseDown={handleOption}>{children}</S.OptionItem>;
return (
<S.OptionItem id={`listbox-option-${id}`} onMouseDown={handleOption}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

키보드 화살표 위아래로도 뭔가 요소를 선택할 수 있으면 더 좋을 것 같아요!

{children}
</S.OptionItem>
);
};

export default Option;
6 changes: 4 additions & 2 deletions src/components/Select/Select.context.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Dispatch, SetStateAction, createContext } from "react";
import { ChangeEvent, Dispatch, SetStateAction, createContext } from "react";

export interface SelectContextTypes {
isOpen: boolean;
option: string;
itemList?: Array<string>;
setIsOpen: Dispatch<SetStateAction<boolean>>;
setOption: Dispatch<SetStateAction<string>>;
onSelect: (value: string) => void;
onSelect: (props: string) => void;
onInputChange?: (event: ChangeEvent<HTMLInputElement>) => void;
}

export const SelectContext = createContext<SelectContextTypes | null>(null);
29 changes: 28 additions & 1 deletion src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Select, { SelectProps } from "./Select";
import { Meta, StoryObj } from "@storybook/react";
import { StoryBookFlex } from "./Select.styles";
import { useState } from "react";
import { ChangeEvent, useState } from "react";

const meta: Meta<typeof Select> = {
title: "Components/Select",
Expand Down Expand Up @@ -45,3 +45,30 @@ export const ExampleSelect: StoryObj<SelectProps> = {
);
},
};

const ITEM_LIST = ["React", "Next.js", "TypeScript"];

export const ExampleComboBox: StoryObj<SelectProps> = {
args: {},
render: (args) => {
const [itemList, setItemList] = useState<Array<string>>(ITEM_LIST);
const onInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const filterItem = itemList.filter((item) => item === value);
setItemList(filterItem.length > 0 ? filterItem : ITEM_LIST);
};

return (
<Select {...args} itemList={ITEM_LIST} onInputChange={onInputChange}>
<Select.Input />
<Select.OptionList>
{itemList.map((item) => (
<Select.Option key={item} value={item}>
{item}
</Select.Option>
))}
</Select.OptionList>
</Select>
);
},
};
Loading