9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ReactでTooltipを自作した

Posted at

概要

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

テキスト

以下、実際に書いたコードです。
CSSはemotionを使用しています。

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

簡単な解説

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

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?