AmazonとTwitterの吹き出しコンポーネントの実装方法を調査してみた

## 吹き出しコンポーネントとは


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481207/b87bf78c-de12-6a0e-2c38-6779e25d95b7.png" width="100px">
吹き出しコンポーネントは会話などを表示するために方向を表示する三角のアローとボーダーのある四角いボックスを組み合わせたUIコンポーネントを意味します。英語では`Speech Balloon`と言うそうです。会話のほかに、ボタンの機能説明やメニューの拡張などにも使います。


## この記事の目的

この記事では吹き出しコンポーネントの実装・拡張しやすいベストプラクティスを探します。

## Amazonパターン
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481207/b353539e-991a-e73d-4dba-97ae1bc50f9d.png" width="400px">

Amazonではトップナビゲーションバーのログインボタンをホバーしている間にログインと新規登録を促す吹き出しが出てきます。

吹き出し部分のHTML構造は下のように、

```html
<header>
	<div id="nav-belt">
		<div class="nav-left"></div>
		<div class="nav-fill"></div>
		<div class="nav-right">
			<div id="nav-tools">
				<a class="nav-a nav-a-2 nav-truncate" id="nav-link-accountList">
					<div class="nav-line-1-container"></div>
					<span class="nav-line-2 nav-long-width"></span>
					<span class="nav-line-2 nav-short-width"></span>
				</a>
			</div>
		</div>
	</div>
	<div id="nav-flyout-anchor">
		<div id="nav-signin-tt nav-flyout">
			<div class="nav-arrow">
				<div class="nav-arrow-inner">
			</div>
			<div id="nav-signin-tooltip"></div>
			<div class="nav-flyout-buffer-left"></div>	
			<div class="nav-flyout-buffer-right"></div>	
			<div class="nav-flyout-buffer-top"></div>	
			<div class="nav-flyout-buffer-bottom"></div>	
		</div>
	</div>
</header>
```

なります。

### ポイント解説

`<div id="nav-belt">`はナビバーを指します。そして、吹き出しの部分は`nav-flyout`と名付けている。このネーミングから、吹き出しはナビバーに専属していることが分かり、さらにログインボタンの吹き出し以外にもほかのナビバー項目も吹き出しが存在することが推測できます。実際に、ログインボタンの左隣のリージョンアイコンの吹き出しも用意されています。

ログインボタンとログインボタンの吹き出しの間は直接なネストではなく、ナビバー全体（`<div id="nav-belt">`）の下にナビバー用の吹き出しセクション（`<div id="nav-flyout-anchor">`）を入れることで、ナビバー項目と吹き出しの上下位置は確定されます。そして、後からに他項目の吹き出しを追加したい時に`<div id="nav-flyout-anchor">`に新しい`<div>`を追加するだけで済むようになります。ナビバーの実装をいじる必要がなくなり、拡張性はよくなります。

吹き出しコンポーネントの表示は`<div id="nav-signin-tt nav-flyout">`に対して`display: none;`指定の有無によってコントロールしています。

吹き出しの左右位置はJSを使って、`<div id="nav-signin-tt nav-flyout" style="left: 120px;">`のように動的に設定します。このために吹き出しdivを`position: absolute;`にする必要があり、同時にその親コンポーネント`<div id="nav-flyout-anchor">`は`position: relative;`にする必要があります。

`style="left: 120px;"`の中は数値は`Element.getBoundingClientRect()`を使ってログインボタンのウインドーに対する位置を取得できます。ウインドーサイズが変わるにつれて、吹き出しの位置も動くとしたいなら、イベントトリガー`window.resize`を追加するといいでしょう。

三角アローはCSSのボータートリックを利用して作られています。上向きのアローならば、

```css
div.arrow-up {
  border-right: 10px solid transparent;
  border-left: 10px solid transparent;
  border-bottom: 10px solid white;
}

```

によって作れます。

三角アローと吹き出し本体の位置調整は`left: 50%; top: -11px;`のように指定しています。当然、三角アローの`position`を`absolute`に指定する必要があります。`left: 50%;`は三角アローを吹き出しの真ん中に持っていくため、`top: -11px;`は三角アラーを吹き出しの外に突っ張るようにするための指定です。同じロジックで、三角アラーを下向きにする時は`bottom`をマイナスの数値に指定すればいいでしょう。


## Twitterパターン
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/481207/dbc0a5dc-1142-7a49-0d28-626e0157bb72.png" width="400px)
"> 

Twitterではログイン後、サードバーの下にある自分のアイコンをクリックした時にアカウントスイッチとログアウトを促す吹き出しが出てきます。

吹き出し部分のHTML構造は下のように、

```html
<div id="layers">
	...
	<div class="css-idbjc4n r-1xcajam" style="bottom: 79px; left: 21px;">
		<div class="css-idbjc4n">
			<svg class="r-uoibet r-9uutrm r-lrsllp" style="left: calc(138px);"></svg>
			<div class="css-1dbjc4n r-1azx6h r-7mdpej r-1vsu8ta r-ek4qxl r-1dqxon3 r-1ipicw7">
				<div class="css-1dbjc4n r-1w50u8q">
					<li></li>
					<div></div>
					<div></div>
				</div>
			</div>
		</div>
	</div>
</div>
```

