Help us understand the problem. What is going on with this article?

getBoundingClientRectとツールチップ

この記事は

JavaScriptのgetBoundingClientRectを用いたツールチップについての解説です。

環境

Vue2
TypeScript(decorator使用)

背景

?アイコンにカーソルを当てた時だけに表示させる汎用的なツールチップを作りたい。

課題

?アイコンの直下に表示させたい。
画面をスクロールしてもそれに追従させたい。

解決

ツールチップコンポーネントtemplate

ソース

<template>
  <span
    @mouseover="activate"
    @mouseleave="deactivate"
  >
    <slot name="activator"></slot>
    <div
      :class="{'tool-tip__content--active': isActive}"
      :style="contentStyleObj"
      ref="content"
    >
      <slot></slot>
    </div>
  </span>
</template>

解説

mouseoverとmouseleaveでツールチップの表示有無処理を発火します。

<span
  @mouseover="activate"
  @mouseleave="deactivate"
>

?アイコンは親コンポーネントからslotしてきます。

<slot name="activator"></slot>

表示フラグが立っている時にツールチップ本体を表示します。

:class="{'tool-tip__content--active': isActive}"

script部で算出した座標をスタイルに反映します。

:style="contentStyleObj"

ツールチップコンポーネントscript

TypeScriptを用いています。

<script lang="ts">
import { Component, Prop } from 'vue-property-decorator';
import BaseView from '@/views/Base';

@Component
export default class ToolTipComponent extends BaseView {
  // ツールチップ表示フラグ
  isActive = false;

  // 表示位置
  contentPosition = {
    top: 0,
    left: 0
  }

  // ツールチップを表示する基準位置
  defaultPosition = {
    top: 40,
    left: 100
  }

  // ?アイコンにマウスオーバーした時の表示処理
  activate() {
    const target = event!.target as any;
    const rect = target!.getBoundingClientRect();
    this.contentPosition.top = rect.top + window.pageYOffset + this.defaultPosition.top;
    this.contentPosition.left = rect.left - this.defaultPosition.left;

    this.isActive = true;
  }

  // マウスオーバーの解除で非表示
  deactivate() {
    this.isActive = false;
  }

  // 座標をスタイルに指定
  get contentStyleObj() {
    return {
      top: `${this.contentPosition.top}px`,
      left: `${this.contentPosition.left}px`,
    }
  }

  // 一番上の親要素の直下にツールチップを配置する
  mounted() {
    const app = document.getElementById('app');
    app!.appendChild(this.$refs.content as any);
  }

  // コンポーネント破壊時にツールチップを削除する
  beforeDestroy() {
    const app = document.getElementById('app');
    app!.removeChild(this.$refs.content as any);
  }
}
</script>

解説

表示処理では、getBoundingClientRect()を用いてビューポートからの座標を取得します。
ツールチップの表示位置をデフォルトポジションとして調節し、表示座標を算出します。
その後、表示スタイルを有効にするためのフラグを立てます。

activate() {
  const target = event!.target as any;
  const rect = target!.getBoundingClientRect();
  this.contentPosition.top = rect.top + window.pageYOffset + this.defaultPosition.top;
  this.contentPosition.left = rect.left - this.defaultPosition.left;

  this.isActive = true;
}

そして算出された座標をスタイルに渡すゲッターを定義します。

get contentStyleObj() {
  return {
    top: `${this.contentPosition.top}px`,
    left: `${this.contentPosition.left}px`,
  }
}

ツールチップは画面描画時には非表示ですが、DOM内には配置しておきます。
一番外側の要素の子要素として配置します。
画面遷移時等のコンポーネント破壊時には配置した要素も忘れず削除するようにしましょう。

// 一番上の親要素の直下にツールチップを配置する
mounted() {
  const app = document.getElementById('app');
  app!.appendChild(this.$refs.content as any);
}

// コンポーネント破壊時にツールチップを削除する
beforeDestroy() {
  const app = document.getElementById('app');
  app!.removeChild(this.$refs.content as any);
}

利用コンポーネント

親コンポーネントでは?アイコンをactivatorスロットに配置。
またツールチップ内の文章はデフォルトスロットに配置します。

<ToolTip class="tool-tip" v-show="toolTipMessage">
  <template slot="activator">
    <img src="@/assets/img/icon/icon-help.png" alt="help">
  </template>
  <div class="tool-tip__help-text">
    {{ toolTipMessage }}
  </div>
</ToolTip>

まとめ

少々トリッキーなコンポーネントとなってしまいましたが、getBoundingClientRectはこういう使い方もできる点は参考になりますね。
応用もできると思うので、座標を取って何かするような動作のコンポーネントに使うと良いかもしれません。
間違っている点、改善点あればご教授頂けますと幸いです。

RINYU_DRVO
TypeScript/Vue 今はフロントエンドエンジニアをしています。
personlink
株式会社パーソンリンクの公式アカウントです。
https://www.person-link.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away