ハコヅミを作る #32:キャンプの期間を、カレンダーで開始日と終了日をまとめて選ぶ
前回(#31)は、小さな直しでした。今回は、少し大きめの使い勝手の改善です。
- キャンプの予定・実績の期間を、カレンダーで選ぶ(#150)
1泊2日でも、カレンダーを2回開く
キャンプの期間は、開始日と終了日を、別々の日付の欄で入れていました。ブラウザの標準の日付の欄です。1泊2日のキャンプでも、カレンダーを2回開くことになります。
航空会社やホテルの予約では、1つのカレンダーの上で、行きと帰りを続けて選びます。あれと同じにしたい、というのが始まりです。
CLAUDE.md には、「スマホ向けの入力の工夫は、Issue で明示されたら対応する」と書いてあります。先回りして作らないための決まりです。今回は、Issue で明示して、期間の欄だけを対象にしました。
先に、出し方を選ぶ
見た目のある変更は、作る前に、画面イメージを見て選んでいます。今回は、出し方を3つ並べてもらいました。

出し方の3つの案(作る前の画面イメージ)。選んだのは、いちばん左の案A
選んだのは、欄の下に開く案です。Claude Code のおすすめも、これでした。
- うしろを暗くする仕組みが要らない
- 「決定」のボタンが要らない(2日目を押したら、閉じる)
- キーボードや読み上げの扱いが、いちばん簡単
広い幅では、2か月を並べることにしました。月をまたぐ期間が、切り替えずに選べます。
自作にした
カレンダーの部品は、パッケージを足す手もありました。CLAUDE.md にも、「ダイアログなどが要るときは、Bits UI を検討する」と書いてあります。
今回は、自作にしました。欄の下に開く形なら、難しいところ(うしろを操作できなくする、フォーカスを閉じ込める)がありません。残るのは、カレンダーの計算と、矢印キーでの移動です。これなら、自分で読んで保守できる量に収まります。
できあがったもの

開始日(10日)を選んだところ。指やカーソルのある日までが、薄い帯で示される
- 1回目に押した日が開始日、2回目が終了日
- 同じ日を2回押すと、日帰り
- 開始日より前の日を押すと、その日を開始日にして、選び直し
- 終了日まで決まると、カレンダーが閉じて、欄に「2026/10/10(土)〜10/12(月)・2泊」と出る

広い幅では、2か月が並ぶ。月をまたぐ期間が、一目で見える
画面イメージになかったものを、1つ足しました。カレンダーの上の、年と月の選択欄です。「前の月」のボタンだけだと、2年前のキャンプを記録するのに、24回押すことになります。作る前に、Claude Code から提案があって、足すことにしました。
送る値は、変えない
画面は大きく変わりましたが、サーバーに送る値は、今までと同じです。開始日と終了日の2つを、2026-10-10 の形で送ります。選んだ結果を、見えない欄に入れているだけです。
これで、サーバーの確かめも、テーブルも、変えずに済みました。変えたのは、入力のしかただけです。
JavaScript が動かないときのために、今までの日付の欄も、<noscript> の中に残してあります。
日付は、文字のまま扱う
カレンダーの計算(その月は何日あるか、1日は何曜日か、この日は期間の中か)は、1つのファイルに切り出して、ユニットテストしています。
日付の扱いで、決めたことがあります。日付は、2026-10-10 という文字のまま扱う、です。
JavaScript の Date は、日付と時刻と時差を、まとめて持ちます。「10月10日」を Date にすると、端末の時差によって、9日の夜になったり、10日の朝になったりします。日本で作って日本で使うぶんには、気づきません。海外から使うと、選んだ日が1日ずれます。
計算のときだけ、時差のない時刻(UTC)として扱い、結果はすぐ文字に戻します。「今日」の印だけは、端末の日付を使います。
これは、テストでも確かめています。ブラウザの時刻をロサンゼルスにして、選んだ日がそのまま保存されることを見ています。
キーボードでも選べるように
マウスや指で使う人が多い部品ですが、キーボードでも選べるようにしました。
- 矢印キーで、前後の日、前後の週に移る。月の外へ出ると、月が切り替わる
- Enter で選ぶ
- Esc で閉じて、もとの欄にフォーカスが戻る
- Tab で入れる日付は、いつも1つだけ(31 個のボタンを、Tab で順にたどらなくて済む)
読み上げでは、日付を「2026年10月10日 土曜日」と伝え、選んだ日には印を付けています。
入力欄を変えると、テストが 40 か所変わる
いちばん手間がかかったのは、テストでした。
キャンプを登録する E2E は、たくさんあります。どれも、日付の欄に、直接 2030-10-03 と入れていました。その欄がなくなったので、8つのファイル、39 か所を書き換えることになります。
カレンダーを操作する手順を、1つの関数にまとめました。
// 年と月の選択欄で月を出してから、日を押す。end を省くと、日帰り
await setPeriod(page, '予定期間', '2030-07-10', '2030-07-11');
テストは、この関数で、本物のカレンダーを操作します。見えない欄に値を直接入れる手もありましたが、それでは、カレンダーが壊れても、ほかのテストが気づけません。
テストが、終わらない
書き換えて、全部のテストを流すと、終わりませんでした。2分で終わるはずが、10分たっても動いています。
原因は、年の選択欄でした。選択欄に出す年を、「今年の10年前から5年後まで」にしていました。ところが、テストの中に、2046 年のキャンプを作るものがありました(ほかのテストのデータと重ならないよう、遠い未来の日付を使っています)。選択欄に 2046 年がないので、テストは、出てくるのを 90 秒待って、時間切れになります。それが何本も続いていました。
年の範囲を、今年の前後 30 年に広げました。選択欄の項目は増えますが、生まれた年を選ぶ欄よりは短く、困りません。
「2分のテストが終わらない」と気づくのに時間がかかったのは、テストの出力を、別のコマンドにつないで絞っていたためです。途中の様子が見えませんでした。出力は、ファイルに書いて、あとから読むようにしています。
作れなくなった状態
「終了日が開始日より前だと、エラーが出る」というテストもありました。カレンダーでは、その状態を作れません(前の日を押すと、選び直しになります)。
サーバーの確かめは、残してあります。画面を通さずに、直接送るテストに変えました。画面で起こせなくなったことでも、サーバーが守っていることは、確かめ続けます。
おわりに
- 見た目のある部品は、作る前に、出し方を並べて選ぶ
- 難しいところのない形を選べば、パッケージを足さずに、自分で保守できる
- 入力のしかたを変えても、送る値を変えなければ、サーバーは触らずに済む
- 日付は、文字のまま扱う。
Dateにするのは、計算の一瞬だけ - 入力欄を変えると、その欄を使っているテストが、全部変わる。操作は、1つの関数にまとめる
- 終わらないテストは、何かを待ち続けている。出力をファイルに書いて、どこで止まっているかを見る
次回は、削除などの確認を、アプリのダイアログにした話です。