-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: seounjin
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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", | ||
}, | ||
} | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
<div id="modal"></div> |
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; |
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); |
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; |
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; |
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; | ||
}; |
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(); | ||
|
||
return isOpen ? ( | ||
<S.Layout | ||
role="dialog" | ||
aria-labelledby="modal-title" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
보통 이렇게 id랑 짝꿍인 aria-속성이 들어가면 그에 맞는 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tabIndex를 넣으신 이유가 있을까요?? |
||
</S.Layout> | ||
) : null; | ||
}; | ||
|
||
export default ModalLayout; |
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"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 모달용 포탈을 만들어주셨군요! 👍 |
||
|
||
if (container === null) { | ||
return null; | ||
} | ||
|
||
return createPortal(children, container); | ||
}; | ||
|
||
export default ModalPortal; |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
</> | ||
); | ||
}, | ||
}; |
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; | ||
`; |
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; |
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; |
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 = () => { | ||
|
@@ -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}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 키보드 화살표 위아래로도 뭔가 요소를 선택할 수 있으면 더 좋을 것 같아요! |
||
{children} | ||
</S.OptionItem> | ||
); | ||
}; | ||
|
||
export default Option; |
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); |
There was a problem hiding this comment.
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
를 누르면 꺼진다) 같은 것도 같이 관리하면 좋을 것 같아요.There was a problem hiding this comment.
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'
일 수도 있습니다!이런 점 참고해서 키보드 인터랙션도 구현하시면 좋을 것 같아요