ReactでTooltipを自作した

## 概要

今回も備忘録的な記事内容ですが、ReactでTooltipを作る際に調べて自作したので、その記録として記事にしました。
下のテーブルは、Tooltipを表示させるにあたって、任意の場所に表示させるためのイメージです。
テキストから見て、「左上」「真上」「右上」「左横」「右横」「左下」「真下」「右下」の8箇所のいずれかに表示させたいと思います。

|◯|◯|◯|
|:---:|:---:|:---:|
|◯|テキスト|◯|
|◯|◯|◯|

以下、実際に書いたコードです。
CSSは[emotion](https://emotion.sh/docs/introduction)を使用しています。

```Tooltip.js
/** @jsxImportSource @emotion/react */
import {css} from "@emotion/react"
import {useState} from "react"

export const Tooltip = ({message, children, offset, vertical, horizontal}) => {
  const [isShow, setIsShow] = useState(false)
  const handleShow = () => {
    setIsShow(true)
  }

  const handleHide = () => {
    setIsShow(false)
  }

  return (
    <div
      css={TooltipWrap}
      onMouseEnter={handleShow}
      onMouseLeave={handleHide}
      onFocus={handleShow}
      onBlur={handleHide}
    >
      <div css={TooltipContent(isShow, vertical, horizontal, offset)}>
        {message}
      </div>
      {children}
    </div>
  )
}

const TooltipWrap = css({
  position: "relative"
})

const TooltipContent = (
  isShow,
  vertical = "top",
  horizontal = "center",
  offset = 0
) =>
  css({
    position: "absolute",
    color: "#fff",
    background: "#000",
    padding: "4px 8px",
    borderRadius: 4,
    visibility: isShow ? "visible" : "hidden",
    maxWidth: 280,
    width: "max-content",

    ...(vertical === "top" &&
      horizontal === "center" && {
        bottom: `calc(100% + ${offset}px + 4px)`,
        left: "50%",
        transform: "translateX(-50%)",
        "&::after": {
          content: "''",
          width: 8,
          height: 4,
          position: "absolute",
          backgroundColor: "#000",
          top: "100%",
          left: "50%",
          transform: "translateX(-50%)",
          clipPath: "polygon(0% 0%, 100% 0%, 50% 100%, 50% 100%)"
        }
      }),

    ...(vertical === "top" &&
      horizontal === "left" && {
        bottom: `calc(100% + ${offset}px + 4px)`,
        right: "calc(50% - 12px)",
        "&::after": {
          content: "''",
          width: 8,
          height: 4,
          position: "absolute",
          backgroundColor: "#000",
          top: "100%",
          right: 8,
          clipPath: "polygon(0% 0%, 100% 0%, 50% 100%, 50% 100%)"
        }
      }),

    ...(vertical === "top" &&
      horizontal === "right" && {
        bottom: `calc(100% + ${offset}px + 4px)`,
        left: "calc(50% - 12px)",
        "&::after": {
          content: "''",
          width: 8,
          height: 4,
          position: "absolute",
          backgroundColor: "#000",
          top: "100%",
          left: 8,
          clipPath: "polygon(0% 0%, 100% 0%, 50% 100%, 50% 100%)"
        }
      }),

    ...(vertical === "middle" &&
      horizontal === "left" && {
        top: "50%",
        transform: "translateY(-50%)",
        right: `calc(100% + ${offset}px + 4px)`,
        "&::after": {
          content: "''",
          width: 4,
          height: 8,
          position: "absolute",
          backgroundColor: "#000",
          top: "50%",
          transform: "translateY(-50%)",
          left: "100%",
          clipPath: "polygon(0% 0%, 100% 50%, 100% 50%, 0% 100%)"
        }
      }),

    ...(vertical === "middle" &&
      horizontal === "right" && {
        top: "50%",
        transform: "translateY(-50%)",
        left: `calc(100% + ${offset}px + 4px)`,
        "&::after": {
          content: "''",
          width: 4,
          height: 8,
          position: "absolute",
          backgroundColor: "#000",
          top: "50%",
          transform: "translateY(-50%)",
          right: "100%",
          clipPath: "polygon(0% 50%, 100% 0%, 100% 100%, 0% 50%)"
        }
      }),

    ...(vertical === "bottom" &&
      horizontal === "left" && {
        top: `calc(100% + ${offset}px + 4px)`,
        right: "calc(50% - 12px)",
        "&::after": {
          content: "''",
          width: 8,
          height: 4,
          position: "absolute",
          backgroundColor: "#000",
          bottom: "100%",
          right: 8,
          clipPath: "polygon(50% 0%, 50% 0%, 100% 100%, 0% 100%)"
        }
      }),

    ...(vertical === "bottom" &&
      horizontal === "center" && {
        top: `calc(100% + ${offset}px + 4px)`,
        left: "50%",
        transform: "translateX(-50%)",
        "&::after": {
          content: "''",
          width: 8,
          height: 4,
          position: "absolute",
          backgroundColor: "#000",
          bottom: "100%",
          left: "50%",
          transform: "translateX(-50%)",
          clipPath: "polygon(50% 0%, 50% 0%, 100% 100%, 0% 100%)"
        }
      }),

    ...(vertical === "bottom" &&
      horizontal === "right" && {
        top: `calc(100% + ${offset}px + 4px)`,
        left: "calc(50% - 12px)",
        "&::after": {
          content: "''",
          width: 8,
          height: 4,
          position: "absolute",
          backgroundColor: "#000",
          bottom: "100%",
          left: 8,
          clipPath: "polygon(50% 0%, 50% 0%, 100% 100%, 0% 100%)"
        }
      })
  })

```

```App.js
/** @jsxImportSource @emotion/react */
import "./styles.css"
import {css} from "@emotion/react"
import {Tooltip} from "./Tootip"

export default function App() {
  return (
    <div className="App">
      <h1>Tooltip TEST</h1>
      <ul css={toolTipList}>
        <li>
          <div css={hoverText}>
            <Tooltip horizontal="left" message="Tooltip Text">
              左上に表示されます。
            </Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip message="Tooltip Text">真上に表示されます。</Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip horizontal="right" message="Tooltip Text">
              右上に表示されます。
            </Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip horizontal="left" vertical="middle" message="Tooltip Text">
              左横に表示されます。
            </Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip
              horizontal="right"
              vertical="middle"
              message="Tooltip Text"
            >
              右横に表示されます。
            </Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip horizontal="left" vertical="bottom" message="Tooltip Text">
              左下に表示されます。
            </Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip vertical="bottom" message="Tooltip Text">
              真下に表示されます。
            </Tooltip>
          </div>
        </li>
        <li>
          <div css={hoverText}>
            <Tooltip
              horizontal="right"
              vertical="bottom"
              message="Tooltip Text"
            >
              右下に表示されます。
            </Tooltip>
          </div>
        </li>
      </ul>
    </div>
  )
}

const toolTipList = css({
  listStyleType: "none",
  display: "grid",
  gap: "32px 0"
})

const hoverText = css({
  textDecoration: "underline dotted",
  display: "inline-block"
})

```

## DEMO

https://codesandbox.io/embed/tooltip-test-y0f0g3?fontsize=14&hidenavigation=1&theme=dark


## 簡単な解説

`vertical`(縦の位置:初期値はtop)、`horizontal`(横の位置:初期値はcenter)、`offset`(対象とツールチップの距離:初期値は0)をPropsで渡しています。
初期値に`vertical="top"`、`horizontal="center"`を設定しているので、何も指定がなければTooltipは真上に表示されます。
また、hoverする対象とTooltipを少し離したい場合は`offset`に数値をいれることにより、離れて表示させることができます。
TypeScriptで書くことによって、verticalやhorizontalのルールを厳格にすることができますが、今回のコードではそこまで厳密に書いておりません。