React のホバー時のスタイル変更を簡単にレスポンシブ対応させる
CATEGORY:技術
TAGS:#JavaScript / #CSS / #React
要素にホバーした時にスタイルを変更するやり方ってどうやるのって聞かれたら普通に :hover
っていう擬似クラスに対してスタイルあててそれで終わり簡単でしょみたいな話になるわけですね。
しかしそこでレスポンシブな要素に対してっていう条件がつくとちょっと難しくなってくる。
:hover
に対してスタイルを当ててしまうとスマートフォンでタップした際におかしな挙動になる。そこで Media Query を使って PC サイズのときは :hover
, スマホサイズのときは :active
に対してスタイルを当てるっていうやり方もあるんだけど、 :active
という擬似クラスは Android でうまく動かなかったりするのでそれもだめだったりする。
(そもそも Media Query だと画面サイズ判定なので PC かスマホかという判定に対して不正確という問題もありつつ)
じゃあ今までどうしてたかっていうとわざわざこの対応のために JavaScript を書いてた。
ユーザーエージェントを見て、 PC だった場合は mouseEnter
時に .hover
というクラスを与えて mouseLeave
時にそれを外す。スマホだった場合は touchStart
時にクラスを与えて touchEnd
時にそれを外す。
この処理を与えたい要素に対して .js-hover
みたいなクラスを与えてその要素を取得して一括で処理とかしてた。
そうすると CSS は .hover
に対してスタイルをあてるだけで良いので CSS はシンプルに保てる。みたいな。
それが昔のやり方。ここまでが前提。
React コンポーネントでどうしようという話
最近はずっと React でクライアントを作っているので、この問題を React コンポーネントではどう解決するかってのを考えてみた。
そもそもとして、一つのコンポーネントに対してレスポンシブ対応させるっていうのが微妙っていう考え方もあったりする。
この記事でどういう話をしているかというと、コンポーネントをレスポンシブ化するのは複雑化しすぎるのでもう PC とスマホでコンポーネント別にしちゃおうよっていうそういう主旨。
この記事を読んだとき、僕が抱えていたコンポーネントのレスポンシブ対応だるすぎる問題に対して「あーもう分けちゃうんで良いんだ、気が楽になったー」となって大分感動した。
ただまあそれでもどうしてもホバー時のスタイルみたいなとこのためだけに対してだと別コンポーネント用意したくないしそこはなんとかしたいみたいなときもあって、そこを解決したかった。
というわけで作ってみた。
const hoverProvider = (WrappedComponent) => {
return class HoverProviderComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
isHover: false,
}
}
onMouseEnter = (e) => {
const { onMouseEnter } = this.props
if (onMouseEnter) onMouseEnter(e)
if (isTouchDevice) return
this.setState({ isHover: true })
}
onMouseLeave = (e) => {
const { onMouseLeave } = this.props
if (onMouseLeave) onMouseLeave(e)
if (isTouchDevice) return
this.setState({ isHover: false })
}
onTouchStart = (e) => {
const { onTouchStart } = this.props
if (onTouchStart) onTouchStart(e)
if (isMouseDevice) return
this.setState({ isHover: true })
}
onTouchEnd = (e) => {
const { onTouchEnd } = this.props
if (onTouchEnd) onTouchEnd(e)
if (isMouseDevice) return
this.setState({ isHover: false })
}
render() {
const { isHover } = this.state
const { children, className } = this.props
return (
<WrappedComponent
{...this.props}
className={`${isHover ? 'hover' : ''} ${className || ''}`}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
onTouchStart={this.onTouchStart}
onTouchEnd={this.onTouchEnd}
>
{children}
</WrappedComponent>
)
}
}
}
これはちょっと前から React 界隈で流行っている HOC パターンというのを使っている。
で、この関数の使い方はこう。
const Link = hoverProvider(
<a href="" className="link">
リンク
</a>,
)
ホバー時のスタイル変更の処理を与えたい要素を hoverProvider
に渡すとレスポンシブ対応された .hover
クラスの着脱機能を持ったコンポーネントが返される。
これでリンクとかそういう要素全部この関数で包んであげればそれで ok な状態になる。シンプル。
CSS 側はこう。シンプル。
.link {
color: blue;
}
.link.hover {
color: green;
}
解説
この関数に渡されたコンポーネントが WrappedComponent
という変数に入ってくる。
で、この WrappedComponent
に対して onMouseEnter
などのイベントリスナを登録してあげて state をごにょごにょしてクラスをつけたり消したりしているだけ。
React のイベントリスナの部分のソースを読んでなくてちょっと自信なかったから自分で isTouchDevice
とか isMouseDevice
みたいなユーザエージェントの判定処理を用意して、万が一スマホで onMouseEnter
が呼ばれてしまったときの保険として処理を止めるようにしている。
けどそこは大丈夫だと思うのでこの部分の処理は外してしまってもいいかも・・・多分・・・。
あと気をつけなきゃいけない点としては WrappedComponent
に対してすでに onMouseEnter
とかが貼られている可能性があるので、そこをこっちのイベントハンドラの中でちゃんと呼んであげること。
同様に className
もすでに与えられているものがあるかと思うので、それと結合して hover
を className
にあてること。
それくらい。
終わり
僕の中ではこれで問題がシンプルに解決できたかなと思うけど、「それセンスないだろ」とか「悪手だろ」とか「ここ考慮漏れしてませんかね?」とかあったら @nabeliwo に教えていただけると嬉しいです。
以上 :beer: