ハコヅミを作る #32:キャンプの期間を、カレンダーで開始日と終了日をまとめて選ぶ

前回(#31)は、小さな直しでした。今回は、少し大きめの使い勝手の改善です。

  • キャンプの予定・実績の期間を、カレンダーで選ぶ(#150)

1泊2日でも、カレンダーを2回開く

キャンプの期間は、開始日と終了日を、別々の日付の欄で入れていました。ブラウザの標準の日付の欄です。1泊2日のキャンプでも、カレンダーを2回開くことになります。

航空会社やホテルの予約では、1つのカレンダーの上で、行きと帰りを続けて選びます。あれと同じにしたい、というのが始まりです。

CLAUDE.md には、「スマホ向けの入力の工夫は、Issue で明示されたら対応する」と書いてあります。先回りして作らないための決まりです。今回は、Issue で明示して、期間の欄だけを対象にしました。


先に、出し方を選ぶ

見た目のある変更は、作る前に、画面イメージを見て選んでいます。今回は、出し方を3つ並べてもらいました。

キャンプの登録画面の画面イメージが3つ並んでいる。案Aは、予定期間の欄のすぐ下にカレンダーが開き、下の欄が押し下げられている。案Bは、画面が暗くなり、下からカレンダーがせり出していて、「決定」のボタンがある。案Cは、画面が暗くなり、まん中にカレンダーのダイアログが出ていて、「決定」のボタンがある

出し方の3つの案(作る前の画面イメージ)。選んだのは、いちばん左の案A

選んだのは、欄の下に開く案です。Claude Code のおすすめも、これでした。

  • うしろを暗くする仕組みが要らない
  • 「決定」のボタンが要らない(2日目を押したら、閉じる)
  • キーボードや読み上げの扱いが、いちばん簡単

広い幅では、2か月を並べることにしました。月をまたぐ期間が、切り替えずに選べます。

自作にした

カレンダーの部品は、パッケージを足す手もありました。CLAUDE.md にも、「ダイアログなどが要るときは、Bits UI を検討する」と書いてあります。

今回は、自作にしました。欄の下に開く形なら、難しいところ(うしろを操作できなくする、フォーカスを閉じ込める)がありません。残るのは、カレンダーの計算と、矢印キーでの移動です。これなら、自分で読んで保守できる量に収まります。


できあがったもの

スマホの幅の、キャンプの登録画面。予定期間の欄に「2026/10/10(土)〜(終了日を選んでください)」と出ていて、その下にカレンダーが開いている。上に、前の月・次の月のボタンと、年と月の選択欄がある。10日が緑の丸で、11日と12日が薄い緑の帯になっている。4日には、今日を示す枠線がある

開始日(10日)を選んだところ。指やカーソルのある日までが、薄い帯で示される

  • 1回目に押した日が開始日、2回目が終了日
  • 同じ日を2回押すと、日帰り
  • 開始日より前の日を押すと、その日を開始日にして、選び直し
  • 終了日まで決まると、カレンダーが閉じて、欄に「2026/10/10(土)〜10/12(月)・2泊」と出る
広い幅の、キャンプの登録画面。予定期間の欄に「2026/10/30(金)〜11/2(月)・3泊」と出ていて、その下に、2026年10月と11月のカレンダーが横に並んでいる。10月30日と11月2日が緑の丸で、その間の10月31日と11月1日が薄い緑の帯になっている

広い幅では、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つの関数にまとめる
  • 終わらないテストは、何かを待ち続けている。出力をファイルに書いて、どこで止まっているかを見る

次回は、削除などの確認を、アプリのダイアログにした話です。

← トップページに戻る