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.
Files changed (103) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +126 -0
  3. package/dist/accordion.js +130 -0
  4. package/dist/avatar.js +54 -0
  5. package/dist/calendar.js +2 -0
  6. package/dist/carousel.js +239 -0
  7. package/dist/chart.js +790 -0
  8. package/dist/checkbox-group.js +70 -0
  9. package/dist/checkbox.js +77 -0
  10. package/dist/chunks/bar-CVafh6C1.js +99 -0
  11. package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
  12. package/dist/chunks/collection-DtRB63U4.js +111 -0
  13. package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
  14. package/dist/chunks/menu-B45IyHHC.js +704 -0
  15. package/dist/chunks/native-BJdhd9XJ.js +20 -0
  16. package/dist/chunks/popup-C8Bb3l_g.js +459 -0
  17. package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
  18. package/dist/chunks/position-D6_i_SRn.js +434 -0
  19. package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
  20. package/dist/collapsible.js +106 -0
  21. package/dist/command.js +263 -0
  22. package/dist/context-menu.js +112 -0
  23. package/dist/data-table.js +5 -0
  24. package/dist/dialog.js +207 -0
  25. package/dist/direction.js +22 -0
  26. package/dist/drawer.js +139 -0
  27. package/dist/field.js +26 -0
  28. package/dist/index.js +38 -0
  29. package/dist/input-date.js +994 -0
  30. package/dist/input-group.js +52 -0
  31. package/dist/input-otp.js +179 -0
  32. package/dist/menu.js +2 -0
  33. package/dist/menubar.js +236 -0
  34. package/dist/message-scroller.js +446 -0
  35. package/dist/navigation-menu.js +330 -0
  36. package/dist/popover.js +307 -0
  37. package/dist/progress.js +39 -0
  38. package/dist/radio-group.js +107 -0
  39. package/dist/resizable.js +172 -0
  40. package/dist/select.js +961 -0
  41. package/dist/sidebar.js +343 -0
  42. package/dist/slider.js +259 -0
  43. package/dist/switch.js +53 -0
  44. package/dist/tabs.js +182 -0
  45. package/dist/toast.js +492 -0
  46. package/dist/toggle-group.js +111 -0
  47. package/dist/toggle.js +52 -0
  48. package/dist/toolbar.js +127 -0
  49. package/dist/tooltip.js +196 -0
  50. package/dist/utils.js +104 -0
  51. package/dist/virtual-list.js +2 -0
  52. package/package.json +250 -0
  53. package/src/accordion.tsx +261 -0
  54. package/src/availability.ts +261 -0
  55. package/src/avatar.tsx +99 -0
  56. package/src/bar.ts +156 -0
  57. package/src/calendar.tsx +1441 -0
  58. package/src/carousel.tsx +424 -0
  59. package/src/chart.tsx +1194 -0
  60. package/src/checkbox-group.tsx +132 -0
  61. package/src/checkbox.tsx +130 -0
  62. package/src/collapsible.tsx +188 -0
  63. package/src/collection.ts +154 -0
  64. package/src/command.tsx +511 -0
  65. package/src/context-menu.tsx +233 -0
  66. package/src/data-table-contract.ts +143 -0
  67. package/src/data-table-model.ts +760 -0
  68. package/src/data-table.tsx +475 -0
  69. package/src/dialog.tsx +393 -0
  70. package/src/direction.tsx +45 -0
  71. package/src/drawer.tsx +251 -0
  72. package/src/field.tsx +61 -0
  73. package/src/index.ts +37 -0
  74. package/src/input-date.tsx +1539 -0
  75. package/src/input-group.tsx +142 -0
  76. package/src/input-otp.tsx +324 -0
  77. package/src/menu-cluster.ts +124 -0
  78. package/src/menu.tsx +1095 -0
  79. package/src/menubar.tsx +459 -0
  80. package/src/message-scroller.tsx +732 -0
  81. package/src/native.ts +26 -0
  82. package/src/navigation-menu.tsx +578 -0
  83. package/src/popover.tsx +519 -0
  84. package/src/popup-surface.tsx +31 -0
  85. package/src/popup.ts +569 -0
  86. package/src/position.ts +523 -0
  87. package/src/progress.tsx +70 -0
  88. package/src/radio-group.tsx +186 -0
  89. package/src/resizable.tsx +310 -0
  90. package/src/segments.ts +922 -0
  91. package/src/select.tsx +1501 -0
  92. package/src/sidebar.tsx +683 -0
  93. package/src/slider.tsx +424 -0
  94. package/src/switch.tsx +104 -0
  95. package/src/tabs.tsx +314 -0
  96. package/src/toast.tsx +923 -0
  97. package/src/toggle-group.tsx +249 -0
  98. package/src/toggle.tsx +91 -0
  99. package/src/toolbar.tsx +212 -0
  100. package/src/tooltip.tsx +359 -0
  101. package/src/utils.ts +204 -0
  102. package/src/virtual-list.tsx +205 -0
  103. package/src/virtual.ts +385 -0
