セマンティックな Vue コンポーネントを作ろう

# セマンティックな実装とは
セマンティックとは「意味」を意味します。Web のフロントエンド、特にHTMLの文脈で言うと、意味に合った正しいタグを使ってマークアップすることを指します。

https://developer.mozilla.org/ja/docs/Glossary/Semantics#html_%E3%81%A7%E3%81%AE%E3%82%BB%E3%83%9E%E3%83%B3%E3%83%86%E3%82%A3%E3%82%AF%E3%82%B9

ブラウザは HTML 要素のタグの種類から意味を解釈し、適切に動作するように表示してくれます。

例えば今回紹介するチェックボックスであれば、`<input type="checkbox">` と `<label>` を使って実装することで、tab キーでの移動とスペースキーでのチェック切り替えをブラウザがサポートしてくれます。
また、スクリーンリーダーでもフォーカスが当たったときに label が読み上げられます。

`<div>` だけでも同じ見た目でクリックするとチェック可能な要素を実装することはできますが、キーボード操作やスクリーンリーダーでの読み上げはサポートされません。

このように、セマンティックな実装により、ユーザビリティを大きく向上することができます。

# 今回作ったもの
## 仕様
- Vue3 + TypeScript + TailwindCSS で実装する
- クリックすると値が選択される
- 選択された値は配列として保持される
- フォーカスがあたっているときは点線で囲まれる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/474068/bc744767-3cf5-3cb1-1822-1b3fd3fa916c.png)

## デモサイト
下記から実際に触ることもできます！

https://punkshiraishi.github.io/punk-components/?path=/docs/arraycheckablelabel--docs

## コード
<details>
<summary>ArrayCheckableLabel</summary>

```vue
<script lang="ts" setup>
import { computed } from 'vue'

const props = defineProps({

  /**
   * 値を追加する配列
   */
  modelValue: {
    type: Array,
    required: true,
  },

  /**
   * チェック時に配列に追加する値
   */
  item: {
    type: null,
    required: true,
  },

  /**
   * ラベルとして表示する値
   */
  label: {
    type: String,
    required: true,
  },

  /**
   * 非活性化どうか
   */
  disabled: {
    type: Boolean,
    default: false,
  },
})

const emit = defineEmits(['update:modelValue'])

const checked = computed({
  get() {
    return props.modelValue.includes(props.item)
  },
  set(state: boolean) {
    emit('update:modelValue', state
      ? [...props.modelValue, props.item]
      : props.modelValue.filter(item => item !== props.item))
  },
})
</script>

<template>
  <label
    class="
      p-2 text-center
      focus-within:outline-dotted focus-within:outline-slate-500
      focus-within:outline-2 focus-within:outline-offset-2
    "
    :class="[
      disabled ? 'cursor-default' : 'cursor-pointer',
      checked ? 'bg-sky-400 hover:bg-sky-300' : 'bg-slate-200 hover:bg-sky-100',
    ]"
  >
    <input
      v-model="checked"
      class="opacity-0 w-0"
      type="checkbox"
      :disabled="disabled"
    >
    <span>
      {{ label }}
    </span>
  </label>
</template>

```
</details>

https://github.com/punkshiraishi/punk-components/blob/main/src/components/ArrayCheckableLabel.vue

## ポイント
### `<label>` と `<input>` で実装
前述したように、`<div>` ではなく `<label>` と `<input>` を使って実装することで、
- tab キーでの移動とスペースキーでのチェック切り替え
- スクリーンリーダーでのラベルとオンオフ切り替えの読み上げ

を実現できています。

```vue
<template>
  <label
    class="中略"
  >
    <input
      v-model="checked"
      class="opacity-0 w-0"
      type="checkbox"
      :disabled="disabled"
    >
    <span>
      {{ label }}
    </span>
  </label>
</template>

```

### `focus-within` で内部のフォーカス状態に応じてフォーカスを表示する
`focus-within` は、CSS の擬似クラスの 1 つで、要素内の子要素がフォーカスされたときに、親要素にスタイルを適用するために使用されます。TailwindCSS では `focus-within:` をクラス名の前につけることで実現できます。

https://tailwindcss.com/docs/hover-focus-and-other-states#focus-within

今回の例ではこのように書くことでフォーカス時の枠線スタイルを実現しています！
```vue
  <label
    class="
      p-2 text-center
      focus-within:outline-dotted focus-within:outline-slate-500
      focus-within:outline-2 focus-within:outline-offset-2
    "
  >
```



# まとめ
セマンティックな実装を行うことで、タブキーでの切り替えやスクリーンリーダーでの読み上げを簡単に実現することができます。
これらの機能をスクラッチから実装するのは非常に手間がかかるため、**セマンティックな実装は非常にコストパフォーマンスが高い**と言えます。

マークアップの経験が少ないエンジニアは、かつての私のように、何でも`<div>`タグで実装しようとする傾向がありますが、セマンティックな実装を心がけていくことが重要です。