React で長い文字列を省略表記にして、ツールチップに全て表示するには

どんなことをしたいかというと、

  • 文字列が長くて表示しきれないときは省略表記する
  • マウスホバーで文字列全体をツールチップ表示する

というコンポーネントを作成してみます。
動作は Chrome バージョン 92.0.4515.107 で確認しています。
この表示をしたコンポーネントのコードは以下の通りです。

export const Simple = () => <span
  style={{
    display: 'block',
    textOverflow: 'ellipsis',
    whiteSpace: 'nowrap',
    overflow: 'hidden',
  }}
  title="abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
>abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz</span>
  • style に、文字列が表示に収まらなかったら省略表記するように指定
  • title に、ツールチップで表示する文字列を設定

ということをしています。
ただ、このままだと省略表記が不要な文字長でもツールチップが表示されます。省略表記していないときはツールチップがでないようにしたいと思います。

作成したコード

.text {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
import React, { useRef, useState, useEffect } from 'react';
import './text.css';

type TextProps = Omit<JSX.IntrinsicElements['span'], 'children'>
& { children: string };

export const Text = (props: TextProps) => {
  const refOuter = useRef<HTMLSpanElement>(null);
  const refInner = useRef<HTMLSpanElement>(null);
  // 省略表記のとき true
  const [isEllipsis, setIsEllipsis] = useState<boolean>(false);

  // リサイズ監視
  useEffect(() =>{
    const resizeObserver = new ResizeObserver(() =>{
      const rectOuter = refOuter.current?.getBoundingClientRect();
      const rectInner = refInner.current?.getBoundingClientRect();
      // 外枠 < 内枠 なら省略表記
      setIsEllipsis((rectOuter?.width ?? 0) < (rectInner?.width ?? 0));
    });
    const el = refOuter.current as Element;
    resizeObserver.observe(el);
    return () => resizeObserver.unobserve(el);
  });

  const {
    children,
    ...etc
  } = props;
  // 省略表記になるとき title を設定
  const title = isEllipsis && { title: children };
  return (
    <span
     ref={refOuter}
     className="text"
     {...etc}
    >
      <span
       ref={refInner}
       {...title}
      >{children}</span>
    </span>
  );
};

という Text コンポーネントにすると、

<Text>abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz<Text>

の様に使えます。

解説

  • 省略表記している → title の props を設定
  • 省略表記していない → title の props は無し

ということをする為に色々していきます。

    <span
     ref={refOuter}
     className="text"
     {...etc}
    >
      <span
       ref={refInner}
       {...title}
      >{children}</span>
    </span>

span を親子な状態にしています。

  • display: block; でブロック要素にして親要素の幅に沿わせる。
  • 文字列が収まらなかったときに省略表記するスタイルの設定。
  • 要素のリサイズを監視し、親と子の幅を比較して、ツールチップが必要かどうかを isEllipsis に反映。
    親は overflow: hidden; にしているので、
    親の幅 < 子の幅 のときは省略表記されている、と判定します。

  • 文字列の表示
  • 文字列が
    省略表記されたときはツールチップで全文表示
    省略表記されていなければツールチップは無し

という役割をしています。

  // リサイズ監視
  useEffect(() =>{
    const resizeObserver = new ResizeObserver(() =>{
      const rectOuter = refOuter.current?.getBoundingClientRect();
      const rectInner = refInner.current?.getBoundingClientRect();
      // 外枠 < 内枠 なら省略表記
      setIsEllipsis((rectOuter?.width ?? 0) < (rectInner?.width ?? 0));
    });
    const el = refOuter.current as Element;
    resizeObserver.observe(el);
    return () => resizeObserver.unobserve(el);
  });

useEffect で親spanのリサイズ監視/監視解除をします。
リサイズの監視には ResizeObserver を使用します。
リサイズが発生したときは、親と子の span の幅を比較して、省略表記されているか判定した結果を isEllipsis に反映します。

ここまでするぐらいなら、もう常に title ありでも良いんじゃないか?という気も湧いてきますが、何かの参考になりましたら幸いです。

プロを目指す人のためのTypeScript入門 (Amazon)

コメント