Bare minimum Requirement

Modal, Toggle, Tab, Autocomplete, ClickToEdit, Tag UI 컴포넌트들을 만들어보는데 목적

Modal Component


Modal UI 컴포넌트는 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것

Modal을 통해 stopPropagation() 알게 되었다.

import { useState } 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;
  /* background-color: white; */
  height: 60%;
  width: 100%;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: rgba(0, 0, 0, 0.3); // rgba (적색, 녹색, 청색, 투명도)
  height: 50%;
  width: 90%;
  position: fixed;
  border-radius: 10px;
`;

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;
  align-items: center;
  justify-content: center;
  background-color: white;
  flex-direction: column;
  height: 100px;
  width: 300px;
  border-radius: 10px;
  .exitBtn {
    font-size: 20px;
    margin: 10px 0px 30px 0px;
    cursor: pointer;
  }
  .helloCodestates {
    font-size: 20px;
    font-weight: bold;
  }
`;

export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = () => {
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    setIsOpen(!isOpen);
  };

  return (
    <>
      <ModalContainer>
        {/* TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.*/}
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 
			ModalBtn의 내부 텍스트가 'Opened!' 로 
			Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 
			ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
        <ModalBtn onClick={openModalHandler}>
          {isOpen === true ? 'Opened' : 'Open Modal'}
        </ModalBtn>
        {/* TODO : 조건부 렌더링을 활용해서 
		Modal이 열린 상태(isOpen이 true인 상태)일 때만 
		모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
        {isOpen === true ? (
          <ModalBackdrop onClick={openModalHandler}>
            {/* 
	             ModalView는 실제 모달의 내용을 나타내는 부분입니다.
	             이 부분을 클릭해도 모달이 닫히지 않도록 e.stopPropagation()을 사용합니다.
            */}
            <ModalView onClick={(e) => e.stopPropagation()}>
              <div className="exitBtn" onClick={openModalHandler}>
                &times; {/* X 버튼 기호 */}
              </div>
              <div className="helloCodestates">HELLO CODESTATES!</div>
            </ModalView>
          </ModalBackdrop>
        ) : null}
      </ModalContainer>
    </>
  );
};

Toggle Component


Toggle UI 컴포넌트는 두 가지 상태만을 가지고 있는 스위치

transition을 통해 왼쪽 오른쪽으로 토글이 이동한다.

.toggle--checked에서 & 없이 하니까 작동이 안되더라… &에 대해 익힐수 있었다.

참고 사이트

https://leesoo7595.github.io/til/css/2021/08/07/TIL_0807/

 

[TIL] styled-component 활용하기

styled-components 사용법 공부하기

leesoo7595.github.io

엠퍼샌드(&)를 사용하여 해당 컴포넌트를 재참조, 중첩된 스타일에서 부모를 참조할 때 사용, 현재의 부모를 참조하여 스타일을 적용

linear-gradient을 통해서 서서히 색이 차는 느낌을 만들 수 있었다

import { useState } from 'react';
import styled from 'styled-components';

