calendar —— カレンダー
カレンダーは常駐する日付パネルです:date-picker がパネルをポップアップレイヤーに隠すのに対し、calendar はそれをページ上に展開します。3 つの選択モード——単一選択、区間、複数選択——と、選択可能日付範囲の制約をサポートします。core/calendar は date-picker のグリッドアルゴリズム(generateCalendarDays)を再利用し、その上にシフト、範囲制約、選択状態のトグルを重ねます。
- 各端の
LhCalendar実装:スケジュールビュー、チェックイン、ホテル / 航空券の日付選択ページ。 - 日付を常駐して表示したい(ポップアップで選択させたくない)シーン。
- 使うべきでない:スペースが限られたフォーム内(
date-pickerのほうが省スペース);日付を 1 つ選ぶだけでページがすでに密集している場合。
インポート方法
Section titled “インポート方法”import { getCalendarClassNames, shiftCalendarDate, shiftCalendarMonth, isCalendarDateDisabled, clampDateToRange, isCalendarSelected, normalizeCalendarSpan, isCalendarInSpan, toggleCalendarDate, getCalendarCellAria, generateCalendarDays, isSameDay,} from '@lh-ui/share/core/calendar'import type { LhCalendarRange, LhCalendarSelection } from '@lh-ui/share/core/calendar'依存方向は意味的なもの:calendar は core/shared/date から再実装するのではなく、date-picker から generateCalendarDays をインポートします——カレンダーグリッドのアルゴリズムは date-picker の責務であり、calendar はそのカレンダービューです。一方で汎用プリミティブ(compareDate / isDateDisabled / isSameDay)は共有コアから直接取り、calendar は date-picker の logic に逆依存しません。これは「汎用プリミティブは下層へ、ドメインアルゴリズムは所定の位置へ」という階層化の規約です。
逆順区間の耐性:isCalendarDateDisabled と clampDateToRange はどちらも先に normalizeCalendarSpan で区間を正規化します——ユーザーが [end, start](逆順)を渡しても、すべて無効化されたりクランプが狂ったりしません。UI 層は呼び出し側に事前のソートを強制しません。
shiftCalendarMonth の正規化:year * 12 + month + delta を計算してから年月に戻し、((total % 12) + 12) % 12 で負数のモジュロを処理します——delta が負で年を跨ぐとき(1 月 -2 など)でも月が -1 になりません。
複数選択のイミュータブルなトグル:toggleCalendarDate は一致すれば削除、そうでなければ追加して時系列でソートします——新しい配列を返すためそのままリアクティブな更新に使え、順序も安定します(UI のちらつきを防ぐ)。
| 関数 | シグネチャ | 戻り値 | 役割 |
|---|---|---|---|
getCalendarClassNames |
() => string[] |
['lh-calendar'] |
コンテナのクラス名 |
shiftCalendarDate |
(date, days) => Date |
新しい Date | 日単位のシフト(月跨ぎ安全) |
shiftCalendarMonth |
(year, month, delta) => { year, month } |
年月 | 月単位のシフト |
isCalendarDateDisabled |
(date, range?) => boolean |
無効かどうか | 制約なしなら常に false |
clampDateToRange |
(date, range?) => Date |
クランプ後の日付 | 範囲外を境界に戻す |
isCalendarSelected |
(date, selected) => boolean |
選択中かどうか | 同日判定 |
normalizeCalendarSpan |
(a, b) => [Date, Date] |
正規化された区間 | start ≤ end を保証 |
isCalendarInSpan |
(date, start, end) => boolean |
閉区間内かどうか | いずれかの端が null なら false |
toggleCalendarDate |
(dates, date) => Date[] |
新しい配列 | 一致すれば削除 / さもなくば追加してソート |
getCalendarCellAria |
(options: { date, selected, range? }) => LhAriaAttrs |
aria 属性パック | aria-current / aria-disabled |
export type LhCalendarRange = [Date, Date]export type LhCalendarSelection = 'single' | 'range' | 'multiple'グリッドのデータ構造は LhDatePickerDayData(date-picker 参照)を再利用します。
アクセシビリティ
Section titled “アクセシビリティ”getCalendarCellAria の出力は控えめです——状態があるときのみ属性を出力します(LhAriaAttrs の偽値省略の規約):
getCalendarCellAria({ date: selectedDate, selected: selectedDate, range: undefined })// { 'aria-current': 'date' }
getCalendarCellAria({ date: new Date(2026, 0, 1), selected: null, range: [new Date(2026, 2, 1), new Date(2026, 3, 1)] })// { 'aria-disabled': true }
getCalendarCellAria({ date: someDate, selected: null })// {}(状態なき場合は空オブジェクト)- 選択日は
aria-current="date"を使います(aria-selectedではなく)——カレンダーはナビゲーション型コントロールであり、APG の規約では「現在位置」を current で表します。 - 無効な日は
aria-disabledとし、UI 層のクリックとキーボードナビゲーションはこれらのセルをスキップします。 - キーボード操作:セルにフォーカスした後 ↑/↓/←/→ で
shiftCalendarDate(date, ±1|±7)を移動し、Enter / Space で選択を確定します。 - グリッド全体には
role="grid"を掛け、date-picker のパネルと同じ構造にします。
import { shiftCalendarMonth, normalizeCalendarSpan, toggleCalendarDate, isCalendarInSpan,} from '@lh-ui/share/core/calendar'
shiftCalendarMonth(2026, 0, -2) // { year: 2025, month: 10 }(負数のモジュロが正しい)shiftCalendarMonth(2026, 11, 1) // { year: 2027, month: 0 }normalizeCalendarSpan(new Date(2026, 3, 1), new Date(2026, 2, 1))// [2026-03-01, 2026-04-01](逆順を自動で正規化)isCalendarInSpan(new Date(2026, 2, 15), new Date(2026, 2, 1), new Date(2026, 2, 28)) // trueisCalendarInSpan(new Date(2026, 2, 15), null, null) // false(いずれかの端が null)toggleCalendarDate([new Date(2026, 2, 1)], new Date(2026, 2, 5))// [2026-03-01, 2026-03-05](追加してソート)toggleCalendarDate([new Date(2026, 2, 1), new Date(2026, 2, 5)], new Date(2026, 2, 1))// [2026-03-05](一致して削除)<!-- Vue3:区間モード --><div :class="getCalendarClassNames()"> <div class="lh-calendar__header"> <button type="button" @click="shift(-1)">‹</button> <span class="lh-calendar__title">{{ year }} 年 {{ month + 1 }} 月</span> <button type="button" @click="shift(1)">›</button> </div> <div class="lh-calendar__body" role="grid"> <div v-for="row in weekRows(days)" :key="row[0].date.getTime()" class="lh-calendar__row" role="row"> <button v-for="cell in row" :key="cell.date.getTime()" type="button" class="lh-calendar__cell" :class="cellClass(cell)" role="gridcell" v-bind="getCalendarCellAria({ date: cell.date, selected: selected, range: allowedRange })" @click="onPick(cell.date)" > {{ cell.day }} </button> </div> </div></div>// 区間モードの 2 段階選択(UI 層のルール、core には内蔵されない)// if (!rangeStart || (rangeStart && rangeEnd)) { rangeStart = date; rangeEnd = null }// else rangeEnd = date (rangeStart より早い場合は normalizeCalendarSpan が正規化)// React:複数選択モード// const [dates, setDates] = useState<Date[]>([])// onClick: setDates(toggleCalendarDate(dates, cell.date))// uniapp:<view> グリッド + @touchstart;core の range 制約と選択トグル関数はそのまま使用スタイルとトークン
Section titled “スタイルとトークン”| クラス名 | 意味 |
|---|---|
.lh-calendar |
基底クラス |
.lh-calendar__header / __title |
ヘッダーナビゲーション |
.lh-calendar__body |
グリッド領域 |
.lh-calendar__row |
1 週間 |
.lh-calendar__cell |
日付セル |
.lh-calendar__cell.is-today / .is-selected / .is-in-range / .is-start / .is-end / .is-disabled / .is-other-month |
状態修飾 |
選択は --lh-primary-color、区間の中間は低不透明度の主色、今日はボーダー、無効は --lh-text-disabled です。
- month は 0 始まり:date-picker とネイティブ
Dateと一致し、UI 表示では+1します。 - 3 つの選択モードと core の関係:
LhCalendarSelectionは型宣言にすぎず、core の関数はモード非依存です——isCalendarSelectedが single、isCalendarInSpanが range、toggleCalendarDateが multiple を担います。モード切り替えの配線は UI 層にあります。 - 区間モードの仮選択状態:
rangeStartが決まりrangeEndが未確定のとき、ホバー中のセルの中間区間をリアルタイムにハイライトします(UI 層が仮の end でレンダリング)。core のgenerateCalendarDaysは null の rangeEnd を受け付け、エラーになりません。 clampDateToRangeの用途:外部からの値設定(v-modelで範囲外の日付が渡ったときなど)を合法な区間へ収束させます。UI 層は値の変化を watch して 1 回呼びます。- 常駐パネルのパフォーマンス:calendar は date-picker のように開いたときだけグリッドを計算するのではなく、月をめくるたびに再計算します。毎フレーム
generateCalendarDaysを再計算してはいけません(35~42 個の Date オブジェクトを生成します)。 - date-picker との境界:両者はグリッドデータを共有しますが状態モデルは共有しません——date-picker の
LhDatePickerStateはポップアップインタラクション向けで、calendar には可視状態がありません。
@lh-ui/share/core/calendar → shiftCalendarMonth( )(填好参数后点运行)Copyright © 2024-2026 FengMo WenXue