スマホでもhoverするボタンをtouchイベントを使ってシンプルに実装する

## 概要
WebサイトやWebアプリを作っていて、「このボタン、スマホで動かすときは『タッチで色が変わって離したら戻る』みたいな感じにしたい…」と感じる人はちょいちょいいると思います。僕がそうです。

そのようなボタンをJavaScriptのtouchイベントを使ってシンプルに実装したので、サンプルを備忘録代わりにここに置いておきます。

サンプルはVueで作成していますが、Vueでなくてもやることは特に変わりません。
完成品を先に置いておくので、PCとモバイル端末でそれぞれ動きを確認してみてください。

完成品 : https://maple0922.github.io/vue-mobile-hover
Github : https://github.com/Maple0922/vue-mobile-hover

## Touchイベントとは

Touchイベントは、PCのクリックではなくスマホやタブレット端末でのタッチ周りの操作に対して発生するイベントです。
これらを設定することで、モバイル独自のタッチ操作に対してイベントを発生させることができます。

こちらに詳しく書いてありますので詳細は割愛します。

<a href="https://web-breeze.net/js-touch-event/">【JavaScript】初心者でもわかるタッチイベント(touch event)</a>

| イベント | 発生するタイミング |
|:-- |:--|
|  touchstart | タッチ面に指が触れた時  |
|  touchend | タッチしていた指を離した時  |
|  touchmove |タッチしながら指を移動させた時  |
|  touchcancel | なんらかの理由でタッチが取り消された時  |

## サンプル

Vue CLIで作ったプロジェクトで、簡単なボタンとイベントを用意しました。

```vue:Main.vue
<template>
  <div class="container">
    <h1>Vue Mobile Hover</h1>
    <p>Hoverのみ</p>
    <button class="btn" @click="countUp">UP</button>
    <p>Touch Event実装</p>
    <button class="btn" @click="countDown">DOWN</button>
    <p>count: <input v-model="count" readonly></p>
  </div>
</template>

<script>
export default {
  name: "Main",

  data () {
    return {
      count: 0,
    }
  },

  methods: {
    countUp() {
      this.count++
    },
    countDown(){
      this.count--
    }
  }
};
</script>

<style lang="scss" scoped>
.btn {
  border: 1px solid blue;
  width: 200px;
  height: 50px;
  cursor: pointer;
  background: white;
  color: blue;
  &:hover {
    background: blue;
    color: white;
  }
  &:active {
    background: white;
    color: blue;
  }
}
</style>
```

ボタンをクリックしたらcountが上がったり下がったりするだけのシンプルなアプリです。
ボタンはhover時に背景色と文字色が入れ替わり、押下時に元に戻るようなcssを当てています。
マウスポインタがなぜか写ってくれませんが、UPのボタンにマウスをホバーさせています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/543238/d95c2305-aada-d684-9e29-f703c3eb76cf.png">


ここから、DOWNボタンのほうだけをモバイル端末でのhoverに対応させていきます。

## ボタン実装

### 1.モバイル判定
まず、モバイルかPCかの判断をするための`isMobile`というパッケージをインストールします。
メディアクエリでの判断ではクリック端末かタッチ端末かの判別ができないので不十分です。

```
$ npm i -D ismobilejs
```

```diff:Main.vue
<script>
+ import isMobile from "ismobilejs";

export default {
  name: "Main",

  data () {
    return {
      count: 0,
    }
  },
+
+  computed: {
+    mobile () {
+      return isMobile(window.navigator).any;
+    }
+  },

  methods: {
    countUp() {
      this.count++
    },
    countDown(){
      this.count--
    }
  },

};
</script>
```

`mobile`は取得したユーザーエージェントをisMobile.anyの判定にかけて、モバイル端末ならtrue,そうでないならfalseを返します。

### 2.pcのみにhoverイベントをつける

```diff:Main.vue
<template>
  <div class="container">
    <h1>Vue Mobile Hover</h1>
    <p>Hoverのみ</p>
-   <button class="btn" @click="countUp">UP</button>
+   <button class="btn" @click="countUp" :class="{ pc: !mobile}">UP</button>
    <p>Touch Event実装</p>
-   <button class="btn" @click="countDown">DOWN</button>
+   <button class="btn" @click="countDown" :class="{ pc: !mobile}">DOWN</button>
    <p>count: <input v-model="count" readonly></p>
  </div>
</template>
```

`!mobile`(モバイルでない)場合にpcというクラスを付けます。

```diff:Main.vue
<style lang="scss" scoped>
.btn {
  border: 1px solid blue;
  width: 200px;
  height: 50px;
  cursor: pointer;
  background: white;
  color: blue;
+ &.pc {
    &:hover {
      background: blue;
      color: white;
    }
    &:active {
      background: white;
      color: blue;
    }
+ }
}
</style>
```

hover,activeをpcの場合のみ適用します。
モバイル端末においてhoverやactiveのあてられている要素は挙動が少し変になるからです。

### 3.touchイベントの追加

``` diff:Main.vue
<template>
  <div class="container">
    <h1>Vue Mobile Hover</h1>
    <p>Hoverのみ</p>
    <button class="btn" @click="countUp" :class="{ pc: !mobile}">UP</button>
    <p>Touch Event実装</p>
-   <button class="btn" @click="countDown" :class="{ pc: !mobile}">DOWN</button>
+   <button class="btn" @click="countDown" :class="{ pc: !mobile}" @touchstart="touchStart" @touchend="touchEnd">DOWN</button>
    <p>count: <input v-model="count" readonly></p>
  </div>
</template>
```
DOWNボタンに@touchstartと@touchendイベントを追加し、それぞれ`touchStart`,`touchEnd`メソッドを実行させます。

```diff:Main.vue
  // 省略
  methods: {
    countUp() {
      this.count++
    },
    countDown(){
      this.count--
    },
+   touchStart(e){
+     e.target.classList.add('touched');
+   },
+   touchEnd(e){
+     e.target.classList.remove('touched');
+   }
  },
```
タッチした要素に対し、に、@touchstart時に`touched`を付与、@touchend時に`touched`を削除するメソッドを定義します。
これで、ボタンに触れている間だけ`touched`が付与されていることになります。

```diff:Main.vue
  // 省略
  &.pc {
    &:hover {
      background: blue;
      color: white;
    }
    &:active {
      background: white;
      color: blue;
    }
  }
+ &.touched {
+   background: blue;
+   color: white;
+ }
}
```
最後に、`touched`に対して`hover`時と同じcssを適用します。
PCではtouchイベントは動かないので、Mobileだけに適用するといったことは必要ありません。


シンプルといいつつ少し長くなってしまいましたが、これでスマホでもいい感じに動くボタンが作れます。

完成品 : https://maple0922.github.io/vue-mobile-hover
Github : https://github.com/Maple0922/vue-mobile-hover

## 最後に
生のJavaScriptでは`addEventListener`や`ontouchstart`などを使って実装する形になりますが、基本的には『タッチしている要素にクラスを付与してCSSを適用させる』という部分は変わらないと思います。

あとボタンボタン言ってますが、むしろ見た目からは押せることがわかりにくい要素に対して「押せるよ！！」という意味で実装するのもいいかもしれません。

主にWeb制作をやっていて「なるべくJSは使わずCSSで済ませたい」という方もいると思います。近いうちに`&:touch`みたいな擬似クラスができてくれるといいですね。