const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  cursor: pointer;
  display: flex;
  flex-direction: column;
  > .toggle-container {
    width: 50px;
    height: 24px;
    border-radius: 30px;
    /* background-color: #8b8b8b; */
    background-position: right;
    background: linear-gradient(to left, blue 50%, red 50%) right;
    background-size: 200%;
    transition: 1s;
    /* background-color: blue; */
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    // 엠퍼샌드(&)를 사용하여 해당 컴포넌트를 재참조, 중첩된 스타일에서 부모를 참조할 때 사용, 현재의 부모를 참조하여 스타일을 적용
    &.toggle--checked {
      background-position: left;
      background: linear-gradient(to right, red 50%, blue 50%) left;
      background-size: 200%;
      transition: 1s;
      /* background-color: red; */
    }
    /* &.toggle--unchecked {
      transition: 0.5s;
      background-color: blue;
    } */
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    transition: 1s;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    &.toggle--checked {
      transition: 1s;
      left: 28px;
    }
    /* &.toggle--unchecked {
      transition: 0.5s;
      left: 0px;
    } */
  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  margin-top: 10px;
  margin-left: -40px;
  .switch--on {
    color: red;
  }
  .switch--off {
    color: blue;
  }
`;

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={`toggle-container ${isOn ? 'toggle--checked' : ''}`} />
        <div className={`toggle-circle ${isOn ? 'toggle--checked' : ''}`} />
        {isOn === true ? (
          <Desc>
            <p className="switch--on">Toggle Switch ON</p>
          </Desc>
        ) : (
          <Desc>
            <p className="switch--off">Toggle Switch OFF</p>
          </Desc>
        )}
      </ToggleContainer>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
    </>
  );
};

Tab Component


Tab UI 컴포넌트는 동일한 메뉴 라인에서 뷰를 전환할 때 사용

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를 구현합니다. */}
    display: flex;
    justify-content: space-between;
    padding: 20px;
    flex-grow: 1;
    cursor: pointer;
  }

  .focused {
    ${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    background-color: darkblue;
    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 menuArr = [
    { name: 'Tab1', content: 'Tab menu ONE' },
    { name: 'Tab2', content: 'Tab menu TWO' },
    { name: 'Tab3', content: 'Tab menu THREE' },
  ];

  const [currentTab, setCurrentTab] = useState(0);

  const selectMenuHandler = (index) => {
    // TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
    // TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
    setCurrentTab(index);
  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됩니다.*/}

          {/* 
          map 메소드를 사용하여 menuArr 배열의 각 요소에 대해 반복합니다.
          각 탭은 li 엘리먼트로 표시되며, 클래스와 클릭 이벤트 핸들러가 지정된다.
        */}
          {menuArr.map((tab, index) => {
            return (
              <li
                key={index}
                className={currentTab === index ? 'submenu focused' : 'submenu'}
                onClick={() => selectMenuHandler(index)}
              >
                {/* 탭의 이름을 표시 */}
                {tab.name}
              </li>
            );
          })}
        </TabMenu>
        <Desc>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

Tag Component


Tag UI 컴포넌트는 레이블 지정을 통해 구성이나 분류에 도움이 되는 키워드 집합을 만들 때 자주 사용

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 : 태그를 삭제하는 메소드를 완성하세요.
    // 태그 상태를 업데이트하여 지정된 인덱스의 태그를 필터링하여 제거
    setTags(
      tags.filter((tag) => {
        // 인덱스가 제거할 인덱스와 같지 않은 태그만 반환
        return tag !== tags[indexToRemove];
      })
    );
  };

  const addTags = (event) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기

    // 입력된 값을 얻고 양 끝의 공백을 제거한다.
    let value = event.target.value.trim();
    // Enter 키가 눌렸고, value가 비어 있지 않고, tags 배열에 해당 값이 포함되어 있지 않으면 실행한다.
    if (event.key === 'Enter' && !tags.includes(value) && value) {
      // 현재 태그 배열에 새로운 값을 추가하고 상태를 업데이트 한다.
      setTags([...tags, value]);
      // 이벤트 타겟의 값을 비워 입력 창을 초기화 한다.
      event.target.value = '';
    }
  };

  return (
    <>
      <TagsInput>
        <ul id="tags">
          {/* 태그 배열을 순회하며 각 태그를 리스트 아이템으로 렌더링한다. */}
          {tags.map((tag, index) => (
            <li key={index} className="tag">
              <span className="tag-title">{tag}</span>
              {/* 태그를 삭제하는 아이콘을 나타내는 부분.
                클릭 시 removeTags 메소드가 실행되어 해당 태그를 삭제. */}
              <span
                className="tag-close-icon"
                onClick={() => removeTags(index)}
              >
                &times;
                {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                            삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(event) => {
            // 키 입력 이벤트가 발생할 때 addTags 메소드를 실행
            addTags(event);
            {
              /* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
            }
          }}
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

+ Recent posts