テキストが一定の横幅以上に長くなった場合、そのテキストを省略し、ホバーでツールチップを表示するというUIコンポーネントの作り方と注意点をまとめました。
デモ
制作中です…
仕様
- 親コンテンツよりもテキストコンテンツがはみ出る場合、はみ出た分は省略(・・・)します
- はみでなければ省略はされず、ツールチップも表示されません
- テキストはリンクもしくはクリックイベントをセット可能で、クリッカブルエリアはテキストのみです
制作方法
[モックアップ]テキストエリア
まずはhtml, cssでモックを作って動きを確認しましょう。
See the Pen Untitled by tora (@-tora-) on CodePen.
以下のスタイルのセットではみ出た分をカットする事が可能です。
overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
この状態でもレスポンシブには対応しています。
codepanへ移動し、ブラウザ幅を変更し確かめてみてください。
[モックアップ]ツールチップ
次にツールチップを制作します。
See the Pen Untitled by tora (@-tora-) on CodePen.
本来はVueから制御しますが、モックという事でツールチップの表示は:hoverを使用して制作しています。
aタグを入れてみる
仕様の方に、テキストコンテンツはリンクもしくは、クリックイベントをセット可能と記載しました。
モックの時点で現在のテキスト部分をリンクタグに変更してうまく機能するか確認してみましょう。
See the Pen Untitled by tora (@-tora-) on CodePen.
結果はこのように省略されずに親からはみ出てしまいました。
これはテキスト要素をリンクタグで囲った事により要素がinline要素になったためです。
ellipsis(省略するcss)を使用する場合は要素はblock要素でなくてはいけません。
ですのでellipsis-itemにdisplay:blockを追加したいところですが、注意が必要です。
block要素は親要素いっぱいに広がってしまいますので、もしも短いテキストが入った場合、クリッカブルエリアがテキストのみではなく、余った余白部分まで広がってしまいます。
ですのでmax-width: max-content;を追加して最大幅をテキスト幅に設定します。
See the Pen Untitled by tora (@-tora-) on CodePen.
これでblock要素が入ってもinline要素が入っても仕様を満たす事ができます。
今回、block要素にmax-contentをセットして対応しましたが、inline-blockでも対応可能では?
とい思うかもしれません。
ですが、inline-blockを使用すると1つデメリットがあります。それは、Macで高さが0になる事です。
親にposition: relative;を指定して、絶対位置の起点などにしていると、位置調整で不具合が出ます。
親にflexを適応してみる
これで実装は十分なのですが、このままだとたまに困る事が起こります。
それは親要素にflexを当てた場合です。
Java Sqriptフレームワークを使用して制作する場合、コンポーネント化し、再利用する事が一般的です。
再利用するという事は制作したコンポーネントがこの場所だけではなく、様々な要素と組み合わせて使用されます。
その際に不具合が起こらないように予め使われ方を予想して制作する必要があります。
例えばテキストの右側にiconを入れたいとします。
対策をしていない状態とどうなるか見てみましょう。
上が対策をしていないテキストで、下が対策をしてるテキストです。
上の対策をしていないテキストの方は横幅が親の.sellいっぱいに伸びてしまい、icon要素が親要素内に収まっていません。
これは、flexboxのmin-widthがデフォルトでautoとなっており、その値を子要素が継承するからです。
min-width: auto;というのは、コンテンツ幅の最小値をコンテンツの幅に合わせるという宣言です。
要はwidth:100%と同じです。
ですので子要素では明示的にmin-widthを設定し値をオーバーライドする必要があります。
.ellipsis-item { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 16px; display: block; max-width: max-content; // ↓追加:最小値を0に設定することで0pxまで縮む事ができるようになる min-width: 0; }
これで解決です。
Vueに落とし込む
制作したモックをVueコンポーネントへ落とし込みましょう。
仕様は以下のようにしたいです。
- 省略したいテキストを囲むだけで動作する
- ツールチップが画面外にはみ出そうな時の対処はここでは実装しない
- ツールチップは囲んたテキストの直後に挿入する
- ツールチップの表示/非表示のステータスをコンポーネントの外から操作・確認可能とする
- 囲む要素はテキストのみの想定
- モックの章で上げた仕様を再現する
ツールチップの位置調整まで行うと記事のコンテンツ量が多くなってしまうので今回は割愛し、別の記事で解説します。
ツールチップの挿入位置に関して、ベストプラクティスはレイヤー管理を行うべきですがコンテンツ量の点から割愛し、こちらも別の記事で解説します。
実装する
コンポーネント全体のコードです。
ファイル名はTextOver.vueとしました。
<template> <div v-bind:class="['tooltip-area', {'-open': isOpen}]" @mouseover="mouseoverHandler" @mouseleave="mouseleaveHandler" > <slot></slot> <div class="tooltip">{{ state.tips }}</div> </div> </template> <script> export default { props: ["isOpen"], data: function () { return { displayToolTip: false, slotEl: null, slotElData: null, spanEl: null, slotElWidth: null, spanElWidth: null, }; }, methods: { mouseoverHandler: function () { this.getDomHeight(); console.log("mouseoverHandler"); if(this.slotElWidth + 1 < this.spanElWidth) { this.displayToolTip = true; }else { this.displayToolTip = false; }; if(this.displayToolTip === false) return; this.isOpen = true; }, mouseleaveHandler: function () { this.isOpen = false; }, // テキストDOM要素のサイズを取得 getDomHeight: function() { this.slotElWidth = (this.slotEl !== null) ? this.slotEl.offsetWidth : 0; this.spanElWidth = (this.spanEl !== null) ? this.spanEl.getBoundingClientRect().width : 0; this.slotElData = this.slotEl.getBoundingClientRect(); }, }, mounted: function(){ this.$nextTick(()=> { // slotの要素を取得 this.slotEl = this.$slots.default[0].elm; // span要素を用意 this.spanEl = document.createElement('span'); const slotText = this.slotEl.textContent; this.spanEl.textContent = slotText; this.slotEl.innerText = ''; this.slotEl.appendChild(this.spanEl); }); }, }; </script> <style scoped> </style>
使用方法
<div class="sell"> <TextOver v-bind:is-open="false" > <span class="ellipsis-item">長いテキスト長いテキスト長いテキスト長いテキスト長いテキスト長いテキスト長いテキスト長いテキスト長いテキスト</span> </TextOver> <div class="icon">hoge</div> </div> <script> import TextOver from "../components/TextOver.vue"; export default { name: "sample", components: { TextOver }, } </script>
省略判定の方法
省略処理はcssで行っております。省略しているかしていないかをcssから取得する方法はないので、Java Scriptからどのような処理で判断するかですが、アイディアとしては以下があります。
- 判断用の要素を設置して、改行を有効にし、省略テキストの縦幅と判断用の要素の縦幅を比べ、判断用の要素の方が大きければテキストが省略されていると判断する
offsetWidth
とs
crollWidth
を比較して判断する- 省略テキストをspanなどで括り、親の幅とspanの幅を比べる
全て試してみましたが、最終的に3の方法がベストでした。
1の問題点は改行の動きが日本語と英数字で異なるため、文字の組み合わせによっては判定がうまく機能しない場合がありました。
2の問題はこちらの記事が参考になります。
三点リーダー(text-overflow: ellipsis)の適用を判定する方法と注意点
現状3の方法で実装していますが、判定に問題は出ていません。
span要素で省略するテキストを括る処理はmountedを利用してマウントが完了したらspanで括る処理を行っています。
mountedについては以下を参照ください。
マウスホバーイベントでmouseoverHandlerメソッドを実行し、親要素の横幅とspanの横幅を少数点まで取得できるgetBoundingClientRectメソッドを使って取得し、親よりもspanが大きくないか判定しています。
cssの変更
コンポーネントとして、省略テキストを囲う形で制作するので、元もとのellipsis-itemに親要素ができてしまいました。
その親要素にはmin-width: 0;が適応されていないため、flexでの継承問題が再び起こってしまいます。
ですので、親要素になるtooltip-areaにもmin-width: 0;を追加して完成です。
さらにブラッシュアップする
Vueに落とし込む際の仕様として以下を割愛しました。
- ツールチップの位置調整
- ツールチップの挿入位置とレイヤー管理
この2点はアプリケーションを制作する上でとても重要なポイントとなってきます。
ただ単独で今回制作したツールチップを動作させる分には問題ありませんが、アプリ上では様々な場所で再利用されたり、ユーザーの様々な操作に対応しなければいけません。
特にレイヤー管理は大切です。
レイヤー管理ができていないとツールチップが他の要素に隠れてしまうなど、ツールチップとしての役割を果たす事ができないなど、致命的な欠陥を誘発します。
後々気づいて修正しようとしても、複雑にアプリケーションを絡み合っており、修正量が膨大になりますので、初めの設計段階から対策をする必用があります。
ですが、これは経験や、誰かに教えてもらわないと自力で対応する事は難しいです。
そんな状況を避けるために対策方法と懸念しておく必要がある事を記事としてまとめ、経験が浅い場合でもうまく対応できるような体制を目指します。