모달,태그,토글,탭 구현
공부

모달,태그,토글,탭 구현

작성일: 2022년 12월 26일0

1. Modal.js

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

export const ModalContainer = styled.div`
  // TODO : Modal을 감싸는 div css
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
`;

export const ModalBackdrop = styled.div`
  // TODO : ModalPopup의 백그라운드 css
  background-color: rgba(0, 0, 0, 0.3);
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
`;

export const ModalBtn = styled.button`
  /* transform: rotate(0.5turn); */
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #333;
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
  width: 150px;
  height: 50px;
  font-size: 16px;
`;

// attrs : 속성의 값을 추가하거나 접근하는 메서드
export const ModalView = styled.div.attrs((props) => ({
  role: "dialog",
}))`
  // TODO : Modal창 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: white;
  width: 300px;
  height: 150px;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  > button {
    display: flex;
    justify-content: end;
    align-items: flex-end;
    background-color: transparent;
    border: none;
    color: black;
    font-size: 12px;
  }
`;

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

  const openModalHandler = () => {.
    setIsOpen(!isOpen);
    // isOpen의 역상태를 설정하므로써 boolean값으로 되어있는 모달 버튼을 껏다켰다 할 수 있음
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}>
          {isOpen ? "Opened!" : "Open Modal"}
        </ModalBtn>
        {isOpen ? (
          <ModalBackdrop onClick={openModalHandler}>
            <ModalView
              onClick={(e) => {
                e.stopPropagation();
              }}
            >
              <button onClick={openModalHandler}>X</button>
              <div>ㅎㅇㅎㅇ</div>
            </ModalView>
          </ModalBackdrop>
        ) : null}
      </ModalContainer>
    </>
  );
};

구현 Point

  • styled-components의 css 작성 시 자식 노드는 > {} 사용
  • styled.input.attrs()
  • 삼항 연산자를 사용하여 true / false 값을 설정하는 방법
  • 이벤트핸들러를 노드의 속성으로 부여할 때 빈 함수의 형태로 이벤트를 호출
  • 2. 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;
        &.toggle--checked {
          background-color: gold;
          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;
        // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
        &.toggle--checked {
          left: 27px;
          transition: 0.5s;
        }
      }
    `;
    
    const Desc = styled.div`
      // TODO : 설명 부분의 CSS를 구현합니다.
      display: flex;
      align-items: center;
      justify-content: center;
      color: #1d1d1d;
      font-size: 18px;
    `;
    
    export const Toggle = () => {
      const [isOn, setisOn] = useState(false);
    
      const toggleHandler = () => {
        setisOn(!isOn);
      };
    
      return (
        <>
          <ToggleContainer
          // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
          >
            {/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
            {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
            <div
              onClick={toggleHandler}
              className={`toggle-container ${isOn ? "toggle--checked" : ""}`}
            />
            <div
              onClick={toggleHandler}
              className={`toggle-circle ${isOn ? "toggle--checked" : ""}`}
            />
          </ToggleContainer>
          {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
          {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
          <Desc>{isOn ? "Toggle Switch ON" : "Toggle Switch OFF"}</Desc>
        </>
      );
    };
    

    구현 Point

  • 노드에 종속된 클래스나 상태를 추가할 때 & 사용.
  • 3. Tab.js

    import { useState } from "react";
    import styled from "styled-components
    
    const TabMenu = styled.ul`
      background-color: #f2f2f2;
      color: rgba(73, 73, 73, 0.5);
      display: flex;
      flex-direction: row;
      justify-items: center;
      align-items: center;
      list-style: none;
      margin-bottom: 7rem;
      height: 60px;
    
      .submenu {
        position: relative;
        display: inline-block;
        font-size: 16px;
        font-family: Arial, Helvetica, sans-serif;
        font-weight: bold;
        margin: 10px 10px 10px 30px;
        color: #1d1d1d;
        cursor: grab;
        ${"" /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
      }
    
      .focused {
        height: 1em;
        border-bottom: 1px solid;
        transition: 0.5s;
        margin-top: 8px;
        color: #ff7f00;
        ${"" /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */};
      }
    
      & 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);
      };
    
      return (
        <>
          <div>
            <TabMenu>
              {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
              {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                      나머지 2개의 tab은 'submenu' 가 됩니다.*/}
              {menuArr.map((element, index) => {
                return (
                  <li
                    key={index}
                    className={`${
                      currentTab === index ? "submenu focused" : "submenu"
                    }`}
                    onClick={() => selectMenuHandler(index)}
                  >
                    {element.name}
                  </li>
                );
              })}
            </TabMenu>
            <Desc>
              {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
              <p>{menuArr[currentTab].content}</p>
            </Desc>
          </div>
        </>
      );
    };
    

    구현 Point

  • map 고차함수를 사용하여 배열 안의 객체 구조인 tab 메뉴를 불러옴
  • 4. Tag.js

    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((element, index) => index !== indexToRemove));
      };
    
      const addTags = (event) => {
        if (
          event.key === "Enter" &&
          event.target.value !== "" &&
          !tags.includes(event.target.value)
        ) {
          setTags([...tags, event.target.value]);
          event.target.value = "";
        }
      };
    
      return (
        <>
          <TagsInput>
            <ul id="tags">
              {tags.map((tag, index) => (
                <li key={index} className="tag">
                  {tag}
                   removeTags(index)}
                    className="tag-close-icon"
                  >
                    X
                  
                </li>
              ))}
            </ul>
            <input
              className="tag-input"
              type="text"
              onKeyUp={(e) => {
                addTags(e);
              }}
              placeholder="Press enter to add tags"
            />
          </TagsInput>
        </>
      );
    };
    

    구현 Point

  • 태그를 삭제하는 이벤트핸들러 작성 시 setTag의 인자로 filter()된 tag를 사용하여 출력된 X버튼이 가지고 있는 index값과 일치하는 태그는 걸러진다
  • 태그를 추가할 때 조건문의 조건 설정을 명확하게 넣어주고 event.key === "Enter" && event.target.value !== "" && !tags.includes(event.target.value) satTag의 인자로 spread된 새로운 배열을 전달setTags([...tags, event.target.value])한다.