【React】アコーディオン風に飛び出す3Dリンクボタンを実装する

<!-- =========================================================== -->
# 概要
<!-- =========================================================== -->
**アコーディオン風に飛び出す3Dリンクボタン**を実装します。

![output(video-cutter-js.com) (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358498/8fcdabe5-efb6-8650-62eb-5df318251b2b.gif)

:::note info
本記事は、以下の動画を<strong>React</strong>で実装したものです。
:::

https://www.youtube.com/watch?v=C8wSNZqktOM

:::note info
スタイリングには、<strong>emotion/css</strong>（CSS in JS）を使用しています。
:::

https://emotion.sh/docs/@emotion/css

https://qiita.com/nemutas/items/dac73aa645f27cde92cd

:::note info
アイコンは、<strong>react-icons</strong>を使用しています。
:::

https://react-icons.github.io/react-icons/

<!-- =========================================================== -->
# 実装
<!-- =========================================================== -->
```.tsx
import React, { useRef, VFC } from 'react';
import { FaFacebookF, FaInstagram, FaLinkedinIn, FaTwitter, FaWhatsapp } from 'react-icons/fa';
import { IconType } from 'react-icons/lib';
import { css } from '@emotion/css';

export const IsometricSocialMediaIcon: VFC = () => {
	const items: { href: string; icon: IconType; themeColor: string }[] = [
		{ href: '#facebook', icon: FaFacebookF, themeColor: '#3b5999' },
		{ href: '#twitter', icon: FaTwitter, themeColor: '#55acee' },
		{ href: '#whatsapp', icon: FaWhatsapp, themeColor: '#25d366' },
		{ href: '#linkedin', icon: FaLinkedinIn, themeColor: '#0077b5' },
		{ href: '#instagram', icon: FaInstagram, themeColor: '#e4405f' }
	]

	return (
		<div className={styles.container}>
			<ul className={styles.list}>
				{items.map((item, i) => (
					<ListItem key={i} href={item.href} icon={<item.icon />} themeColor={item.themeColor} />
				))}
			</ul>
		</div>
	)
}

// ==============================================

type ListItemProps = {
	href: string
	icon: React.ReactNode
	themeColor: string
}

const ListItem: VFC<ListItemProps> = props => {
	const { href, icon, themeColor } = props

	const layer1Ref = useRef<HTMLSpanElement>(null)
	const layer2Ref = useRef<HTMLSpanElement>(null)
	const layer3Ref = useRef<HTMLSpanElement>(null)
	const layer4Ref = useRef<HTMLSpanElement>(null)

	const hoverHandler = () => {
		layer1Ref.current!.classList.toggle('hover')
		layer2Ref.current!.classList.toggle('hover')
		layer3Ref.current!.classList.toggle('hover')
		layer4Ref.current!.classList.toggle('hover')
	}

	return (
		<li className={styles.item(themeColor)} onMouseEnter={hoverHandler} onMouseLeave={hoverHandler}>
			<a href={href}>
				<span ref={layer1Ref} className={styles.layer(themeColor)}></span>
				<span ref={layer2Ref} className={styles.layer(themeColor)}></span>
				<span ref={layer3Ref} className={styles.layer(themeColor)}></span>
				<span ref={layer4Ref} className={styles.layer(themeColor)}>
					{icon}
				</span>
			</a>
		</li>
	)
}

// ==============================================
// styles

const styles = {
	container: css`
		position: relative;
		width: 100%;
		height: 100%;
		display: flex;
		justify-content: center;
		align-items: center;
		background-color: #222;
	`,
	list: css`
		position: relative;
		display: flex;
		transform-style: preserve-3d;
		transform: rotate(-25deg) skew(25deg);
	`,
	item: (themeColor: string) => css`
		position: relative;
		list-style: none;
		width: 60px;
		height: 60px;
		margin: 0 10px;

		&::before {
			content: '';
			position: absolute;
			bottom: -10px;
			left: 0;
			width: 100%;
			height: 10px;
			background-color: #2a2a2a;
			transform-origin: top;
			transform: skewX(-41deg);
			transition: 0.5s;
		}
		&::after {
			content: '';
			position: absolute;
			top: 0;
			left: -9px;
			width: 9px;
			height: 100%;
			background-color: #2a2a2a;
			transform-origin: right;
			transform: skewY(-49deg);
			transition: 0.5s;
		}

		&:hover::before,
		&:hover::after {
			opacity: 0.2;
			background-color: ${themeColor};
		}
	`,
	layer: (hoveredColor: string) => css`
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		display: flex;
		justify-content: center;
		align-items: center;
		background-color: #333;
		color: rgba(255, 255, 255, 0.2);
		font-size: 30px;
		transition: 0.2s;

		&.hover {
			z-index: 1000;
			transition: 0.5s;
			color: #fff;
			box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.05);
			background-color: ${hoveredColor};

			&:nth-child(5) {
				transform: translate(40px, -40px);
				opacity: 1;
			}
			&:nth-child(4) {
				transform: translate(30px, -30px);
				opacity: 0.8;
			}
			&:nth-child(3) {
				transform: translate(20px, -20px);
				opacity: 0.6;
			}
			&:nth-child(2) {
				transform: translate(10px, -10px);
				opacity: 0.4;
			}
			&:nth-child(1) {
				transform: translate(0px, 0px);
				opacity: 0.2;
			}
		}
	`
}
```
* CSSでの3Dは、`transform-style`に**preserve-3d**を指定することで表現できます。

```.tsx
list: css`
	position: relative;
	display: flex;
	transform-style: preserve-3d;
	transform: rotate(-25deg) skew(25deg);
`
```
https://developer.mozilla.org/ja/docs/Web/CSS/transform-style

* 親要素のhoverについて

親要素がhoverされたときに、子要素に特定のスタイルを割り当てようとする場合、バニラのCSSだと以下のように記述できます。

```.css
li:hover span {
...
}
```

CSS in JSでは、タグごとにスタイルを割り当てるため、他のタグの状態を知ることはできません。（私が記述方法を知らないだけかもしれませんが...）
なので、親要素がhoverされたタイミングで、アクションを起こしたい要素にクラスを割り当てる必要があります。

```.tsx
const hoverHandler = () => {
	layer1Ref.current!.classList.toggle('hover')
	layer2Ref.current!.classList.toggle('hover')
	layer3Ref.current!.classList.toggle('hover')
	layer4Ref.current!.classList.toggle('hover')
}
```
これをスタイル内で呼びだして、親要素（li tag）がhoverされたときの子要素（span tag）の動作を記述します。

```.tsx
layer: (hoveredColor: string) => css`
	・・・
	&.hover {
		・・・
	}
`
```

もしくは、hover時のスタイルを用意して、親要素がhoverされたときに、子要素にhover時のスタイルを割り当てるようにします。

この処理は、以下を参考にしてください。
[【React】emotion/css の使い方メモ／classの動的な追加による遷移（transition）／スタイルの統合（cx）を使った書き方](https://qiita.com/nemutas/items/dac73aa645f27cde92cd#class%E3%81%AE%E5%8B%95%E7%9A%84%E3%81%AA%E8%BF%BD%E5%8A%A0%E3%81%AB%E3%82%88%E3%82%8B%E9%81%B7%E7%A7%BBtransition)