ajo-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
package/src/calendar.tsx
ADDED
|
@@ -0,0 +1,1441 @@
|
|
|
1
|
+
import type { Children, IntrinsicElements, Stateful, Stateless } from 'ajo'
|
|
2
|
+
import { controlled, dom, grid, remember, statefulRootAttrs as rootAttrs, type GridMove } from 'ajo-cloves'
|
|
3
|
+
import { calendarDate, compile, type Availability, type AvailabilityMatcher, type CalendarMatcher } from './availability'
|
|
4
|
+
import { DirectionContext } from './direction'
|
|
5
|
+
import type { FixedArgs, OmitArg } from './utils'
|
|
6
|
+
|
|
7
|
+
export type { AvailabilityMatcher, CalendarMatcher, TimeWindow } from './availability'
|
|
8
|
+
|
|
9
|
+
/** Selection model supported by a Calendar root. */
|
|
10
|
+
export type CalendarMode =
|
|
11
|
+
| 'multiple'
|
|
12
|
+
| 'range'
|
|
13
|
+
| 'single'
|
|
14
|
+
|
|
15
|
+
/** Calendar scale used for day navigation and whole-month/year picking. */
|
|
16
|
+
export type CalendarView =
|
|
17
|
+
| 'day'
|
|
18
|
+
| 'month'
|
|
19
|
+
| 'year'
|
|
20
|
+
|
|
21
|
+
/** Available navigation controls for a Calendar caption. */
|
|
22
|
+
export type CalendarCaptionLayout =
|
|
23
|
+
| 'button'
|
|
24
|
+
| 'dropdown'
|
|
25
|
+
| 'dropdown-months'
|
|
26
|
+
| 'dropdown-years'
|
|
27
|
+
| 'label'
|
|
28
|
+
|
|
29
|
+
/** Inclusive date range selected by a range Calendar. */
|
|
30
|
+
export type CalendarDateRange = {
|
|
31
|
+
from?: Date
|
|
32
|
+
to?: Date
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Built-in state flags resolved for one Calendar day. */
|
|
36
|
+
export type CalendarModifiers = {
|
|
37
|
+
disabled: boolean
|
|
38
|
+
outside: boolean
|
|
39
|
+
range_end: boolean
|
|
40
|
+
range_middle: boolean
|
|
41
|
+
range_start: boolean
|
|
42
|
+
selected: boolean
|
|
43
|
+
today: boolean
|
|
44
|
+
unavailable: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Structural Calendar part names accepted by `classNames`. */
|
|
48
|
+
export type CalendarClassName =
|
|
49
|
+
| 'caption'
|
|
50
|
+
| 'day'
|
|
51
|
+
| 'day_button'
|
|
52
|
+
| 'dropdowns'
|
|
53
|
+
| 'grid'
|
|
54
|
+
| 'head'
|
|
55
|
+
| 'month'
|
|
56
|
+
| 'month_cell'
|
|
57
|
+
| 'month_view'
|
|
58
|
+
| 'months'
|
|
59
|
+
| 'outside'
|
|
60
|
+
| 'range_end'
|
|
61
|
+
| 'range_middle'
|
|
62
|
+
| 'range_start'
|
|
63
|
+
| 'selected'
|
|
64
|
+
| 'today'
|
|
65
|
+
| 'week'
|
|
66
|
+
| 'week_number'
|
|
67
|
+
| 'weekday'
|
|
68
|
+
| 'year_cell'
|
|
69
|
+
| 'year_view'
|
|
70
|
+
|
|
71
|
+
/** Locale-aware formatting hooks used by Calendar views. */
|
|
72
|
+
export type CalendarFormatters = {
|
|
73
|
+
day: (date: Date, locale: string | undefined, timeZone?: string) => Children
|
|
74
|
+
fullDate: (date: Date, locale: string | undefined, timeZone?: string) => string
|
|
75
|
+
monthCaption: (date: Date, locale: string | undefined, timeZone?: string) => string
|
|
76
|
+
monthDropdown: (date: Date, locale: string | undefined, timeZone?: string) => string
|
|
77
|
+
weekNumber: (week: number, locale: string | undefined) => Children
|
|
78
|
+
weekday: (date: Date, locale: string | undefined, timeZone?: string) => Children
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One value and label rendered by a Calendar caption dropdown. */
|
|
82
|
+
export type CalendarDropdownOption = {
|
|
83
|
+
label: Children
|
|
84
|
+
value: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Arguments supplied to a custom Calendar caption dropdown. */
|
|
88
|
+
export type CalendarDropdownArgs = {
|
|
89
|
+
label: string
|
|
90
|
+
onValueChange: (value: string, event?: Event) => void
|
|
91
|
+
options: CalendarDropdownOption[]
|
|
92
|
+
value: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Date and resolved state supplied to day styling callbacks. */
|
|
96
|
+
export type CalendarDayState = {
|
|
97
|
+
date: Date
|
|
98
|
+
hidden: boolean
|
|
99
|
+
modifierNames: string[]
|
|
100
|
+
modifiers: CalendarModifiers
|
|
101
|
+
range: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Arguments shared by every Calendar selection mode. */
|
|
105
|
+
export type CalendarCommonArgs = OmitArg<IntrinsicElements['div'], 'children' | 'defaultValue' | 'onSelect'> & {
|
|
106
|
+
/** Allow range selection to span unavailable days without selecting or painting them. */
|
|
107
|
+
allowNonContiguous?: boolean
|
|
108
|
+
/** Caption layout. `dropdown` shows month and year selectors. */
|
|
109
|
+
captionLayout?: CalendarCaptionLayout
|
|
110
|
+
/** Class names for structural calendar parts. */
|
|
111
|
+
classNames?: Partial<Record<CalendarClassName, string>>
|
|
112
|
+
/** Class for the visible caption label. */
|
|
113
|
+
captionLabelClass?: string
|
|
114
|
+
/** Initial visible month for uncontrolled usage. */
|
|
115
|
+
defaultMonth?: Date
|
|
116
|
+
/** Initial uncontrolled view. Defaults to `minView`. */
|
|
117
|
+
defaultView?: CalendarView
|
|
118
|
+
/** Disable dates by date, range, weekday, list, or predicate. */
|
|
119
|
+
disabled?: CalendarMatcher | CalendarMatcher[]
|
|
120
|
+
/** Last navigable month. */
|
|
121
|
+
endMonth?: Date
|
|
122
|
+
/** Custom formatters for labels and visible date text. */
|
|
123
|
+
formatters?: Partial<CalendarFormatters>
|
|
124
|
+
/** Always render six weeks per month. */
|
|
125
|
+
fixedWeeks?: boolean
|
|
126
|
+
/** First navigable year. */
|
|
127
|
+
fromYear?: number
|
|
128
|
+
/** Class for hidden outside-day placeholders. */
|
|
129
|
+
hiddenDayClass?: string
|
|
130
|
+
/** Locale code or DayPicker-like locale object with `code`. */
|
|
131
|
+
locale?: string | { code?: string }
|
|
132
|
+
/** Controlled visible month. */
|
|
133
|
+
month?: Date
|
|
134
|
+
/** Lowest view that commits a value instead of drilling down. */
|
|
135
|
+
minView?: CalendarView
|
|
136
|
+
/** Month dropdown component. */
|
|
137
|
+
monthDropdown?: Stateless<CalendarDropdownArgs>
|
|
138
|
+
/** Class for the month label when month dropdown is hidden. */
|
|
139
|
+
monthLabelClass?: string
|
|
140
|
+
/** Extra date matchers exposed as `data-modifier-*` on day cells. */
|
|
141
|
+
modifiers?: Record<string, CalendarMatcher | CalendarMatcher[]>
|
|
142
|
+
/** Class for previous and next buttons. */
|
|
143
|
+
navButtonClass?: string
|
|
144
|
+
/** Class for caption spacers standing in for the nav buttons on middle months. */
|
|
145
|
+
navSpacerClass?: string
|
|
146
|
+
/** Number of visible months. */
|
|
147
|
+
numberOfMonths?: number
|
|
148
|
+
/** Called when the visible month changes. */
|
|
149
|
+
onMonthChange?: (month: Date, event?: Event) => void
|
|
150
|
+
/** Called when the calendar view changes. */
|
|
151
|
+
onViewChange?: (view: CalendarView, event?: Event) => void
|
|
152
|
+
/** Icon rendered in the next-month button. */
|
|
153
|
+
nextIcon?: Children
|
|
154
|
+
/** Icon class for the next-month button icon span. */
|
|
155
|
+
nextIconClass?: string
|
|
156
|
+
/** Accessible label for the next-month button. */
|
|
157
|
+
nextMonthLabel?: string
|
|
158
|
+
/** Accessible label for the previous-month button. */
|
|
159
|
+
previousMonthLabel?: string
|
|
160
|
+
/** Accessible label for the month dropdown. */
|
|
161
|
+
monthSelectLabel?: string
|
|
162
|
+
/** Accessible label for the year dropdown. */
|
|
163
|
+
yearSelectLabel?: string
|
|
164
|
+
/** Icon rendered in the previous-month button. */
|
|
165
|
+
previousIcon?: Children
|
|
166
|
+
/** Icon class for the previous-month button icon span. */
|
|
167
|
+
previousIconClass?: string
|
|
168
|
+
/** Render custom day content. */
|
|
169
|
+
renderDay?: (date: Date, modifiers: CalendarModifiers) => Children
|
|
170
|
+
/** Keep at least one selection. */
|
|
171
|
+
required?: boolean
|
|
172
|
+
/** Returns the class for a day grid cell from its state. */
|
|
173
|
+
dayClassName?: (state: CalendarDayState) => string | undefined
|
|
174
|
+
/** Class for each day button. */
|
|
175
|
+
dayButtonClass?: string
|
|
176
|
+
/** Show days from adjacent months. */
|
|
177
|
+
showOutsideDays?: boolean
|
|
178
|
+
/** Show ISO week numbers. */
|
|
179
|
+
showWeekNumber?: boolean
|
|
180
|
+
/** First navigable month. */
|
|
181
|
+
startMonth?: Date
|
|
182
|
+
/** IANA time zone used to derive, format, and emit calendar dates. */
|
|
183
|
+
timeZone?: string
|
|
184
|
+
/** Dates that remain selectable but carry unavailable state. */
|
|
185
|
+
unavailable?: AvailabilityMatcher | AvailabilityMatcher[]
|
|
186
|
+
/** Controlled calendar view. Values below `minView` clamp to it. */
|
|
187
|
+
view?: CalendarView
|
|
188
|
+
/** Last navigable year. */
|
|
189
|
+
toYear?: number
|
|
190
|
+
/** First day of week. 0 is Sunday. */
|
|
191
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
|
|
192
|
+
/** Year dropdown component. */
|
|
193
|
+
yearDropdown?: Stateless<CalendarDropdownArgs>
|
|
194
|
+
/** Class for the year label when year dropdown is hidden. */
|
|
195
|
+
yearLabelClass?: string
|
|
196
|
+
/** Additional classes for the calendar root. */
|
|
197
|
+
class?: string
|
|
198
|
+
} & FixedArgs<'children' | 'defaultValue'>
|
|
199
|
+
|
|
200
|
+
/** Arguments for a Calendar that selects one date or period. */
|
|
201
|
+
export type CalendarSingleArgs = CalendarCommonArgs & {
|
|
202
|
+
defaultSelected?: Date
|
|
203
|
+
mode?: 'single'
|
|
204
|
+
onSelect?: (date: Date | null, event: Event) => void
|
|
205
|
+
selected?: Date | null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Arguments for a Calendar that selects multiple dates or periods. */
|
|
209
|
+
export type CalendarMultipleArgs = CalendarCommonArgs & {
|
|
210
|
+
defaultSelected?: Date[]
|
|
211
|
+
mode: 'multiple'
|
|
212
|
+
onSelect?: (dates: Date[], event: Event) => void
|
|
213
|
+
selected?: Date[]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Arguments for a Calendar that selects an inclusive range. */
|
|
217
|
+
export type CalendarRangeArgs = CalendarCommonArgs & {
|
|
218
|
+
defaultSelected?: CalendarDateRange
|
|
219
|
+
mode: 'range'
|
|
220
|
+
onSelect?: (range: CalendarDateRange | null, event: Event) => void
|
|
221
|
+
selected?: CalendarDateRange | null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Public discriminated arguments accepted by the Calendar root. */
|
|
225
|
+
export type CalendarArgs =
|
|
226
|
+
| CalendarMultipleArgs
|
|
227
|
+
| CalendarRangeArgs
|
|
228
|
+
| CalendarSingleArgs
|
|
229
|
+
|
|
230
|
+
/** Arguments supplied to the Calendar day-button renderer. */
|
|
231
|
+
export type CalendarDayButtonArgs = OmitArg<IntrinsicElements['button'], 'children'> & {
|
|
232
|
+
date: Date
|
|
233
|
+
day: Children
|
|
234
|
+
modifiers: CalendarModifiers
|
|
235
|
+
/** IANA time zone used to derive the button's `data-day`. */
|
|
236
|
+
timeZone?: string
|
|
237
|
+
/** Additional classes. */
|
|
238
|
+
class?: string
|
|
239
|
+
} & FixedArgs<'children'>
|
|
240
|
+
|
|
241
|
+
type PlainDate = {
|
|
242
|
+
day: number
|
|
243
|
+
month: number
|
|
244
|
+
year: number
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type MonthData = {
|
|
248
|
+
month: PlainDate
|
|
249
|
+
weeks: PlainDate[][]
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const localeCode = (locale: CalendarCommonArgs['locale']) =>
|
|
253
|
+
typeof locale === 'string' ? locale : locale?.code
|
|
254
|
+
|
|
255
|
+
const pad = (value: number) => String(value).padStart(2, '0')
|
|
256
|
+
|
|
257
|
+
const iso = ({ day, month, year }: PlainDate) =>
|
|
258
|
+
`${year}-${pad(month)}-${pad(day)}`
|
|
259
|
+
|
|
260
|
+
const monthIso = ({ month, year }: PlainDate) =>
|
|
261
|
+
`${year}-${pad(month)}`
|
|
262
|
+
|
|
263
|
+
const parseIso = (value: string): PlainDate | undefined => {
|
|
264
|
+
const [year, month, day] = value.split('-').map(Number)
|
|
265
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) return undefined
|
|
266
|
+
return normalize(year!, month!, day!)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const parseMonthIso = (value: string): PlainDate | undefined => {
|
|
270
|
+
const [year, month] = value.split('-').map(Number)
|
|
271
|
+
if (!Number.isFinite(year) || !Number.isFinite(month) || month! < 1 || month! > 12) return undefined
|
|
272
|
+
return { day: 1, month: month!, year: year! }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const utcDate = (year: number, month: number, day: number) => {
|
|
276
|
+
const date = new Date(0)
|
|
277
|
+
date.setUTCFullYear(year, month - 1, day)
|
|
278
|
+
date.setUTCHours(0, 0, 0, 0)
|
|
279
|
+
return date
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const normalize = (year: number, month: number, day: number): PlainDate => {
|
|
283
|
+
const date = utcDate(year, month, day)
|
|
284
|
+
return {
|
|
285
|
+
day: date.getUTCDate(),
|
|
286
|
+
month: date.getUTCMonth() + 1,
|
|
287
|
+
year: date.getUTCFullYear(),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const addDays = (date: PlainDate, days: number) =>
|
|
292
|
+
normalize(date.year, date.month, date.day + days)
|
|
293
|
+
|
|
294
|
+
const addMonths = (date: PlainDate, monthCount: number) =>
|
|
295
|
+
normalize(date.year, date.month + monthCount, Math.min(date.day, daysInMonth(normalize(date.year, date.month + monthCount, 1))))
|
|
296
|
+
|
|
297
|
+
const monthStart = (date: PlainDate) =>
|
|
298
|
+
({ year: date.year, month: date.month, day: 1 })
|
|
299
|
+
|
|
300
|
+
const daysInMonth = (date: PlainDate) =>
|
|
301
|
+
utcDate(date.year, date.month + 1, 0).getUTCDate()
|
|
302
|
+
|
|
303
|
+
const comparePlain = (first: PlainDate, second: PlainDate) =>
|
|
304
|
+
iso(first).localeCompare(iso(second))
|
|
305
|
+
|
|
306
|
+
const samePlain = (first: PlainDate, second: PlainDate) =>
|
|
307
|
+
first.year === second.year && first.month === second.month && first.day === second.day
|
|
308
|
+
|
|
309
|
+
const viewRank: Record<CalendarView, number> = { day: 0, month: 1, year: 2 }
|
|
310
|
+
|
|
311
|
+
const minimumView = (view: CalendarView | undefined) => view ?? 'day'
|
|
312
|
+
|
|
313
|
+
const clampView = (view: CalendarView, minView: CalendarView | undefined) =>
|
|
314
|
+
viewRank[view] < viewRank[minimumView(minView)] ? minimumView(minView) : view
|
|
315
|
+
|
|
316
|
+
const periodStart = (date: PlainDate, view: CalendarView): PlainDate => {
|
|
317
|
+
if (view === 'year') return { day: 1, month: 1, year: date.year }
|
|
318
|
+
if (view === 'month') return monthStart(date)
|
|
319
|
+
return date
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const periodEnd = (date: PlainDate, view: CalendarView): PlainDate => {
|
|
323
|
+
if (view === 'year') return { day: 31, month: 12, year: date.year }
|
|
324
|
+
if (view === 'month') {
|
|
325
|
+
const start = monthStart(date)
|
|
326
|
+
return { ...start, day: daysInMonth(start) }
|
|
327
|
+
}
|
|
328
|
+
return date
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const samePeriod = (first: PlainDate, second: PlainDate, view: CalendarView) => {
|
|
332
|
+
if (view === 'year') return first.year === second.year
|
|
333
|
+
if (view === 'month') return first.year === second.year && first.month === second.month
|
|
334
|
+
return samePlain(first, second)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const periodIntersects = (
|
|
338
|
+
range: { from?: PlainDate; to?: PlainDate },
|
|
339
|
+
date: PlainDate,
|
|
340
|
+
view: CalendarView,
|
|
341
|
+
) => Boolean(range.from && range.to
|
|
342
|
+
&& comparePlain(range.from, periodEnd(date, view)) <= 0
|
|
343
|
+
&& comparePlain(periodStart(date, view), range.to) <= 0)
|
|
344
|
+
|
|
345
|
+
const weekday = (date: PlainDate) =>
|
|
346
|
+
utcDate(date.year, date.month, date.day).getUTCDay()
|
|
347
|
+
|
|
348
|
+
const plainToDate = (date: PlainDate, timeZone?: string) =>
|
|
349
|
+
calendarDate({ ...date, hour: 12, minute: 0, second: 0 }, timeZone)
|
|
350
|
+
|
|
351
|
+
const zonedDateFormatters = new Map<string, Intl.DateTimeFormat>()
|
|
352
|
+
|
|
353
|
+
const zonedDateFormatter = (timeZone: string) => {
|
|
354
|
+
let format = zonedDateFormatters.get(timeZone)
|
|
355
|
+
if (!format) {
|
|
356
|
+
format = new Intl.DateTimeFormat('en-CA', {
|
|
357
|
+
day: '2-digit',
|
|
358
|
+
month: '2-digit',
|
|
359
|
+
timeZone,
|
|
360
|
+
year: 'numeric',
|
|
361
|
+
})
|
|
362
|
+
remember(zonedDateFormatters, timeZone, format)
|
|
363
|
+
}
|
|
364
|
+
return format
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const dateToPlain = (date: Date, timeZone?: string): PlainDate => {
|
|
368
|
+
if (timeZone) {
|
|
369
|
+
const parts = zonedDateFormatter(timeZone).formatToParts(date)
|
|
370
|
+
const value = (type: string) => Number(parts.find(part => part.type === type)?.value)
|
|
371
|
+
return normalize(value('year'), value('month'), value('day'))
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return normalize(date.getFullYear(), date.getMonth() + 1, date.getDate())
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const today = (timeZone?: string) =>
|
|
378
|
+
dateToPlain(new Date(), timeZone)
|
|
379
|
+
|
|
380
|
+
const startOfWeek = (date: PlainDate, weekStartsOn: number) =>
|
|
381
|
+
addDays(date, -((weekday(date) - weekStartsOn + 7) % 7))
|
|
382
|
+
|
|
383
|
+
const startOfGrid = (month: PlainDate, weekStartsOn: number) => {
|
|
384
|
+
return startOfWeek(month, weekStartsOn)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const weeksForMonth = (
|
|
388
|
+
month: PlainDate,
|
|
389
|
+
fixedWeeks: boolean,
|
|
390
|
+
weekStartsOn: number,
|
|
391
|
+
) => {
|
|
392
|
+
const start = startOfGrid(month, weekStartsOn)
|
|
393
|
+
const length = fixedWeeks ? 42 : Math.ceil(((weekday(month) - weekStartsOn + 7) % 7 + daysInMonth(month)) / 7) * 7
|
|
394
|
+
const weeks: PlainDate[][] = []
|
|
395
|
+
|
|
396
|
+
for (let index = 0; index < length; index += 7) {
|
|
397
|
+
weeks.push(Array.from({ length: 7 }, (_, day) => addDays(start, index + day)))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return weeks
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const months = (start: PlainDate, count: number, fixedWeeks: boolean, weekStartsOn: number): MonthData[] =>
|
|
404
|
+
Array.from({ length: Math.max(1, count) }, (_, index) => {
|
|
405
|
+
const month = monthStart(addMonths(start, index))
|
|
406
|
+
return { month, weeks: weeksForMonth(month, fixedWeeks, weekStartsOn) }
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const DATE_FORMAT_OPTIONS = {
|
|
410
|
+
day: { day: 'numeric' },
|
|
411
|
+
fullDate: { dateStyle: 'full' },
|
|
412
|
+
monthCaption: { month: 'long', year: 'numeric' },
|
|
413
|
+
monthDropdown: { month: 'short' },
|
|
414
|
+
monthLabel: { month: 'long' },
|
|
415
|
+
weekday: { weekday: 'short' },
|
|
416
|
+
} satisfies Record<string, Intl.DateTimeFormatOptions>
|
|
417
|
+
|
|
418
|
+
type DateFormat = keyof typeof DATE_FORMAT_OPTIONS
|
|
419
|
+
const dateFormatters = new Map<string, Partial<Record<DateFormat, Intl.DateTimeFormat>>>()
|
|
420
|
+
|
|
421
|
+
const formatter = (
|
|
422
|
+
locale: string | undefined,
|
|
423
|
+
name: DateFormat,
|
|
424
|
+
timeZone?: string,
|
|
425
|
+
) => {
|
|
426
|
+
const key = `${locale ?? ''}\0${timeZone ?? ''}`
|
|
427
|
+
let formats = dateFormatters.get(key)
|
|
428
|
+
if (!formats) {
|
|
429
|
+
formats = {}
|
|
430
|
+
remember(dateFormatters, key, formats)
|
|
431
|
+
}
|
|
432
|
+
return formats[name] ??= new Intl.DateTimeFormat(locale, { ...DATE_FORMAT_OPTIONS[name], timeZone })
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const defaultFormatters: CalendarFormatters = {
|
|
436
|
+
day: (date, locale, timeZone) => formatter(locale, 'day', timeZone).format(date),
|
|
437
|
+
fullDate: (date, locale, timeZone) => formatter(locale, 'fullDate', timeZone).format(date),
|
|
438
|
+
monthCaption: (date, locale, timeZone) => formatter(locale, 'monthCaption', timeZone).format(date),
|
|
439
|
+
monthDropdown: (date, locale, timeZone) => formatter(locale, 'monthDropdown', timeZone).format(date),
|
|
440
|
+
weekNumber: week => pad(week),
|
|
441
|
+
weekday: (date, locale, timeZone) => formatter(locale, 'weekday', timeZone).format(date),
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const toPlainArray = (dates: Date[] | undefined, timeZone?: string) =>
|
|
445
|
+
(dates ?? []).map(date => dateToPlain(date, timeZone))
|
|
446
|
+
|
|
447
|
+
const toRangePlain = (range: CalendarDateRange | undefined, timeZone?: string) => ({
|
|
448
|
+
from: range?.from ? dateToPlain(range.from, timeZone) : undefined,
|
|
449
|
+
to: range?.to ? dateToPlain(range.to, timeZone) : undefined,
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
const plainIn = (date: PlainDate, dates: PlainDate[]) =>
|
|
453
|
+
dates.some(item => samePlain(item, date))
|
|
454
|
+
|
|
455
|
+
const rangeContains = (range: { from?: PlainDate; to?: PlainDate }, date: PlainDate) =>
|
|
456
|
+
Boolean(range.from && range.to && comparePlain(range.from, date) <= 0 && comparePlain(date, range.to) <= 0)
|
|
457
|
+
|
|
458
|
+
const rangeMiddle = (range: { from?: PlainDate; to?: PlainDate }, date: PlainDate) =>
|
|
459
|
+
Boolean(range.from && range.to && comparePlain(range.from, date) < 0 && comparePlain(date, range.to) < 0)
|
|
460
|
+
|
|
461
|
+
const weekNumber = (date: PlainDate) => {
|
|
462
|
+
const target = utcDate(date.year, date.month, date.day)
|
|
463
|
+
const day = target.getUTCDay() || 7
|
|
464
|
+
target.setUTCDate(target.getUTCDate() + 4 - day)
|
|
465
|
+
const yearStart = utcDate(target.getUTCFullYear(), 1, 1)
|
|
466
|
+
return Math.ceil((((target.getTime() - yearStart.getTime()) / 86400000) + 1) / 7)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Day columns grow with the calendar width (a wide caption widens the month)
|
|
470
|
+
// while never dropping below the cell size.
|
|
471
|
+
const gridTemplate = (showWeekNumber: boolean) =>
|
|
472
|
+
showWeekNumber
|
|
473
|
+
? 'grid-template-columns:var(--cell-size) repeat(7,minmax(var(--cell-size),1fr))'
|
|
474
|
+
: 'grid-template-columns:repeat(7,minmax(var(--cell-size),1fr))'
|
|
475
|
+
|
|
476
|
+
const monthOptions = (year: number) =>
|
|
477
|
+
Array.from({ length: 12 }, (_, month) => ({ day: 1, month: month + 1, year }))
|
|
478
|
+
|
|
479
|
+
const yearBounds = (current: PlainDate, args: CalendarArgs, now: PlainDate) => {
|
|
480
|
+
const first = args.startMonth ? dateToPlain(args.startMonth, args.timeZone).year : args.fromYear ?? now.year - 100
|
|
481
|
+
const last = args.endMonth ? dateToPlain(args.endMonth, args.timeZone).year : args.toYear ?? Math.max(now.year, current.year)
|
|
482
|
+
return { first, last: Math.max(first, last) }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const yearRange = (current: PlainDate, args: CalendarArgs, now: PlainDate) => {
|
|
486
|
+
const { first, last } = yearBounds(current, args, now)
|
|
487
|
+
return Array.from({ length: last - first + 1 }, (_, index) => first + index)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const yearPage = (current: PlainDate, args: CalendarArgs, now: PlainDate) => {
|
|
491
|
+
const allowed = yearRange(current, args, now)
|
|
492
|
+
const first = allowed[0]!
|
|
493
|
+
const last = allowed[allowed.length - 1]!
|
|
494
|
+
const anchor = Math.min(last, Math.max(first, current.year))
|
|
495
|
+
const start = first + Math.floor((anchor - first) / 12) * 12
|
|
496
|
+
return {
|
|
497
|
+
allowed,
|
|
498
|
+
start,
|
|
499
|
+
years: Array.from({ length: 12 }, (_, index) => start + index),
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const canNavigateYear = (year: number, current: PlainDate, args: CalendarArgs, now: PlainDate) => {
|
|
504
|
+
const { first, last } = yearBounds(current, args, now)
|
|
505
|
+
return first <= year && year <= last
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const canNavigateTo = (month: PlainDate, args: CalendarArgs) => {
|
|
509
|
+
const start = args.startMonth ? monthStart(dateToPlain(args.startMonth, args.timeZone)) : args.fromYear ? { day: 1, month: 1, year: args.fromYear } : undefined
|
|
510
|
+
const end = args.endMonth ? monthStart(dateToPlain(args.endMonth, args.timeZone)) : args.toYear ? { day: 1, month: 12, year: args.toYear } : undefined
|
|
511
|
+
if (start && comparePlain(monthStart(month), start) < 0) return false
|
|
512
|
+
if (end && comparePlain(monthStart(month), end) > 0) return false
|
|
513
|
+
return true
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const modifierNames = (
|
|
517
|
+
modifiers: Map<string, Availability>,
|
|
518
|
+
date: Date,
|
|
519
|
+
) => Array.from(modifiers)
|
|
520
|
+
.filter(([, availability]) => availability.day(date))
|
|
521
|
+
.map(([name]) => name)
|
|
522
|
+
|
|
523
|
+
const modifierAttributes = (names: string[]) =>
|
|
524
|
+
Object.fromEntries(names.map(name => [`data-modifier-${name}`, 'true']))
|
|
525
|
+
|
|
526
|
+
const defaultSelectedRange = (value: CalendarArgs['defaultSelected']) =>
|
|
527
|
+
value && !Array.isArray(value) && !(value instanceof Date) ? value : undefined
|
|
528
|
+
|
|
529
|
+
const initialMonthDate = (
|
|
530
|
+
month: Date | undefined,
|
|
531
|
+
defaultMonth: Date | undefined,
|
|
532
|
+
defaultSelected: CalendarArgs['defaultSelected'],
|
|
533
|
+
) => {
|
|
534
|
+
if (month) return month
|
|
535
|
+
if (defaultMonth) return defaultMonth
|
|
536
|
+
if (defaultSelected instanceof Date) return defaultSelected
|
|
537
|
+
if (Array.isArray(defaultSelected)) return defaultSelected[0] ?? new Date()
|
|
538
|
+
return defaultSelectedRange(defaultSelected)?.from ?? new Date()
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Unstyled native month or year picker used when no custom dropdown is supplied. */
|
|
542
|
+
const CalendarDropdown: Stateless<CalendarDropdownArgs> = ({
|
|
543
|
+
label,
|
|
544
|
+
onValueChange,
|
|
545
|
+
options,
|
|
546
|
+
value,
|
|
547
|
+
}) => (
|
|
548
|
+
<select
|
|
549
|
+
aria-label={label}
|
|
550
|
+
data-slot="calendar-dropdown"
|
|
551
|
+
set:onchange={(event: Event) => onValueChange((event.currentTarget as HTMLSelectElement).value, event)}
|
|
552
|
+
set:value={value}
|
|
553
|
+
>
|
|
554
|
+
{options.map(option => <option key={option.value} value={option.value}>{option.label}</option>)}
|
|
555
|
+
</select>
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
const CalendarRoot: Stateful<CalendarArgs> = function* ({
|
|
559
|
+
defaultMonth,
|
|
560
|
+
defaultSelected,
|
|
561
|
+
defaultView,
|
|
562
|
+
minView,
|
|
563
|
+
month,
|
|
564
|
+
timeZone,
|
|
565
|
+
view,
|
|
566
|
+
}) {
|
|
567
|
+
const initial = initialMonthDate(
|
|
568
|
+
month as Date | undefined,
|
|
569
|
+
defaultMonth as Date | undefined,
|
|
570
|
+
defaultSelected as CalendarArgs['defaultSelected'],
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
const initialMonth = monthStart(dateToPlain(initial, timeZone))
|
|
574
|
+
const initialView = clampView(view ?? defaultView ?? minimumView(minView), minView)
|
|
575
|
+
const initialSingle = defaultSelected instanceof Date ? dateToPlain(defaultSelected, timeZone) : null
|
|
576
|
+
const initialMultiple = Array.isArray(defaultSelected) ? toPlainArray(defaultSelected, timeZone) : []
|
|
577
|
+
const initialRange = !Array.isArray(defaultSelected) && !(defaultSelected instanceof Date)
|
|
578
|
+
? toRangePlain(defaultSelected as CalendarDateRange | undefined, timeZone)
|
|
579
|
+
: {}
|
|
580
|
+
let currentArgs = {} as CalendarArgs
|
|
581
|
+
let currentView = initialView
|
|
582
|
+
let renderedView = initialView
|
|
583
|
+
let visible = initialMonth
|
|
584
|
+
const domReady = dom(this)
|
|
585
|
+
let disabledSource: CalendarCommonArgs['disabled']
|
|
586
|
+
let disabledTimeZone: string | undefined
|
|
587
|
+
let disabledAvailability: Availability | undefined
|
|
588
|
+
let modifiersSource: CalendarCommonArgs['modifiers']
|
|
589
|
+
let modifiersTimeZone: string | undefined
|
|
590
|
+
let modifierAvailability = new Map<string, Availability>()
|
|
591
|
+
let unavailableSource: CalendarCommonArgs['unavailable']
|
|
592
|
+
let unavailableTimeZone: string | undefined
|
|
593
|
+
let unavailableAvailability: Availability | undefined
|
|
594
|
+
|
|
595
|
+
const syncAvailability = (args: CalendarArgs) => {
|
|
596
|
+
if (args.disabled !== disabledSource || args.timeZone !== disabledTimeZone) {
|
|
597
|
+
disabledSource = args.disabled
|
|
598
|
+
disabledTimeZone = args.timeZone
|
|
599
|
+
disabledAvailability = compile(args.disabled, { timeZone: args.timeZone })
|
|
600
|
+
}
|
|
601
|
+
if (args.modifiers !== modifiersSource || args.timeZone !== modifiersTimeZone) {
|
|
602
|
+
modifiersSource = args.modifiers
|
|
603
|
+
modifiersTimeZone = args.timeZone
|
|
604
|
+
modifierAvailability = new Map(Object.entries(args.modifiers ?? {}).flatMap(([name, matcher]) => {
|
|
605
|
+
const availability = compile(matcher, { timeZone: args.timeZone })
|
|
606
|
+
return availability ? [[name, availability]] : []
|
|
607
|
+
}))
|
|
608
|
+
}
|
|
609
|
+
if (args.unavailable !== unavailableSource || args.timeZone !== unavailableTimeZone) {
|
|
610
|
+
unavailableSource = args.unavailable
|
|
611
|
+
unavailableTimeZone = args.timeZone
|
|
612
|
+
unavailableAvailability = compile(args.unavailable, { timeZone: args.timeZone })
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const monthState = controlled<PlainDate>(this, {
|
|
617
|
+
fallback: initialMonth,
|
|
618
|
+
onChange: (next, event) => currentArgs.onMonthChange?.(plainToDate(next, currentArgs.timeZone), event),
|
|
619
|
+
})
|
|
620
|
+
const viewState = controlled<CalendarView>(this, {
|
|
621
|
+
fallback: initialView,
|
|
622
|
+
onChange: (next, event) => currentArgs.onViewChange?.(next, event),
|
|
623
|
+
})
|
|
624
|
+
// Empty emissions: single and range emit null, multiple emits [] (never null).
|
|
625
|
+
const singleState = controlled<PlainDate | null>(this, {
|
|
626
|
+
fallback: initialSingle,
|
|
627
|
+
onChange: (next, event) => {
|
|
628
|
+
if (currentArgs.mode === 'multiple' || currentArgs.mode === 'range' || !event) return
|
|
629
|
+
currentArgs.onSelect?.(next && plainToDate(next, currentArgs.timeZone), event)
|
|
630
|
+
},
|
|
631
|
+
})
|
|
632
|
+
const multipleState = controlled<PlainDate[]>(this, {
|
|
633
|
+
fallback: initialMultiple,
|
|
634
|
+
onChange: (next, event) => {
|
|
635
|
+
if (currentArgs.mode !== 'multiple' || !event) return
|
|
636
|
+
currentArgs.onSelect?.(next.map(item => plainToDate(item, currentArgs.timeZone)), event)
|
|
637
|
+
},
|
|
638
|
+
})
|
|
639
|
+
const rangeState = controlled<{ from?: PlainDate; to?: PlainDate }>(this, {
|
|
640
|
+
fallback: initialRange,
|
|
641
|
+
onChange: (next, event) => {
|
|
642
|
+
if (currentArgs.mode !== 'range' || !event) return
|
|
643
|
+
currentArgs.onSelect?.(next.from || next.to
|
|
644
|
+
? {
|
|
645
|
+
from: next.from ? plainToDate(next.from, currentArgs.timeZone) : undefined,
|
|
646
|
+
to: next.to ? plainToDate(next.to, currentArgs.timeZone) : undefined,
|
|
647
|
+
}
|
|
648
|
+
: null, event)
|
|
649
|
+
},
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
const focusDay = (date: PlainDate, args: CalendarArgs) => {
|
|
653
|
+
queueMicrotask(() => {
|
|
654
|
+
const find = () =>
|
|
655
|
+
this.querySelector<HTMLButtonElement>(`button[data-day="${iso(date)}"]:not(:disabled)`)
|
|
656
|
+
const target = find()
|
|
657
|
+
|
|
658
|
+
if (target) {
|
|
659
|
+
target.focus()
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const visible = args.month ? monthStart(dateToPlain(args.month, args.timeZone)) : monthState.value
|
|
664
|
+
const count = Math.max(1, args.numberOfMonths ?? 1)
|
|
665
|
+
const start = monthStart(date)
|
|
666
|
+
|
|
667
|
+
if (comparePlain(start, visible) < 0) moveMonth(addMonths(visible, -1), args)
|
|
668
|
+
else if (comparePlain(start, addMonths(visible, count - 1)) > 0) moveMonth(addMonths(visible, 1), args)
|
|
669
|
+
else return
|
|
670
|
+
|
|
671
|
+
queueMicrotask(() => find()?.focus())
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const focusMonthCell = (month: PlainDate, args: CalendarArgs, event?: Event) => {
|
|
676
|
+
const target = monthStart(month)
|
|
677
|
+
queueMicrotask(() => {
|
|
678
|
+
const find = () => this.querySelector<HTMLButtonElement>(`button[data-month="${monthIso(target)}"]:not(:disabled)`)
|
|
679
|
+
const found = find()
|
|
680
|
+
if (found) {
|
|
681
|
+
found.focus()
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
if (!canNavigateTo(target, args)) return
|
|
685
|
+
moveMonth(target, args, event)
|
|
686
|
+
queueMicrotask(() => find()?.focus())
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const navigableMonth = (year: number, args: CalendarArgs) => {
|
|
691
|
+
const options = monthOptions(year)
|
|
692
|
+
return options.find(month => month.month === visible.month && canNavigateTo(month, args))
|
|
693
|
+
?? options.find(month => canNavigateTo(month, args))
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const canCommitYear = (year: number, args: CalendarArgs) =>
|
|
697
|
+
canNavigateTo({ day: 1, month: 1, year }, args)
|
|
698
|
+
&& canNavigateTo({ day: 1, month: 12, year }, args)
|
|
699
|
+
|
|
700
|
+
const canUseYearCell = (year: number, args: CalendarArgs) =>
|
|
701
|
+
minimumView(args.minView) === 'year'
|
|
702
|
+
? canCommitYear(year, args)
|
|
703
|
+
: canNavigateYear(year, visible, args, today(args.timeZone))
|
|
704
|
+
|
|
705
|
+
const focusYearCell = (year: number, args: CalendarArgs, event?: Event) => {
|
|
706
|
+
queueMicrotask(() => {
|
|
707
|
+
const find = () => this.querySelector<HTMLButtonElement>(`button[data-year="${year}"]:not(:disabled)`)
|
|
708
|
+
const found = find()
|
|
709
|
+
if (found) {
|
|
710
|
+
found.focus()
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
if (!canUseYearCell(year, args)) return
|
|
714
|
+
const month = navigableMonth(year, args)
|
|
715
|
+
if (!month) return
|
|
716
|
+
moveMonth(month, args, event)
|
|
717
|
+
queueMicrotask(() => find()?.focus())
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const moveMonth = (next: PlainDate, args: CalendarArgs, event?: Event) => {
|
|
722
|
+
const target = monthStart(next)
|
|
723
|
+
if (!canNavigateTo(target, args)) return
|
|
724
|
+
monthState.set(target, event)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const selectPeriod = (date: PlainDate, view: CalendarView, args: CalendarArgs, event: Event) => {
|
|
728
|
+
const start = periodStart(date, view)
|
|
729
|
+
const end = periodEnd(date, view)
|
|
730
|
+
if (args.mode === 'multiple') {
|
|
731
|
+
const current = multipleState.value
|
|
732
|
+
const exists = current.some(item => samePeriod(item, start, view))
|
|
733
|
+
const next = exists ? current.filter(item => !samePeriod(item, start, view)) : [...current, start]
|
|
734
|
+
if (args.required && !next.length) return
|
|
735
|
+
multipleState.set(next, event)
|
|
736
|
+
return
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (args.mode === 'range') {
|
|
740
|
+
const current = rangeState.value
|
|
741
|
+
let next: { from?: PlainDate; to?: PlainDate }
|
|
742
|
+
|
|
743
|
+
if (!current.from || current.to) next = { from: start }
|
|
744
|
+
else if (comparePlain(start, periodStart(current.from, view)) < 0) next = { from: start, to: periodEnd(current.from, view) }
|
|
745
|
+
else if (samePeriod(start, current.from, view) && !args.required) next = {}
|
|
746
|
+
else next = { from: periodStart(current.from, view), to: end }
|
|
747
|
+
|
|
748
|
+
rangeState.set(next, event)
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const current = singleState.value
|
|
753
|
+
const next = current && samePeriod(current, start, view) && !args.required ? null : start
|
|
754
|
+
singleState.set(next, event)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const selectDay = (date: PlainDate, args: CalendarArgs, event: Event) => {
|
|
758
|
+
if (disabledAvailability?.day(plainToDate(date, args.timeZone))) return
|
|
759
|
+
selectPeriod(date, 'day', args, event)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const changeView = (next: CalendarView, args: CalendarArgs, event?: Event) => {
|
|
763
|
+
const target = clampView(next, args.minView)
|
|
764
|
+
if (target === currentView) return
|
|
765
|
+
viewState.set(target, event)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const drillUp = (anchor: PlainDate, args: CalendarArgs, event: Event) => {
|
|
769
|
+
if (currentView === 'day') {
|
|
770
|
+
if (!samePeriod(anchor, visible, 'month')) moveMonth(anchor, args, event)
|
|
771
|
+
changeView('month', args, event)
|
|
772
|
+
focusMonthCell(anchor, args)
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
if (currentView === 'month') {
|
|
776
|
+
changeView('year', args, event)
|
|
777
|
+
focusYearCell(anchor.year, args)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const selectMonthCell = (month: PlainDate, args: CalendarArgs, event: Event) => {
|
|
782
|
+
if (minimumView(args.minView) === 'month') {
|
|
783
|
+
selectPeriod(month, 'month', args, event)
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
moveMonth(month, args, event)
|
|
787
|
+
changeView('day', args, event)
|
|
788
|
+
focusDay(monthStart(month), args)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const selectYearCell = (year: number, args: CalendarArgs, event: Event) => {
|
|
792
|
+
if (!canUseYearCell(year, args)) return
|
|
793
|
+
const month = navigableMonth(year, args)
|
|
794
|
+
if (!month) return
|
|
795
|
+
if (minimumView(args.minView) === 'year') {
|
|
796
|
+
selectPeriod(month, 'year', args, event)
|
|
797
|
+
return
|
|
798
|
+
}
|
|
799
|
+
moveMonth(month, args, event)
|
|
800
|
+
changeView('month', args, event)
|
|
801
|
+
focusMonthCell(month, args)
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const onMove = (move: GridMove, event: KeyboardEvent) => {
|
|
805
|
+
const button = event.target instanceof Element
|
|
806
|
+
? event.target.closest<HTMLButtonElement>('button[data-day],button[data-month],button[data-year]')
|
|
807
|
+
: null
|
|
808
|
+
const args = currentArgs
|
|
809
|
+
const month = button?.dataset.month ? parseMonthIso(button.dataset.month) : undefined
|
|
810
|
+
const year = button?.dataset.year ? Number(button.dataset.year) : undefined
|
|
811
|
+
|
|
812
|
+
if (month) {
|
|
813
|
+
if ('cols' in move) focusMonthCell(addMonths(month, move.cols), args, event)
|
|
814
|
+
else if ('rows' in move) focusMonthCell(addMonths(month, move.rows * 3), args, event)
|
|
815
|
+
else if ('page' in move) focusMonthCell(addMonths(month, move.page * 12), args, event)
|
|
816
|
+
else {
|
|
817
|
+
const rowStart = Math.floor((month.month - 1) / 3) * 3 + 1
|
|
818
|
+
const candidates = monthOptions(month.year).filter(candidate =>
|
|
819
|
+
canNavigateTo(candidate, args)
|
|
820
|
+
&& (move.extent === 'all' || (rowStart <= candidate.month && candidate.month <= rowStart + 2)))
|
|
821
|
+
const target = move.edge === 'start' ? candidates[0] : candidates[candidates.length - 1]
|
|
822
|
+
if (target) focusMonthCell(target, args, event)
|
|
823
|
+
}
|
|
824
|
+
return
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (year != null && Number.isFinite(year)) {
|
|
828
|
+
if ('cols' in move) focusYearCell(year + move.cols, args, event)
|
|
829
|
+
else if ('rows' in move) focusYearCell(year + move.rows * 3, args, event)
|
|
830
|
+
else if ('page' in move) focusYearCell(year + move.page * 12, args, event)
|
|
831
|
+
else {
|
|
832
|
+
const page = yearPage(visible, args, today(args.timeZone))
|
|
833
|
+
const rowStart = page.start + Math.floor((year - page.start) / 3) * 3
|
|
834
|
+
const candidates = page.years.filter(candidate =>
|
|
835
|
+
canUseYearCell(candidate, args)
|
|
836
|
+
&& (move.extent === 'all' || (rowStart <= candidate && candidate <= rowStart + 2)))
|
|
837
|
+
const target = move.edge === 'start' ? candidates[0] : candidates[candidates.length - 1]
|
|
838
|
+
if (target != null) focusYearCell(target, args, event)
|
|
839
|
+
}
|
|
840
|
+
return
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const day = button?.dataset.day ? parseIso(button.dataset.day) : undefined
|
|
844
|
+
if (!day) return
|
|
845
|
+
|
|
846
|
+
if ('cols' in move) {
|
|
847
|
+
focusDay(addDays(day, move.cols), args)
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if ('rows' in move) {
|
|
852
|
+
focusDay(addDays(day, move.rows * 7), args)
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if ('page' in move) {
|
|
857
|
+
const count = move.page * (move.large ? 12 : 1)
|
|
858
|
+
const target = addMonths(day, count)
|
|
859
|
+
if (!canNavigateTo(monthStart(target), args)) return
|
|
860
|
+
|
|
861
|
+
moveMonth(addMonths(visible, count), args, event)
|
|
862
|
+
focusDay(target, args)
|
|
863
|
+
return
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (move.extent === 'row') {
|
|
867
|
+
const start = startOfWeek(day, args.weekStartsOn ?? 0)
|
|
868
|
+
focusDay(move.edge === 'start' ? start : addDays(start, 6), args)
|
|
869
|
+
return
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const start = monthStart(day)
|
|
873
|
+
focusDay(move.edge === 'start' ? start : { ...start, day: daysInMonth(start) }, args)
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
const nav = grid(this, {
|
|
877
|
+
rtl: () => currentArgs.dir === 'rtl',
|
|
878
|
+
onMove,
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
const onCellKeydown = (event: KeyboardEvent) => {
|
|
882
|
+
if (event.key !== 'Escape' || viewRank[currentView] <= viewRank[minimumView(currentArgs.minView)]) {
|
|
883
|
+
nav.handle(event)
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
event.preventDefault()
|
|
887
|
+
event.stopPropagation()
|
|
888
|
+
if (currentView === 'year') {
|
|
889
|
+
changeView('month', currentArgs, event)
|
|
890
|
+
focusMonthCell(visible, currentArgs)
|
|
891
|
+
return
|
|
892
|
+
}
|
|
893
|
+
changeView('day', currentArgs, event)
|
|
894
|
+
focusDay(visible, currentArgs)
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
for (const args of this) {
|
|
898
|
+
currentArgs = args
|
|
899
|
+
syncAvailability(args)
|
|
900
|
+
monthState.sync(args.month ? monthStart(dateToPlain(args.month, args.timeZone)) : undefined)
|
|
901
|
+
viewState.sync(args.view === undefined ? undefined : clampView(args.view, args.minView))
|
|
902
|
+
const clampedView = clampView(viewState.value, args.minView)
|
|
903
|
+
if (!viewState.controlled && clampedView !== viewState.value) viewState.init(clampedView)
|
|
904
|
+
// selected !== undefined binds; null (single, range) and [] (multiple) are controlled-empty.
|
|
905
|
+
singleState.sync(args.mode === 'multiple' || args.mode === 'range' || args.selected === undefined
|
|
906
|
+
? undefined
|
|
907
|
+
: args.selected && dateToPlain(args.selected, args.timeZone))
|
|
908
|
+
multipleState.sync(args.mode === 'multiple' && args.selected !== undefined
|
|
909
|
+
? toPlainArray(args.selected, args.timeZone)
|
|
910
|
+
: undefined)
|
|
911
|
+
rangeState.sync(args.mode === 'range' && args.selected !== undefined
|
|
912
|
+
? toRangePlain(args.selected ?? undefined, args.timeZone)
|
|
913
|
+
: undefined)
|
|
914
|
+
|
|
915
|
+
const now = today(args.timeZone)
|
|
916
|
+
visible = monthState.value
|
|
917
|
+
currentView = clampedView
|
|
918
|
+
if (renderedView !== currentView && domReady) {
|
|
919
|
+
const active = document.activeElement
|
|
920
|
+
if (active instanceof HTMLElement && this.contains(active)) {
|
|
921
|
+
if (currentView === 'day') focusDay(visible, args)
|
|
922
|
+
else if (currentView === 'month') focusMonthCell(visible, args)
|
|
923
|
+
else focusYearCell(visible.year, args)
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
renderedView = currentView
|
|
927
|
+
const weekStartsOn = args.weekStartsOn ?? 0
|
|
928
|
+
const count = Math.max(1, args.numberOfMonths ?? 1)
|
|
929
|
+
const locale = localeCode(args.locale)
|
|
930
|
+
const formats = { ...defaultFormatters, ...(args.formatters ?? {}) }
|
|
931
|
+
const shown = months(visible, count, Boolean(args.fixedWeeks), weekStartsOn)
|
|
932
|
+
const single = singleState.value
|
|
933
|
+
const multiple = multipleState.value
|
|
934
|
+
const range = rangeState.value
|
|
935
|
+
const years = yearRange(visible, args, now)
|
|
936
|
+
const dayColumns = gridTemplate(Boolean(args.showWeekNumber))
|
|
937
|
+
const captionLayout = args.captionLayout ?? 'button'
|
|
938
|
+
const MonthDropdown = args.monthDropdown ?? CalendarDropdown
|
|
939
|
+
const YearDropdown = args.yearDropdown ?? CalendarDropdown
|
|
940
|
+
const page = yearPage(visible, args, now)
|
|
941
|
+
const periodFlags = (date: PlainDate, view: CalendarView) => {
|
|
942
|
+
const range_start = args.mode === 'range' && Boolean(range.from && samePeriod(range.from, date, view))
|
|
943
|
+
const range_end = args.mode === 'range' && Boolean(range.to && samePeriod(range.to, date, view))
|
|
944
|
+
const range_middle = args.mode === 'range' && periodIntersects(range, date, view) && !range_start && !range_end
|
|
945
|
+
const selected = args.mode === 'multiple'
|
|
946
|
+
? multiple.some(item => samePeriod(item, date, view))
|
|
947
|
+
: args.mode === 'range'
|
|
948
|
+
? range_start || range_end || range_middle
|
|
949
|
+
: Boolean(single && samePeriod(single, date, view))
|
|
950
|
+
return { range_end, range_middle, range_start, selected }
|
|
951
|
+
}
|
|
952
|
+
const canPreviousView = currentView === 'day'
|
|
953
|
+
? canNavigateTo(addMonths(visible, -1), args)
|
|
954
|
+
: currentView === 'month'
|
|
955
|
+
? canNavigateYear(visible.year - 1, visible, args, now)
|
|
956
|
+
: page.start > page.allowed[0]!
|
|
957
|
+
const canNextView = currentView === 'day'
|
|
958
|
+
? canNavigateTo(addMonths(visible, count), args)
|
|
959
|
+
: currentView === 'month'
|
|
960
|
+
? canNavigateYear(visible.year + 1, visible, args, now)
|
|
961
|
+
: page.start + 12 <= page.allowed[page.allowed.length - 1]!
|
|
962
|
+
const navigateView = (direction: -1 | 1, event: Event) => {
|
|
963
|
+
if (currentView === 'day') {
|
|
964
|
+
moveMonth(addMonths(visible, direction < 0 ? -1 : count), args, event)
|
|
965
|
+
return
|
|
966
|
+
}
|
|
967
|
+
const year = visible.year + direction * (currentView === 'month' ? 1 : 12)
|
|
968
|
+
const month = navigableMonth(year, args)
|
|
969
|
+
if (month) moveMonth(month, args, event)
|
|
970
|
+
}
|
|
971
|
+
const previousLabel = args.previousMonthLabel ?? (currentView === 'day'
|
|
972
|
+
? 'Previous month'
|
|
973
|
+
: currentView === 'month' ? 'Previous year' : 'Previous 12 years')
|
|
974
|
+
const nextLabel = args.nextMonthLabel ?? (currentView === 'day'
|
|
975
|
+
? 'Next month'
|
|
976
|
+
: currentView === 'month' ? 'Next year' : 'Next 12 years')
|
|
977
|
+
|
|
978
|
+
yield (
|
|
979
|
+
<>
|
|
980
|
+
<div class={args.classNames?.months} data-slot="calendar-months">
|
|
981
|
+
{currentView === 'day' ? shown.map(({ month: item, weeks }, monthIndex) => {
|
|
982
|
+
const monthDate = plainToDate(item, args.timeZone)
|
|
983
|
+
const showMonthDropdown = captionLayout === 'dropdown' || captionLayout === 'dropdown-months'
|
|
984
|
+
const showYearDropdown = captionLayout === 'dropdown' || captionLayout === 'dropdown-years'
|
|
985
|
+
const firstMonth = monthIndex === 0
|
|
986
|
+
const lastMonth = monthIndex === shown.length - 1
|
|
987
|
+
|
|
988
|
+
return (
|
|
989
|
+
<div key={iso(item)} class={args.classNames?.month} data-month={iso(item)} data-slot="calendar-month">
|
|
990
|
+
{/* Nav buttons are caption-row siblings, so a wide month or year
|
|
991
|
+
select grows the calendar instead of colliding with them. */}
|
|
992
|
+
<div class={args.classNames?.caption} data-slot="calendar-caption">
|
|
993
|
+
{firstMonth ? (
|
|
994
|
+
<button
|
|
995
|
+
aria-disabled={canPreviousView ? undefined : 'true'}
|
|
996
|
+
aria-label={previousLabel}
|
|
997
|
+
class={args.navButtonClass}
|
|
998
|
+
data-slot="calendar-previous"
|
|
999
|
+
disabled={!canPreviousView}
|
|
1000
|
+
type="button"
|
|
1001
|
+
set:onclick={(event: Event) => navigateView(-1, event)}
|
|
1002
|
+
>
|
|
1003
|
+
{args.previousIcon ?? <span aria-hidden="true" class={args.previousIconClass} data-slot="calendar-previous-icon" />}
|
|
1004
|
+
</button>
|
|
1005
|
+
) : (
|
|
1006
|
+
<span aria-hidden="true" class={args.navSpacerClass} data-slot="calendar-nav-spacer" />
|
|
1007
|
+
)}
|
|
1008
|
+
{captionLayout === 'button' ? (
|
|
1009
|
+
<button
|
|
1010
|
+
class={args.captionLabelClass}
|
|
1011
|
+
data-slot="calendar-view-trigger"
|
|
1012
|
+
type="button"
|
|
1013
|
+
set:onclick={(event: Event) => drillUp(item, args, event)}
|
|
1014
|
+
>
|
|
1015
|
+
{formats.monthCaption(monthDate, locale, args.timeZone)}
|
|
1016
|
+
</button>
|
|
1017
|
+
) : captionLayout === 'label' ? (
|
|
1018
|
+
<div class={args.captionLabelClass} data-slot="calendar-caption-label">
|
|
1019
|
+
{formats.monthCaption(monthDate, locale, args.timeZone)}
|
|
1020
|
+
</div>
|
|
1021
|
+
) : (
|
|
1022
|
+
<div class={args.classNames?.dropdowns} data-slot="calendar-dropdowns">
|
|
1023
|
+
{showMonthDropdown ? (
|
|
1024
|
+
<MonthDropdown
|
|
1025
|
+
label={args.monthSelectLabel ?? 'Month'}
|
|
1026
|
+
options={monthOptions(item.year).map(option => ({
|
|
1027
|
+
label: formats.monthDropdown(plainToDate(option, args.timeZone), locale, args.timeZone),
|
|
1028
|
+
value: String(option.month),
|
|
1029
|
+
}))}
|
|
1030
|
+
onValueChange={(value, event) => moveMonth({ ...item, month: Number(value) }, args, event)}
|
|
1031
|
+
value={String(item.month)}
|
|
1032
|
+
/>
|
|
1033
|
+
) : (
|
|
1034
|
+
<div class={args.monthLabelClass} data-slot="calendar-month-label">{formatter(locale, 'monthLabel', args.timeZone).format(monthDate)}</div>
|
|
1035
|
+
)}
|
|
1036
|
+
{showYearDropdown ? (
|
|
1037
|
+
<YearDropdown
|
|
1038
|
+
label={args.yearSelectLabel ?? 'Year'}
|
|
1039
|
+
onValueChange={(value, event) => moveMonth({ ...item, year: Number(value) }, args, event)}
|
|
1040
|
+
options={years.map(year => ({ label: year, value: String(year) }))}
|
|
1041
|
+
value={String(item.year)}
|
|
1042
|
+
/>
|
|
1043
|
+
) : (
|
|
1044
|
+
<div class={args.yearLabelClass} data-slot="calendar-year-label">{item.year}</div>
|
|
1045
|
+
)}
|
|
1046
|
+
</div>
|
|
1047
|
+
)}
|
|
1048
|
+
{lastMonth ? (
|
|
1049
|
+
<button
|
|
1050
|
+
aria-disabled={canNextView ? undefined : 'true'}
|
|
1051
|
+
aria-label={nextLabel}
|
|
1052
|
+
class={args.navButtonClass}
|
|
1053
|
+
data-slot="calendar-next"
|
|
1054
|
+
disabled={!canNextView}
|
|
1055
|
+
type="button"
|
|
1056
|
+
set:onclick={(event: Event) => navigateView(1, event)}
|
|
1057
|
+
>
|
|
1058
|
+
{args.nextIcon ?? <span aria-hidden="true" class={args.nextIconClass} data-slot="calendar-next-icon" />}
|
|
1059
|
+
</button>
|
|
1060
|
+
) : (
|
|
1061
|
+
<span aria-hidden="true" class={args.navSpacerClass} data-slot="calendar-nav-spacer" />
|
|
1062
|
+
)}
|
|
1063
|
+
</div>
|
|
1064
|
+
<div class={args.classNames?.grid} data-slot="calendar-grid" role="grid" aria-label={formats.monthCaption(monthDate, locale, args.timeZone)}>
|
|
1065
|
+
<div class={args.classNames?.head} data-slot="calendar-weekdays" role="row" style={dayColumns}>
|
|
1066
|
+
{args.showWeekNumber && <div aria-hidden="true" class={args.classNames?.week_number} data-slot="calendar-week-number-header" />}
|
|
1067
|
+
{Array.from({ length: 7 }, (_, index) => addDays({ year: 2026, month: 7, day: 5 }, weekStartsOn + index)).map((day, index) => (
|
|
1068
|
+
<div key={index} class={args.classNames?.weekday} data-slot="calendar-weekday" role="columnheader">
|
|
1069
|
+
{formats.weekday(plainToDate(day, args.timeZone), locale, args.timeZone)}
|
|
1070
|
+
</div>
|
|
1071
|
+
))}
|
|
1072
|
+
</div>
|
|
1073
|
+
{weeks.map((week, row) => (
|
|
1074
|
+
<div key={row} class={args.classNames?.week} data-slot="calendar-week" role="row" style={dayColumns}>
|
|
1075
|
+
{args.showWeekNumber && (
|
|
1076
|
+
<div class={args.classNames?.week_number} data-slot="calendar-week-number" role="rowheader">
|
|
1077
|
+
{formats.weekNumber(weekNumber(week[0]!), locale)}
|
|
1078
|
+
</div>
|
|
1079
|
+
)}
|
|
1080
|
+
{week.map(day => {
|
|
1081
|
+
const outside = day.month !== item.month || day.year !== item.year
|
|
1082
|
+
const date = plainToDate(day, args.timeZone)
|
|
1083
|
+
const disabled = outside && args.showOutsideDays === false
|
|
1084
|
+
? true
|
|
1085
|
+
: Boolean(disabledAvailability?.day(date))
|
|
1086
|
+
const unavailable = Boolean(unavailableAvailability?.day(date))
|
|
1087
|
+
const range_start = !outside && args.mode === 'range' && Boolean(range.from && samePlain(range.from, day))
|
|
1088
|
+
const range_end = !outside && args.mode === 'range' && Boolean(range.to && samePlain(range.to, day))
|
|
1089
|
+
const rawRangeMiddle = !outside && args.mode === 'range' && rangeMiddle(range, day)
|
|
1090
|
+
const rangeGap = rawRangeMiddle && unavailable && Boolean(args.allowNonContiguous)
|
|
1091
|
+
const selected = !rangeGap && !outside && (args.mode === 'multiple'
|
|
1092
|
+
? plainIn(day, multiple)
|
|
1093
|
+
: args.mode === 'range'
|
|
1094
|
+
? Boolean((range.from && samePlain(range.from, day)) || (range.to && samePlain(range.to, day)) || rangeContains(range, day))
|
|
1095
|
+
: Boolean(single && samePlain(single, day)))
|
|
1096
|
+
const range_middle = rawRangeMiddle && !rangeGap
|
|
1097
|
+
const current = samePlain(day, now)
|
|
1098
|
+
const modifierList = modifierNames(modifierAvailability, date)
|
|
1099
|
+
const modifiers: CalendarModifiers = {
|
|
1100
|
+
disabled,
|
|
1101
|
+
outside,
|
|
1102
|
+
range_end,
|
|
1103
|
+
range_middle,
|
|
1104
|
+
range_start,
|
|
1105
|
+
selected,
|
|
1106
|
+
today: current,
|
|
1107
|
+
unavailable,
|
|
1108
|
+
}
|
|
1109
|
+
const hidden = outside && args.showOutsideDays === false
|
|
1110
|
+
const band = args.mode === 'range' && Boolean(range.from && range.to)
|
|
1111
|
+
const state: CalendarDayState = {
|
|
1112
|
+
date,
|
|
1113
|
+
hidden,
|
|
1114
|
+
modifierNames: modifierList,
|
|
1115
|
+
modifiers,
|
|
1116
|
+
range: band,
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
return (
|
|
1120
|
+
<div
|
|
1121
|
+
key={iso(day)}
|
|
1122
|
+
class={args.dayClassName?.(state) ?? args.classNames?.day}
|
|
1123
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
1124
|
+
data-outside={outside ? 'true' : undefined}
|
|
1125
|
+
data-selected={selected ? 'true' : undefined}
|
|
1126
|
+
data-slot="calendar-day"
|
|
1127
|
+
data-today={current ? 'true' : undefined}
|
|
1128
|
+
data-unavailable={unavailable ? 'true' : undefined}
|
|
1129
|
+
role="gridcell"
|
|
1130
|
+
{...modifierAttributes(modifierList)}
|
|
1131
|
+
>
|
|
1132
|
+
{hidden ? (
|
|
1133
|
+
<span aria-hidden="true" class={args.hiddenDayClass}>{day.day}</span>
|
|
1134
|
+
) : (
|
|
1135
|
+
<CalendarDayButton
|
|
1136
|
+
aria-label={formats.fullDate(date, locale, args.timeZone)}
|
|
1137
|
+
class={args.dayButtonClass}
|
|
1138
|
+
date={date}
|
|
1139
|
+
day={args.renderDay?.(date, modifiers) ?? formats.day(date, locale, args.timeZone)}
|
|
1140
|
+
timeZone={args.timeZone}
|
|
1141
|
+
disabled={disabled || outside}
|
|
1142
|
+
modifiers={modifiers}
|
|
1143
|
+
set:onclick={(event: Event) => selectDay(day, args, event)}
|
|
1144
|
+
set:onkeydown={onCellKeydown}
|
|
1145
|
+
/>
|
|
1146
|
+
)}
|
|
1147
|
+
</div>
|
|
1148
|
+
)
|
|
1149
|
+
})}
|
|
1150
|
+
</div>
|
|
1151
|
+
))}
|
|
1152
|
+
</div>
|
|
1153
|
+
</div>
|
|
1154
|
+
)
|
|
1155
|
+
}) : (
|
|
1156
|
+
<div key={`${currentView}-${currentView === 'year' ? page.start : visible.year}`} class={args.classNames?.month}>
|
|
1157
|
+
<div class={args.classNames?.caption} data-slot="calendar-caption">
|
|
1158
|
+
<button
|
|
1159
|
+
aria-disabled={canPreviousView ? undefined : 'true'}
|
|
1160
|
+
aria-label={previousLabel}
|
|
1161
|
+
class={args.navButtonClass}
|
|
1162
|
+
data-slot="calendar-previous"
|
|
1163
|
+
disabled={!canPreviousView}
|
|
1164
|
+
type="button"
|
|
1165
|
+
set:onclick={(event: Event) => navigateView(-1, event)}
|
|
1166
|
+
>
|
|
1167
|
+
{args.previousIcon ?? <span aria-hidden="true" class={args.previousIconClass} data-slot="calendar-previous-icon" />}
|
|
1168
|
+
</button>
|
|
1169
|
+
{captionLayout === 'button' ? (
|
|
1170
|
+
<button
|
|
1171
|
+
aria-disabled={currentView === 'year' ? 'true' : undefined}
|
|
1172
|
+
class={args.captionLabelClass}
|
|
1173
|
+
data-slot="calendar-view-trigger"
|
|
1174
|
+
disabled={currentView === 'year'}
|
|
1175
|
+
type="button"
|
|
1176
|
+
set:onclick={(event: Event) => drillUp(visible, args, event)}
|
|
1177
|
+
>
|
|
1178
|
+
{currentView === 'month' ? visible.year : `${page.start}–${page.start + 11}`}
|
|
1179
|
+
</button>
|
|
1180
|
+
) : (
|
|
1181
|
+
<div class={args.captionLabelClass} data-slot="calendar-caption-label">
|
|
1182
|
+
{currentView === 'month' ? visible.year : `${page.start}–${page.start + 11}`}
|
|
1183
|
+
</div>
|
|
1184
|
+
)}
|
|
1185
|
+
<button
|
|
1186
|
+
aria-disabled={canNextView ? undefined : 'true'}
|
|
1187
|
+
aria-label={nextLabel}
|
|
1188
|
+
class={args.navButtonClass}
|
|
1189
|
+
data-slot="calendar-next"
|
|
1190
|
+
disabled={!canNextView}
|
|
1191
|
+
type="button"
|
|
1192
|
+
set:onclick={(event: Event) => navigateView(1, event)}
|
|
1193
|
+
>
|
|
1194
|
+
{args.nextIcon ?? <span aria-hidden="true" class={args.nextIconClass} data-slot="calendar-next-icon" />}
|
|
1195
|
+
</button>
|
|
1196
|
+
</div>
|
|
1197
|
+
{currentView === 'month' ? (
|
|
1198
|
+
<div
|
|
1199
|
+
aria-label={String(visible.year)}
|
|
1200
|
+
class={args.classNames?.month_view}
|
|
1201
|
+
data-slot="calendar-month-view"
|
|
1202
|
+
role="grid"
|
|
1203
|
+
style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr))"
|
|
1204
|
+
>
|
|
1205
|
+
{monthOptions(visible.year).map(month => {
|
|
1206
|
+
const disabled = !canNavigateTo(month, args)
|
|
1207
|
+
const flags = periodFlags(month, 'month')
|
|
1208
|
+
const current = samePeriod(month, now, 'month')
|
|
1209
|
+
const value = monthIso(month)
|
|
1210
|
+
return (
|
|
1211
|
+
<div key={value} role="gridcell">
|
|
1212
|
+
<button
|
|
1213
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
1214
|
+
aria-label={formats.monthCaption(plainToDate(month, args.timeZone), locale, args.timeZone)}
|
|
1215
|
+
class={args.classNames?.month_cell}
|
|
1216
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
1217
|
+
data-month={value}
|
|
1218
|
+
data-range-end={flags.range_end ? 'true' : undefined}
|
|
1219
|
+
data-range-middle={flags.range_middle ? 'true' : undefined}
|
|
1220
|
+
data-range-start={flags.range_start ? 'true' : undefined}
|
|
1221
|
+
data-selected={flags.selected ? 'true' : undefined}
|
|
1222
|
+
data-slot="calendar-month-cell"
|
|
1223
|
+
data-today={current ? 'true' : undefined}
|
|
1224
|
+
disabled={disabled}
|
|
1225
|
+
type="button"
|
|
1226
|
+
set:onclick={(event: Event) => selectMonthCell(month, args, event)}
|
|
1227
|
+
set:onkeydown={onCellKeydown}
|
|
1228
|
+
>
|
|
1229
|
+
{formatter(locale, 'monthLabel', args.timeZone).format(plainToDate(month, args.timeZone))}
|
|
1230
|
+
</button>
|
|
1231
|
+
</div>
|
|
1232
|
+
)
|
|
1233
|
+
})}
|
|
1234
|
+
</div>
|
|
1235
|
+
) : (
|
|
1236
|
+
<div
|
|
1237
|
+
aria-label={`${page.start}–${page.start + 11}`}
|
|
1238
|
+
class={args.classNames?.year_view}
|
|
1239
|
+
data-slot="calendar-year-view"
|
|
1240
|
+
role="grid"
|
|
1241
|
+
style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr))"
|
|
1242
|
+
>
|
|
1243
|
+
{page.years.map(year => {
|
|
1244
|
+
const disabled = !canUseYearCell(year, args)
|
|
1245
|
+
const target = { day: 1, month: 1, year }
|
|
1246
|
+
const flags = periodFlags(target, 'year')
|
|
1247
|
+
const current = year === now.year
|
|
1248
|
+
return (
|
|
1249
|
+
<div key={year} role="gridcell">
|
|
1250
|
+
<button
|
|
1251
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
1252
|
+
class={args.classNames?.year_cell}
|
|
1253
|
+
data-disabled={disabled ? 'true' : undefined}
|
|
1254
|
+
data-range-end={flags.range_end ? 'true' : undefined}
|
|
1255
|
+
data-range-middle={flags.range_middle ? 'true' : undefined}
|
|
1256
|
+
data-range-start={flags.range_start ? 'true' : undefined}
|
|
1257
|
+
data-selected={flags.selected ? 'true' : undefined}
|
|
1258
|
+
data-slot="calendar-year-cell"
|
|
1259
|
+
data-today={current ? 'true' : undefined}
|
|
1260
|
+
data-year={String(year)}
|
|
1261
|
+
disabled={disabled}
|
|
1262
|
+
type="button"
|
|
1263
|
+
set:onclick={(event: Event) => selectYearCell(year, args, event)}
|
|
1264
|
+
set:onkeydown={onCellKeydown}
|
|
1265
|
+
>
|
|
1266
|
+
{year}
|
|
1267
|
+
</button>
|
|
1268
|
+
</div>
|
|
1269
|
+
)
|
|
1270
|
+
})}
|
|
1271
|
+
</div>
|
|
1272
|
+
)}
|
|
1273
|
+
</div>
|
|
1274
|
+
)}
|
|
1275
|
+
</div>
|
|
1276
|
+
</>
|
|
1277
|
+
)
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
/** Unstyled calendar with single, multiple, and range selection. */
|
|
1283
|
+
const Calendar: Stateless<CalendarArgs> = ({
|
|
1284
|
+
allowNonContiguous,
|
|
1285
|
+
captionLabelClass,
|
|
1286
|
+
captionLayout,
|
|
1287
|
+
class: classes,
|
|
1288
|
+
classNames,
|
|
1289
|
+
dayButtonClass,
|
|
1290
|
+
dayClassName,
|
|
1291
|
+
defaultMonth,
|
|
1292
|
+
defaultSelected,
|
|
1293
|
+
defaultView,
|
|
1294
|
+
disabled,
|
|
1295
|
+
endMonth,
|
|
1296
|
+
fixedWeeks,
|
|
1297
|
+
formatters,
|
|
1298
|
+
fromYear,
|
|
1299
|
+
hiddenDayClass,
|
|
1300
|
+
locale,
|
|
1301
|
+
minView,
|
|
1302
|
+
month,
|
|
1303
|
+
monthDropdown,
|
|
1304
|
+
monthLabelClass,
|
|
1305
|
+
monthSelectLabel,
|
|
1306
|
+
nextMonthLabel,
|
|
1307
|
+
previousMonthLabel,
|
|
1308
|
+
yearSelectLabel,
|
|
1309
|
+
modifiers,
|
|
1310
|
+
navButtonClass,
|
|
1311
|
+
navSpacerClass,
|
|
1312
|
+
nextIcon,
|
|
1313
|
+
nextIconClass,
|
|
1314
|
+
numberOfMonths,
|
|
1315
|
+
onMonthChange,
|
|
1316
|
+
onViewChange,
|
|
1317
|
+
previousIcon,
|
|
1318
|
+
previousIconClass,
|
|
1319
|
+
renderDay,
|
|
1320
|
+
required,
|
|
1321
|
+
showOutsideDays = true,
|
|
1322
|
+
showWeekNumber,
|
|
1323
|
+
startMonth,
|
|
1324
|
+
timeZone,
|
|
1325
|
+
toYear,
|
|
1326
|
+
unavailable,
|
|
1327
|
+
view,
|
|
1328
|
+
weekStartsOn,
|
|
1329
|
+
yearDropdown,
|
|
1330
|
+
yearLabelClass,
|
|
1331
|
+
...attrs
|
|
1332
|
+
}) => {
|
|
1333
|
+
const {
|
|
1334
|
+
dir,
|
|
1335
|
+
mode,
|
|
1336
|
+
onSelect,
|
|
1337
|
+
selected,
|
|
1338
|
+
...rest
|
|
1339
|
+
} = attrs as CalendarArgs & Record<string, unknown>
|
|
1340
|
+
const resolvedDir = (dir as 'ltr' | 'rtl' | undefined) ?? DirectionContext()
|
|
1341
|
+
const rootArgs = {
|
|
1342
|
+
allowNonContiguous,
|
|
1343
|
+
captionLabelClass,
|
|
1344
|
+
captionLayout,
|
|
1345
|
+
classNames,
|
|
1346
|
+
dayButtonClass,
|
|
1347
|
+
dayClassName,
|
|
1348
|
+
defaultMonth,
|
|
1349
|
+
defaultSelected,
|
|
1350
|
+
defaultView,
|
|
1351
|
+
disabled,
|
|
1352
|
+
dir: resolvedDir,
|
|
1353
|
+
endMonth,
|
|
1354
|
+
fixedWeeks,
|
|
1355
|
+
formatters,
|
|
1356
|
+
fromYear,
|
|
1357
|
+
hiddenDayClass,
|
|
1358
|
+
locale,
|
|
1359
|
+
minView,
|
|
1360
|
+
mode,
|
|
1361
|
+
month,
|
|
1362
|
+
monthDropdown,
|
|
1363
|
+
monthLabelClass,
|
|
1364
|
+
monthSelectLabel,
|
|
1365
|
+
nextMonthLabel,
|
|
1366
|
+
previousMonthLabel,
|
|
1367
|
+
yearSelectLabel,
|
|
1368
|
+
modifiers,
|
|
1369
|
+
navButtonClass,
|
|
1370
|
+
navSpacerClass,
|
|
1371
|
+
nextIcon,
|
|
1372
|
+
nextIconClass,
|
|
1373
|
+
numberOfMonths,
|
|
1374
|
+
onMonthChange,
|
|
1375
|
+
onSelect,
|
|
1376
|
+
onViewChange,
|
|
1377
|
+
previousIcon,
|
|
1378
|
+
previousIconClass,
|
|
1379
|
+
renderDay,
|
|
1380
|
+
required,
|
|
1381
|
+
selected,
|
|
1382
|
+
showOutsideDays,
|
|
1383
|
+
showWeekNumber,
|
|
1384
|
+
startMonth,
|
|
1385
|
+
timeZone,
|
|
1386
|
+
toYear,
|
|
1387
|
+
unavailable,
|
|
1388
|
+
view,
|
|
1389
|
+
weekStartsOn,
|
|
1390
|
+
yearDropdown,
|
|
1391
|
+
yearLabelClass,
|
|
1392
|
+
} as CalendarArgs
|
|
1393
|
+
|
|
1394
|
+
return (
|
|
1395
|
+
<CalendarRoot
|
|
1396
|
+
{...rootArgs}
|
|
1397
|
+
{...rootAttrs(rest)}
|
|
1398
|
+
attr:class={classes}
|
|
1399
|
+
attr:data-slot="calendar"
|
|
1400
|
+
attr:dir={resolvedDir}
|
|
1401
|
+
/>
|
|
1402
|
+
)
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
/** Unstyled day button used by Calendar. */
|
|
1406
|
+
const CalendarDayButton: Stateless<CalendarDayButtonArgs> = ({
|
|
1407
|
+
class: classes,
|
|
1408
|
+
date,
|
|
1409
|
+
day,
|
|
1410
|
+
modifiers,
|
|
1411
|
+
timeZone,
|
|
1412
|
+
type = 'button',
|
|
1413
|
+
...attrs
|
|
1414
|
+
}) => {
|
|
1415
|
+
const value = dateToPlain(date, timeZone)
|
|
1416
|
+
const single = modifiers.selected && !modifiers.range_start && !modifiers.range_end && !modifiers.range_middle
|
|
1417
|
+
|
|
1418
|
+
return (
|
|
1419
|
+
<button
|
|
1420
|
+
{...attrs}
|
|
1421
|
+
aria-disabled={modifiers.unavailable ? 'true' : attrs['aria-disabled']}
|
|
1422
|
+
class={classes}
|
|
1423
|
+
data-day={iso(value)}
|
|
1424
|
+
data-disabled={modifiers.disabled ? 'true' : undefined}
|
|
1425
|
+
data-outside={modifiers.outside ? 'true' : undefined}
|
|
1426
|
+
data-range-end={modifiers.range_end ? 'true' : undefined}
|
|
1427
|
+
data-range-middle={modifiers.range_middle ? 'true' : undefined}
|
|
1428
|
+
data-range-start={modifiers.range_start ? 'true' : undefined}
|
|
1429
|
+
data-selected-single={single ? 'true' : undefined}
|
|
1430
|
+
data-slot="calendar-day-button"
|
|
1431
|
+
data-state={modifiers.selected ? 'selected' : 'unselected'}
|
|
1432
|
+
data-today={modifiers.today ? 'true' : undefined}
|
|
1433
|
+
data-unavailable={modifiers.unavailable ? 'true' : undefined}
|
|
1434
|
+
type={type}
|
|
1435
|
+
>
|
|
1436
|
+
{day}
|
|
1437
|
+
</button>
|
|
1438
|
+
)
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
export { Calendar, CalendarDayButton }
|