├── .gitignore
├── .prettierrc
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.jsx
├── components
│ ├── @elem
│ │ └── Stack.jsx
│ ├── button
│ │ ├── Button.jsx
│ │ └── Icon.jsx
│ ├── input
│ │ └── Input.jsx
│ ├── modal
│ │ └── Modal.jsx
│ └── select
│ │ └── Select.jsx
├── features
│ ├── Button.jsx
│ ├── Input.jsx
│ ├── Modal.jsx
│ └── Select.jsx
└── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react components
2 |
3 | - modal
4 | - button
5 | - input
6 | - select
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "second_week_lv2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.14.1",
7 | "@testing-library/react": "^13.0.0",
8 | "@testing-library/user-event": "^13.2.1",
9 | "react": "^18.2.0",
10 | "react-dom": "^18.2.0",
11 | "react-scripts": "5.0.1",
12 | "styled-components": "^5.3.6",
13 | "web-vitals": "^2.1.0"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test",
19 | "eject": "react-scripts eject"
20 | },
21 | "eslintConfig": {
22 | "extends": [
23 | "react-app",
24 | "react-app/jest"
25 | ]
26 | },
27 | "browserslist": {
28 | "production": [
29 | ">0.2%",
30 | "not dead",
31 | "not op_mini all"
32 | ],
33 | "development": [
34 | "last 1 chrome version",
35 | "last 1 firefox version",
36 | "last 1 safari version"
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-key/react_components/aa9ddbfdc85c827ad65ec9d821a9c8279476376d/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
33 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-key/react_components/aa9ddbfdc85c827ad65ec9d821a9c8279476376d/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/with-key/react_components/aa9ddbfdc85c827ad65ec9d821a9c8279476376d/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FeatureButton from "./features/Button";
3 | import FeatureModal from "./features/Modal";
4 | import FeatureSelect from "./features/Select";
5 | import FeatureInput from "./features/Input";
6 |
7 | const App = () => {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 | >
15 | );
16 | };
17 |
18 | export default App;
19 |
--------------------------------------------------------------------------------
/src/components/@elem/Stack.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Stack = ({ children, ...restProps }) => {
5 | return {children};
6 | };
7 |
8 | export default Stack;
9 |
10 | const StyledStack = styled.div`
11 | display: flex;
12 | flex-direction: ${({ row = "row" }) => (row ? "row" : "column")};
13 | gap: ${({ gap }) => `${gap}px`};
14 | `;
15 |
--------------------------------------------------------------------------------
/src/components/button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled, { css } from "styled-components";
3 |
4 | /*--------------------------------------------------------*
5 | * Primitive Button
6 | *--------------------------------------------------------*/
7 |
8 | const PrimitiveButton = ({ children, rightSlot, ...restProps }) => {
9 | return (
10 |
11 | {rightSlot ? (
12 |
13 | <>{children}>
14 | <>{rightSlot}>
15 |
16 | ) : (
17 | <>{children}>
18 | )}
19 |
20 | );
21 | };
22 |
23 | /*--------------------------------------------------------*
24 | * Primary Style
25 | *--------------------------------------------------------*/
26 |
27 | const PrimaryButton = (props) => {
28 | return (
29 |
35 | );
36 | };
37 |
38 | /*--------------------------------------------------------*
39 | * Negative Style
40 | *--------------------------------------------------------*/
41 |
42 | const NegativeButton = (props) => {
43 | return (
44 |
50 | );
51 | };
52 |
53 | const Primary = PrimaryButton;
54 | const Negative = NegativeButton;
55 |
56 | const Button = { Negative, Primary };
57 | export default Button;
58 |
59 | const StyledButton = styled.button`
60 | border: none;
61 | cursor: pointer;
62 |
63 | border-radius: 8px;
64 | background-color: ${({ bc }) => bc};
65 | color: ${({ color }) => color};
66 | font-weight: ${({ fw }) => fw};
67 |
68 | &:active {
69 | background-color: ${({ activeBc }) => activeBc};
70 | }
71 |
72 | ${({ size }) => {
73 | switch (size) {
74 | case "large":
75 | return css`
76 | height: 50px;
77 | width: 200px;
78 | `;
79 | case "medium":
80 | return css`
81 | height: 45px;
82 | width: 130px;
83 | `;
84 | case "small":
85 | return css`
86 | height: 40px;
87 | width: 100px;
88 | `;
89 | default:
90 | return css`
91 | height: 40px;
92 | width: 100px;
93 | `;
94 | }
95 | }}
96 |
97 | ${({ outlined, bc }) => {
98 | if (outlined) {
99 | return css`
100 | border: 3px solid ${bc};
101 | background-color: #fff;
102 | font-weight: 600;
103 |
104 | &:active {
105 | background-color: #eeeeee;
106 | }
107 | `;
108 | }
109 | }}
110 | `;
111 |
112 | const ButtonInner = styled.div`
113 | display: flex;
114 | align-items: center;
115 | justify-content: center;
116 | gap: 7px;
117 | `;
118 |
--------------------------------------------------------------------------------
/src/components/button/Icon.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | export const IconArrow = ({ color }) => {
5 | return (
6 |
7 |
22 |
23 | );
24 | };
25 |
26 | export const IconBell = () => {
27 | return (
28 |
29 |
54 |
55 | );
56 | };
57 |
58 | const Container = styled.div`
59 | display: flex;
60 | align-items: center;
61 | justify-content: center;
62 | `;
63 |
--------------------------------------------------------------------------------
/src/components/input/Input.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled from "styled-components";
3 |
4 | /*--------------------------------------------------------*
5 | * Primitive Input
6 | *--------------------------------------------------------*/
7 |
8 | const PrmitiveInput = ({ value, ...restProps }) => {
9 | return (
10 |
11 | );
12 | };
13 |
14 | /*--------------------------------------------------------*
15 | * normal input
16 | *--------------------------------------------------------*/
17 |
18 | const NormalInput = ({ getValues }) => {
19 | const [value, setValue] = useState("");
20 |
21 | const onChageHandler = (e) => {
22 | const { value } = e.target;
23 | setValue(e.target.value);
24 | getValues(value);
25 | };
26 |
27 | return ;
28 | };
29 |
30 | /*--------------------------------------------------------*
31 | * price format input
32 | *--------------------------------------------------------*/
33 |
34 | const PriceFormatInput = ({ getValues }) => {
35 | const [value, setValue] = useState({
36 | raw: "0",
37 | format: "0",
38 | });
39 |
40 | const onChageHandler = ({ target }) => {
41 | const rex = /\D/g;
42 |
43 | const raw = target.value.replaceAll(",", "");
44 | const format = new Intl.NumberFormat().format(
45 | target.value.replaceAll(",", "")
46 | );
47 |
48 | if (!rex.test(target.value.replaceAll(",", ""))) {
49 | setValue((old) => ({
50 | ...old,
51 | raw,
52 | format,
53 | }));
54 |
55 | getValues && getValues({ raw, format });
56 | }
57 | };
58 |
59 | return ;
60 | };
61 |
62 | const Price = PriceFormatInput;
63 | const Normal = NormalInput;
64 |
65 | export { Price, Normal };
66 |
67 | const StyledInput = styled.input`
68 | border: 1px solid #333333;
69 | height: 40px;
70 | width: 200px;
71 | outline: none;
72 | border-radius: 8px;
73 | padding-left: 12px;
74 | padding-right: 12px;
75 |
76 | &:focus-within {
77 | box-shadow: 0 0 0 1px #000;
78 | }
79 | `;
80 |
--------------------------------------------------------------------------------
/src/components/modal/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | createContext,
3 | useContext,
4 | useEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import ReactDOM from "react-dom";
9 | import styled from "styled-components";
10 |
11 | const ModalContext = createContext({});
12 |
13 | /*--------------------------------------------------------*
14 | * Modal Root
15 | *--------------------------------------------------------*/
16 |
17 | const ModalRoot = ({ children }) => {
18 | const [open, setOpen] = useState(false);
19 | return (
20 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | /*--------------------------------------------------------*
32 | * Modal Trigger
33 | *--------------------------------------------------------*/
34 |
35 | const ModalTrigger = ({ children, asChild, ...rest }) => {
36 | const { setOpen } = useContext(ModalContext);
37 | return asChild ? (
38 | setOpen((pre) => !pre)} {...rest}>
39 | {children}
40 |
41 | ) : (
42 |
45 | );
46 | };
47 |
48 | /*--------------------------------------------------------*
49 | * Modal Portal
50 | *--------------------------------------------------------*/
51 |
52 | const ModalPortal = ({ children }) => {
53 | const portalTarget = document.getElementById("portal-target");
54 |
55 | if (!portalTarget) {
56 | return null;
57 | }
58 |
59 | return ReactDOM.createPortal(children, portalTarget);
60 | };
61 |
62 | /*--------------------------------------------------------*
63 | * Modal Overlay
64 | *--------------------------------------------------------*/
65 |
66 | const ModalOverlay = ({ onClose }) => {
67 | const { open, setOpen } = useContext(ModalContext);
68 | return open ? (
69 | (onClose ? setOpen(false) : null)} />
70 | ) : (
71 | <>>
72 | );
73 | };
74 |
75 | const StyledOverlay = styled.div`
76 | width: 100%;
77 | height: 100vh;
78 | inset: 0;
79 | position: fixed;
80 | opacity: 80%;
81 | background-color: #ddd;
82 | `;
83 |
84 | /*--------------------------------------------------------*
85 | * Modal Content
86 | *--------------------------------------------------------*/
87 |
88 | const ModalContent = ({ children, ...rest }) => {
89 | const { open } = useContext(ModalContext);
90 | const ref = useRef(null);
91 |
92 | const clickHandler = () => {
93 | if (ref) {
94 | console.log(ref.current);
95 | }
96 | };
97 |
98 | useEffect(() => {
99 | document.addEventListener("click", clickHandler);
100 | return () => document.removeEventListener("click", clickHandler);
101 | }, []);
102 |
103 | return open ? (
104 |
105 | {children}
106 |
107 | ) : (
108 | <>>
109 | );
110 | };
111 |
112 | const StyledContent = styled.div`
113 | position: absolute;
114 | `;
115 |
116 | /*--------------------------------------------------------*
117 | * Modal Close
118 | *--------------------------------------------------------*/
119 |
120 | const ModalClose = ({ children, asChild, ...rest }) => {
121 | const { setOpen } = useContext(ModalContext);
122 | return asChild ? (
123 | setOpen(false)} {...rest}>
124 | {children}
125 |
126 | ) : (
127 |
130 | );
131 | };
132 |
133 | const Root = ModalRoot;
134 | const Trigger = ModalTrigger;
135 | const Portal = ModalPortal;
136 | const Overlay = ModalOverlay;
137 | const Close = ModalClose;
138 | const Content = ModalContent;
139 |
140 | export { Root, Trigger, Portal, Overlay, Close, Content };
141 |
--------------------------------------------------------------------------------
/src/components/select/Select.jsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useState } from "react";
2 | import ReactDOM from "react-dom";
3 |
4 | const SelectContext = createContext({});
5 |
6 | /*--------------------------------------------------------*
7 | * Select Root
8 | *--------------------------------------------------------*/
9 | const SelectRoot = ({ children }) => {
10 | const [open, setOpen] = useState(false);
11 | const [selected, setSelected] = useState("1");
12 | const [options, setOptions] = useState([]);
13 |
14 | return (
15 |
25 | {children}
26 |
27 | );
28 | };
29 |
30 | /*--------------------------------------------------------*
31 | * Select Trigger
32 | *--------------------------------------------------------*/
33 |
34 | const SelectTrigger = ({ children, ...rest }) => {
35 | const { setOpen, selected, options } = useContext(SelectContext);
36 | const [value] = options.filter((o) => o.value === selected);
37 |
38 | return (
39 |
53 | );
54 | };
55 |
56 | /*--------------------------------------------------------*
57 | * Select List
58 | *--------------------------------------------------------*/
59 |
60 | const SelectList = ({ children, ...rest }) => {
61 | const { open, setOptions } = useContext(SelectContext);
62 |
63 | useEffect(() => {
64 | setOptions(
65 | React.Children.toArray(children)
66 | .map((c) => c.props)
67 | .map(({ value, children: label }) => ({
68 | value,
69 | label,
70 | }))
71 | );
72 | }, []);
73 |
74 | return open ? {children}
: <>>;
75 | };
76 |
77 | /*--------------------------------------------------------*
78 | * Select Option
79 | *--------------------------------------------------------*/
80 |
81 | const SelectOption = ({ children, value, ...rest }) => {
82 | const { setSelected } = useContext(SelectContext);
83 | return (
84 | setSelected(value)}>
85 | {children}
86 |
87 | );
88 | };
89 |
90 | /*--------------------------------------------------------*
91 | * Select Portal
92 | *--------------------------------------------------------*/
93 |
94 | const SelectPortal = ({ children }) => {
95 | const portalTarget = document.getElementById("portal-target");
96 |
97 | if (!portalTarget) {
98 | return null;
99 | }
100 |
101 | return ReactDOM.createPortal(children, portalTarget);
102 | };
103 |
104 | /*--------------------------------------------------------*
105 | * Result
106 | *--------------------------------------------------------*/
107 |
108 | const Root = SelectRoot;
109 | const Trigger = SelectTrigger;
110 | const List = SelectList;
111 | const Option = SelectOption;
112 | const Portal = SelectPortal;
113 |
114 | export { Root, Trigger, List, Option, Portal };
115 |
--------------------------------------------------------------------------------
/src/features/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Stack from "../components/@elem/Stack";
3 | import Button from "../components/button/Button";
4 | import { IconArrow, IconBell } from "../components/button/Icon";
5 |
6 | const FeatureButton = () => {
7 | return (
8 |
9 | Button
10 |
11 | }
13 | size="large"
14 | outlined
15 | onClick={() => window.alert("버튼을 만들어보세요")}
16 | >
17 | Large Primary Button
18 |
19 | Medium
20 | Small
21 |
22 |
23 |
24 | }
28 | onClick={() => console.log(window.prompt("어렵나요?"))}
29 | >
30 | Large Negative Button
31 |
32 | Medium
33 | Small
34 |
35 |
36 | );
37 | };
38 |
39 | export default FeatureButton;
40 |
--------------------------------------------------------------------------------
/src/features/Input.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Stack from "../components/@elem/Stack";
3 | import Button from "../components/button/Button";
4 | import * as Input from "../components/input/Input";
5 |
6 | const FeatureInput = () => {
7 | const [form, setForm] = useState({
8 | name: "",
9 | price: "",
10 | });
11 |
12 | const onSubmitHandler = (e) => {
13 | e.preventDefault();
14 |
15 | Object.values(form).filter((el) => el !== "").length === 0
16 | ? window.alert("이름과 가격 모두 입력해주세요.")
17 | : window.alert(`{ name: ${form.name}, price: ${form.price} }`);
18 | };
19 |
20 | return (
21 | <>
22 | Input
23 |
41 | >
42 | );
43 | };
44 |
45 | export default FeatureInput;
46 |
--------------------------------------------------------------------------------
/src/features/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Stack from "../components/@elem/Stack";
4 | import Button from "../components/button/Button";
5 | import * as Modal from "../components/modal/Modal";
6 |
7 | const FeatureModal = () => {
8 | return (
9 |
10 |
Modal
11 |
12 |
13 |
14 | open modal
15 |
16 |
17 |
18 |
19 |
20 | 닫기와 확인 버튼 2개가 있고, 외부 영역을 눌러도 모달이 닫히지
21 | 않아요.
22 |
23 |
24 |
25 | 닫기
26 |
27 | {
29 | console.log("on!");
30 | }}
31 | >
32 | 확인
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | open modal
42 |
43 |
44 |
45 |
46 |
47 |
48 | 닫기 버튼 1개가 있고,
49 |
50 | 외부 영역을 누르면 모달이 닫혀요.
51 |
52 |
53 |
54 | X
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | };
64 |
65 | export default FeatureModal;
66 |
67 | const ModalContent = styled(Modal.Content)`
68 | left: 50%;
69 | top: 50%;
70 | transform: translate(-50%, -50%);
71 | border-radius: 12px;
72 | box-sizing: border-box;
73 | padding: 24px;
74 | background-color: #fff;
75 | width: 500px;
76 | height: 300px;
77 | `;
78 |
79 | const ModalButtonSetter = styled.div`
80 | position: absolute;
81 | bottom: 12px;
82 | right: 12px;
83 | display: flex;
84 | gap: 5px;
85 | `;
86 |
87 | const ModalButtonSetterSecond = styled.div`
88 | position: absolute;
89 | top: 12px;
90 | right: 12px;
91 | `;
92 |
93 | const StyledModalClose = styled.button`
94 | border: 1px solid #ddd;
95 | width: 40px;
96 | height: 40px;
97 | border-radius: 100%;
98 | cursor: pointer;
99 | :hover {
100 | border: 1px solid #333;
101 | }
102 | `;
103 |
104 | const MiniModalContent = styled(Modal.Content)`
105 | left: 50%;
106 | top: 50%;
107 | transform: translate(-50%, -50%);
108 | border-radius: 12px;
109 | box-sizing: border-box;
110 | padding: 24px;
111 | background-color: #fff;
112 | width: 300px;
113 | height: 200px;
114 | `;
115 |
--------------------------------------------------------------------------------
/src/features/Select.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import * as Select from "../components/select/Select";
4 |
5 | const FeatureSelect = () => {
6 | return (
7 |
8 |
9 | Select
10 |
11 |
12 | 트리거
13 |
14 |
15 | 리액트
16 | 자바
17 | 스프링
18 | 리액트네이티브
19 |
20 |
21 |
22 |
23 |
24 | 트리거
25 |
26 | 리액트
27 | 자바
28 | 스프링
29 | 리액트네이티브
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default FeatureSelect;
39 |
40 | const SelectTrigger = styled(Select.Trigger)`
41 | border: 1px solid #ddd;
42 | height: 40px;
43 | width: 300px;
44 | background-color: #fff;
45 | border-radius: 12px;
46 | `;
47 |
48 | const OverSelectList = styled(Select.List)`
49 | border: 1px solid #eee;
50 | border-radius: 12px;
51 | z-index: 2;
52 | background-color: #fff;
53 | width: 300px;
54 | position: absolute;
55 | top: 650px;
56 | `;
57 |
58 | const SelectList = styled(Select.List)`
59 | border: 1px solid #eee;
60 | border-radius: 12px;
61 | z-index: 2;
62 | background-color: #fff;
63 | width: 300px;
64 | position: absolute;
65 | top: 50px;
66 | `;
67 |
68 | const SelectOption = styled(Select.Option)`
69 | font-size: 12px;
70 | display: flex;
71 | align-items: center;
72 | padding-left: 12px;
73 | height: 40px;
74 |
75 | :hover {
76 | background-color: #eee;
77 | }
78 |
79 | :first-child {
80 | border-top-left-radius: 12px;
81 | border-top-right-radius: 12px;
82 | }
83 |
84 | :last-child {
85 | border-bottom-left-radius: 12px;
86 | border-bottom-right-radius: 12px;
87 | }
88 | `;
89 |
90 | const Container = styled.div`
91 | border: 3px solid #ddd;
92 | height: 200px;
93 | overflow: hidden;
94 | position: relative;
95 | margin-top: 50px;
96 | `;
97 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App";
4 |
5 | const root = ReactDOM.createRoot(document.getElementById("root"));
6 | root.render(
7 |
8 |
9 |
10 | );
11 |
--------------------------------------------------------------------------------