@@ -0,0 +1,1539 @@
1
+ import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
2
+ import { callHandler, callRef, controlled, dom, id, listen, restore, roving, spin, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
3
+ import { context } from 'ajo/context'
4
+ import { compile, type Availability, type AvailabilityMatcher } from './availability'
5
+ import type { ReservedPositionArg } from './position'
6
+ import type { FixedArgs, OmitArg, PopupPosition } from './utils'
7
+ import { Calendar, type CalendarArgs, type CalendarCommonArgs, type CalendarDateRange, type CalendarMatcher } from './calendar'
8
+ import { FieldContext } from './field'
9
+ import { contentAttrs, popup, type PopupView } from './popup'
10
+ import {
11
+ defaultMessage,
12
+ field,
13
+ formatValue,
14
+ fromISO,
15
+ isReversed,
16
+ timeRun,
17
+ unitLabel,
18
+ type FieldOptions,
19
+ type FieldView,
20
+ type Granularity,
21
+ type InputResult,
22
+ type Reason,
23
+ type Segment,
24
+ type SegmentUnit,
25
+ type SegmentsKind,
26
+ type Units,
27
+ } from './segments'
28
+ import { flag, triggerAttrs } from './utils'
29
+ export type { PopupPlacement, PopupPosition } from './utils'
30
+
31
+ /** Range endpoint edited by a segmented date or time field. */
32
+ export type InputDateSide = 'from' | 'to'
33
+
34
+ /** Serialized start and end values emitted by a range field. */
35
+ export type InputDateRangeValue = {
36
+ /** Range start in the family's own format; null while empty. */
37
+ from: string | null
38
+ /** Range end in the family's own format; null while empty. */
39
+ to: string | null
40
+ }
41
+
42
+ /** Public value shape selected by a field's range mode. */
43
+ export type InputDateValue<Range extends boolean = false> = Range extends true ? InputDateRangeValue : string
44
+
45
+ /** Labelled value that may be committed from an InputDate popover. */
46
+ export type InputDatePreset<Range extends boolean = boolean> = {
47
+ /** Preset button label. */
48
+ label: string
49
+ /** Committed on pick, in the family's own value format. */
50
+ value: InputDateValue<Range>
51
+ }
52
+
53
+ type InputDateCalendarOwnedArgs = 'allowNonContiguous' | 'defaultSelected' | 'mode' | 'onSelect' | 'selected' | 'unavailable'
54
+
55
+ /** Calendar presentation arguments that remain consumer-configurable inside InputDate. */
56
+ export type InputDateCalendarArgs = OmitArg<CalendarCommonArgs, InputDateCalendarOwnedArgs> & {
57
+ /** Calendar implementation; the themed layer injects its Calendar here. */
58
+ component?: Stateless<CalendarArgs>
59
+ } & FixedArgs<InputDateCalendarOwnedArgs>
60
+
61
+ type CommonArgs<Range extends boolean> = WithChildren<OmitArg<IntrinsicElements['div'], 'children' | 'defaultValue' | 'onchange' | ReservedPositionArg> & {
62
+ /** Allow a range to span unavailable days without treating its interior gaps as selected. */
63
+ allowNonContiguous?: boolean
64
+ /** Range mode: two field groups, `{ from, to } | null` value. */
65
+ range?: Range
66
+ /** Controlled value; null means controlled-empty. */
67
+ value?: InputDateValue<Range> | null
68
+ /** Initial value for uncontrolled usage; what a form reset restores. */
69
+ defaultValue?: InputDateValue<Range>
70
+ /** Called on every commit; null when the field empties. */
71
+ onValueChange?: (value: InputDateValue<Range> | null, event?: Event) => void
72
+ /** BCP 47 tag or `{ code }`; falls back to `<html lang>`, then 'en-US'. Never navigator. */
73
+ locale?: string | { code?: string }
74
+ /** Lower bound in the family's format; stamps invalid, never blocks commits. */
75
+ min?: string
76
+ /** Upper bound in the family's format; stamps invalid, never blocks commits. */
77
+ max?: string
78
+ /** Unavailable dates or times; remain committable and stamp reason-coded invalid state. */
79
+ unavailable?: AvailabilityMatcher | AvailabilityMatcher[]
80
+ /** Seeds the first arrow press on an empty segment; never emitted by itself. */
81
+ placeholderValue?: string
82
+ /** Reason-coded message override; undefined falls back to the localized default. */
83
+ errorMessage?: (reason: Reason) => string | undefined
84
+ /** Screen-reader label for an empty editable segment. */
85
+ emptyLabel?: string
86
+ /** Hidden input name; range submits `name[from]` / `name[to]`. */
87
+ name?: string
88
+ /** Mirrors to aria-required only. */
89
+ required?: boolean
90
+ /** Unfocusable segments, no submission. */
91
+ disabled?: boolean
92
+ /** Focusable no-op segments. */
93
+ readOnly?: boolean
94
+ /** Additional classes. */
95
+ class?: string
96
+ }> & FixedArgs<'onchange' | ReservedPositionArg>
97
+
98
+ type PopupArgs<Range extends boolean> = {
99
+ /** Opt into the calendar popover; an object forwards args to InputDateCalendar. */
100
+ calendar?: boolean | InputDateCalendarArgs
101
+ /** Preset values rendered by InputDatePresets; a pick commits and closes. */
102
+ presets?: InputDatePreset<Range>[]
103
+ /** Close on a pick. Defaults to single/range-complete for dates, and false while a datetime time surface is composed. */
104
+ closeOnSelect?: boolean
105
+ /** Controlled popover state. */
106
+ open?: boolean
107
+ /** Initial popover state for uncontrolled usage. */
108
+ defaultOpen?: boolean
109
+ /** Called when the popover opens or closes. */
110
+ onOpenChange?: (open: boolean, event?: Event) => void
111
+ }
112
+
113
+ type TimeArgs = {
114
+ /** Overrides the locale's resolved hour cycle. */
115
+ hourCycle?: 12 | 24
116
+ /** Segment shape for an empty field; values with seconds force the segment. */
117
+ granularity?: 'minute' | 'second'
118
+ /** Minute arrow step; typing and PageUp/Down are unaffected. */
119
+ step?: number
120
+ }
121
+
122
+ /** Arguments for a segmented date field with optional Calendar composition. */
123
+ export type InputDateArgs<Range extends boolean = false> = CommonArgs<Range> & PopupArgs<Range> & PopupPosition
124
+
125
+ /** Arguments for a segmented wall-time field. */
126
+ export type InputTimeArgs<Range extends boolean = false> = CommonArgs<Range> & TimeArgs & FixedArgs<'gap' | 'placement'>
127
+
128
+ /** Arguments for a segmented date-time field with optional Calendar composition. */
129
+ export type InputDateTimeArgs<Range extends boolean = false> = CommonArgs<Range> & PopupArgs<Range> & TimeArgs & PopupPosition
130
+
131
+ /** Arguments for the rendered date-segment group of an InputDate family root. */
132
+ export type InputDateFieldArgs = OmitArg<IntrinsicElements['div'], 'children'> & {
133
+ /** Which range side this group edits; required in range mode, absent in single. */
134
+ side?: InputDateSide
135
+ /** Accessible group label; range sides default to Start/End date. */
136
+ label?: string
137
+ /** Class for each editable segment. */
138
+ segmentClass?: string
139
+ /** Class for literal separators. */
140
+ literalClass?: string
141
+ /** Additional classes. */
142
+ class?: string
143
+ } & FixedArgs<'children'>
144
+
145
+ /** Arguments for the secondary time-segment group of InputDateTime. */
146
+ export type InputDateTimeFieldArgs = OmitArg<IntrinsicElements['div'], 'children'> & {
147
+ /** Which range side this time surface edits; absent in single mode. */
148
+ side?: InputDateSide
149
+ /** Accessible group label; defaults to Time, Start time, or End time. */
150
+ label?: string
151
+ /** Class for each editable time segment. */
152
+ segmentClass?: string
153
+ /** Class for literal separators between time segments. */
154
+ literalClass?: string
155
+ /** Additional classes. */
156
+ class?: string
157
+ } & FixedArgs<'children'>
158
+
159
+ /** Arguments for the button that toggles an InputDate Calendar popover. */
160
+ export type InputDateTriggerArgs = WithChildren<IntrinsicElements['button'] & {
161
+ /** Additional classes. */
162
+ class?: string
163
+ iconClass?: string
164
+ }>
165
+
166
+ /** Arguments for positioned InputDate popover content. */
167
+ export type InputDateContentArgs = WithChildren<OmitArg<IntrinsicElements['div'], 'hidden' | 'id' | 'popover' | 'role' | 'tabindex' | 'tabIndex' | ReservedPositionArg> & {
168
+ /** Additional classes. */
169
+ class?: string
170
+ /** Inline CSS declarations composed with live positioning styles. */
171
+ style?: string
172
+ }> & FixedArgs<'gap' | 'hidden' | 'id' | 'placement' | 'popover' | 'role' | 'tabindex' | 'tabIndex' | ReservedPositionArg>
173
+
174
+ /** Arguments for the button that clears an InputDate family value. */
175
+ export type InputDateClearArgs = WithChildren<IntrinsicElements['button'] & {
176
+ /** Additional classes. */
177
+ class?: string
178
+ iconClass?: string
179
+ }>
180
+
181
+ /** Arguments for the list of preset values rendered in a date popover. */
182
+ export type InputDatePresetsArgs = WithChildren<IntrinsicElements['div'] & {
183
+ /** Additional classes. */
184
+ class?: string
185
+ buttonClass?: string
186
+ }>
187
+
188
+ type PublicValue = string | InputDateRangeValue | null
189
+ type InputDateSurface = 'field' | 'popover'
190
+
191
+ type InputDateRootArgs = WithChildren<{
192
+ allowNonContiguous?: boolean
193
+ calendar?: boolean | InputDateCalendarArgs
194
+ closeOnSelect?: boolean
195
+ defaultOpen?: boolean
196
+ defaultValue?: string | InputDateRangeValue
197
+ disabled?: boolean
198
+ emptyLabel?: string
199
+ errorMessage?: (reason: Reason) => string | undefined
200
+ granularity?: Granularity
201
+ hourCycle?: 12 | 24
202
+ kind: SegmentsKind
203
+ locale?: string | { code?: string }
204
+ max?: string
205
+ min?: string
206
+ name?: string
207
+ onOpenChange?: (open: boolean, event?: Event) => void
208
+ onValueChange?: (value: PublicValue, event?: Event) => void
209
+ open?: boolean
210
+ placeholderValue?: string
211
+ presets?: InputDatePreset[]
212
+ range?: boolean
213
+ readOnly?: boolean
214
+ required?: boolean
215
+ step?: number
216
+ unavailable?: AvailabilityMatcher | AvailabilityMatcher[]
217
+ value?: string | InputDateRangeValue | null
218
+ }> & PopupPosition
219
+
220
+ type InputDateContextValue = {
221
+ allowNonContiguous: boolean
222
+ applyPreset: (value: string | InputDateRangeValue, event: Event) => void
223
+ calendarDisabled: (user?: CalendarMatcher | CalendarMatcher[]) => CalendarMatcher[] | undefined
224
+ calendarUnavailable: AvailabilityMatcher | AvailabilityMatcher[] | undefined
225
+ clear: (event?: Event) => void
226
+ contentId: string
227
+ contentStyle: PopupView['contentStyle']
228
+ daySelected: () => Date | null
229
+ disabled: boolean
230
+ field: (side: InputDateSide) => FieldView
231
+ groupAttrs: (side: InputDateSide, label?: string, surface?: InputDateSurface) => Record<string, unknown>
232
+ hasValue: boolean
233
+ kind: SegmentsKind
234
+ locale: string
235
+ message: string | null
236
+ monthOf: () => Date | undefined
237
+ onMonthChange: (month: Date, event?: Event) => void
238
+ open: boolean
239
+ pickDay: (next: Date | null, event: Event) => void
240
+ pickRange: (next: CalendarDateRange | null, event: Event) => void
241
+ popup: (part: string) => boolean
242
+ presets: InputDatePreset[]
243
+ range: boolean
244
+ rangeSelected: () => CalendarDateRange | null
245
+ readOnly: boolean
246
+ segmentAttrs: (side: InputDateSide, segment: Segment, label?: string, surface?: InputDateSurface) => Record<string, unknown>
247
+ segmentText: (side: InputDateSide, segment: Segment, surface?: InputDateSurface) => string
248
+ setContent: (element: HTMLDivElement | null) => void
249
+ registerTimeSurface: (element: HTMLDivElement) => void
250
+ unregisterTimeSurface: (element: HTMLDivElement) => void
251
+ setTrigger: (element: HTMLButtonElement | null) => void
252
+ timeSurface: (part: string) => boolean
253
+ toggleOpen: (event: Event) => void
254
+ triggerId: string
255
+ }
256
+
257
+ const InputDateContext = context<InputDateContextValue | null>(null)
258
+
259
+ // Locale resolution is gated on document, NEVER navigator: Node ships a global
260
+ // navigator and the ambient machine locale is banned on both passes.
261
+ const resolveLocale = (locale: string | { code?: string } | undefined): string => {
262
+ const code = typeof locale === 'string' ? locale : locale?.code
263
+ if (code) return code
264
+ if (typeof document !== 'undefined' && document.documentElement.lang) return document.documentElement.lang
265
+ return 'en-US'
266
+ }
267
+
268
+ const InputDateRoot: Stateful<InputDateRootArgs> = function* (initial) {
269
+ let kind = initial.kind
270
+ const rootId = id('input-date')
271
+ const domReady = dom(this)
272
+ const ownerDocument = domReady ? this.ownerDocument : null
273
+ const node = (value: unknown): value is Node => {
274
+ const view = ownerDocument?.defaultView
275
+ return Boolean(view && value instanceof view.Node)
276
+ }
277
+ // iOS VoiceOver cannot focus spinbuttons: role textbox, no aria-value*.
278
+ // SSR emits spinbutton; the hydration attr rewrite is an accepted divergence.
279
+ const ios = domReady && typeof navigator !== 'undefined' &&
280
+ (/iP(ad|hone|od)/.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && navigator.maxTouchPoints > 1))
281
+ const warned = new Set<string>()
282
+ const timeSurfaces = new Set<HTMLDivElement>()
283
+ let closeOnSelect: boolean | undefined
284
+ let composing: { side: InputDateSide; unit: SegmentUnit; element: HTMLElement; text: string } | null = null
285
+ let controlId = `${rootId}-control`
286
+ let availabilitySource = initial.unavailable
287
+ let availability = compile(initial.unavailable)
288
+ let boundsSource: string | undefined
289
+ let boundsAvailability: Availability | undefined
290
+ let calendarUserSource: CalendarMatcher | CalendarMatcher[] | undefined
291
+ let calendarUserAvailability: Availability | undefined
292
+ let allowNonContiguous = Boolean(initial.allowNonContiguous)
293
+ let disabled = Boolean(initial.disabled)
294
+ let isRange = Boolean(initial.range)
295
+ let locale = resolveLocale(initial.locale)
296
+ let onOpenChange: InputDateRootArgs['onOpenChange']
297
+ let onValueChange: InputDateRootArgs['onValueChange']
298
+ let pendingFocus = false
299
+ let pendingReference: HTMLElement | null = null
300
+ let closingInside = false
301
+ let pop: PopupView<HTMLButtonElement, HTMLDivElement>
302
+ let presets: InputDatePreset[] = []
303
+ let readOnly = Boolean(initial.readOnly)
304
+ let required = Boolean(initial.required)
305
+ let visibleMonth: Date | undefined
306
+
307
+ const unavailableValue = (value: string) => availability?.value(kind, value) ?? false
308
+
309
+ const sideDefault = (value: string | InputDateRangeValue | undefined, side: InputDateSide): string | undefined => {
310
+ if (value == null) return undefined
311
+ if (typeof value === 'string') return side === 'from' ? value : undefined
312
+ return value[side] ?? undefined
313
+ }
314
+
315
+ const optionsFor = (side: InputDateSide, args: InputDateRootArgs): FieldOptions => ({
316
+ kind,
317
+ locale,
318
+ granularity: args.granularity,
319
+ hourCycle: args.hourCycle,
320
+ step: args.step,
321
+ placeholderValue: args.placeholderValue,
322
+ defaultValue: sideDefault(args.defaultValue, side),
323
+ min: args.min,
324
+ max: args.max,
325
+ unavailable: availability ? unavailableValue : undefined,
326
+ errorMessage: args.errorMessage,
327
+ })
328
+
329
+ const fields: Record<InputDateSide, FieldView> = {
330
+ from: field(optionsFor('from', initial)),
331
+ to: field(optionsFor('to', initial)),
332
+ }
333
+
334
+ const composeValue = (): PublicValue => {
335
+ const from = fields.from.value()
336
+ if (!isRange) return from
337
+ const to = fields.to.value()
338
+ return from == null && to == null ? null : { from, to }
339
+ }
340
+
341
+ const valueState = controlled<PublicValue>(this, {
342
+ fallback: initial.defaultValue ?? null,
343
+ onChange: (next, event) => onValueChange?.(next, event),
344
+ })
345
+
346
+ const sideValue = (side: InputDateSide): string | null | undefined => {
347
+ if (!valueState.controlled) return undefined
348
+ const value = valueState.value
349
+ if (value == null) return null
350
+ if (typeof value === 'string') return side === 'from' ? value : null
351
+ return value[side]
352
+ }
353
+
354
+ // Range emissions compose once: both sides may merge in one gesture.
355
+ const emitIfChanged = (results: InputResult[], event?: Event) => {
356
+ if (results.every(result => result.emit === undefined)) return
357
+ valueState.set(composeValue(), event)
358
+ }
359
+
360
+ const segmentsOf = (surface: InputDateSurface = 'field', rendered = true): HTMLElement[] =>
361
+ domReady
362
+ ? Array.from(this.querySelectorAll<HTMLElement>('[data-segment]'))
363
+ .filter(item => item.dataset.surface === surface && (!rendered || item.offsetParent !== null))
364
+ : []
365
+
366
+ const segmentOf = (event: Event): HTMLElement | null =>
367
+ event.target instanceof Element ? event.target.closest<HTMLElement>('[data-segment]') : null
368
+
369
+ const sideOf = (segment: HTMLElement): InputDateSide =>
370
+ (segment.dataset.side as InputDateSide) ?? 'from'
371
+
372
+ const surfaceOf = (segment: HTMLElement): InputDateSurface =>
373
+ segment.dataset.surface === 'popover' ? 'popover' : 'field'
374
+
375
+ const unitOf = (segment: HTMLElement): SegmentUnit => segment.dataset.segment as SegmentUnit
376
+
377
+ // Each rendered surface owns one flat list. Range sides stay adjacent within
378
+ // the field or popover, but navigation never crosses between those surfaces.
379
+ const moveFocus = (from: HTMLElement, step: number) => {
380
+ const list = segmentsOf(surfaceOf(from))
381
+ list[list.indexOf(from) + step]?.focus()
382
+ }
383
+
384
+ const apply = (result: InputResult, event: Event, segment: HTMLElement): boolean => {
385
+ if (!result.handled) return false
386
+ emitIfChanged([result], event)
387
+ if (result.advance) moveFocus(segment, 1)
388
+ if (result.retreat) moveFocus(segment, -1)
389
+ this.next()
390
+ return true
391
+ }
392
+
393
+ const focusMemory = restore(this)
394
+
395
+ const clearFocusIntent = () => {
396
+ pendingFocus = false
397
+ pendingReference = null
398
+ }
399
+
400
+ const restoreAfterClose = () => {
401
+ clearFocusIntent()
402
+ const active = ownerDocument?.activeElement
403
+ if (dom(active) && pop.content?.contains(active) && !closingInside) focusMemory.restore()
404
+ else focusMemory.capture(null)
405
+ closingInside = false
406
+ }
407
+
408
+ pop = popup<HTMLButtonElement, HTMLDivElement>(this, {
409
+ prefix: 'input-date',
410
+ profile: 'date',
411
+ initialOpen: Boolean(initial.open ?? initial.defaultOpen),
412
+ disabled: () => disabled,
413
+ onOpenChange: (next, event) => onOpenChange?.(next, event),
414
+ // The popover anchors to the complete field root, not a segment group or
415
+ // the 28px icon trigger.
416
+ reference: view => view.reference,
417
+ // Native invoker semantics still belong to the calendar button.
418
+ source: view => view.trigger,
419
+ referenceHidden: 'close',
420
+ dismiss: {
421
+ escape: false,
422
+ outside: true,
423
+ inside: view => [
424
+ dom(view.reference) ? view.reference : null,
425
+ view.trigger,
426
+ view.content,
427
+ ],
428
+ onDismiss: event => setOpen(false, event),
429
+ },
430
+ onPosition: () => {
431
+ if (!pendingFocus || pop.reference !== pendingReference) return
432
+ clearFocusIntent()
433
+ if (!pop.content) return
434
+ // The dialog autofocuses the calendar: selected day, today, first enabled.
435
+ const target = pop.content.querySelector<HTMLElement>('[data-slot="calendar-day-button"][data-state="selected"]:not(:disabled)')
436
+ ?? pop.content.querySelector<HTMLElement>('[data-slot="calendar-day-button"][data-today]:not(:disabled)')
437
+ ?? pop.content.querySelector<HTMLElement>('[data-slot="calendar-day-button"]:not(:disabled)')
438
+ ?? pop.content
439
+ target.focus()
440
+ },
441
+ onSync: opened => {
442
+ if (!opened) restoreAfterClose()
443
+ },
444
+ })
445
+ pop.setReference(this)
446
+
447
+ const setOpen = (next: boolean, event?: Event) => {
448
+ if (disabled && next) return
449
+ if (next === pop.open) return
450
+ if (next) {
451
+ // A fresh session follows the committed value again.
452
+ visibleMonth = undefined
453
+ pendingFocus = true
454
+ pendingReference = pop.reference as HTMLElement | null
455
+ } else {
456
+ clearFocusIntent()
457
+ // A pointerdown landing back on our field keeps the click's own focus.
458
+ closingInside = event?.type === 'pointerdown' && node(event.target) && this.contains(event.target)
459
+ }
460
+ pop.setOpen(next, event)
461
+ }
462
+
463
+ const toggleOpen = (event: Event) => {
464
+ const next = !pop.open
465
+ if (next) focusMemory.capture(pop.trigger)
466
+ setOpen(next, event)
467
+ }
468
+
469
+ const popupPart = (part: string) => {
470
+ if (kind !== 'time') return true
471
+ if (!warned.has(part)) {
472
+ warned.add(part)
473
+ console.warn(`[input-date] <${part}> has no effect under InputTime; popup parts require a date family root.`)
474
+ }
475
+ return false
476
+ }
477
+
478
+ const timeSurface = (part: string) => {
479
+ if (kind === 'datetime') return true
480
+ if (!warned.has(part)) {
481
+ warned.add(part)
482
+ console.warn(`[input-date] <${part}> has no effect outside InputDateTime.`)
483
+ }
484
+ return false
485
+ }
486
+
487
+ const hasTimeSurface = () => timeSurfaces.size > 0
488
+
489
+ // Calendar wiring:
490
+
491
+ const dateOfValue = (side: InputDateSide): Date | undefined => {
492
+ const units = fields[side].units
493
+ return units.year != null && units.month != null && units.day != null
494
+ ? new Date(units.year, units.month - 1, units.day, 12)
495
+ : undefined
496
+ }
497
+
498
+ const daySelected = (): Date | null => dateOfValue('from') ?? null
499
+
500
+ const rangeSelected = (): CalendarDateRange | null => {
501
+ const from = dateOfValue('from')
502
+ const to = dateOfValue('to')
503
+ return from || to ? { from, to } : null
504
+ }
505
+
506
+ const monthOf = () => visibleMonth ?? dateOfValue('from')
507
+
508
+ const onMonthChange = (month: Date, event?: Event) => {
509
+ const slot = event?.target instanceof Element ? event.target.closest<HTMLElement>('[data-slot]')?.dataset.slot : undefined
510
+ this.next(() => visibleMonth = month)
511
+ // The Calendar re-creates its keyed month subtree (nav buttons included):
512
+ // when paging drops focus with it, land on the equivalent control.
513
+ if (!domReady || !slot) return
514
+ const active = document.activeElement
515
+ if (active && active !== document.body) return
516
+ pop.content?.querySelector<HTMLElement>(`[data-slot="${slot}"]`)?.focus()
517
+ }
518
+
519
+ const dayOfBound = (value: string | undefined): Date | undefined => {
520
+ if (!value) return undefined
521
+ const units = fromISO(kind, value)
522
+ return units && units.year != null && units.month != null && units.day != null
523
+ ? new Date(units.year, units.month - 1, units.day, 12)
524
+ : undefined
525
+ }
526
+
527
+ const syncAvailability = (args: InputDateRootArgs) => {
528
+ if (args.unavailable !== availabilitySource) {
529
+ availabilitySource = args.unavailable
530
+ availability = compile(args.unavailable)
531
+ }
532
+ const bounds = `${args.min ?? ''}\0${args.max ?? ''}`
533
+ if (bounds !== boundsSource) {
534
+ boundsSource = bounds
535
+ const matcher: CalendarMatcher[] = []
536
+ const before = dayOfBound(args.min)
537
+ const after = dayOfBound(args.max)
538
+ if (before) matcher.push({ before })
539
+ if (after) matcher.push({ after })
540
+ boundsAvailability = compile(matcher)
541
+ }
542
+ }
543
+
544
+ const hardDisabled = (date: Date) =>
545
+ Boolean(calendarUserAvailability?.day(date) || boundsAvailability?.day(date))
546
+ const hardDisabledMatcher: CalendarMatcher[] = [hardDisabled]
547
+
548
+ const calendarDisabled = (user?: CalendarMatcher | CalendarMatcher[]): CalendarMatcher[] | undefined => {
549
+ if (user !== calendarUserSource) {
550
+ calendarUserSource = user
551
+ calendarUserAvailability = compile(user)
552
+ }
553
+ return calendarUserAvailability || boundsAvailability ? hardDisabledMatcher : undefined
554
+ }
555
+
556
+ const datePatch = (editor: FieldView, date: Date): Partial<Units> => {
557
+ const patch: Partial<Units> = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() }
558
+ if (kind === 'datetime') {
559
+ // A picked day must commit: empty time units seed from the placeholder.
560
+ const units = editor.units
561
+ const seeded = editor.placeholder()
562
+ if (units.hour == null) patch.hour = seeded.hour
563
+ if (units.minute == null) patch.minute = seeded.minute
564
+ if (units.second == null && editor.segments.some(segment => segment.type === 'second')) patch.second = seeded.second
565
+ if (units.dayPeriod == null && editor.hourCycle === 'h12') patch.dayPeriod = seeded.dayPeriod
566
+ }
567
+ return patch
568
+ }
569
+
570
+ const emptyDate: Partial<Units> = { year: null, month: null, day: null }
571
+
572
+ const pickDay = (next: Date | null, event: Event) => {
573
+ if (disabled || readOnly) return
574
+ const result = fields.from.merge(next ? datePatch(fields.from, next) : emptyDate)
575
+ emitIfChanged([result], event)
576
+ if (closeOnSelect ?? (Boolean(next) && !hasTimeSurface())) setOpen(false, event)
577
+ this.next()
578
+ }
579
+
580
+ const pickRange = (next: CalendarDateRange | null, event: Event) => {
581
+ if (disabled || readOnly) return
582
+ const patchFor = (editor: FieldView, date: Date | undefined) => date ? datePatch(editor, date) : emptyDate
583
+ emitIfChanged([
584
+ fields.from.merge(patchFor(fields.from, next?.from)),
585
+ fields.to.merge(patchFor(fields.to, next?.to)),
586
+ ], event)
587
+ if (closeOnSelect ?? (Boolean(next?.from && next?.to) && !hasTimeSurface())) setOpen(false, event)
588
+ this.next()
589
+ }
590
+
591
+ const applyPreset = (value: string | InputDateRangeValue, event: Event) => {
592
+ if (disabled || readOnly) return
593
+ const mergeSide = (side: InputDateSide, next: string | null): InputResult => {
594
+ if (next == null) return fields[side].clear()
595
+ const units = fromISO(kind, next, fields[side].hourCycle)
596
+ if (!units) {
597
+ console.warn(`[input-date] invalid ${kind} preset value: "${next}"`)
598
+ return { handled: false }
599
+ }
600
+ return fields[side].merge(units)
601
+ }
602
+ const results = typeof value === 'string'
603
+ ? [mergeSide('from', value)]
604
+ : [mergeSide('from', value.from), mergeSide('to', value.to)]
605
+ emitIfChanged(results, event)
606
+ setOpen(false, event)
607
+ this.next()
608
+ }
609
+
610
+ const clearValue = (event?: Event) => {
611
+ if (disabled || readOnly) return
612
+ const results = [fields.from.clear()]
613
+ if (isRange) results.push(fields.to.clear())
614
+ emitIfChanged(results, event)
615
+ // The clear button unmounts with the value (combobox precedent:
616
+ // clearing refocuses the input): focus lands on the first segment.
617
+ if (domReady) queueMicrotask(() => segmentsOf('field')[0]?.focus())
618
+ this.next()
619
+ }
620
+
621
+ // A sync reshape (external narrower value, hourCycle flip) can drop the
622
+ // focused segment: move focus to the nearest surviving one, preferring the
623
+ // previous in display order, so it never falls to body.
624
+ const relocateFocus = () => {
625
+ const active = document.activeElement
626
+ if (!(active instanceof HTMLElement) || !active.dataset.segment || !this.contains(active)) return
627
+ const survives = (item: HTMLElement) =>
628
+ fields[sideOf(item)].segments.some(segment => segment.type === unitOf(item))
629
+ if (survives(active)) return
630
+ const surface = surfaceOf(active)
631
+ const list = segmentsOf(surface, false)
632
+ const index = list.indexOf(active)
633
+ const target = list.slice(0, Math.max(index, 0)).reverse().find(survives) ?? list.slice(index + 1).find(survives)
634
+ if (!target) return
635
+ const side = sideOf(target)
636
+ const unit = unitOf(target)
637
+ // After the render pass removes the segment, focus its surviving neighbor.
638
+ queueMicrotask(() => {
639
+ const current = document.activeElement
640
+ if (current instanceof HTMLElement && current !== document.body && this.contains(current)) return
641
+ segmentsOf(surface).find(item => sideOf(item) === side && unitOf(item) === unit)?.focus()
642
+ })
643
+ }
644
+
645
+ // Keyboard: spin resolves the APG protocol, roving is Left/Right only.
646
+ const stepper = spin(this, {
647
+ onMove: (move, event) => {
648
+ const segment = segmentOf(event)
649
+ if (!segment || readOnly || disabled) return
650
+ const side = sideOf(segment)
651
+ apply(fields[side].spin(unitOf(segment), move), event, segment)
652
+ },
653
+ })
654
+
655
+ const nav = roving(this, {
656
+ items: () => {
657
+ const active = document.activeElement
658
+ return segmentsOf(active instanceof HTMLElement && active.dataset.segment ? surfaceOf(active) : 'field')
659
+ },
660
+ orientation: () => 'horizontal',
661
+ dir: () => domReady && getComputedStyle(this).direction === 'rtl' ? 'rtl' : 'ltr',
662
+ loop: () => false,
663
+ onMove: target => target.focus(),
664
+ })
665
+
666
+ listen(this, 'keydown', event => {
667
+ // Consumed only while open, so an ancestor Dialog does not also close;
668
+ // Escape never clears the value.
669
+ if (event.key === 'Escape') {
670
+ if (!pop.open) return
671
+ event.preventDefault()
672
+ setOpen(false, event)
673
+ return
674
+ }
675
+ const segment = segmentOf(event)
676
+ if (!segment) return
677
+ const side = sideOf(segment)
678
+ const unit = unitOf(segment)
679
+ // Pipeline contract: Alt+ArrowDown → spin.handle → roving Left/Right.
680
+ // Alt-combos never reach spin: without popup parts composed,
681
+ // Alt+ArrowDown is a pinned no-op, not a step.
682
+ if (event.altKey && event.key.startsWith('Arrow')) {
683
+ if (event.key === 'ArrowDown' && pop.content) {
684
+ event.preventDefault()
685
+ focusMemory.capture(segment)
686
+ setOpen(true, event)
687
+ }
688
+ return
689
+ }
690
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'a') {
691
+ event.preventDefault()
692
+ return
693
+ }
694
+ // Hardware delete acts here and preventDefaults, so the subsequent
695
+ // beforeinput never double-fires; mobile deletion arrives there instead.
696
+ if (event.key === 'Backspace' || event.key === 'Delete') {
697
+ event.preventDefault()
698
+ if (!readOnly && !disabled) apply(fields[side].erase(unit), event, segment)
699
+ return
700
+ }
701
+ if (event.key === 'Enter') {
702
+ event.preventDefault()
703
+ moveFocus(segment, 1)
704
+ return
705
+ }
706
+ if (!readOnly && stepper.handle(event)) return
707
+ if (!readOnly && (event.key === '+' || event.key === '-')) {
708
+ event.preventDefault()
709
+ apply(fields[side].spin(unit, { step: event.key === '+' ? 1 : -1 }), event, segment)
710
+ return
711
+ }
712
+ if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') nav.handle(event)
713
+ })
714
+
715
+ listen(this, 'beforeinput', event => {
716
+ const segment = segmentOf(event)
717
+ if (!segment) return
718
+ // Every cancelable edit is ours (insertParagraph and paste included).
719
+ if (event.cancelable) event.preventDefault()
720
+ if (disabled || readOnly) return
721
+ const side = sideOf(segment)
722
+ const unit = unitOf(segment)
723
+ if (event.inputType === 'insertCompositionText') {
724
+ // Uncancelable: snapshot now, restore on input, feed the engine on
725
+ // compositionend so reconciliation and the restore never double-write.
726
+ composing ??= { side, unit, element: segment, text: segment.textContent ?? '' }
727
+ return
728
+ }
729
+ if (event.inputType === 'deleteContentBackward' || event.inputType === 'deleteContentForward') {
730
+ apply(fields[side].erase(unit), event, segment)
731
+ return
732
+ }
733
+ if (event.inputType === 'insertText' && event.data) {
734
+ for (const key of event.data) apply(fields[side].type(unit, key), event, segment)
735
+ }
736
+ })
737
+
738
+ listen(this, 'input', event => {
739
+ if (!composing) return
740
+ const segment = segmentOf(event)
741
+ if (segment !== composing.element) return
742
+ segment.textContent = composing.text
743
+ this.next()
744
+ })
745
+
746
+ listen(this, 'compositionend', event => {
747
+ if (!composing) return
748
+ const { side, unit, element } = composing
749
+ composing = null
750
+ if (!disabled && !readOnly && event.data) {
751
+ for (const key of event.data) apply(fields[side].type(unit, key), event, element)
752
+ }
753
+ this.next()
754
+ })
755
+
756
+ listen(this, 'paste', event => {
757
+ if (segmentOf(event)) event.preventDefault()
758
+ })
759
+
760
+ listen(this, 'pointerdown', event => {
761
+ if (event.button) return
762
+ const target = event.target instanceof Element ? event.target : null
763
+ if (!target) return
764
+ // Dismiss never sees clicks inside the root (its host containment covers
765
+ // everything): clicking back into the field/chrome/segments while open
766
+ // closes here, and setOpen skips the restore so the click's focus wins.
767
+ if (pop.open && !pop.content?.contains(target) && !pop.trigger?.contains(target)) setOpen(false, event)
768
+ const segment = target.closest<HTMLElement>('[data-segment]')
769
+ if (segment) {
770
+ // Focus is ours to place: never drop a caret into the contenteditable.
771
+ event.preventDefault()
772
+ if (!disabled) segment.focus()
773
+ return
774
+ }
775
+ const group = target.closest<HTMLElement>('[data-slot="input-date-field"]')
776
+ if (!group || !this.contains(group)) return
777
+ // The trigger addon (and any interactive chrome) is exempt.
778
+ if (target.closest('[data-slot="input-date-trigger"], a, button, input, select, textarea')) return
779
+ event.preventDefault()
780
+ if (disabled) return
781
+ const list = Array.from(group.querySelectorAll<HTMLElement>('[data-segment]'))
782
+ const filled = [...list].reverse().find(item => item.dataset.placeholder !== 'true')
783
+ ;(filled ?? list[0])?.focus()
784
+ })
785
+
786
+ listen(this, 'focusout', event => {
787
+ const segment = segmentOf(event)
788
+ if (!segment) return
789
+ fields[sideOf(segment)].blur()
790
+ this.next()
791
+ })
792
+
793
+ if (domReady) {
794
+ // Segments never hold a selection: force-collapse anything that forms.
795
+ document.addEventListener('selectionchange', () => {
796
+ const selection = document.getSelection()
797
+ if (!selection || selection.isCollapsed || !selection.anchorNode) return
798
+ const anchor = selection.anchorNode instanceof Element ? selection.anchorNode : selection.anchorNode.parentElement
799
+ if (anchor?.closest('[data-segment]') && this.contains(anchor)) selection.collapseToEnd()
800
+ }, { signal: this.signal })
801
+
802
+ // No segment is labelable, so the label's `for` dangles: click focuses first.
803
+ document.addEventListener('click', event => {
804
+ const label = event.target instanceof Element ? event.target.closest('label') : null
805
+ if (!label || label.getAttribute('for') !== controlId) return
806
+ segmentsOf('field')[0]?.focus()
807
+ }, { signal: this.signal })
808
+
809
+ // type=hidden resets its own value silently; segment state must follow.
810
+ document.addEventListener('reset', event => {
811
+ if (!(event.target instanceof HTMLFormElement) || !event.target.contains(this)) return
812
+ if (valueState.controlled) return
813
+ fields.from.reset()
814
+ if (isRange) fields.to.reset()
815
+ valueState.init(composeValue())
816
+ this.next()
817
+ }, { signal: this.signal })
818
+ }
819
+
820
+ for (const args of this) {
821
+ const kindChanged = args.kind !== kind
822
+ kind = args.kind
823
+ syncAvailability(args)
824
+ allowNonContiguous = Boolean(args.allowNonContiguous)
825
+ closeOnSelect = args.closeOnSelect
826
+ disabled = Boolean(args.disabled)
827
+ isRange = Boolean(args.range)
828
+ locale = resolveLocale(args.locale)
829
+ const emptyLabel = args.emptyLabel ?? 'Empty'
830
+ onOpenChange = args.onOpenChange
831
+ onValueChange = args.onValueChange
832
+ presets = args.presets ?? []
833
+ readOnly = Boolean(args.readOnly)
834
+ required = Boolean(args.required)
835
+
836
+ valueState.sync(args.value)
837
+ const opened = pop.sync(args.open == null ? undefined : Boolean(args.open), {
838
+ placement: args.placement,
839
+ gap: args.gap,
840
+ })
841
+ if (!opened) clearFocusIntent()
842
+ else closingInside = false
843
+ if (kindChanged && args.value === undefined) {
844
+ fields.from.sync(sideDefault(args.defaultValue, 'from') ?? null, optionsFor('from', args))
845
+ fields.to.sync(sideDefault(args.defaultValue, 'to') ?? null, optionsFor('to', args))
846
+ valueState.init(composeValue())
847
+ } else {
848
+ fields.from.sync(sideValue('from'), optionsFor('from', args))
849
+ if (isRange) fields.to.sync(sideValue('to'), optionsFor('to', args))
850
+ }
851
+ if (domReady) relocateFocus()
852
+
853
+ const fieldCtx = FieldContext()
854
+ controlId = fieldCtx?.ids.control ?? `${rootId}-control`
855
+ const externalDescribedby = fieldCtx?.groupAttrs['aria-describedby']
856
+
857
+ const reasonFrom = fields.from.reason()
858
+ const reasonTo = isRange ? fields.to.reason() : null
859
+ const reversed = isRange && isReversed(fields.from.value(), fields.to.value())
860
+ const rangeFrom = isRange ? dateOfValue('from') : undefined
861
+ const rangeTo = isRange ? dateOfValue('to') : undefined
862
+ const unavailableRange = !allowNonContiguous && Boolean(rangeFrom && rangeTo && availability?.crosses(rangeFrom, rangeTo))
863
+ const rangeReason: Reason | null = reversed
864
+ ? { code: 'reversed' }
865
+ : unavailableRange
866
+ ? { code: 'unavailableRange' }
867
+ : null
868
+ const invalidOf = (side: InputDateSide) => (side === 'from' ? reasonFrom : reasonTo) != null || rangeReason != null
869
+ const hourCycle = fields.from.hourCycle
870
+ const message = fields.from.message()
871
+ ?? (isRange ? fields.to.message() : null)
872
+ ?? (rangeReason
873
+ ? args.errorMessage?.(rangeReason) ?? defaultMessage(rangeReason, { kind, locale, hourCycle })
874
+ : null)
875
+
876
+ const descriptionId = `${rootId}-description`
877
+ const messageId = `${rootId}-message`
878
+ const human = (side: InputDateSide) => {
879
+ const committed = fields[side].value()
880
+ return committed == null ? null : formatValue(committed, { kind, locale, hourCycle })
881
+ }
882
+ const formatted = isRange ? [human('from'), human('to')].filter(Boolean).join(' – ') : human('from')
883
+ const descriptionText = formatted ? `${kind === 'time' ? 'Selected time' : 'Selected date'}: ${formatted}` : null
884
+
885
+ const value = composeValue()
886
+ const firstUnit = fields.from.segments.find(segment => segment.editable)?.type
887
+
888
+ const sideLabel = (side: InputDateSide, override?: string) =>
889
+ override ?? (kind === 'time' ? (side === 'from' ? 'Start time' : 'End time') : side === 'from' ? 'Start date' : 'End date')
890
+
891
+ // describedby rides the first segment only until invalid stamps every one.
892
+ const describedbyOf = (side: InputDateSide, first: boolean): string | undefined => {
893
+ const parts: string[] = []
894
+ if ((first || invalidOf(side)) && typeof externalDescribedby === 'string') parts.push(externalDescribedby)
895
+ if (first && descriptionText) parts.push(descriptionId)
896
+ if (invalidOf(side) && message) parts.push(messageId)
897
+ return parts.join(' ') || undefined
898
+ }
899
+
900
+ const valuetextOf = (side: InputDateSide, unit: SegmentUnit): string => {
901
+ const editor = fields[side]
902
+ const current = editor.units[unit]
903
+ if (current == null) return emptyLabel
904
+ if (unit === 'month' && editor.monthNames.length) return `${current} – ${editor.monthNames[current - 1]}`
905
+ if (unit === 'hour' && editor.hourCycle === 'h12' && editor.units.dayPeriod != null) return `${current} ${editor.periods[editor.units.dayPeriod]}`
906
+ return editor.text(unit)
907
+ }
908
+
909
+ const groupAttrs = (side: InputDateSide, label?: string, surface: InputDateSurface = 'field'): Record<string, unknown> => {
910
+ const invalid = invalidOf(side)
911
+ const record: Record<string, unknown> = {
912
+ 'aria-disabled': flag(disabled),
913
+ 'aria-invalid': flag(invalid),
914
+ 'data-disabled': flag(disabled),
915
+ 'data-invalid': flag(invalid),
916
+ 'data-readonly': flag(readOnly),
917
+ 'data-side': side,
918
+ 'data-slot': surface === 'field' ? 'input-date-field' : 'input-date-time-field',
919
+ 'data-surface': surface,
920
+ role: 'group',
921
+ }
922
+ if (surface === 'popover') {
923
+ record['aria-label'] = label
924
+ } else if (isRange) {
925
+ // Each side is its own labelled group; the root carries the field label.
926
+ record['aria-label'] = sideLabel(side, label)
927
+ } else {
928
+ if (label) record['aria-label'] = label
929
+ if (fieldCtx) {
930
+ record['aria-labelledby'] = fieldCtx.groupAttrs['aria-labelledby']
931
+ record['aria-describedby'] = fieldCtx.groupAttrs['aria-describedby']
932
+ }
933
+ }
934
+ return record
935
+ }
936
+
937
+ const segmentAttrs = (side: InputDateSide, segment: Segment, label?: string, surface: InputDateSurface = 'field'): Record<string, unknown> => {
938
+ const unit = segment.type as SegmentUnit
939
+ const editor = fields[side]
940
+ const first = surface === 'field' && side === 'from' && unit === firstUnit
941
+ const filled = editor.units[unit] != null
942
+ const bounds = editor.bounds(unit)
943
+ const unitName = unitLabel(locale, unit)
944
+ const segmentId = first ? controlId : `${rootId}-${surface}-${side}-${unit}`
945
+ // Self-reference technique: aria-labelledby chains the segment itself
946
+ // (contributing its unit-name aria-label) with the field label, so
947
+ // every segment announces "month, Date of birth" — iOS VoiceOver
948
+ // does not announce groups. Range chains the outer label the same way.
949
+ const labelId = fieldCtx?.ids.label
950
+ const surfaceLabel = surface === 'popover'
951
+ ? label
952
+ : isRange
953
+ ? sideLabel(side, label)
954
+ : label
955
+ const record: Record<string, unknown> = {
956
+ 'aria-describedby': describedbyOf(side, first),
957
+ 'aria-invalid': flag(invalidOf(side)),
958
+ 'aria-label': surfaceLabel ? `${unitName}, ${surfaceLabel}` : unitName,
959
+ 'aria-labelledby': surface === 'field' && labelId ? `${segmentId} ${labelId}` : undefined,
960
+ 'aria-readonly': readOnly ? 'true' : undefined,
961
+ 'aria-required': first && required ? 'true' : undefined,
962
+ autocapitalize: 'off',
963
+ autocorrect: 'off',
964
+ 'data-placeholder': flag(!editor.text(unit)),
965
+ 'data-side': side,
966
+ 'data-segment': unit,
967
+ 'data-slot': 'input-date-segment',
968
+ 'data-surface': surface,
969
+ enterkeyhint: 'next',
970
+ id: segmentId,
971
+ role: ios ? 'textbox' : 'spinbutton',
972
+ spellcheck: 'false',
973
+ style: 'caret-color:transparent',
974
+ tabindex: disabled ? undefined : '0',
975
+ }
976
+ if (!ios) {
977
+ // dayPeriod carries no numeric value; empty units announce no valuenow.
978
+ if (unit === 'dayPeriod') {
979
+ record['aria-valuetext'] = filled ? editor.text(unit) : emptyLabel
980
+ } else {
981
+ record['aria-valuemin'] = bounds.min
982
+ record['aria-valuemax'] = bounds.max
983
+ if (filled) record['aria-valuenow'] = editor.units[unit]
984
+ record['aria-valuetext'] = valuetextOf(side, unit)
985
+ }
986
+ }
987
+ // Editability is stamped client-side only: server-rendered contenteditable
988
+ // divs would be freely editable before hydration.
989
+ if (domReady && !disabled && !readOnly) {
990
+ record.contenteditable = 'true'
991
+ if (unit !== 'dayPeriod') record.inputmode = 'numeric'
992
+ }
993
+ return record
994
+ }
995
+
996
+ const segmentText = (side: InputDateSide, segment: Segment, surface: InputDateSurface = 'field'): string => {
997
+ const unit = segment.type as SegmentUnit
998
+ // The focused segment holds its snapshot until compositionend.
999
+ if (composing && composing.side === side && composing.unit === unit && surfaceOf(composing.element) === surface) return composing.text
1000
+ return fields[side].text(unit) || segment.placeholder
1001
+ }
1002
+
1003
+ InputDateContext({
1004
+ allowNonContiguous,
1005
+ applyPreset,
1006
+ calendarDisabled,
1007
+ calendarUnavailable: availabilitySource,
1008
+ clear: clearValue,
1009
+ contentId: pop.contentId,
1010
+ contentStyle: pop.contentStyle,
1011
+ daySelected,
1012
+ disabled,
1013
+ field: side => fields[side],
1014
+ groupAttrs,
1015
+ hasValue: value != null,
1016
+ kind,
1017
+ locale,
1018
+ message,
1019
+ monthOf,
1020
+ onMonthChange,
1021
+ open: pop.open,
1022
+ pickDay,
1023
+ pickRange,
1024
+ popup: popupPart,
1025
+ presets,
1026
+ range: isRange,
1027
+ rangeSelected,
1028
+ readOnly,
1029
+ registerTimeSurface: element => timeSurfaces.add(element),
1030
+ segmentAttrs,
1031
+ segmentText,
1032
+ setContent: pop.setContent,
1033
+ setTrigger: pop.setTrigger,
1034
+ timeSurface,
1035
+ toggleOpen,
1036
+ triggerId: pop.triggerId,
1037
+ unregisterTimeSurface: element => timeSurfaces.delete(element),
1038
+ })
1039
+
1040
+ yield (
1041
+ <>
1042
+ {args.name && !isRange
1043
+ ? <input data-slot="input-date-hidden" disabled={disabled} name={args.name} set:value={fields.from.value() ?? ''} type="hidden" value={fields.from.value() ?? ''} />
1044
+ : null}
1045
+ {args.name && isRange
1046
+ ? <>
1047
+ <input data-slot="input-date-hidden" disabled={disabled} name={`${args.name}[from]`} set:value={fields.from.value() ?? ''} type="hidden" value={fields.from.value() ?? ''} />
1048
+ <input data-slot="input-date-hidden" disabled={disabled} name={`${args.name}[to]`} set:value={fields.to.value() ?? ''} type="hidden" value={fields.to.value() ?? ''} />
1049
+ </>
1050
+ : null}
1051
+ {args.children ?? (
1052
+ <>
1053
+ {isRange ? <><InputDateField side="from" /><InputDateField side="to" /></> : <InputDateField />}
1054
+ {args.calendar && kind !== 'time' ? (
1055
+ <>
1056
+ <InputDateTrigger />
1057
+ <InputDateContent>
1058
+ <InputDateCalendar {...(typeof args.calendar === 'object' ? args.calendar : {})} />
1059
+ {kind === 'datetime'
1060
+ ? isRange
1061
+ ? <><InputDateTimeField side="from" /><InputDateTimeField side="to" /></>
1062
+ : <InputDateTimeField />
1063
+ : null}
1064
+ {presets.length ? <InputDatePresets /> : null}
1065
+ </InputDateContent>
1066
+ </>
1067
+ ) : null}
1068
+ </>
1069
+ )}
1070
+ {descriptionText ? <span data-slot="input-date-description" hidden id={descriptionId}>{descriptionText}</span> : null}
1071
+ {message ? <span data-slot="input-date-message" hidden id={messageId}>{message}</span> : null}
1072
+ </>
1073
+ )
1074
+ }
1075
+ }
1076
+
1077
+
1078
+ /** Segment-based date field; the calendar popover is an optional part. */
1079
+ const InputDate = <Range extends boolean = false>({
1080
+ allowNonContiguous,
1081
+ calendar,
1082
+ children,
1083
+ class: classes,
1084
+ closeOnSelect,
1085
+ defaultOpen,
1086
+ defaultValue,
1087
+ disabled,
1088
+ emptyLabel,
1089
+ errorMessage,
1090
+ gap,
1091
+ locale,
1092
+ max,
1093
+ min,
1094
+ name,
1095
+ onOpenChange,
1096
+ onValueChange,
1097
+ open,
1098
+ placement,
1099
+ placeholderValue,
1100
+ presets,
1101
+ range,
1102
+ readOnly,
1103
+ required,
1104
+ unavailable,
1105
+ value,
1106
+ ...attrs
1107
+ }: InputDateArgs<Range>) => {
1108
+ // In range mode the root is the labelled outer group; the sides label themselves.
1109
+ const fieldCtx = FieldContext()
1110
+
1111
+ return (
1112
+ <InputDateRoot
1113
+ {...rootAttrs(attrs as Record<string, unknown>)}
1114
+ {...(range ? { 'attr:role': 'group', 'attr:aria-describedby': fieldCtx?.groupAttrs['aria-describedby'], 'attr:aria-labelledby': fieldCtx?.groupAttrs['aria-labelledby'] } : {})}
1115
+ allowNonContiguous={allowNonContiguous}
1116
+ calendar={calendar}
1117
+ closeOnSelect={closeOnSelect}
1118
+ defaultOpen={defaultOpen}
1119
+ defaultValue={defaultValue as string | InputDateRangeValue | undefined}
1120
+ disabled={disabled}
1121
+ emptyLabel={emptyLabel}
1122
+ errorMessage={errorMessage}
1123
+ gap={gap}
1124
+ kind="date"
1125
+ locale={locale}
1126
+ max={max}
1127
+ min={min}
1128
+ name={name}
1129
+ onOpenChange={onOpenChange}
1130
+ onValueChange={onValueChange as ((value: PublicValue, event?: Event) => void) | undefined}
1131
+ open={open}
1132
+ placement={placement}
1133
+ placeholderValue={placeholderValue}
1134
+ presets={presets as InputDatePreset[] | undefined}
1135
+ range={range}
1136
+ readOnly={readOnly}
1137
+ required={required}
1138
+ unavailable={unavailable}
1139
+ value={value as PublicValue | undefined}
1140
+ attr:class={classes}
1141
+ attr:data-slot="input-date"
1142
+ >
1143
+ {children}
1144
+ </InputDateRoot>
1145
+ )
1146
+ }
1147
+
1148
+ /** Segment-based time field; canonical 24h values regardless of display cycle. */
1149
+ const InputTime = <Range extends boolean = false>({
1150
+ allowNonContiguous,
1151
+ children,
1152
+ class: classes,
1153
+ defaultValue,
1154
+ disabled,
1155
+ emptyLabel,
1156
+ errorMessage,
1157
+ granularity,
1158
+ hourCycle,
1159
+ locale,
1160
+ max,
1161
+ min,
1162
+ name,
1163
+ onValueChange,
1164
+ placeholderValue,
1165
+ range,
1166
+ readOnly,
1167
+ required,
1168
+ step,
1169
+ unavailable,
1170
+ value,
1171
+ ...attrs
1172
+ }: InputTimeArgs<Range>) => {
1173
+ const fieldCtx = FieldContext()
1174
+
1175
+ return (
1176
+ <InputDateRoot
1177
+ {...rootAttrs(attrs as Record<string, unknown>)}
1178
+ {...(range ? { 'attr:role': 'group', 'attr:aria-describedby': fieldCtx?.groupAttrs['aria-describedby'], 'attr:aria-labelledby': fieldCtx?.groupAttrs['aria-labelledby'] } : {})}
1179
+ allowNonContiguous={allowNonContiguous}
1180
+ defaultValue={defaultValue as string | InputDateRangeValue | undefined}
1181
+ disabled={disabled}
1182
+ emptyLabel={emptyLabel}
1183
+ errorMessage={errorMessage}
1184
+ granularity={granularity}
1185
+ hourCycle={hourCycle}
1186
+ kind="time"
1187
+ locale={locale}
1188
+ max={max}
1189
+ min={min}
1190
+ name={name}
1191
+ onValueChange={onValueChange as ((value: PublicValue, event?: Event) => void) | undefined}
1192
+ placeholderValue={placeholderValue}
1193
+ range={range}
1194
+ readOnly={readOnly}
1195
+ required={required}
1196
+ step={step}
1197
+ unavailable={unavailable}
1198
+ value={value as PublicValue | undefined}
1199
+ attr:class={classes}
1200
+ attr:data-slot="input-time"
1201
+ >
1202
+ {children}
1203
+ </InputDateRoot>
1204
+ )
1205
+ }
1206
+
1207
+ /** Segment-based date-time field; the calendar merges picked days with entered time. */
1208
+ const InputDateTime = <Range extends boolean = false>({
1209
+ allowNonContiguous,
1210
+ calendar,
1211
+ children,
1212
+ class: classes,
1213
+ closeOnSelect,
1214
+ defaultOpen,
1215
+ defaultValue,
1216
+ disabled,
1217
+ emptyLabel,
1218
+ errorMessage,
1219
+ gap,
1220
+ granularity,
1221
+ hourCycle,
1222
+ locale,
1223
+ max,
1224
+ min,
1225
+ name,
1226
+ onOpenChange,
1227
+ onValueChange,
1228
+ open,
1229
+ placement,
1230
+ placeholderValue,
1231
+ presets,
1232
+ range,
1233
+ readOnly,
1234
+ required,
1235
+ step,
1236
+ unavailable,
1237
+ value,
1238
+ ...attrs
1239
+ }: InputDateTimeArgs<Range>) => {
1240
+ const fieldCtx = FieldContext()
1241
+
1242
+ return (
1243
+ <InputDateRoot
1244
+ {...rootAttrs(attrs as Record<string, unknown>)}
1245
+ {...(range ? { 'attr:role': 'group', 'attr:aria-describedby': fieldCtx?.groupAttrs['aria-describedby'], 'attr:aria-labelledby': fieldCtx?.groupAttrs['aria-labelledby'] } : {})}
1246
+ allowNonContiguous={allowNonContiguous}
1247
+ calendar={calendar}
1248
+ closeOnSelect={closeOnSelect}
1249
+ defaultOpen={defaultOpen}
1250
+ defaultValue={defaultValue as string | InputDateRangeValue | undefined}
1251
+ disabled={disabled}
1252
+ emptyLabel={emptyLabel}
1253
+ errorMessage={errorMessage}
1254
+ gap={gap}
1255
+ granularity={granularity}
1256
+ hourCycle={hourCycle}
1257
+ kind="datetime"
1258
+ locale={locale}
1259
+ max={max}
1260
+ min={min}
1261
+ name={name}
1262
+ onOpenChange={onOpenChange}
1263
+ onValueChange={onValueChange as ((value: PublicValue, event?: Event) => void) | undefined}
1264
+ open={open}
1265
+ placement={placement}
1266
+ placeholderValue={placeholderValue}
1267
+ presets={presets as InputDatePreset[] | undefined}
1268
+ range={range}
1269
+ readOnly={readOnly}
1270
+ required={required}
1271
+ step={step}
1272
+ unavailable={unavailable}
1273
+ value={value as PublicValue | undefined}
1274
+ attr:class={classes}
1275
+ attr:data-slot="input-datetime"
1276
+ >
1277
+ {children}
1278
+ </InputDateRoot>
1279
+ )
1280
+ }
1281
+
1282
+ /** Segmented group surface; renders the derived segments and literals closed. */
1283
+ const InputDateField: Stateless<InputDateFieldArgs> = ({
1284
+ class: classes,
1285
+ label,
1286
+ literalClass,
1287
+ ref,
1288
+ segmentClass,
1289
+ side,
1290
+ ...attrs
1291
+ }) => {
1292
+ const ctx = InputDateContext()
1293
+ if (!ctx) throw new Error('InputDateField must be used within an InputDate family root.')
1294
+ if (ctx.range && !side) console.warn('[input-date] <InputDateField side> is required in range mode.')
1295
+ const current: InputDateSide = ctx.range ? side ?? 'from' : 'from'
1296
+ const editor = ctx.field(current)
1297
+
1298
+ return (
1299
+ <div {...attrs} {...ctx.groupAttrs(current, label)} class={classes} ref={ref}>
1300
+ {editor.segments.map((segment, index) => segment.editable ? (
1301
+ // Keyed by unit type: a locale flip reorders through ajo's
1302
+ // focus-preserving keyed path instead of repurposing the focused div.
1303
+ <div key={segment.type} {...ctx.segmentAttrs(current, segment, label)} class={segmentClass}>
1304
+ {ctx.segmentText(current, segment)}
1305
+ </div>
1306
+ ) : (
1307
+ <div aria-hidden="true" class={literalClass} data-slot="input-date-literal" key={`literal-${index}`}>
1308
+ {segment.text}
1309
+ </div>
1310
+ ))}
1311
+ </div>
1312
+ )
1313
+ }
1314
+
1315
+ /** Time run of the bound datetime field, rendered as a second popover surface. */
1316
+ const InputDateTimeField: Stateless<InputDateTimeFieldArgs> = ({
1317
+ class: classes,
1318
+ label,
1319
+ literalClass,
1320
+ ref,
1321
+ segmentClass,
1322
+ side,
1323
+ ...attrs
1324
+ }) => {
1325
+ const ctx = InputDateContext()
1326
+ if (!ctx || !ctx.timeSurface('InputDateTimeField')) return null
1327
+ if (ctx.range && !side) console.warn('[input-date] <InputDateTimeField side> is required in range mode.')
1328
+ const current: InputDateSide = ctx.range ? side ?? 'from' : 'from'
1329
+ const resolvedLabel = label ?? (ctx.range ? current === 'from' ? 'Start time' : 'End time' : 'Time')
1330
+ const editor = ctx.field(current)
1331
+ let registered: HTMLDivElement | null = null
1332
+ const reference = (element: HTMLDivElement | null) => {
1333
+ if (registered && registered !== element) ctx.unregisterTimeSurface(registered)
1334
+ if (element && registered !== element) ctx.registerTimeSurface(element)
1335
+ registered = element
1336
+ callRef(ref, element)
1337
+ }
1338
+
1339
+ return (
1340
+ <div {...attrs} {...ctx.groupAttrs(current, resolvedLabel, 'popover')} class={classes} ref={reference}>
1341
+ {timeRun(editor.segments).map((segment, index) => segment.editable ? (
1342
+ <div key={segment.type} {...ctx.segmentAttrs(current, segment, resolvedLabel, 'popover')} class={segmentClass}>
1343
+ {ctx.segmentText(current, segment, 'popover')}
1344
+ </div>
1345
+ ) : (
1346
+ <div aria-hidden="true" class={literalClass} data-slot="input-date-time-literal" key={`literal-${index}`}>
1347
+ {segment.text}
1348
+ </div>
1349
+ ))}
1350
+ </div>
1351
+ )
1352
+ }
1353
+
1354
+ /** Optional calendar button; the popover still anchors to the field group. */
1355
+ const InputDateTrigger: Stateless<InputDateTriggerArgs> = ({
1356
+ children,
1357
+ class: classes,
1358
+ disabled,
1359
+ iconClass,
1360
+ id: idArg,
1361
+ ref,
1362
+ type = 'button',
1363
+ 'set:onclick': onClick,
1364
+ ...attrs
1365
+ }) => {
1366
+ const ctx = InputDateContext()
1367
+ if (ctx && !ctx.popup('InputDateTrigger')) return null
1368
+ const disabledFlag = Boolean(disabled ?? ctx?.disabled)
1369
+
1370
+ return (
1371
+ <button
1372
+ {...attrs}
1373
+ {...triggerAttrs({
1374
+ controls: ctx?.contentId,
1375
+ expanded: Boolean(ctx?.open),
1376
+ haspopup: 'dialog',
1377
+ id: idArg,
1378
+ open: Boolean(ctx?.open),
1379
+ ref,
1380
+ setTrigger: ctx?.setTrigger,
1381
+ triggerId: ctx?.triggerId,
1382
+ })}
1383
+ aria-label={attrs['aria-label'] ?? 'Show calendar'}
1384
+ class={classes}
1385
+ data-slot="input-date-trigger"
1386
+ disabled={disabledFlag}
1387
+ set:onclick={(event: Event) => {
1388
+ callHandler(onClick, event)
1389
+ if (event.defaultPrevented) return
1390
+ ctx?.toggleOpen(event)
1391
+ }}
1392
+ type={type}
1393
+ >
1394
+ {children ?? <span aria-hidden="true" class={iconClass} data-slot="input-date-trigger-icon" />}
1395
+ </button>
1396
+ )
1397
+ }
1398
+
1399
+ /** Popover panel for the calendar; a dialog anchored to the field group. */
1400
+ const InputDateContent: Stateless<InputDateContentArgs> = ({ children, class: classes, ref, style, ...attrs }) => {
1401
+ const ctx = InputDateContext()
1402
+ if (ctx && !ctx.popup('InputDateContent')) return null
1403
+
1404
+ return (
1405
+ <div
1406
+ {...attrs}
1407
+ {...contentAttrs({
1408
+ id: ctx?.contentId,
1409
+ open: Boolean(ctx?.open),
1410
+ ref,
1411
+ setContent: ctx?.setContent,
1412
+ style: ctx?.contentStyle(style),
1413
+ tabindex: '-1',
1414
+ })}
1415
+ aria-label={attrs['aria-label'] ?? 'Calendar'}
1416
+ class={classes}
1417
+ data-slot="input-date-content"
1418
+ role="dialog"
1419
+ >
1420
+ {children}
1421
+ </div>
1422
+ )
1423
+ }
1424
+
1425
+ /** Calendar wired to the field: picked days fill the date units and commit. */
1426
+ const InputDateCalendar: Stateless<InputDateCalendarArgs> = ({ component, ...attrs }) => {
1427
+ const ctx = InputDateContext()
1428
+ const CurrentCalendar = component ?? Calendar
1429
+ if (!ctx) return <CurrentCalendar {...attrs as CalendarArgs} />
1430
+ if (!ctx.popup('InputDateCalendar')) return null
1431
+
1432
+ const common = {
1433
+ ...attrs,
1434
+ allowNonContiguous: ctx.allowNonContiguous,
1435
+ disabled: ctx.calendarDisabled(attrs.disabled),
1436
+ locale: attrs.locale ?? ctx.locale,
1437
+ month: attrs.month ?? ctx.monthOf(),
1438
+ onMonthChange: (month: Date, event?: Event) => {
1439
+ attrs.onMonthChange?.(month, event)
1440
+ ctx.onMonthChange(month, event)
1441
+ },
1442
+ unavailable: ctx.calendarUnavailable,
1443
+ }
1444
+
1445
+ if (ctx.range) {
1446
+ return (
1447
+ <CurrentCalendar {...{
1448
+ ...common,
1449
+ mode: 'range',
1450
+ selected: ctx.rangeSelected(),
1451
+ onSelect: (next: CalendarDateRange | null, event: Event) => ctx.pickRange(next, event),
1452
+ } as CalendarArgs} />
1453
+ )
1454
+ }
1455
+
1456
+ return (
1457
+ <CurrentCalendar {...{
1458
+ ...common,
1459
+ mode: 'single',
1460
+ selected: ctx.daySelected(),
1461
+ onSelect: (next: Date | null, event: Event) => ctx.pickDay(next, event),
1462
+ } as CalendarArgs} />
1463
+ )
1464
+ }
1465
+
1466
+ /** Clear button; renders only while a value exists and emits null. */
1467
+ const InputDateClear: Stateless<InputDateClearArgs> = ({
1468
+ children,
1469
+ class: classes,
1470
+ disabled,
1471
+ iconClass,
1472
+ type = 'button',
1473
+ 'set:onclick': onClick,
1474
+ ...attrs
1475
+ }) => {
1476
+ const ctx = InputDateContext()
1477
+ if (ctx && !ctx.hasValue) return null
1478
+ const disabledFlag = Boolean(disabled ?? ctx?.disabled)
1479
+
1480
+ return (
1481
+ <button
1482
+ {...attrs}
1483
+ aria-label={attrs['aria-label'] ?? 'Clear'}
1484
+ class={classes}
1485
+ data-slot="input-date-clear"
1486
+ disabled={disabledFlag}
1487
+ set:onclick={(event: Event) => {
1488
+ callHandler(onClick, event)
1489
+ if (event.defaultPrevented) return
1490
+ ctx?.clear(event)
1491
+ }}
1492
+ type={type}
1493
+ >
1494
+ {children ?? <span aria-hidden="true" class={iconClass} data-slot="input-date-clear-icon" />}
1495
+ </button>
1496
+ )
1497
+ }
1498
+
1499
+ /** Preset value buttons for the popover; a pick commits and closes. */
1500
+ const InputDatePresets: Stateless<InputDatePresetsArgs> = ({
1501
+ buttonClass,
1502
+ children,
1503
+ class: classes,
1504
+ ...attrs
1505
+ }) => {
1506
+ const ctx = InputDateContext()
1507
+ if (!ctx || !ctx.popup('InputDatePresets')) return null
1508
+ if (!children && !ctx.presets.length) return null
1509
+
1510
+ return (
1511
+ <div {...attrs} class={classes} data-slot="input-date-presets">
1512
+ {children ?? ctx.presets.map(preset => (
1513
+ <button
1514
+ class={buttonClass}
1515
+ data-slot="input-date-preset"
1516
+ disabled={ctx.disabled}
1517
+ key={preset.label}
1518
+ set:onclick={(event: Event) => ctx.applyPreset(preset.value, event)}
1519
+ type="button"
1520
+ >
1521
+ {preset.label}
1522
+ </button>
1523
+ ))}
1524
+ </div>
1525
+ )
1526
+ }
1527
+
1528
+ export {
1529
+ InputDate,
1530
+ InputDateCalendar,
1531
+ InputDateClear,
1532
+ InputDateContent,
1533
+ InputDateField,
1534
+ InputDatePresets,
1535
+ InputDateTime,
1536
+ InputDateTimeField,
1537
+ InputDateTrigger,
1538
+ InputTime,
1539
+ }