React - Tab 컴포넌트 만들기

Moolbum·2023년 11월 19일
0

React

목록 보기
20/23
post-thumbnail

이전에 블로그에 적었던 버튼 클릭 후 원하는 컴포넌트를 렌더링 하는 방법을 고도화했어요

구현 목표

- Headless 기반으로 제작하여 원하는 방식으로 스타일을 지정할 수 있도록 대응
- 키보드 이벤트를 추가해서 화살표 이동에 따라 TabItem이 Focus가 되어 해당 Tab의 값에 따라 원하는 컴포넌트를 렌더링

구현 방식

  • 밖에서 버튼의 Ref에 접근하기 위해 forwardRef를 사용합니다.
  • 컴포넌트의 interface는 HTML의 Button의 기본 타입을 사용할 수 있도록
    ButtonHTMLAttributes 을 확장해서 사용합니다.
  • 시맨틱 한 코드를 위해 button element에 role을 부여해서 tab이란 것을 알립니다.
  • onKeyDown이벤트에 ArrowRight, ArrowLeft 일 때의 이벤트를 추가합니다.
    ArrowRight: buttonRef.current.nextSibling 현재 버튼 엘리먼트의 다음 엘리먼트 감지
    ArrowLeft: buttonRef.current.previousSibling 현재 버튼 엘리먼트의 이전 엘리먼트 감지

TabItem.tsx

import React, {
  ButtonHTMLAttributes,
  forwardRef,
  useEffect,
  useRef,
} from "react";

interface TabItemProps<T> extends ButtonHTMLAttributes<HTMLButtonElement> {
  tabValue: T;
  onFocusItem?: (item: T) => void;
}

const TabItem = forwardRef<HTMLButtonElement, TabItemProps<any>>(
  (props, ref) => {
    const buttonRef = useRef<HTMLButtonElement>();
    const { tabValue, onFocusItem, onFocus, onKeyDown, ...restProps } = props;

    useEffect(() => {
      buttonRef.current?.focus();
    }, []);

    return (
      <button
        ref={(node) => {
          if (node) {
            buttonRef.current = node;
          }
          if (ref) {
            if (typeof ref === "function") {
              ref(node);
            } else {
              ref.current = node;
            }
          }
        }}
        onFocus={(e) => {
          onFocusItem?.(tabValue);
          onFocus?.(e);
        }}
        role="tab"
        onKeyDown={(e) => {
          if (buttonRef.current) {
            if (e.key === "ArrowRight") {
              const next = buttonRef.current.nextSibling as HTMLButtonElement;

              if (next) {
                next.focus();
              }
            }

            if (e.key === "ArrowLeft") {
              const prev = buttonRef.current
                .previousSibling as HTMLButtonElement;

              if (prev) {
                prev.focus();
              }
            }
          }
          onKeyDown?.(e);
        }}
        {...restProps}
      />
    );
  }
);

export default TabItem;

실제 사용 예시

  • TabItem을 import 후 위에서 Styled-component로 스타일을 추가합니다.
    스타일을 추가하지 않으면 HTML 기본 button이 나와요
  • TabList는 컴포넌트의 상단에 문자열의 상수로 관리
import React, { useRef, useState } from "react";
import styled, { css } from "styled-components";
import TabItem from "../../components/atoms/TabItem";

type TabType = "NAME" | "ADDRESS" | "AGE";
const TAB_LIST: TabType[] = ["NAME", "ADDRESS", "AGE"];

function Tabs() {
  const buttonRef = useRef<HTMLButtonElement>(null);
  const [selectTab, setSelectTab] = useState<TabType>("NAME");

  const renderTabItemTitle = (type: TabType) => {
    switch (type) {
      case "NAME":
        return <span>Name</span>;

      case "ADDRESS":
        return <span>Address</span>;

      case "AGE":
        return <span>Age</span>;
    }
  };

  const renderTabItemContent = (tabType: TabType) => {
    switch (tabType) {
      case "NAME":
        return <>Name</>;

      case "ADDRESS":
        return <>Address</>;

      case "AGE":
        return <>Age</>;
    }
  };

  return (
    <>
      <StyledTabs role="tablist">
        {TAB_LIST.map((item, idx) => {
          return (
            <StyeldTabItem
              ref={item === selectTab ? buttonRef : null}
              key={idx.toString()}
              tabIndex={item === selectTab ? 0 : -1}
              isSelect={item === selectTab}
              tabValue={item}
              onFocusItem={(value) => {
                setSelectTab(value as TabType);
              }}
            >
              {renderTabItemTitle(item)}
            </StyeldTabItem>
          );
        })}
      </StyledTabs>

      {renderTabItemContent(selectTab)}
    </>
  );
}

export default Tabs;

const StyledTabs = styled.nav`
  display: flex;
  margin: 24px 0;
  border-bottom: 1px solid gray;
`;

const StyeldTabItem = styled(TabItem)<{
  isSelect?: boolean;
}>`
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
  padding: 8px 16px;
  cursor: pointer;
  background: none;
  box-shadow: none;

  ${({ isSelect }) => {
    return isSelect
      ? css`
          border: none;
          border-bottom: 3px solid red;
        `
      : css`
          border: none;
        `;
  }}
`;

키보드 화살표 leftArrow , rightArrow와 원하는 Tab 클릭시
하단의 원하는 컴포넌트를 렌더링 시킬 수 있습니다!

배포 주소 : https://react-masterclass-eta.vercel.app/tabs

profile
Junior Front-End Developer 👨‍💻

0개의 댓글