期間選択カレンダー
stimeo--date-range-picker
calendar 派生の、プレビューとプリセットに対応した 2 点の期間選択カレンダー。
stimeo--date-range-picker コントローラは Calendar の日付グリッドを土台に、開始 / 終了の 2 点選択を加えます。1 回目のクリックで開始日を確定して選択中モードに入り(ポイント中の日まで仮範囲をプレビュー)、2 回目で終了日を確定します(開始より前なら自動で入れ替え)。範囲端は aria-selected で支援技術へ伝え、内側のセルは描画用に data-in-range のみを用います。グリッドのキーボード移動は roving tabindex で行い、プリセット(今日 / 直近 7 日 / 今月)で表示月を移動、 Escape で選択途中の仮範囲を破棄します。確定した期間はライブリージョンと隠しフィールドへ反映します。ライブラリは挙動のみを提供します。
キーボード操作
| キー | 動作 |
|---|---|
| ← | フォーカスを前日に移動する。月境界をまたぐ場合は自動的に前月に遷移する。 |
| → | フォーカスを翌日に移動する。月境界をまたぐ場合は自動的に翌月に遷移する。 |
| ↑ | フォーカスを1週間前の同じ曜日に移動する。月境界をまたぐ場合は自動的に前月に遷移する。 |
| ↓ | フォーカスを1週間後の同じ曜日に移動する。月境界をまたぐ場合は自動的に翌月に遷移する。 |
| PageUp | フォーカスを前月の同じ日に移動する。 |
| PageDown | フォーカスを翌月の同じ日に移動する。 |
| Home | フォーカスを現在週の開始曜日のセルに移動する。 |
| End | フォーカスを現在週の終了曜日のセルに移動する。 |
| Enter / Space | 現在フォーカスされている日を、開始 → 終了の順で確定する。 |
| Escape | 選択途中の仮範囲を破棄する(確定済みの範囲は保持する)。 |
<%# Markup for the date-range-picker (range-selection calendar) demo.
stimeo--date-range-picker derives from calendar and handles two-point start/end
selection, a provisional range preview mid-selection, marking in-range cells,
auto-swapping when start > end, applying presets, and grid keyboard movement
(roving). Range ends are shown with aria-selected and the inside with data-in-range;
placement and styling live in demo.css. %>
<div class="drp demo-card" data-controller="stimeo--date-range-picker">
<div class="drp__header">
<button class="drp__nav" type="button"
data-action="click->stimeo--date-range-picker#prev"
aria-label="<%= t("components.date_range_picker.demo.prev_month") %>">‹</button>
<span class="drp__label" id="drp-month" aria-live="polite"
data-stimeo--date-range-picker-target="monthLabel"></span>
<button class="drp__nav" type="button"
data-action="click->stimeo--date-range-picker#next"
aria-label="<%= t("components.date_range_picker.demo.next_month") %>">›</button>
</div>
<div class="drp__weekdays" aria-hidden="true">
<% t("components.date_range_picker.demo.weekdays").each do |day| %>
<span class="drp__weekday"><%= day %></span>
<% end %>
</div>
<div class="drp__grid" role="grid" aria-labelledby="drp-month"
data-stimeo--date-range-picker-target="grid">
<% 6.times do %>
<div class="drp__row" role="row">
<% 7.times do %>
<button class="drp__cell" type="button" role="gridcell" tabindex="-1"
data-stimeo--date-range-picker-target="cell"
data-action="click->stimeo--date-range-picker#selectDate
mouseenter->stimeo--date-range-picker#previewTo
focus->stimeo--date-range-picker#previewTo
keydown->stimeo--date-range-picker#onKeydown"></button>
<% end %>
</div>
<% end %>
</div>
<div class="drp__presets" role="group"
aria-label="<%= t("components.date_range_picker.demo.presets_label") %>">
<button class="drp__preset" type="button" data-range="today"
data-action="click->stimeo--date-range-picker#applyPreset">
<%= t("components.date_range_picker.demo.preset_today") %>
</button>
<button class="drp__preset" type="button" data-range="last7"
data-action="click->stimeo--date-range-picker#applyPreset">
<%= t("components.date_range_picker.demo.preset_last7") %>
</button>
<button class="drp__preset" type="button" data-range="thisMonth"
data-action="click->stimeo--date-range-picker#applyPreset">
<%= t("components.date_range_picker.demo.preset_this_month") %>
</button>
</div>
<span class="drp__status" role="status" aria-live="polite"
data-stimeo--date-range-picker-target="status"></span>
<input type="hidden" name="start_date" data-stimeo--date-range-picker-target="startField" />
<input type="hidden" name="end_date" data-stimeo--date-range-picker-target="endField" />
</div>
/*
* Presentation-only styles for the date-range-picker demo.
* Range shading reacts to the state hooks the library sets on each cell:
* - [data-range-start] / [data-range-end] … the range ends
* - [data-in-range] … inside start–end (incl. the preview range)
* - [aria-selected="true"] … the two end cells (for AT announcement)
* - [aria-disabled="true"] … outside min/max, not selectable
*/
/* Card surface comes from the shared .demo-card primitive (base/demo-primitives.css),
the same one the calendar demo uses; only layout-specific bits stay here. */
.drp {
width: min(20rem, 100%);
font-variant-numeric: tabular-nums;
}
.drp__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.drp__label {
font-weight: 600;
}
.drp__nav {
width: 2rem;
height: 2rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface, var(--surface-card));
cursor: pointer;
font-size: 1.1rem;
line-height: 1;
}
.drp__weekdays,
.drp__row {
display: grid;
grid-template-columns: repeat(7, 1fr);
}
.drp__weekday {
padding: 0.25rem 0;
text-align: center;
font-size: 0.75rem;
color: var(--text-muted, var(--color-text-muted));
}
.drp__cell {
aspect-ratio: 1;
border: 0;
background: transparent;
cursor: pointer;
font: inherit;
border-radius: 0;
}
.drp__cell[data-outside="true"] {
color: var(--color-text-muted);
}
.drp__cell[data-today="true"] {
font-weight: 700;
text-decoration: underline;
}
.drp__cell[data-in-range] {
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.drp__cell[data-range-start],
.drp__cell[data-range-end],
.drp__cell[aria-selected="true"] {
background: var(--accent);
color: var(--white);
}
.drp__cell[data-range-start] {
border-top-left-radius: 999px;
border-bottom-left-radius: 999px;
}
.drp__cell[data-range-end] {
border-top-right-radius: 999px;
border-bottom-right-radius: 999px;
}
.drp__cell[aria-disabled="true"] {
color: var(--color-text-muted);
cursor: not-allowed;
}
.drp__cell:focus-visible {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
.drp__presets {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
.drp__preset {
padding: 0.375rem 0.625rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background: var(--surface, var(--surface-card));
cursor: pointer;
font-size: 0.85rem;
}
.drp__status {
display: block;
margin-top: 0.625rem;
min-height: 1.25rem;
color: var(--text-muted, var(--color-text-muted));
}
// Demo that subscribes to the date-range-picker change event to inspect the confirmed
// range (ISO dates). The library also announces the confirmed range in a status live
// region, but the consumer can use the detail too.
document.querySelectorAll('[data-controller~="stimeo--date-range-picker"]').forEach((root) => {
root.addEventListener('stimeo--date-range-picker:change', (e) => {
// In real use, e.detail.start / e.detail.end (ISO date strings) feed submission or filtering.
console.log('range:', e.detail.start, '→', e.detail.end);
});
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。 共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--date-range-picker"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
grid
必須
|
6 週分の月グリッドのコンテナ。 | data-stimeo--date-range-picker-target="grid" |
monthLabel
|
ローカライズされた表示月・年を示す live な span。 | data-stimeo--date-range-picker-target="monthLabel" |
cell
必須
|
42 個の gridcell の 1 つ。日付・範囲・roving tabindex・無効状態が各セルに反映される。 |
data-stimeo--date-range-picker-target="cell" |
status
|
確定した範囲を読み上げる live な status 領域。 | data-stimeo--date-range-picker-target="status" |
startField
|
確定した範囲開始日を反映する隠し input。 | data-stimeo--date-range-picker-target="startField" |
endField
|
確定した範囲終了日を反映する隠し input。 | data-stimeo--date-range-picker-target="endField" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
min
|
選択可能な最小日付(YYYY-MM-DD)。それ以前のセルは aria-disabled(既定は空)。 |
data-stimeo--date-range-picker-min-value |
max
|
選択可能な最大日付(YYYY-MM-DD)。それ以降のセルは aria-disabled(既定は空)。 |
data-stimeo--date-range-picker-max-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
applyPreset
|
クリックされたボタンの data-range から名前付きプリセット範囲(today/last7/last30/thisMonth)を適用する。 |
stimeo--date-range-picker#applyPreset |
next
|
翌月へ移動する。 | stimeo--date-range-picker#next |
onKeydown
|
グリッドのキーボード操作(矢印、Home/End、PageUp/Down)、Enter/Space による選択、Escape による選択中断を処理する。 | stimeo--date-range-picker#onKeydown |
prev
|
前月へ移動する。 | stimeo--date-range-picker#prev |
previewTo
|
選択中、ホバー/フォーカスしたセルまでの範囲をプレビュー表示する。 | stimeo--date-range-picker#previewTo |
selectDate
|
クリックされたセルで範囲の端点を確定する(1 回目は開始、2 回目は終了)。 | stimeo--date-range-picker#selectDate |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
範囲が確定したときに発火。detail は { start, end }(ISO 日付、start ≤ end)。 |
stimeo--date-range-picker:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-selected |
セル | 範囲の開始 / 終了の端。 |
data-range-start / data-range-end |
セル | 範囲端の視覚表現用フック。 |
data-in-range |
セル | 開始〜終了の内側セル(仮範囲を含む)。 |
aria-disabled |
セル | min / max 範囲外で選択不可。 |
tabindex |
セル | ロービング。フォーカス可能なセルは常に 1 つ。 |
テキスト |
status リージョン | 確定した期間をライブリージョンで通知。 |