date pickerとdate range pickerができるまで

この記事はHRBrain Advent Calendar 2022の1日目の記事です。

https://qiita.com/advent-calendar/2022/hrbrain

こんにちは。フロントエンドエンジニアの塚本です。
この記事では、date pickerとdate range pickerを作成するまでに行ったトライアンドエラーについてお話しようと思っています。

## どうして作ったのか
date pickerやdate range pickerは実装コストが高く、そもそも標準のHTMLや外部ライブラリを使用するケースもあるかと思います。
弊社の場合はtoBeかつマルチプロダクトであることから、全プロダクト共通で標準UIよりもわかりやすいインターフェイスでなければならない等の都合があることや、
このようなdate pickerやdate range pickerをプロダクト内で使用する頻度が高いことから、
ユーザー体験に関わってくるため社内のコンポーネントライブラリの一部として自前実装することとなりました。

## どんなものを作ったのか
たとえばdate range pickerはこのようなものを作成しました。

![date_picker1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982722/2d4c700f-4e0b-b7d0-d564-fbcb1808f7ac.gif)

以下のような仕様をもとに作成されています。

- インプットエリアは自由入力が可能
- 不正な入力値はバリデーションされる
- パネルによる日付選択が可能
- パネルは年・月・日の3タイプ
- disabledにする期間を設定できる

## 大変だったこと
作成にあたってトライアンドエラーをした部分があったので、そちらについて下記に書きます。

### 1888年以前の日付はJSTと時差がある
テストデータを1234年5月6日にしていたら、タイムゾーンが見慣れない`GMT+0918`になっていることに気が付きました。
日本標準時は`GMT+0900`のはずなので、これは一体なんだろう…？となりました。
詳しくは下記ブログを参照していただければと思いますが、
`GMT+0918`は東京の地方時で、標準時が施行される1888年以前の日時を扱う際は時差が`GMT+0918`と解釈されてしまうようでした。

```typescript
const testDate1 = new Date(1234, 4, 6)
const testDate2 = new Date(2022, 11, 1)

console.log(testDate1)
//　Sat May 06 1234 00:00:00 GMT+0918（日本標準時）

console.log(testDate2)
//　Thu Dec 01 2022 00:00:00 GMT+0900（日本標準時）
```

>参考にさせて頂いたブログ
>https://www.m3tech.blog/entry/timezone-091859

### 複数の`input`を1つの要素にまとめて使う
date range pickerは`input`を2個もっており、開始日と終了日をそれぞれ入力できます。
しかし、UIの見た目としては1つの`input`になっており、これらのスタイルを綺麗にすることにも苦労しました。

![date_picker_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982722/470a51ab-3d32-0b64-fb5d-92914687a842.png)

`input`のブラウザ標準のスタイルを上書きしたり、`input`のカーソルの反応範囲を適切に設定するなど、細かな部分に気を使う実装が多く、面白かったです。
ちなみに、たとえば標準スタイルの上書きについては次のようなコードで実装すると、
![sample_input.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982722/e7f94310-632a-ef16-944c-beb825822f99.png)
こういった`input`ができます。`div`は`position: relative`であれば好きなスタイルを当てることができます。

```html
 <div class="input-wrapper">
    <text>placeholder</text>
    <input class="sample-input" type="date"/>
 </div>
```

```css
.input-wrapper {
  display: flex;
  position: relative;
  align-items: center;
  justify-content: start;
  border-radius: 6px;
  border: 1px solid;
  height: 40px;
  padding: 0px 12px;
  background-color: white;
  width: 124px;
}
.sample-input {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background: transparent;
  border: none;
  outline: none;
  position: absolute;
  bottom: 0;
  left: 0;
  top: 0;
  opacity: 0;
}

```


### 最小値や最大値について
今回は許容する日付の範囲を指定することができる設計にしました。
たとえばアンケートの回答日時を選択させる場合は「今日以降の日付のみ選択できる」といったカスタマイズが可能です。
この際、基準の値を含めるのかといった仕様設定が難しかったです。
結局は、ユーザーの慣れ親しんでいる仕様であれば学習コストが少ないだろう、という考えのもと
HTML標準と仕様を合わせることになりました。

###  eventが難しい
`input`を導入すると、`onBlur`や`onKeyDown`等を設定する必要があります。
またdate pickerはパネルを有するので`onClick`も設定する必要があり、パネルの入力と`input`の入力を適切に処理しなければいけません。
これが意外と難しく、`input`の`onBlur`を素直に設定するとパネルでの`onChange`が発火しなくなるというバグに直面しました。
理由は単純で、マウスイベントは下記の順に発火します。

```
onMouseDown → onBlur → onMouseUp → onClick
```

バグの原因は、`input`に`onBlur`を設定すると、パネルを`onClick`する前に`onBlur`が効いてパネルが消えてしまうことでした。
わかってみると単純なのですが、マウスイベントはコードだけ確認しても正しく機能しているか判断がつきにくく、個人的にはとても苦労しました。
またこれ以外にも、IMEの変換のためのEnter押下とインプットから離脱するEnter押下の判別など、イベント操作を監視する必要が多く学びになりました。

## まとめ
アドベントカレンダー1日目ということで何を書くか迷ったのですが、今回はdate pickerとdate range pickerができるまで、試行錯誤をした内容を紹介しました。
実はdate pickerは入社してからはじめて自分がメインで実装を担当したコンポーネントだったのですが、
その当時から社内の先輩エンジニアやデザイナーの方が良い意味で自分をフラットなメンバーの一員として扱ってくださり、ありがたいなと感じていました。
ということで、そんな弊社に興味を持った方がいればぜひ下記ページからご応募ください！

https://www.hrbrain.co.jp/recruit