티스토리 뷰
Modal.js
import { useState } from 'react';
import styled from 'styled-components';
export const ModalContainer = styled.div`
// TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
display:flex;
align-items:center;
justify-content:center;
//가운데 정렬 하지만 높이가 가운데가 되지 않았다. 이유는 부모가 높이가 없었다.
height:950px;
//그래서 부모에게 높이를 주었다.
`;
export const ModalBackdrop = styled.div`
// TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
display:flex;
align-items:center;
justify-content: center;
height:100%;
width:100%;
position:fixed;
background-color: lightblue;
`;
export const ModalBtn = styled.button`
background-color: lightpink;
text-decoration: none;
border: none;
padding: 20px;
color: black;
border-radius: 30px;
cursor: grab;
//올려놨을 때 마우스가 변함
`;
export const ModalView = styled.div.attrs((props) => ({
// attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
role: 'dialog',
//styled.div은 div 태그를 스타일링 하기 위한 것이며,
//attrs() 메소드를 이용하여 div 엘리먼트에 role 속성을 추가하고 있다.
//role 속성은 웹 접근성을 위한 것으로, 스크린 리더 사용자에게 모달 창임을 알려즌디/
}))`
display: flex;
align-items: center;
justify-content: center;
background-color: white;
width:200px;
height:100px;
border-radius:30px;
position: relative;
//position: relative는 요소를 자신의 원래 위치를 기준으로
//상대적으로 이동시키기 위해 사용하는 속성
>.close-btn{
position: absolute;
top:3px;
right:10px;
cursor:pointer;
}
`;
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = () => {
// TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
setIsOpen(!isOpen);
};
return (
<>
<ModalContainer>
<ModalBtn onClick={openModalHandler}>
{isOpen ? 'Opened!' : 'Open Modal'}
</ModalBtn>
{/* TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.*/}
{/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
{/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
{isOpen ? <ModalBackdrop onClick={openModalHandler}>
//isOpen이 true면 열렸다는 뜻
//모달외 배경을 클릭하면 isOpen로 false로 닫힌다.
<ModalView onClick={(event) => {event.stopPropagation()}}>
{/*부모 컴포넌트에 이벤트 걸려있으면 자식 컴포넌트에도 이벤트가 걸려서 막기 위해서 stopPropagation()을 쓴다.*/}
<div className="close-btn" onClick={openModalHandler}>×</div>
//x를 클릭하면 isOpen로 false로 닫힌다.
<div>HELLO CODESTATES!</div>
</ModalView>
</ModalBackdrop> : null}
</ModalContainer>
</>
);
};
position:fixed
해당 요소를 고정 위치에 놓고 스크롤해도 화면상에서 고정되도록 만들어주는 속성
position: relative를 사용하면, 해당 요소는 일반적인 문서 흐름을 유지하면서, 이동할 위치를 기준으로 상대적으로 위치를 이동할 수 있습니다. 이때, top, right, bottom, left 속성을 사용하여 이동할 위치를 지정할 수 있습니다.
반면에 position: absolute를 사용하면, 해당 요소는 부모 요소를 기준으로 절대적인 위치를 이동할 수 있습니다. 즉, 일반적인 문서 흐름에서 벗어나서 위치를 지정할 수 있습니다. 이때, top, right, bottom, left 속성을 사용하여 위치를 지정
isOpen이 true여야 모달이 렌더링이 되는 건데 false가 되면 렌더링 되던게 사리는게 옳다.
event.stopPropagation() 함수는 이벤트 캡처링과 버블링을 막는 역할을 한다.
모달창 내부의 요소를 클릭했을 때 이벤트가 모달창 밖으로 전파되지 않도록 막아주는 역할을 한다.
Toggle.js
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;
// TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
background-position: right;
//배경 이미지의 위치를 컨테이너 오른쪽 끝으로 설정
background: linear-gradient(to left, green 50%,pink 50%) right;
//그라데이션을 왼쪽으로 향하도록 설정
//green 색상으로 50%까지 채우고, 그 이후부터 pink 색상으로 50%까지 채우도록 설정
//배경 이미지의 오른쪽 끝부터 그라데이션 배경을 채우도록 설정
background-size: 200%;
transition: 1s;
&.toggle--check{
background-position: left;
background: linear-gradient(to right, pink 50%, green 50%) left;
background-size: 200%;
transition: 1s;
}
}
> .toggle-circle {
position: absolute;
//.toggle-circle 클래스는 토글 버튼의 원형 부분을 나타내며,
//해당 요소는 position 속성을 absolute로 설정하여 부모 요소를 기준으로 상대적 위치를 지정
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 100%;
background-color: #ffffff;
// TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
transition: 1s;
&.toggle--check {
left: 30px;
transition: 1s;
}
}
`;
//toggle-container와 toggle-circle은 각각 토글 스위치의 배경과 스위치 원형을 표시
const Desc = styled.div`
// TODO : 설명 부분의 CSS를 구현합니다.
display: flex;
justify-content: center;
//margin-top: 0.5rem;
`;
//Desc 컴포넌트는 토글 스위치의 상태를 설명하는 텍스트를 표시
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
setisOn(!isOn);
};
//toggleHandler 함수는 클릭할 때마다 isOn 변수의 상태를 변경하는 역할을 한다.
//ToggleContainer의 className 속성값에 isOn 변수의 상태에 따라
//toggle--checked 클래스를 추가하거나 제거함으로써,
//토글 스위치가 켜졌는지 꺼졌는지 시각적으로 나타낸다.
return (
<>
<ToggleContainer onClick={toggleHandler}>
//ToggleContainer는 토글 스위치 전체 영역을 감싸는 컨테이너 역할을 한다.
//onClick 이벤트 핸들러를 등록하여,
//해당 영역을 클릭할 때마다 toggleHandler 함수를 호출
<div className={`toggle-container ${isOn ? 'toggle--check' : ''}`}/>
<div className={`toggle-circle ${isOn ? 'toggle--check' : ''}`}/>
</ToggleContainer>
{/*TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.*/}
{/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
{/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
{/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
{/* TIP: Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
<Desc><div>{isOn ? 'Toggle Switch ON' : 'Toggle Switch OFF'}</div></Desc>
//isOn 변수의 상태에 따라 'Toggle Switch ON' 또는 'Toggle Switch OFF'가 표시
</>
);
};
Tab.js
import { useState } from 'react';
import styled from 'styled-components';
// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.
const TabMenu = styled.ul`
background-color: lightgray;
color: black;
font-weight: bold;
border-radius : 20px;
display: flex;
flex-direction: row;
justify-items: center;
align-items: center;
list-style: none;
.submenu {
${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
display: flex;
justify-content: center;
flex-grow: 1;
cursor: pointer;
}
.focused {
${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다. */}
background-color: pink;
color: white;
height: 100%;
display: flex;
align-items: center;
transition: 1s;
}
& div.desc {
text-align: center;
}
`;
const Desc = styled.div`
text-align: center;
`;
export const Tab = () => {
// TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
// currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
const menuArr = [
{ name: 'Tab1', content: 'Tab menu ONE' },
{ name: 'Tab2', content: 'Tab menu TWO' },
{ name: 'Tab3', content: 'Tab menu THREE' },
];
//menuArr 상수 배열에는 탭 메뉴의 이름과 내용이 객체 형태로 저장
const [currentTab,setCurrentTab] = useState(0);
//currentTab과 setCurrentTab은 useState를 이용하여
//초기값 0을 가진 상태 변수와 해당 상태를 갱신할 수 있는 함수
//currentTab은 현재 선택된 탭 메뉴의 인덱스를 저장하는 상태값
//초기 값으로는 첫 번째 탭 메뉴(인덱스 0)가 선택
const selectMenuHandler = (index) => {
// TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
// TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
setCurrentTab(index);
};
//selectMenuHandler 함수는 인덱스 값을 매개변수로 받아와
//해당 인덱스 값에 해당하는 탭 메뉴를 현재 선택된 탭 메뉴로 설정하는 함수
//화면에 표시될 JSX 코드가 작성
return (
<>
<div>
<TabMenu>
{/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
{/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며,
나머지 2개의 tab은 'submenu' 가 됩니다.*/}
{menuArr.map((el, index) => {
return <li key={index}
className={`${index === currentTab ? 'submenu focused' : 'submenu'}`}
onClick={() => selectMenuHandler(index)}>{el.name}</li>
})}
//TabMenu 컴포넌트 내부에는 menuArr 배열의 내용을
//map 메소드를 이용해 반복적으로 생성한 li 엘리먼트들이 포함
//currentTab 조건에 따라 submenu focused 클래스가 추가되며,
//그렇지 않을 경우에는 submenu 클래스가 추가
//li 엘리먼트를 클릭할 경우, 해당 인덱스에 대한 selectMenuHandler 함수가 호출
{/*} <li className="submenu">{menuArr[0].name}</li>
<li className="submenu">{menuArr[1].name}</li>
<li className="submenu">{menuArr[2].name}</li>*/}
</TabMenu>
<Desc>
{/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
<p>{menuArr[currentTab].content}</p>
//Desc 컴포넌트에는 menuArr[currentTab] 배열에서
//현재 선택된 인덱스에 해당하는 객체의 content를 표시하는 <p> 엘리먼트가 작성
</Desc>
</div>
</>
);
};
Tag.js
outline: transparent는 웹 페이지에서 포커스를 받은 요소에 대한 아웃라인을 숨기는 CSS 속성
filter 메소드는 주어진 함수가 true를 반환하는 배열 요소만을 모아 새로운 배열을 만든다.
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: 30px;
> 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: pink;
> .tag-close-icon {
display: block;
width: 16px;
height: 16px;
line-height: 16px;
text-align: center;
font-size: 14px;
margin-left: 8px;
color: black;
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 ;
}
`;
export const Tag = () => {
const initialTags = ['CodeStates', 'kimcoding'];
const [tags, setTags] = useState(initialTags);
const removeTags = (indexToRemove) => {
// TODO : 태그를 삭제하는 메소드를 완성하세요.
setTags(tags.filter((tag) =>{
return tag !== tags[indexToRemove]
}));
};
// tag와 tags[indexToRemove]를 비교하여, 제거할 인덱스를 제외한 나머지 태그만을 반환
//addTags 함수는 event 객체를 인자로 받아 태그를 추가하는 함수
//그를 추가하는 것뿐만 아니라, 기존 태그가 중복되지 않도록 체크하고,
//입력 값이 없을 경우 실행되지 않도록 막는 것
const addTags = (event) => {
// TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
// 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
// - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
// - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
// - 태그가 추가되면 input 창 비우기
let value = event.target.value.trim();
//event.target.value로 입력한 값을 가져와 trim() 메서드로
//앞뒤 공백을 제거한 후 value 변수에 할당
if(event.key === 'Enter' && !tags.includes(value) && value){
setTags([...tags, value]);
event.target.value ="";
}
//이후, 입력한 값이 빈 문자열이 아니고,
//tags 배열에 없는 새로운 값이면 setTags 함수를 통해 새로운 값을 tags 배열에 추가
else if(event.key === 'Enter' && !value){
event.target.value ="";
}
//마지막으로, event.target.value를 빈 문자열로 초기화하여 input 창을 비움
};
return (
<>
{/*tags 배열을 사용하여 ul 태그 안에 li 태그를 생성하고
, map 함수를 사용하여 각각의 태그에 대한 요소를 생성
각 태그 안에는 tag-title 클래스를 가진 span 태그와 tag-close-icon 클래스를 가진 span 태그
tag-title 클래스를 가진 span 태그는 태그 이름을 나타내고,
tag-close-icon 클래스를 가진 span 태그는 삭제 버튼 역할*/}
<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)}>×
</span>
{/* TODO : tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
</li>
))}
</ul>
{/*마지막으로, input 태그를 사용하여 새로운 태그를 입력
className 속성으로 tag-input 클래스를 할당
onKeyUp 이벤트를 사용하여 Enter 키 입력 시 addTags 함수를 실행하도록 설정*/ }
<input
className="tag-input"
type="text"
onKeyUp={(event) => {
/* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
addTags(event)
}}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};
'codestates > section3' 카테고리의 다른 글
과제2 - Cmarket redux (0) | 2023.02.24 |
---|---|
Unit4 - [React] 상태 관리 (0) | 2023.02.23 |
과제 - React Custom Component advanced (0) | 2023.02.22 |
과제2 - Figma 클론 (0) | 2023.02.17 |
과제1 - Figma 컴포넌트 구현 (0) | 2023.02.16 |