なります。

### ポイント解説

Twitterの場合、`scoped`スタイルではなく、1種類のスタイルを1クラス名に集約するようにスタイルを定義しています。例えば、上の例に何度も出てきた`class="css-idbjc4n"`はFlexboxのスタイルを指定するクラスとなっており、このクラスのスタイルをいじると、クラス名に`css-idbjc4n`が入っている全`div`が影響を受けます。そのため、HTMLのDOMツリーを見る時の可読性は非常に低くなっています。一方的に、スタイル共通化によって、画面全体のスタイルの統一感が向上します。

`<div id="layers">`で分かるように、Twitterでは吹き出しのために画面全体に新しいレイヤーを追加しています。

三行目の`<div class="css-idbjc4n r-1xcajam" style="bottom: 79px; left: 21px;">`は吹き出しのルートノードとなっています。`positon: fixed;`にすることで、画面（Viewport）に対する位置を設定することが可能となります。`style="bottom: 79px; left: 21px;"`はその位置の指定となります。具体的な数値はAmazonパターンと同様、JSを使って、実際のログインアイコン（上のスクリーンショットの一番下）の位置から計算している可能性が高いでしょう。ただ、画面全体に対して位置を指定しているから、水平位置`left`のほかに、垂直位置の`bottom`も指定しなくてはなりません。

吹き出しコンポーネントの表示は、上の吹き出しルートノードの生成と削除によってコントロールしています。実際、ログインボタンをホバーすると、吹き出しのDOMノードは生成されます。

`<svg class="r-uoibet r-9uutrm r-lrsllp" style="left: calc(138px);"></svg>`は三角アローを指しています。TwitterではSVGで三角を描画しています。SVGで描画した三角はデフォルトで上向きとなっており、三角アローの向き先の調整は`transform: rotate(180deg);`によって実現しています。

水平位置では`style="left: calc(138px);`のように調整していて、`138px`という数値はおそらく吹き出しのサイズと三角アローのサイズから計算した値と考えられます。CSSを確認したところ、吹き出しボックスは`width: 300px;`で、三角アローのサイズは`width: 24px;`となっているから、`left`からは`300/2 - 24/2 = 138px`を移動すれば、三角アローは吹き出しの真ん中に行くでしょう。



## 比較

 |**Amazon**|**Twitter**|**コメント**
:-----:|:-----:|:-----:|:-----:
吹き出しコンポーネントの親| `<div id="nav-flyout-anchor">` | `<div id="layers">` | Amazonではナビバー専用の吹き出しのために、ナビバーの下に吹き出しの`anchor`が置いてある。Twitterでは画面全体に新しいレイヤーを追加。`anchor`を入れることのメリットは吹き出しとその対象の間の垂直位置は気にしなくて済むこと。吹き出しと吹き出し対象のコンポーネントを一緒に変更する場合は便利だろう。一方で、`anchor`の範囲以外に吹き出しを追加すると、まず`anchor`を追加する必要がある。画面全体にレイヤーを追加することで元のコンポーネントと吹き出しは完全に独立となる。よって、各所に吹き出しを追加したり、削除したりすることも容易となるだろう
吹き出しコンポーネントの位置| `position: absolute;` | `position: fixed;` | `position: fixed;`は画面全体（viewport）に対して位置を指定することになる
吹き出しコンポーネントの位置指定| `style="left: 120px;"` | `style="bottom: 79px; left: 21px;"` | Twitterパターンは画面全体に対して位置指定となるので、`bottom`と`left`両方の指定が必要
吹き出しコンポーネント中身のレイアウト| `display: flex;` | `display: flex;` | 
吹き出しコンポーネントの表示| `display: none;` | HTML要素の生成と削除 | `display: none;` に設定する場合、レンダリング時に吹き出しなどのサブエレメントも一緒に生成することになるから、処理が重くなる可能性はある。一方、吹き出しコンポーネントが他の機能的コンポーネントから独立していない場合、吹き出しHTML要素の生成と削除する時に影響範囲の把握が難しくなる
三角アローの作り方|　CSSボーター |　SVG　| CSSボーターのトリックはとても賢いと思う
三角アローの位置| `position: absolute;`  | `position: absolute;` | 
三角アローの位置指定| `left: 50%; top: -10px;` | `left: calc(138px); bottom: -10px;` | topとbottomにマイナスの数値を指定することで、実質三角アローを吹き出しの内部に治ることができ、三角アローによって位置がずれるなどの問題を解消
三角アローの向き先| `border-top`と`border-bottom`の設定による |　`transform: rotate(180deg);`　| transform本当にいろんな使い方があるなと感心し、もっと活用して行きたい


## 参考

https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
https://www.amazon.co.jp/
https://www.twitter.com/