Jieunny์ ๋ธ๋ก๊ทธ
S3) Unit 3. [์ค์ต] Styled Component ๋ก UI ๊ตฌํํ๊ธฐ ๋ณธ๋ฌธ
S3) Unit 3. [์ค์ต] Styled Component ๋ก UI ๊ตฌํํ๊ธฐ
Jieunny 2023. 2. 21. 18:00๐ฃ ์์ฃผ ์ฌ์ฉํ๋ UI๋ฅผ Styled Component๋ก ๊ตฌํํด๋ณด์
1๏ธโฃ Modal
โ๏ธ ์กฐ๊ฑด
โ Modal ์ฐฝ์ ๋์ ์ค Open Modal ๋ฒํผ์ด ์์ด์ผ ํฉ๋๋ค. (24 ms)
โ ๋ฒํผ์ ํด๋ฆญํ๋ฉด Modal ์ปดํฌ๋ํธ ๋ด๋ถ์ Modal๋ฐฐ๊ฒฝ, Modal์ฐฝ div ์๋ฆฌ๋จผํธ๊ฐ ๋ ๋๋ง๋์ด์ผ ํฉ๋๋ค. (162 ms)
โ Modal์ฐฝ์ด ๋ ๋๋ง ๋ ์ํ์์ ๋ฒํผ์ ๋ค์ ํด๋ฆญํ๋ฉด Modal๋ฐฐ๊ฒฝ, Modal์ฐฝ div ์๋ฆฌ๋จผํธ๊ฐ ์ฌ๋ผ์ง๋๋ค. (84 ms)
โ Modal ์ฐฝ ๋ฐ์ ํด๋ฆญํ๋ฉด, Modal๋ฐฐ๊ฒฝ, Modal์ฐฝ div ์๋ฆฌ๋จผํธ๊ฐ ์ฌ๋ผ์ง๋๋ค. (46 ms)
โ๏ธ ๊ตฌํ ์ฝ๋
import { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
export const ModalContainer = styled.div`
// TODO : Modal์ ๊ตฌํํ๋๋ฐ ์ ์ฒด์ ์ผ๋ก ํ์ํ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
display: flex;
justify-content: center;
align-items: center;
position: relative;
height: 100%;
`;
export const ModalBackdrop = styled.div`
// TODO : Modal์ด ๋ด์ ๋์ ๋ฐฐ๊ฒฝ์ ๊น์์ฃผ๋ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
position: absolute;
justify-content: center;
align-items: center;
background-color: rgb(0,0,0,0.3);
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
width: 100%;
height: 100%;
`;
export const ModalBtn = styled.button`
background-color: var(--coz-purple-600);
text-decoration: none;
border: none;
padding: 20px;
color: white;
border-radius: 30px;
cursor: grab;
`;
export const ModalView = styled.div.attrs((props) => ({
// attrs ๋ฉ์๋๋ฅผ ์ด์ฉํด์ ์๋์ ๊ฐ์ด div ์๋ฆฌ๋จผํธ์ ์์ฑ์ ์ถ๊ฐํ ์ ์์ต๋๋ค.
role: 'dialog',
}))`
// TODO : Modal์ฐฝ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
display: flex;
justify-content: center;
align-items: center;
background-color: white;
flex-direction: column;
width: 50%;
height: 50%;
border-radius: 20px;
> .text {
font-size: 2rem;
margin-bottom: 10px;
}
`;
export const ExitBtn = styled(ModalBtn)`
display: flex;
justify-content: center;
align-items: center;
width: 30%;
height: 30%;
font-size: 0.8rem;
`
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
//const modalOutside = useRef();
const openModalHandler = () => {
// TODO : isOpen์ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฉ์๋๋ฅผ ๊ตฌํํฉ๋๋ค.
setIsOpen(!isOpen);
};
// const handleClickOutside = ({ target }) => {
// if (isOpen && !modalOutside.current.contains(target)) setIsOpen(false);
// };
// useEffect(() => {
// window.addEventListener("click", handleClickOutside);
// return () => {
// window.removeEventListener("click", handleClickOutside);
// };
// }, []);
return (
<>
<ModalContainer>
<ModalBtn onClick={openModalHandler}>
{isOpen ? 'Opened!' : 'Open Modal'}
{/* TODO : ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ํ์ฉํด์ Modal์ด ์ด๋ฆฐ ์ํ(isOpen์ด true์ธ ์ํ)์ผ ๋๋ ModalBtn์ ๋ด๋ถ ํ
์คํธ๊ฐ 'Opened!' ๋ก Modal์ด ๋ซํ ์ํ(isOpen์ด false์ธ ์ํ)์ผ ๋๋ ModalBtn ์ ๋ด๋ถ ํ
์คํธ๊ฐ 'Open Modal'์ด ๋๋๋ก ๊ตฌํํด์ผ ํฉ๋๋ค. */}
</ModalBtn>
{isOpen ?
<ModalBackdrop onClick={openModalHandler}>
<ModalView onClick={(event) => event.stopPropagation()}>
<div className="text">์๋
ํ์ธ์. ๋ชจ๋ฌ์
๋๋ค!</div>
<ExitBtn>Close</ExitBtn>
</ModalView>
</ModalBackdrop>
: null}
{/* TODO : ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ํ์ฉํด์ Modal์ด ์ด๋ฆฐ ์ํ(isOpen์ด true์ธ ์ํ)์ผ ๋๋ง ๋ชจ๋ฌ์ฐฝ๊ณผ ๋ฐฐ๊ฒฝ์ด ๋ฐ ์ ์๊ฒ ๊ตฌํํด์ผ ํฉ๋๋ค. */}
</ModalContainer>
</>
);
};
โฐ ๋ชจ๋ฌ ์ฐฝ ๋ฐ๊นฅ ๋ถ๋ถ์ ๋๋ฅด๋ฉด ๋ชจ๋ฌ ์ฐฝ์ด ๊บผ์ ธ์ผ ํ๋ ๋ถ๋ถ์ด ๊ตฌํํ๊ธฐ ๊น๋ค๋ก์ ๋ค.
โฐ ์ง๊ธ๋ ํ
์คํธ๋ ํต๊ณผํ์ง๋ง ๊น๋ํ์ง ๋ชปํ๋ค. Close ๋ฒํผ, ๋ชจ๋ฌ ์ฐฝ, ๋ชจ๋ฌ ๋ฐ๊นฅ ๋ถ๋ถ์ ๋๋ ์ ๋ ์ฐฝ์ด ๊บผ์ง๋ค.
โฐ ๋ฑ ๋ชจ๋ฌ ๋ฐ๊นฅ ๋ถ๋ถ๋ง ๋๋ ์ ๋ ๊บผ์ง๊ฒ ํ๋ ๋ฐฉ๋ฒ์ด ์์ํ
๋ฐใ
=> ์ด๋ฐ ํ์์ ์ด๋ฒคํธ ๋ฒ๋ธ๋ง์ด๋ผ๊ณ ํ๋ฉฐ, stopPropagation์ ์ฌ์ฉํด์ ๋ฐฉ์งํ ์ ์๋ค!!
(์์์ ํด๋ฆญํ๋๋ฐ ๋ค์ ์๋ ๋ถ๋ชจ๊ฐ ํด๋ฆญ๋ฌ๋ค๊ณ ์ธ์ํด์ ๋ถ๋ชจ์ ๊ฑธ๋ฆฐ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ด๋ค)
โ๏ธ ์์ฐ ํ๋ฉด
2๏ธโฃ Toggle
โ๏ธ ์กฐ๊ฑด
โ Toggle container๋ฅผ ํด๋ฆญํ๋ฉด 'toggle--checked' class๊ฐ ์ถ๊ฐ๋์ด์ผ ํฉ๋๋ค. (29 ms)
โ Toggle switch๋ฅผ ํด๋ฆญํ๋ฉด 'toggle--checked' class๊ฐ ์ถ๊ฐ๋์ด์ผ ํฉ๋๋ค. (14 ms)
โ๏ธ ๊ตฌํ ์ฝ๋
import { useState } from 'react';
import styled from 'styled-components';
const ToggleContainer = styled.div`
position: relative;
margin-top: 8rem;
left: 47%;
cursor: pointer;
> .toggle-container {
width: 50px;
height: 24px;
border-radius: 30px;
background-color: #8b8b8b;
transition: 0.5s;
} >.toggle--checked {
background-color: rgb(55, 0, 195);
transition: 0.5s;
}
// TODO : .toggle--checked ํด๋์ค๊ฐ ํ์ฑํ ๋์์ ๊ฒฝ์ฐ์ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
> .toggle-circle {
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
transition: 0.5s;
} >.toggle--checked {
left: 27px;
transition: 0.5s;
}
// TODO : .toggle--checked ํด๋์ค๊ฐ ํ์ฑํ ๋์์ ๊ฒฝ์ฐ์ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
`;
const Desc = styled.div`
// TODO : ์ค๋ช
๋ถ๋ถ์ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
width: 100%;
height: 30%;
display: flex;
justify-content: center;
align-items: center;
`;
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// TODO : isOn์ ์ํ๋ฅผ ๋ณ๊ฒฝํ๋ ๋ฉ์๋๋ฅผ ๊ตฌํํฉ๋๋ค.
setisOn(!isOn);
};
return (
<>
<ToggleContainer
// TODO : ํด๋ฆญํ๋ฉด ํ ๊ธ์ด ์ผ์ง ์ํ(isOn)๋ฅผ boolean ํ์
์ผ๋ก ๋ณ๊ฒฝํ๋ ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค.
onClick={toggleHandler}
>
{/* TODO : ์๋์ div ์๋ฆฌ๋จผํธ 2๊ฐ๊ฐ ์์ต๋๋ค. ๊ฐ๊ฐ์ ํด๋์ค๋ฅผ 'toggle-container', 'toggle-circle' ๋ก ์ง์ ํ์ธ์. */}
{/* TIP : Toggle Switch๊ฐ ON์ธ ์ํ์ผ ๊ฒฝ์ฐ์๋ง toggle--checked ํด๋์ค๋ฅผ div ์๋ฆฌ๋จผํธ 2๊ฐ์ ๋ชจ๋ ์ถ๊ฐํฉ๋๋ค. ์กฐ๊ฑด๋ถ ์คํ์ผ๋ง์ ํ์ฉํ์ธ์. */}
<div className={isOn ? "toggle-container toggle--checked" : "toggle-container"} />
<div className={isOn ? "toggle-circle toggle--checked" : "toggle-circle"} />
</ToggleContainer>
{/* TODO : Desc ์ปดํฌ๋ํธ๋ฅผ ํ์ฉํด์ผ ํฉ๋๋ค. */}
<Desc>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</Desc>
{/* TIP: Toggle Switch๊ฐ ON์ธ ์ํ์ผ ๊ฒฝ์ฐ์ Desc ์ปดํฌ๋ํธ ๋ด๋ถ์ ํ
์คํธ๋ฅผ 'Toggle Switch ON'์ผ๋ก, ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ 'Toggle Switch OFF'๊ฐ ๋ฉ๋๋ค. ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ํ์ฉํ์ธ์. */}
</>
);
};
โ๏ธ ์์ฐ ํ๋ฉด
3๏ธโฃ Tab
โ๏ธ ์กฐ๊ฑด
Tab Menu๋ map์ ์ด์ฉํ ๋ฐ๋ณต์ ํตํด ๋ณด์ฌ์ผ ํฉ๋๋ค.
โ ul ์๋ฆฌ๋จผํธ ์๋์๋ li ์๋ฆฌ๋จผํธ๊ฐ 3๊ฐ ์์ด์ผ ํฉ๋๋ค. (20 ms)
Tab Menu ์กฐ์์ ์ํ currentTab ์ํ๊ฐ ์กด์ฌํด์ผ ํฉ๋๋ค.
โ currentTab ์ด๊ธฐ๊ฐ์ 0๋ฒ์งธ ์ธ๋ฑ์ค์ฌ์ผ ํฉ๋๋ค. (7 ms)
Tab Menu๋ฅผ ํด๋ฆญํ๋ฉด currentTab ์ํ๊ฐ ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
โ Tab ๋ฉ๋ด๋ฅผ ํด๋ฆญํ๋ฉด selectMenuHandler ํจ์๊ฐ ์คํ๋๊ณ , ํด๋น Tab ๋ฉ๋ด์ index ๊ฐ์ด ์ธ์๋ก ์ ๋ฌ๋ฉ๋๋ค (7 ms)
โ ํด๋ฆญํ Tab ๋ฉ๋ด๋ง className ์ด "submenu focused"๋ก ๋ณ๊ฒฝ๋ฉ๋๋ค. (17 ms)
โ๏ธ ๊ตฌํ ์ฝ๋
import { useState } from 'react';
import styled from 'styled-components';
// TODO: Styled-Component ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด TabMenu ์ Desc ์ปดํฌ๋ํธ์ CSS๋ฅผ ๊ตฌํํฉ๋๋ค.
const TabMenu = styled.ul`
background-color: #dcdcdc;
color: rgba(73, 73, 73, 0.5);
font-weight: bold;
display: flex;
flex-direction: row;
justify-items: center;
align-items: center;
list-style: none;
margin-bottom: 7rem;
.submenu {
${'' /* ๊ธฐ๋ณธ Tabmenu ์ ๋ํ CSS๋ฅผ ๊ตฌํํฉ๋๋ค. */}
width: calc(100% / 3);
display: flex;
justify-content: center;
align-items: center;
height: 50px;
}
.focused {
${'' /* ์ ํ๋ Tabmenu ์๋ง ์ ์ฉ๋๋ CSS๋ฅผ ๊ตฌํํฉ๋๋ค. */}
background-color: rgb(55, 0, 195);
color: white;
}
& div.desc {
text-align: center;
}
`;
const Desc = styled.div`
text-align: center;
`;
export const Tab = () => {
// TIP: Tab Menu ์ค ํ์ฌ ์ด๋ค Tab์ด ์ ํ๋์ด ์๋์ง ํ์ธํ๊ธฐ ์ํ
// currentTab ์ํ์ currentTab์ ๊ฐฑ์ ํ๋ ํจ์๊ฐ ์กด์ฌํด์ผ ํ๊ณ , ์ด๊ธฐ๊ฐ์ 0 ์
๋๋ค.
const [currentTab, setCurrentTab] = useState(0);
const menuArr = [
{ name: 'Tab1', content: 'Tab menu ONE' },
{ name: 'Tab2', content: 'Tab menu TWO' },
{ name: 'Tab3', content: 'Tab menu THREE' },
];
const selectMenuHandler = (index) => {
// TIP: parameter๋ก ํ์ฌ ์ ํํ ์ธ๋ฑ์ค ๊ฐ์ ์ ๋ฌํด์ผ ํ๋ฉฐ, ์ด๋ฒคํธ ๊ฐ์ฒด(event)๋ ์ฐ์ง ์์ต๋๋ค
// TODO : ํด๋น ํจ์๊ฐ ์คํ๋๋ฉด ํ์ฌ ์ ํ๋ Tab Menu ๊ฐ ๊ฐฑ์ ๋๋๋ก ํจ์๋ฅผ ์์ฑํ์ธ์.
setCurrentTab(index);
//console.log(index);
};
return (
<>
<div>
<TabMenu>
{/*TODO: ์๋ ํ๋์ฝ๋ฉ๋ ๋ด์ฉ ๋์ ์, map์ ์ด์ฉํ ๋ฐ๋ณต์ผ๋ก ์ฝ๋๋ฅผ ์์ ํฉ๋๋ค.*/}
{/*TIP: li ์๋ฆฌ๋จผํธ์ class๋ช
์ ๊ฒฝ์ฐ ์ ํ๋ tab ์ 'submenu focused' ๊ฐ ๋๋ฉฐ,
๋๋จธ์ง 2๊ฐ์ tab์ 'submenu' ๊ฐ ๋ฉ๋๋ค.*/}
{/* <li className="submenu">{menuArr[0].name}</li>
<li className="submenu">{menuArr[1].name}</li>
<li className="submenu">{menuArr[2].name}</li> */}
{menuArr.map((menu, index) => {
return(
<li key={menu.name.slice(3)} className={currentTab === index ? "submenu focused" : "submenu"} onClick={() => selectMenuHandler(index)}>
{menu.name}
</li>
)
})}
</TabMenu>
<Desc>
{/*TODO: ์๋ ํ๋์ฝ๋ฉ๋ ๋ด์ฉ ๋์ ์, ํ์ฌ ์ ํ๋ ๋ฉ๋ด ๋ฐ๋ฅธ content๋ฅผ ํ์ํ์ธ์*/}
<p>{menuArr[currentTab].content}</p>
</Desc>
</div>
</>
);
};
โ๏ธ ์์ฐ ํ๋ฉด
4๏ธโฃ Tag
โ๏ธ ์กฐ๊ฑด
Enter ํค ํ
์คํธ
โ ์๋ก์ด ํ๊ทธ๋ฅผ ์ถ๊ฐํ๋ ๊ธฐ๋ฅ์ Enter ํค์ ์ํด ์คํ๋์ด์ผ ํฉ๋๋ค. (20 ms)
โ Enter ํค๋ฅผ ๋๋ฅด๋ฉด tag ๋ฅผ ์ถ๊ฐํ๋ addTags ํจ์๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค. (10 ms)
โ Enterํค๋ฅผ ๋๋ฅด๋ฉด ์ค์ ํ๊ทธ๊ฐ ์ถ๊ฐ๋์ด์ผ ํฉ๋๋ค. (96 ms)
โ ์๋ฌด๊ฒ๋ ์
๋ ฅํ์ง ์์ ๊ฒฝ์ฐ, Enter๋ฅผ ๋๋ฌ๋ ํ๊ทธ๊ฐ ์ถ๊ฐ๋์ง ์์์ผ ํฉ๋๋ค. (29 ms)
โ ์ค๋ณต๋ ๊ฐ์ด ์ด๋ฏธ ์กด์ฌํ๋ ๊ฒฝ์ฐ, Enter๋ฅผ ๋๋ฌ๋ ํ๊ทธ๊ฐ ์ถ๊ฐ๋์ง ์์์ผ ํฉ๋๋ค. (39 ms)
โ ์๋ก์ด ํ๊ทธ๊ฐ ์ถ๊ฐ๋๋ฉด ์
๋ ฅ์ฐฝ์ ์ด๊ธฐํ๋์ด์ผ ํฉ๋๋ค. (9 ms)
tags์ ํ๋ฉด ์ถ๋ ฅ๊ณผ ์ ๊ฑฐ ๊ธฐ๋ฅ ํ
์คํธ
โ tags ๋ฐฐ์ด์ ๋ชจ๋ ํ๊ทธ๊ฐ ํ๋ฉด์ ๋ณด์ฌ์ ธ์ผ ํฉ๋๋ค. (19 ms)
โ tag ๋ฅผ ์ญ์ ํ ์ ์๋ ์์ด์ฝ(x)์ด ์์ด์ผ ํ๋ฉฐ, ํด๋น ์์ด์ฝ(x)์ ํด๋ฆญํ๋ฉด removeTags ํจ์๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค. (4 ms)
โ ์ญ์ ์์ด์ฝ์ ๋๋ฅด๋ฉด ํ๋ฉด์์ Tag๊ฐ ์ญ์ ๋์ด์ผ ํฉ๋๋ค. (17 ms)
โ๏ธ ๊ตฌํ ์ฝ๋
import { useState } from 'react';
import styled from 'styled-components';
// TODO: Styled-Component ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ํ์ฉํด ์ฌ๋ฌ๋ถ๋ง์ tag ๋ฅผ ์์ ๋กญ๊ฒ ๊พธ๋ฉฐ ๋ณด์ธ์!
export const TagsInput = styled.div`
margin: 8rem auto;
display: flex;
align-items: flex-start;
flex-wrap: wrap;
min-height: 48px;
width: 480px;
padding: 0 8px;
border: 1px solid rgb(214, 216, 218);
border-radius: 6px;
> ul {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 8px 0 0 0;
> .tag {
width: auto;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
padding: 0 8px;
font-size: 14px;
list-style: none;
border-radius: 6px;
margin: 0 8px 8px 0;
background: var(--coz-purple-600);
> .tag-close-icon {
display: block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
font-size: 14px;
margin-left: 8px;
color: var(--coz-purple-600);
border-radius: 50%;
background: #fff;
cursor: pointer;
}
}
}
> input {
flex: 1;
border: none;
height: 46px;
font-size: 14px;
padding: 4px 0 0 0;
:focus {
outline: transparent;
}
}
&:focus-within {
border: 1px solid var(--coz-purple-600);
}
`;
export const Tag = () => {
const initialTags = ['CodeStates', 'kimcoding'];
const [tags, setTags] = useState(initialTags);
const removeTags = (indexToRemove) => {
// TODO : ํ๊ทธ๋ฅผ ์ญ์ ํ๋ ๋ฉ์๋๋ฅผ ์์ฑํ์ธ์.
const removeArr = tags.filter((tag, index) => {
return index !== indexToRemove;
})
setTags(removeArr);
};
const addTags = (event) => {
// TODO : tags ๋ฐฐ์ด์ ์๋ก์ด ํ๊ทธ๋ฅผ ์ถ๊ฐํ๋ ๋ฉ์๋๋ฅผ ์์ฑํ์ธ์.
// ์ด ๋ฉ์๋๋ ํ๊ทธ ์ถ๊ฐ ์ธ์๋ ์๋ 3 ๊ฐ์ง ๊ธฐ๋ฅ์ ์ํํ ์ ์์ด์ผ ํฉ๋๋ค.
// - ์ด๋ฏธ ์
๋ ฅ๋์ด ์๋ ํ๊ทธ์ธ์ง ๊ฒ์ฌํ์ฌ ์ด๋ฏธ ์๋ ํ๊ทธ๋ผ๋ฉด ์ถ๊ฐํ์ง ๋ง๊ธฐ
// - ์๋ฌด๊ฒ๋ ์
๋ ฅํ์ง ์์ ์ฑ Enter ํค ์
๋ ฅ์ ๋ฉ์๋ ์คํํ์ง ๋ง๊ธฐ
// - ํ๊ทธ๊ฐ ์ถ๊ฐ๋๋ฉด input ์ฐฝ ๋น์ฐ๊ธฐ
//console.log(event);
if(event.key==="Enter" && tags.includes(event.target.value)===false && event.target.value.length !== 0){
setTags([...tags,event.target.value]);
event.target.value = '';
}
};
return (
<>
<TagsInput>
<ul id="tags">
{tags.map((tag, index) => (
<li key={index} className="tag">
<span className="tag-title">{tag}</span>
<span className="tag-close-icon" onClick={() => removeTags(index)}>
{/* TODO : tag-close-icon์ด tag-title ์ค๋ฅธ์ชฝ์ x ๋ก ํ์๋๋๋ก ํ๊ณ ,
์ญ์ ์์ด์ฝ์ click ํ์ ๋ removeTags ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค. */}
x
</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
onKeyUp={(e) => {
{
/* ํค๋ณด๋์ Enter ํค์ ์ํด addTags ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค. */
addTags(e)
}
}}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};
โ๏ธ ์์ฐ ํ๋ฉด
5๏ธโฃ Autocomplete
โ๏ธ ์กฐ๊ฑด
input ๊ธฐ๋ฅ ํ
์คํธ
โ input ์๋ฆฌ๋จผํธ์ onChange ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ ๋ถ๋ ค์์ผ ํฉ๋๋ค. (9 ms)
โ input ๊ฐ์ ์ญ์ ํ ์ ์๋ ๋ฒํผ์ด ์์ด์ผ ํฉ๋๋ค. (5 ms)
โ ์ญ์ ๋ฒํผ ํด๋ฆญ ์ input value๊ฐ ์ญ์ ๋์ด์ผ ํฉ๋๋ค. (47 ms)
drop down ๊ธฐ๋ฅ ํ
์คํธ
โ input ๊ฐ์ด ํฌํจ ๋ ์๋ ์์ฑ ์ถ์ฒ drop down ๋ฆฌ์คํธ๊ฐ ๋ณด์ฌ์ผ ํฉ๋๋ค. (8 ms)
โ drop down ํญ๋ชฉ์ ๋ง์ฐ์ค๋ก ํด๋ฆญ ์, input ๊ฐ ๋ณ๊ฒฝ์ ๋ฐ๋ผ drop down ๋ชฉ๋ก์ด ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค. (52 ms)
โ drop down ํญ๋ชฉ์ ๋ง์ฐ์ค๋ก ํด๋ฆญ ์, input ๊ฐ์ด ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค. (45 ms)
โ drop down ํญ๋ชฉ์ ๋ง์ฐ์ค๋ก ํด๋ฆญ ์, input ๊ฐ์ด ์ด๋ฏธ ์์ด๋ input ๊ฐ์ด drop down ํญ๋ชฉ์ ๊ฐ์ผ๋ก ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค. (33 ms)
โ๏ธ ๊ตฌํ ์ฝ๋
import { useState, useEffect } from 'react';
import styled from 'styled-components';
const deselectedOptions = [
'rustic',
'antique',
'vinyl',
'vintage',
'refurbished',
'์ ํ',
'๋นํฐ์ง',
'์ค๊ณ A๊ธ',
'์ค๊ณ B๊ธ',
'๊ณจ๋ํ'
];
/* TODO : ์๋ CSS๋ฅผ ์์ ๋กญ๊ฒ ์์ ํ์ธ์. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '1rem 1rem 0 0';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';
export const InputContainer = styled.div`
margin-top: 8rem;
background-color: #ffffff;
display: flex;
flex-direction: row;
padding: 1rem;
border: 1px solid rgb(223, 225, 229);
border-radius: ${inactiveBorderRadius};
z-index: 3;
box-shadow: 0;
&:focus-within {
box-shadow: ${boxShadow};
}
> input {
flex: 1 0 0;
background-color: transparent;
border: none;
margin: 0;
padding: 0;
outline: none;
font-size: 16px;
}
> div.delete-button {
cursor: pointer;
}
`;
export const DropDownContainer = styled.ul`
background-color: #ffffff;
display: block;
margin-left: auto;
margin-right: auto;
list-style-type: none;
margin-block-start: 0;
margin-block-end: 0;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 0px;
margin-top: -1px;
padding: 0.5rem 0;
border: 1px solid rgb(223, 225, 229);
border-radius: 0 0 1rem 1rem;
box-shadow: ${boxShadow};
z-index: 3;
> li {
padding: 0 1rem;
}
`;
export const Autocomplete = () => {
/**
* Autocomplete ์ปดํฌ๋ํธ๋ ์๋ 3๊ฐ์ง state๊ฐ ์กด์ฌํฉ๋๋ค. ํ์์ ๋ฐ๋ผ์ state๋ฅผ ๋ ๋ง๋ค ์๋ ์์ต๋๋ค.
* - hasText state๋ input๊ฐ์ ์ ๋ฌด๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
* - inputValue state๋ input๊ฐ์ ์ํ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
* - options state๋ input๊ฐ์ ํฌํจํ๋ autocomplete ์ถ์ฒ ํญ๋ชฉ ๋ฆฌ์คํธ๋ฅผ ํ์ธํ ์ ์์ต๋๋ค.
*/
const [hasText, setHasText] = useState(false);
let [inputValue, setInputValue] = useState('');
const [options, setOptions] = useState(deselectedOptions);
// useEffect๋ฅผ ์๋์ ๊ฐ์ด ํ์ฉํ ์๋ ์์ต๋๋ค.
useEffect(() => {
if (inputValue === '') {
setHasText(false);
}
}, [inputValue]);
// TODO : input๊ณผ dropdown ์ํ ๊ด๋ฆฌ๋ฅผ ์ํ handler๊ฐ ์์ด์ผ ํฉ๋๋ค.
const handleInputChange = (event) => {
/**
* handleInputChange ํจ์๋
* - input๊ฐ ๋ณ๊ฒฝ ์ ๋ฐ์๋๋ change ์ด๋ฒคํธ ํธ๋ค๋ฌ์
๋๋ค.
* - input๊ฐ๊ณผ ์ํ๋ฅผ ์ฐ๊ฒฐ์ํฌ ์ ์๊ฒ controlled component๋ก ๋ง๋ค ์ ์๊ณ
* - autocomplete ์ถ์ฒ ํญ๋ชฉ์ด dropdown์ผ๋ก ์์๊ฐ๊ฐ ๋ณํ๋์ด ๋ณด์ฌ์ง ์ ์๋๋ก ์ํ๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค.
*
* handleInputChange ํจ์๋ฅผ ์์ฑํ์ฌ ์๋ 3๊ฐ์ง ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
*
* onChange ์ด๋ฒคํธ ๋ฐ์ ์
* 1. input๊ฐ ์ํ์ธ inputValue๊ฐ ์ ์ ํ๊ฒ ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
* 2. input๊ฐ ์ ๋ฌด ์ํ์ธ hasText๊ฐ ์ ์ ํ๊ฒ ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
* 3. autocomplete ์ถ์ฒ ํญ๋ชฉ์ธ options์ ์ํ๊ฐ ์ ์ ํ๊ฒ ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
* Tip : options์ ์ํ์ ๋ฐ๋ผ dropdown์ผ๋ก ๋ณด์ฌ์ง๋ ํญ๋ชฉ์ด ๋ฌ๋ผ์ง๋๋ค.
*/
setInputValue(event.target.value);
setHasText(true);
};
const handleDropDownClick = (clickedOption) => {
/**
* handleDropDownClick ํจ์๋
* - autocomplete ์ถ์ฒ ํญ๋ชฉ์ ํด๋ฆญํ ๋ ๋ฐ์๋๋ click ์ด๋ฒคํธ ํธ๋ค๋ฌ์
๋๋ค.
* - dropdown์ ์ ์๋ ํญ๋ชฉ์ ๋๋ ์ ๋, input๊ฐ์ด ํด๋น ํญ๋ชฉ์ ๊ฐ์ผ๋ก ๋ณ๊ฒฝ๋๋ ๊ธฐ๋ฅ์ ์ํํฉ๋๋ค.
*
* handleInputChange ํจ์๋ฅผ ์์ฑํ์ฌ ์๋ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
*
* onClick ์ด๋ฒคํธ ๋ฐ์ ์
* 1. input๊ฐ ์ํ์ธ inputValue๊ฐ ์ ์ ํ๊ฒ ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
* 2. autocomplete ์ถ์ฒ ํญ๋ชฉ์ธ options์ ์ํ๊ฐ ์ ์ ํ๊ฒ ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
*/
setInputValue(deselectedOptions[clickedOption]);
};
const handleDeleteButtonClick = () => {
/**
* handleDeleteButtonClick ํจ์๋
* - input์ ์ค๋ฅธ์ชฝ์ ์๋ X๋ฒํผ ํด๋ฆญ ์ ๋ฐ์๋๋ click ์ด๋ฒคํธ ํธ๋ค๋ฌ์
๋๋ค.
* - ํจ์ ์์ฑ์ ์๋ฃํ์ฌ input๊ฐ์ ํ ๋ฒ์ ์ญ์ ํ๋ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
*
* handleDeleteButtonClick ํจ์๋ฅผ ์์ฑํ์ฌ ์๋ ๊ธฐ๋ฅ์ ๊ตฌํํฉ๋๋ค.
*
* onClick ์ด๋ฒคํธ ๋ฐ์ ์
* 1. input๊ฐ ์ํ์ธ inputValue๊ฐ ๋น ๋ฌธ์์ด์ด ๋์ด์ผ ํฉ๋๋ค.
*/
setInputValue('');
};
// Advanced Challenge: ์ํ ํ์ดํ ํค ์
๋ ฅ ์ dropdown ํญ๋ชฉ์ ์ ํํ๊ณ , Enter ํค ์
๋ ฅ ์ input๊ฐ์ ์ ํ๋ dropdown ํญ๋ชฉ์ ๊ฐ์ผ๋ก ๋ณ๊ฒฝํ๋ handleKeyUp ํจ์๋ฅผ ๋ง๋ค๊ณ ,
// ์ ์ ํ ์ปดํฌ๋ํธ์ onKeyUp ํธ๋ค๋ฌ๋ฅผ ํ ๋นํฉ๋๋ค. state๊ฐ ์ถ๊ฐ๋ก ํ์ํ์ง ๊ณ ๋ฏผํ๊ณ , ํ์ ์ state๋ฅผ ์ถ๊ฐํ์ฌ ์ ์ํ์ธ์.
return (
<div className='autocomplete-wrapper'>
<InputContainer>
{/* TODO : input ์๋ฆฌ๋จผํธ๋ฅผ ์์ฑํ๊ณ input๊ฐ(value)์ state์ ์ฐ๊ฒฐํฉ๋๋ค. handleInputChange ํจ์์ input๊ฐ ๋ณ๊ฒฝ ์ ํธ์ถ๋ ์ ์๊ฒ ์ฐ๊ฒฐํฉ๋๋ค. */}
<input onChange={handleInputChange} value={inputValue} type="text"></input>
{/* TODO : ์๋ div.delete-button ๋ฒํผ์ ๋๋ฅด๋ฉด input ๊ฐ์ด ์ญ์ ๋์ด dropdown์ด ์์ด์ง๋ handler ํจ์๋ฅผ ์์ฑํฉ๋๋ค. */}
<div className='delete-button' onClick={handleDeleteButtonClick}>×</div>
</InputContainer>
{/* TODO : input ๊ฐ์ด ์์ผ๋ฉด dropdown์ด ๋ณด์ด์ง ์์์ผ ํฉ๋๋ค. ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ ์ด์ฉํด์ ๊ตฌํํ์ธ์. */}
{inputValue ? <DropDown options={options} handleComboBox={handleDropDownClick} input={inputValue} /> : null}
</div>
);
};
export const DropDown = ({ options, handleComboBox, input }) => {
return (
<DropDownContainer>
{/* TODO : input ๊ฐ์ ๋ง๋ autocomplete ์ ํ ์ต์
์ด ๋ณด์ฌ์ง๋ ์ญํ ์ ํฉ๋๋ค. */}
{options && options.map((option, index) => {
if(option.includes(input)){
return <li key={index} onClick={() => handleComboBox(index)}>{option}</li>
}
})}
</DropDownContainer>
);
};
โ๏ธ ์์ฐ ํ๋ฉด
6๏ธโฃ ClickToEdit
โ๏ธ ์กฐ๊ฑด
input ์ฐฝ ํด๋ฆญ ๊ธฐ๋ฅ ํ
์คํธ
โ ์
๋ ฅ ๊ฐ๋ฅ ์ํ๋ก ๋ณ๊ฒฝํ ์ ์๋ onClick ์ด๋ฒคํธ ํธ๋ค๋ฌ๊ฐ span ์๋ฆฌ๋จผํธ์ ์์ด์ผ ํฉ๋๋ค. (4 ms)
โ ํฌ์ปค์ค๊ฐ ์ ์ธ๋๋ ์ด๋ฒคํธ onBlur์ ํธ๋ค๋ฌ๊ฐ input ์๋ฆฌ๋จผํธ์ ์์ด์ผ ํฉ๋๋ค. (5 ms)
โ ํ
์คํธ ์์ญ์ ํด๋ฆญํ๋ฉด ์
๋ ฅ ๊ฐ๋ฅ ์ํ๋ก ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค. (4 ms)
โ ์
๋ ฅ ๊ฐ๋ฅ ์ํ์ผ ๋ ๋ณํ๊ฐ ๊ฐ์ง๋๋ฉด ์๋ก์ด ๊ฐ์ ์ค์ ํ๋ ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค. (5 ms)
โ ์
๋ ฅ ๊ฐ๋ฅ ์ํ์ผ ๋ input์ด ์๋ ๋ค๋ฅธ ๊ณณ์ ํด๋ฆญํ๋ฉด ์
๋ ฅ ๋ถ๊ฐ ์ํ๊ฐ ๋์ด์ผ ํฉ๋๋ค. (9 ms)
โ ์
๋ ฅ ๊ฐ๋ฅ ์ํ์ผ ๋ input์ด ์๋ ๋ค๋ฅธ ๊ณณ์ ํด๋ฆญํ๋ฉด input์ ๊ฐ์ด span์ ๋ด๊ฒจ์ผ ํฉ๋๋ค. (10 ms)
โ๏ธ ๊ตฌํ ์ฝ๋
import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';
export const InputBox = styled.div`
text-align: center;
display: inline-block;
width: 150px;
height: 30px;
border: 1px #bbb dashed;
border-radius: 10px;
margin-left: 1rem;
`;
export const InputEdit = styled.input`
text-align: center;
display: inline-block;
width: 150px;
height: 30px;
`;
export const InputView = styled.div`
text-align: center;
align-items: center;
margin-top: 3rem;
div.view {
margin-top: 3rem;
}
`;
export const MyInput = ({ value, handleValueChange }) => {
const inputEl = useRef(null);
const [isEditMode, setEditMode] = useState(false);
const [newValue, setNewValue] = useState(value);
useEffect(() => {
if (isEditMode) {
inputEl.current.focus();
}
}, [isEditMode]);
useEffect(() => {
setNewValue(value);
}, [value]);
const handleClick = () => {
// TODO : isEditMode ์ํ๋ฅผ ๋ณ๊ฒฝํฉ๋๋ค.
setEditMode(!isEditMode);
};
const handleBlur = () => {
// TODO : Edit๊ฐ ๋ถ๊ฐ๋ฅํ ์ํ๋ก ๋ณ๊ฒฝํฉ๋๋ค.
setEditMode(false);
handleValueChange(newValue);
};
const handleInputChange = (e) => {
// TODO : ์ ์ฅ๋ value๋ฅผ ์
๋ฐ์ดํธํฉ๋๋ค.
setNewValue(e.target.value);
};
return (
<InputBox>
{isEditMode ? (
<InputEdit
type='text'
value={newValue}
ref={inputEl}
// TODO : ํฌ์ปค์ค๋ฅผ ์์ผ๋ฉด Edit๊ฐ ๋ถ๊ฐ๋ฅํ ์ํ๋ก ๋ณ๊ฒฝ๋๋ ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค.
onBlur={handleBlur} // ํฌ์ปค์ค ํด์ฌ -> onBlur
// TODO : ๋ณ๊ฒฝ ์ฌํญ์ด ๊ฐ์ง๋๋ฉด ์ ์ฅ๋ value๋ฅผ ์
๋ฐ์ดํธ ๋๋ ๋ฉ์๋๊ฐ ์คํ๋์ด์ผ ํฉ๋๋ค.
onChange={handleInputChange}l
/>
) : (
<span
// TODO : ํด๋ฆญํ๋ฉด Edit๊ฐ ๊ฐ๋ฅํ ์ํ๋ก ๋ณ๊ฒฝ๋์ด์ผ ํฉ๋๋ค.
onClick={handleClick}
>{newValue}</span>
)}
</InputBox>
);
}
const cache = {
name: '๊น์ฝ๋ฉ',
age: 20
};
export const ClickToEdit = () => {
const [name, setName] = useState(cache.name);
const [age, setAge] = useState(cache.age);
return (
<>
<InputView>
<label>์ด๋ฆ</label>
<MyInput value={name} handleValueChange={(newValue) => setName(newValue)} />
</InputView>
<InputView>
<label>๋์ด</label>
<MyInput value={age} handleValueChange={(newValue) => setAge(newValue)} />
</InputView>
<InputView>
<div className='view'>์ด๋ฆ {name} ๋์ด {age}</div>
</InputView>
</>
);
};
โ๏ธ ์์ฐ ํ๋ฉด
๐ ์ฐธ๊ณ ์๋ฃ https://infiduk.github.io/2022/09/08/react-onclick.html
'CodeStates > Training' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
S3) Unit 4. [์ค์ต] CMarket Redux (0) | 2023.02.27 |
---|---|
S3) Unit 4. [์ค์ต] CMarket Hooks (2) | 2023.02.24 |
S3) Unit 2. [์ค์ต] Figma ํด๋ก (์ธ์คํ๊ทธ๋จ) (0) | 2023.02.17 |
S3) Unit 2. [์ค์ต] Figma (0) | 2023.02.16 |
S3) Unit 1. [์ค์ต] Tree UI (0) | 2023.02.14 |