foldkit 0.111.1 → 0.112.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 (222) hide show
  1. package/README.md +2 -2
  2. package/dist/devTools/host.d.ts +6 -0
  3. package/dist/devTools/host.d.ts.map +1 -0
  4. package/dist/devTools/host.js +4 -0
  5. package/dist/dom/dom.d.ts +6 -1
  6. package/dist/dom/dom.d.ts.map +1 -1
  7. package/dist/dom/dom.js +24 -1
  8. package/dist/html/index.d.ts +4 -3
  9. package/dist/html/index.d.ts.map +1 -1
  10. package/dist/html/index.js +4 -3
  11. package/dist/html/submodel.d.ts +2 -2
  12. package/dist/html/submodel.d.ts.map +1 -1
  13. package/dist/index.d.ts +0 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +0 -1
  16. package/dist/runtime/public.d.ts +1 -1
  17. package/dist/runtime/public.d.ts.map +1 -1
  18. package/dist/runtime/runtime.d.ts +14 -1
  19. package/dist/runtime/runtime.d.ts.map +1 -1
  20. package/dist/runtime/runtime.js +7 -4
  21. package/dist/runtime/subscription.d.ts +1 -1
  22. package/dist/subscription/public.d.ts +1 -1
  23. package/dist/subscription/public.d.ts.map +1 -1
  24. package/package.json +12 -93
  25. package/dist/devTools/overlay-styles.d.ts +0 -3
  26. package/dist/devTools/overlay-styles.d.ts.map +0 -1
  27. package/dist/devTools/overlay-styles.js +0 -706
  28. package/dist/devTools/overlay.d.ts +0 -63
  29. package/dist/devTools/overlay.d.ts.map +0 -1
  30. package/dist/devTools/overlay.js +0 -1244
  31. package/dist/test/apps/disabledButton.d.ts +0 -38
  32. package/dist/test/apps/disabledButton.d.ts.map +0 -1
  33. package/dist/test/apps/disabledButton.js +0 -71
  34. package/dist/ui/anchor.d.ts +0 -38
  35. package/dist/ui/anchor.d.ts.map +0 -1
  36. package/dist/ui/anchor.js +0 -142
  37. package/dist/ui/animation/index.d.ts +0 -49
  38. package/dist/ui/animation/index.d.ts.map +0 -1
  39. package/dist/ui/animation/index.js +0 -74
  40. package/dist/ui/animation/public.d.ts +0 -3
  41. package/dist/ui/animation/public.d.ts.map +0 -1
  42. package/dist/ui/animation/public.js +0 -1
  43. package/dist/ui/animation/schema.d.ts +0 -43
  44. package/dist/ui/animation/schema.d.ts.map +0 -1
  45. package/dist/ui/animation/schema.js +0 -41
  46. package/dist/ui/animation/update.d.ts +0 -24
  47. package/dist/ui/animation/update.d.ts.map +0 -1
  48. package/dist/ui/animation/update.js +0 -67
  49. package/dist/ui/button/index.d.ts +0 -17
  50. package/dist/ui/button/index.d.ts.map +0 -1
  51. package/dist/ui/button/index.js +0 -22
  52. package/dist/ui/button/public.d.ts +0 -3
  53. package/dist/ui/button/public.d.ts.map +0 -1
  54. package/dist/ui/button/public.js +0 -1
  55. package/dist/ui/calendar/index.d.ts +0 -462
  56. package/dist/ui/calendar/index.d.ts.map +0 -1
  57. package/dist/ui/calendar/index.js +0 -824
  58. package/dist/ui/calendar/public.d.ts +0 -3
  59. package/dist/ui/calendar/public.d.ts.map +0 -1
  60. package/dist/ui/calendar/public.js +0 -1
  61. package/dist/ui/checkbox/index.d.ts +0 -119
  62. package/dist/ui/checkbox/index.d.ts.map +0 -1
  63. package/dist/ui/checkbox/index.js +0 -110
  64. package/dist/ui/checkbox/public.d.ts +0 -3
  65. package/dist/ui/checkbox/public.d.ts.map +0 -1
  66. package/dist/ui/checkbox/public.js +0 -1
  67. package/dist/ui/combobox/multi.d.ts +0 -184
  68. package/dist/ui/combobox/multi.d.ts.map +0 -1
  69. package/dist/ui/combobox/multi.js +0 -81
  70. package/dist/ui/combobox/multiPublic.d.ts +0 -3
  71. package/dist/ui/combobox/multiPublic.d.ts.map +0 -1
  72. package/dist/ui/combobox/multiPublic.js +0 -1
  73. package/dist/ui/combobox/public.d.ts +0 -7
  74. package/dist/ui/combobox/public.d.ts.map +0 -1
  75. package/dist/ui/combobox/public.js +0 -3
  76. package/dist/ui/combobox/shared.d.ts +0 -422
  77. package/dist/ui/combobox/shared.d.ts.map +0 -1
  78. package/dist/ui/combobox/shared.js +0 -707
  79. package/dist/ui/combobox/single.d.ts +0 -199
  80. package/dist/ui/combobox/single.d.ts.map +0 -1
  81. package/dist/ui/combobox/single.js +0 -106
  82. package/dist/ui/datePicker/index.d.ts +0 -457
  83. package/dist/ui/datePicker/index.d.ts.map +0 -1
  84. package/dist/ui/datePicker/index.js +0 -317
  85. package/dist/ui/datePicker/public.d.ts +0 -3
  86. package/dist/ui/datePicker/public.d.ts.map +0 -1
  87. package/dist/ui/datePicker/public.js +0 -1
  88. package/dist/ui/dialog/index.d.ts +0 -155
  89. package/dist/ui/dialog/index.d.ts.map +0 -1
  90. package/dist/ui/dialog/index.js +0 -208
  91. package/dist/ui/dialog/public.d.ts +0 -3
  92. package/dist/ui/dialog/public.d.ts.map +0 -1
  93. package/dist/ui/dialog/public.js +0 -1
  94. package/dist/ui/disclosure/index.d.ts +0 -110
  95. package/dist/ui/disclosure/index.d.ts.map +0 -1
  96. package/dist/ui/disclosure/index.js +0 -110
  97. package/dist/ui/disclosure/public.d.ts +0 -3
  98. package/dist/ui/disclosure/public.d.ts.map +0 -1
  99. package/dist/ui/disclosure/public.js +0 -1
  100. package/dist/ui/dragAndDrop/index.d.ts +0 -540
  101. package/dist/ui/dragAndDrop/index.d.ts.map +0 -1
  102. package/dist/ui/dragAndDrop/index.js +0 -535
  103. package/dist/ui/dragAndDrop/public.d.ts +0 -3
  104. package/dist/ui/dragAndDrop/public.d.ts.map +0 -1
  105. package/dist/ui/dragAndDrop/public.js +0 -1
  106. package/dist/ui/fieldset/index.d.ts +0 -21
  107. package/dist/ui/fieldset/index.d.ts.map +0 -1
  108. package/dist/ui/fieldset/index.js +0 -25
  109. package/dist/ui/fieldset/public.d.ts +0 -3
  110. package/dist/ui/fieldset/public.d.ts.map +0 -1
  111. package/dist/ui/fieldset/public.js +0 -1
  112. package/dist/ui/fileDrop/index.d.ts +0 -109
  113. package/dist/ui/fileDrop/index.d.ts.map +0 -1
  114. package/dist/ui/fileDrop/index.js +0 -126
  115. package/dist/ui/fileDrop/public.d.ts +0 -3
  116. package/dist/ui/fileDrop/public.d.ts.map +0 -1
  117. package/dist/ui/fileDrop/public.js +0 -1
  118. package/dist/ui/group.d.ts +0 -8
  119. package/dist/ui/group.d.ts.map +0 -1
  120. package/dist/ui/group.js +0 -13
  121. package/dist/ui/index.d.ts +0 -25
  122. package/dist/ui/index.d.ts.map +0 -1
  123. package/dist/ui/index.js +0 -24
  124. package/dist/ui/input/index.d.ts +0 -26
  125. package/dist/ui/input/index.d.ts.map +0 -1
  126. package/dist/ui/input/index.js +0 -43
  127. package/dist/ui/input/public.d.ts +0 -3
  128. package/dist/ui/input/public.d.ts.map +0 -1
  129. package/dist/ui/input/public.js +0 -1
  130. package/dist/ui/keyboard.d.ts +0 -6
  131. package/dist/ui/keyboard.d.ts.map +0 -1
  132. package/dist/ui/keyboard.js +0 -9
  133. package/dist/ui/listbox/multi.d.ts +0 -190
  134. package/dist/ui/listbox/multi.d.ts.map +0 -1
  135. package/dist/ui/listbox/multi.js +0 -65
  136. package/dist/ui/listbox/multiPublic.d.ts +0 -3
  137. package/dist/ui/listbox/multiPublic.d.ts.map +0 -1
  138. package/dist/ui/listbox/multiPublic.js +0 -1
  139. package/dist/ui/listbox/public.d.ts +0 -7
  140. package/dist/ui/listbox/public.d.ts.map +0 -1
  141. package/dist/ui/listbox/public.js +0 -3
  142. package/dist/ui/listbox/shared.d.ts +0 -431
  143. package/dist/ui/listbox/shared.d.ts.map +0 -1
  144. package/dist/ui/listbox/shared.js +0 -669
  145. package/dist/ui/listbox/single.d.ts +0 -208
  146. package/dist/ui/listbox/single.d.ts.map +0 -1
  147. package/dist/ui/listbox/single.js +0 -73
  148. package/dist/ui/menu/index.d.ts +0 -367
  149. package/dist/ui/menu/index.d.ts.map +0 -1
  150. package/dist/ui/menu/index.js +0 -681
  151. package/dist/ui/menu/public.d.ts +0 -4
  152. package/dist/ui/menu/public.d.ts.map +0 -1
  153. package/dist/ui/menu/public.js +0 -1
  154. package/dist/ui/popover/index.d.ts +0 -267
  155. package/dist/ui/popover/index.d.ts.map +0 -1
  156. package/dist/ui/popover/index.js +0 -345
  157. package/dist/ui/popover/public.d.ts +0 -4
  158. package/dist/ui/popover/public.d.ts.map +0 -1
  159. package/dist/ui/popover/public.js +0 -1
  160. package/dist/ui/radioGroup/index.d.ts +0 -169
  161. package/dist/ui/radioGroup/index.d.ts.map +0 -1
  162. package/dist/ui/radioGroup/index.js +0 -196
  163. package/dist/ui/radioGroup/public.d.ts +0 -3
  164. package/dist/ui/radioGroup/public.d.ts.map +0 -1
  165. package/dist/ui/radioGroup/public.js +0 -1
  166. package/dist/ui/select/index.d.ts +0 -24
  167. package/dist/ui/select/index.d.ts.map +0 -1
  168. package/dist/ui/select/index.js +0 -40
  169. package/dist/ui/select/public.d.ts +0 -3
  170. package/dist/ui/select/public.d.ts.map +0 -1
  171. package/dist/ui/select/public.js +0 -1
  172. package/dist/ui/slider/index.d.ts +0 -318
  173. package/dist/ui/slider/index.d.ts.map +0 -1
  174. package/dist/ui/slider/index.js +0 -336
  175. package/dist/ui/slider/public.d.ts +0 -3
  176. package/dist/ui/slider/public.d.ts.map +0 -1
  177. package/dist/ui/slider/public.js +0 -1
  178. package/dist/ui/switch/index.d.ts +0 -99
  179. package/dist/ui/switch/index.d.ts.map +0 -1
  180. package/dist/ui/switch/index.js +0 -106
  181. package/dist/ui/switch/public.d.ts +0 -3
  182. package/dist/ui/switch/public.d.ts.map +0 -1
  183. package/dist/ui/switch/public.js +0 -1
  184. package/dist/ui/tabs/index.d.ts +0 -155
  185. package/dist/ui/tabs/index.d.ts.map +0 -1
  186. package/dist/ui/tabs/index.js +0 -184
  187. package/dist/ui/tabs/public.d.ts +0 -3
  188. package/dist/ui/tabs/public.d.ts.map +0 -1
  189. package/dist/ui/tabs/public.js +0 -1
  190. package/dist/ui/textarea/index.d.ts +0 -26
  191. package/dist/ui/textarea/index.d.ts.map +0 -1
  192. package/dist/ui/textarea/index.js +0 -44
  193. package/dist/ui/textarea/public.d.ts +0 -3
  194. package/dist/ui/textarea/public.d.ts.map +0 -1
  195. package/dist/ui/textarea/public.js +0 -1
  196. package/dist/ui/toast/index.d.ts +0 -608
  197. package/dist/ui/toast/index.d.ts.map +0 -1
  198. package/dist/ui/toast/index.js +0 -145
  199. package/dist/ui/toast/public.d.ts +0 -4
  200. package/dist/ui/toast/public.d.ts.map +0 -1
  201. package/dist/ui/toast/public.js +0 -1
  202. package/dist/ui/toast/schema.d.ts +0 -154
  203. package/dist/ui/toast/schema.d.ts.map +0 -1
  204. package/dist/ui/toast/schema.js +0 -93
  205. package/dist/ui/toast/update.d.ts +0 -510
  206. package/dist/ui/toast/update.d.ts.map +0 -1
  207. package/dist/ui/toast/update.js +0 -225
  208. package/dist/ui/tooltip/index.d.ts +0 -170
  209. package/dist/ui/tooltip/index.d.ts.map +0 -1
  210. package/dist/ui/tooltip/index.js +0 -252
  211. package/dist/ui/tooltip/public.d.ts +0 -4
  212. package/dist/ui/tooltip/public.d.ts.map +0 -1
  213. package/dist/ui/tooltip/public.js +0 -1
  214. package/dist/ui/typeahead.d.ts +0 -4
  215. package/dist/ui/typeahead.d.ts.map +0 -1
  216. package/dist/ui/typeahead.js +0 -14
  217. package/dist/ui/virtualList/index.d.ts +0 -202
  218. package/dist/ui/virtualList/index.d.ts.map +0 -1
  219. package/dist/ui/virtualList/index.js +0 -391
  220. package/dist/ui/virtualList/public.d.ts +0 -3
  221. package/dist/ui/virtualList/public.d.ts.map +0 -1
  222. package/dist/ui/virtualList/public.js +0 -1
@@ -1,824 +0,0 @@
1
- import { Array, Effect, Function, Match as M, Number, Option, Schema as S, pipe, } from 'effect';
2
- import * as Calendar from '../../calendar/index.js';
3
- import * as Command from '../../command/index.js';
4
- import * as Dom from '../../dom/index.js';
5
- import { OptionExt } from '../../effectExtensions/index.js';
6
- import { childAttributes, defineView, html, } from '../../html/index.js';
7
- import { m } from '../../message/index.js';
8
- import { evo } from '../../struct/index.js';
9
- // MODEL
10
- /** Which grid the calendar is currently displaying. `Days` is the standard
11
- * 6×7 day grid; `Months` is a 3×4 month-name grid for fast month jumps;
12
- * `Years` is a 3×4 year grid paged in 12-year windows for fast year jumps. */
13
- export const ViewMode = S.Literals(['Days', 'Months', 'Years']);
14
- /** Schema for the calendar component's state. Tracks the visible month/year,
15
- * the keyboard-focused and user-selected dates, the active view mode, and
16
- * the configuration that governs navigation (locale, min/max, disabled
17
- * days). */
18
- export const Model = S.Struct({
19
- id: S.String,
20
- today: Calendar.CalendarDate,
21
- viewYear: S.Int,
22
- viewMonth: S.Int.check(S.isBetween({ minimum: 1, maximum: 12 })),
23
- viewMode: ViewMode,
24
- maybeFocusedDate: S.Option(Calendar.CalendarDate),
25
- maybeSelectedDate: S.Option(Calendar.CalendarDate),
26
- isGridFocused: S.Boolean,
27
- locale: Calendar.LocaleConfig,
28
- maybeMinDate: S.Option(Calendar.CalendarDate),
29
- maybeMaxDate: S.Option(Calendar.CalendarDate),
30
- disabledDaysOfWeek: S.Array(Calendar.DayOfWeek),
31
- disabledDates: S.Array(Calendar.CalendarDate),
32
- });
33
- // MESSAGE
34
- /** Sent when the user clicks a day cell in the grid. */
35
- export const ClickedDay = m('ClickedDay', { date: Calendar.CalendarDate });
36
- /** Sent when the user presses a key on the grid container. The update maps
37
- * the key to a navigation or selection action. */
38
- export const PressedKeyOnGrid = m('PressedKeyOnGrid', {
39
- key: S.String,
40
- isShift: S.Boolean,
41
- });
42
- /** Sent when the user clicks the previous-month navigation button in Days
43
- * mode. (The Years mode prev/next-page buttons dispatch `PagedYears`.) */
44
- export const ClickedPreviousMonthButton = m('ClickedPreviousMonthButton');
45
- /** Sent when the user clicks the next-month navigation button in Days
46
- * mode. (The Years mode prev/next-page buttons dispatch `PagedYears`.) */
47
- export const ClickedNextMonthButton = m('ClickedNextMonthButton');
48
- /** Sent when the user clicks the calendar heading. Zooms out one mode
49
- * level: Days → Months, Months → Years. Terminal in Years mode. */
50
- export const ClickedHeading = m('ClickedHeading');
51
- /** Sent when the user picks a month from the months grid. Jumps the view
52
- * to that month and returns the calendar to Days mode. */
53
- export const SelectedMonth = m('SelectedMonth', { month: S.Int });
54
- /** Sent when the user picks a year from the years grid. Jumps the view to
55
- * that year and transitions the calendar to Months mode for further drilling. */
56
- export const SelectedYear = m('SelectedYear', { year: S.Int });
57
- /** Sent when the user pages the years grid forward or backward by one
58
- * window. Direction is `1` for next, `-1` for previous. */
59
- export const PagedYears = m('PagedYears', {
60
- direction: S.Literals([1, -1]),
61
- });
62
- /** Sent when the grid container receives DOM focus. */
63
- export const FocusedGrid = m('FocusedGrid');
64
- /** Sent when the grid container loses DOM focus. */
65
- export const BlurredGrid = m('BlurredGrid');
66
- /** Sent when a long-lived session's "today" reference should be refreshed. */
67
- export const RefreshedToday = m('RefreshedToday', {
68
- today: Calendar.CalendarDate,
69
- });
70
- /** Sent when a FocusGrid command completes. */
71
- export const CompletedFocusGrid = m('CompletedFocusGrid');
72
- /** Union of all messages the calendar component can produce. */
73
- export const Message = S.Union([
74
- ClickedDay,
75
- PressedKeyOnGrid,
76
- ClickedPreviousMonthButton,
77
- ClickedNextMonthButton,
78
- ClickedHeading,
79
- SelectedMonth,
80
- SelectedYear,
81
- PagedYears,
82
- FocusedGrid,
83
- BlurredGrid,
84
- RefreshedToday,
85
- CompletedFocusGrid,
86
- ]);
87
- // OUT MESSAGE
88
- /** Emitted when the visible month changes due to navigation. Consumers of an
89
- * inline calendar may use this to load month-scoped data (holidays, events).
90
- * A click that commits a date in a different month emits `SelectedDate`, not
91
- * `ChangedViewMonth`. The parent infers the month change from the date. */
92
- export const ChangedViewMonth = m('ChangedViewMonth', {
93
- year: S.Int,
94
- month: S.Int,
95
- });
96
- /** Emitted when the user commits a date selection via click or keyboard. The
97
- * calendar's internal state already reflects the new selection by the time
98
- * this fires; consumers react by lifting the date into their domain state
99
- * (closing a popover, advancing a form step, etc.). */
100
- export const SelectedDate = m('SelectedDate', {
101
- date: Calendar.CalendarDate,
102
- });
103
- /** Union of the calendar's OutMessages. */
104
- export const OutMessage = S.Union([ChangedViewMonth, SelectedDate]);
105
- /** Creates an initial calendar model. The view month defaults to the month
106
- * of the initial selected date, or today if no date is pre-selected. */
107
- export const init = (config) => {
108
- const maybeInitialSelectedDate = Option.fromNullishOr(config.initialSelectedDate);
109
- const initialFocus = Option.getOrElse(maybeInitialSelectedDate, () => config.today);
110
- return {
111
- id: config.id,
112
- today: config.today,
113
- viewYear: initialFocus.year,
114
- viewMonth: initialFocus.month,
115
- viewMode: 'Days',
116
- maybeFocusedDate: Option.some(initialFocus),
117
- maybeSelectedDate: maybeInitialSelectedDate,
118
- isGridFocused: false,
119
- locale: config.locale ?? Calendar.defaultEnglishLocale,
120
- maybeMinDate: Option.fromNullishOr(config.minDate),
121
- maybeMaxDate: Option.fromNullishOr(config.maxDate),
122
- disabledDaysOfWeek: config.disabledDaysOfWeek ?? [],
123
- disabledDates: config.disabledDates ?? [],
124
- };
125
- };
126
- const withUpdateReturn = M.withReturnType();
127
- const gridId = (modelId) => `${modelId}-grid`;
128
- const gridSelector = (modelId) => `#${gridId(modelId)}`;
129
- /** Focuses the calendar grid container. Parent components like DatePicker
130
- * dispatch this after opening to hand focus to the grid's keyboard layer. */
131
- export const FocusGrid = Command.define('FocusGrid', { id: S.String }, CompletedFocusGrid)(({ id }) => Dom.focus(gridSelector(id)).pipe(Effect.ignore, Effect.as(CompletedFocusGrid())));
132
- /** Programmatically selects a date on the calendar, committing it as the
133
- * chosen value and moving the cursor onto it. Use this in controlled-mode
134
- * handlers (when the view's `onSelectedDate` callback is provided) to write
135
- * the selection back to the calendar's internal state.
136
- *
137
- * Equivalent to dispatching `ClickedDay({ date })` through `update`. */
138
- export const selectDate = (model, date) => update(model, ClickedDay({ date }));
139
- /** Reflects an externally-sourced selected date onto the model without
140
- * emitting an OutMessage. When a date is given, sets the selection and
141
- * moves the view to the date's month so it stays visible, mirroring
142
- * `selectDate`'s state change minus the `SelectedDate` announcement. Pass
143
- * `Option.none()` to clear the selection (the view is left where it is).
144
- * Use this to mirror external truth (a URL parameter, a saved draft) onto
145
- * the calendar. Contrast with `selectDate`, a user or programmatic
146
- * *choice* that emits `SelectedDate`. Returns the model directly because
147
- * it produces no commands and no OutMessage. */
148
- export const reflectSelectedDate = Function.dual(2, (model, maybeDate) => Option.match(maybeDate, {
149
- onNone: () => evo(model, { maybeSelectedDate: () => Option.none() }),
150
- onSome: date => evo(model, {
151
- maybeSelectedDate: () => Option.some(date),
152
- maybeFocusedDate: () => Option.some(date),
153
- viewYear: () => date.year,
154
- viewMonth: () => date.month,
155
- }),
156
- }));
157
- /** Reflects the minimum selectable date onto the model. Pass `Option.none()`
158
- * to remove the minimum. Use this when the minimum derives from other Model
159
- * state (e.g. a start date field whose current selection constrains an end
160
- * date picker).
161
- *
162
- * Does NOT reconcile the current selection. If a previously-selected date
163
- * is now below the new minimum, it remains selected. Callers should clear or
164
- * reassign the selection explicitly if their domain requires it. */
165
- export const reflectMinDate = Function.dual(2, (model, maybeMinDate) => evo(model, { maybeMinDate: () => maybeMinDate }));
166
- /** Reflects the maximum selectable date onto the model. Pass `Option.none()`
167
- * to remove the maximum. Does NOT reconcile the current selection. */
168
- export const reflectMaxDate = Function.dual(2, (model, maybeMaxDate) => evo(model, { maybeMaxDate: () => maybeMaxDate }));
169
- /** Reflects the list of individually-disabled dates onto the model. Pass an
170
- * empty array to clear. Does NOT reconcile the current selection. */
171
- export const reflectDisabledDates = Function.dual(2, (model, disabledDates) => evo(model, { disabledDates: () => disabledDates }));
172
- /** Reflects the days of the week that are disabled (e.g. weekends) onto the
173
- * model. Pass an empty array to clear. Does NOT reconcile the current
174
- * selection. */
175
- export const reflectDisabledDaysOfWeek = Function.dual(2, (model, disabledDaysOfWeek) => evo(model, { disabledDaysOfWeek: () => disabledDaysOfWeek }));
176
- /** Returns the calendar to Days mode regardless of current depth. Useful for
177
- * standalone (non-popovered) consumers that want to wire their own back-out
178
- * gesture. Popovered consumers like `Ui.DatePicker` don't need this. Escape
179
- * closes the popover, and the calendar resets to Days on next open.
180
- *
181
- * Reconciles `maybeFocusedDate` to a date inside the visible (`viewYear`,
182
- * `viewMonth`). Months/Years navigation can leave the cursor on a date
183
- * outside the days grid (paged-away year, etc.), which would otherwise
184
- * cause `aria-activedescendant` to point at a non-rendered cell and the
185
- * next ArrowLeft to jump to the cursor's stale year. */
186
- export const dropToDays = (model) => {
187
- const focusedDay = Option.match(model.maybeFocusedDate, {
188
- onNone: () => 1,
189
- onSome: date => Math.min(date.day, Calendar.daysInMonth(model.viewYear, model.viewMonth)),
190
- });
191
- return evo(model, {
192
- viewMode: () => 'Days',
193
- maybeFocusedDate: () => Option.some(Calendar.make(model.viewYear, model.viewMonth, focusedDay)),
194
- });
195
- };
196
- const DAY_SKIP_CAP = 31;
197
- const MONTH_SKIP_CAP = 12;
198
- /** Number of years per Years-mode page. A 3×4 grid renders one window. */
199
- const YEARS_PAGE_SIZE = 12;
200
- const isDateDisabled = (model, date) => Option.exists(model.maybeMinDate, min => Calendar.isBefore(date, min)) ||
201
- Option.exists(model.maybeMaxDate, max => Calendar.isAfter(date, max)) ||
202
- model.disabledDaysOfWeek.includes(Calendar.dayOfWeek(date)) ||
203
- model.disabledDates.some(Calendar.isEqual(date));
204
- /** Walks from `start` in `direction`, returning the first non-disabled date
205
- * within `cap` steps. Falls back to `start` if every candidate is disabled. */
206
- const skipDisabled = (model, start, direction, cap) => pipe(cap, Array.makeBy(step => Calendar.addDays(start, step * direction)), Array.findFirst(date => !isDateDisabled(model, date)), Option.getOrElse(() => start));
207
- const clampToRange = (model, candidate) => {
208
- const afterMin = Option.match(model.maybeMinDate, {
209
- onNone: () => candidate,
210
- onSome: min => Calendar.max(candidate, min),
211
- });
212
- return Option.match(model.maybeMaxDate, {
213
- onNone: () => afterMin,
214
- onSome: max => Calendar.min(afterMin, max),
215
- });
216
- };
217
- /** Resolves a navigation key press to the next focused date candidate,
218
- * along with the direction and search cap for disabled-date skipping. */
219
- const resolveNavigationKey = (key, isShift, focused, firstDayOfWeek) => M.value(key).pipe(M.withReturnType(), M.when('ArrowLeft', () => [
220
- Calendar.addDays(focused, -1),
221
- -1,
222
- DAY_SKIP_CAP,
223
- ]), M.when('ArrowRight', () => [Calendar.addDays(focused, 1), 1, DAY_SKIP_CAP]), M.when('ArrowUp', () => [Calendar.addDays(focused, -7), -1, DAY_SKIP_CAP]), M.when('ArrowDown', () => [Calendar.addDays(focused, 7), 1, DAY_SKIP_CAP]), M.when('Home', () => [
224
- Calendar.startOfWeek(focused, firstDayOfWeek),
225
- -1,
226
- DAY_SKIP_CAP,
227
- ]), M.when('End', () => [
228
- Calendar.endOfWeek(focused, firstDayOfWeek),
229
- 1,
230
- DAY_SKIP_CAP,
231
- ]), M.when('PageUp', () => [
232
- isShift
233
- ? Calendar.addYears(focused, -1)
234
- : Calendar.addMonths(focused, -1),
235
- -1,
236
- MONTH_SKIP_CAP,
237
- ]), M.when('PageDown', () => [
238
- isShift ? Calendar.addYears(focused, 1) : Calendar.addMonths(focused, 1),
239
- 1,
240
- MONTH_SKIP_CAP,
241
- ]), M.option);
242
- const isCommitKey = (key) => key === 'Enter' || key === ' ';
243
- const currentOrFallbackFocus = (model) => Option.getOrElse(model.maybeFocusedDate, () => Calendar.make(model.viewYear, model.viewMonth, 1));
244
- /** Applies a date selection to the model: commits the selection, moves the
245
- * cursor onto the date, and syncs the view month if the selection crosses a
246
- * month boundary. Always emits `SelectedDate` carrying the committed date;
247
- * the parent infers month transitions from the date itself rather than from
248
- * a separate `ChangedViewMonth` signal that would race with the selection. */
249
- const commitSelection = (model, date) => {
250
- const nextModel = evo(model, {
251
- maybeSelectedDate: () => Option.some(date),
252
- maybeFocusedDate: () => Option.some(date),
253
- viewYear: () => date.year,
254
- viewMonth: () => date.month,
255
- });
256
- return [nextModel, Option.some(SelectedDate({ date }))];
257
- };
258
- /** Applies a focus move to the model, clamping to the allowed range and
259
- * skipping disabled dates. Emits `ChangedViewMonth` if the move crossed a
260
- * month boundary. */
261
- const applyFocusMove = (model, candidate, direction, cap) => {
262
- const clamped = clampToRange(model, candidate);
263
- const nextFocus = skipDisabled(model, clamped, direction, cap);
264
- const crossedMonth = nextFocus.year !== model.viewYear || nextFocus.month !== model.viewMonth;
265
- const nextModel = evo(model, {
266
- maybeFocusedDate: () => Option.some(nextFocus),
267
- viewYear: () => nextFocus.year,
268
- viewMonth: () => nextFocus.month,
269
- });
270
- const maybeOutMessage = OptionExt.when(crossedMonth, ChangedViewMonth({ year: nextFocus.year, month: nextFocus.month }));
271
- return [nextModel, maybeOutMessage];
272
- };
273
- /** Computes the focused-date cursor for a view-month change. Preserves the
274
- * current day-of-month (clamping to the new month's length when needed),
275
- * then runs the candidate through min/max clamping and disabled-date skipping
276
- * so the cursor always lands on a real, navigable cell. */
277
- const moveFocusForViewChange = (model, year, month, direction) => {
278
- const currentDay = Option.match(model.maybeFocusedDate, {
279
- onNone: () => 1,
280
- onSome: focused => focused.day,
281
- });
282
- const dayInNewMonth = Math.min(currentDay, Calendar.daysInMonth(year, month));
283
- const candidate = Calendar.make(year, month, dayInNewMonth);
284
- const clamped = clampToRange(model, candidate);
285
- return skipDisabled(model, clamped, direction, DAY_SKIP_CAP);
286
- };
287
- const applyViewMonthChange = (model, year, month, direction) => {
288
- if (year === model.viewYear && month === model.viewMonth) {
289
- return [model, [], Option.none()];
290
- }
291
- const nextFocus = moveFocusForViewChange(model, year, month, direction);
292
- const nextModel = evo(model, {
293
- viewYear: () => year,
294
- viewMonth: () => month,
295
- maybeFocusedDate: () => Option.some(nextFocus),
296
- });
297
- return [nextModel, [], Option.some(ChangedViewMonth({ year, month }))];
298
- };
299
- /** Direction the user moved when jumping to a new view year/month via grid
300
- * selection. Used by `skipDisabled` so a forward jump skips forward through
301
- * disabled dates and a backward jump skips backward. */
302
- const jumpDirection = (model, year, month) => {
303
- const next = Calendar.make(year, month, 1);
304
- const current = Calendar.make(model.viewYear, model.viewMonth, 1);
305
- return Calendar.isAfter(next, current) ? 1 : -1;
306
- };
307
- /** Maps a keyboard key to a months-grid focus shift (in months). Months
308
- * mode supports horizontal (±1), vertical (±row width), and PageUp/Down
309
- * (±12) navigation. */
310
- const resolveMonthsKey = (key) => M.value(key).pipe(M.withReturnType(), M.when('ArrowLeft', () => -1), M.when('ArrowRight', () => 1), M.when('ArrowUp', () => -MONTHS_GRID_COLUMNS), M.when('ArrowDown', () => MONTHS_GRID_COLUMNS), M.when('PageUp', () => -MONTHS_IN_YEAR), M.when('PageDown', () => MONTHS_IN_YEAR), M.option);
311
- /** Maps a keyboard key to a years-grid focus shift (in years). Years mode
312
- * supports horizontal (±1), vertical (±row width), and PageUp/Down (±12 =
313
- * one window) navigation. */
314
- const resolveYearsKey = (key) => M.value(key).pipe(M.withReturnType(), M.when('ArrowLeft', () => -1), M.when('ArrowRight', () => 1), M.when('ArrowUp', () => -YEARS_GRID_COLUMNS), M.when('ArrowDown', () => YEARS_GRID_COLUMNS), M.when('PageUp', () => -YEARS_PAGE_SIZE), M.when('PageDown', () => YEARS_PAGE_SIZE), M.option);
315
- /** Applies a months-grid focus shift, updating `maybeFocusedDate` and
316
- * `viewYear` to reflect the new focused date. `viewMonth` is preserved.
317
- * Months mode keyboard navigation moves the cursor without committing. */
318
- const applyMonthsFocusShift = (model, monthShift) => {
319
- const focused = currentOrFallbackFocus(model);
320
- const nextFocus = Calendar.addMonths(focused, monthShift);
321
- return [
322
- evo(model, {
323
- maybeFocusedDate: () => Option.some(nextFocus),
324
- viewYear: () => nextFocus.year,
325
- }),
326
- [],
327
- Option.none(),
328
- ];
329
- };
330
- /** Applies a years-grid focus shift, updating only `maybeFocusedDate`.
331
- * `viewYear` is preserved so the "selected" highlight (`year === viewYear`)
332
- * stays on the calendar's centered year while the cursor moves freely. The
333
- * visible 12-year page is derived from the cursor in the view layer. */
334
- const applyYearsFocusShift = (model, yearShift) => {
335
- const focused = currentOrFallbackFocus(model);
336
- const nextFocus = Calendar.addYears(focused, yearShift);
337
- return [
338
- evo(model, {
339
- maybeFocusedDate: () => Option.some(nextFocus),
340
- }),
341
- [],
342
- Option.none(),
343
- ];
344
- };
345
- /** Processes a calendar message and returns the next model, commands, and
346
- * optional OutMessage. */
347
- export const update = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
348
- ClickedDay: ({ date }) => {
349
- if (isDateDisabled(model, date)) {
350
- return [model, [], Option.none()];
351
- }
352
- else {
353
- const [nextModel, maybeOutMessage] = commitSelection(model, date);
354
- return [nextModel, [], maybeOutMessage];
355
- }
356
- },
357
- PressedKeyOnGrid: ({ key, isShift }) => M.value(model.viewMode).pipe(withUpdateReturn, M.when('Days', () => {
358
- const focused = currentOrFallbackFocus(model);
359
- if (isCommitKey(key)) {
360
- if (isDateDisabled(model, focused)) {
361
- return [model, [], Option.none()];
362
- }
363
- else {
364
- const [nextModel, maybeOutMessage] = commitSelection(model, focused);
365
- return [nextModel, [], maybeOutMessage];
366
- }
367
- }
368
- else {
369
- return Option.match(resolveNavigationKey(key, isShift, focused, model.locale.firstDayOfWeek), {
370
- onNone: () => [model, [], Option.none()],
371
- onSome: ([candidate, direction, cap]) => {
372
- const [nextModel, maybeOutMessage] = applyFocusMove(model, candidate, direction, cap);
373
- return [nextModel, [], maybeOutMessage];
374
- },
375
- });
376
- }
377
- }), M.when('Months', () => Option.match(resolveMonthsKey(key), {
378
- onNone: () => [model, [], Option.none()],
379
- onSome: shift => applyMonthsFocusShift(model, shift),
380
- })), M.when('Years', () => Option.match(resolveYearsKey(key), {
381
- onNone: () => [model, [], Option.none()],
382
- onSome: shift => applyYearsFocusShift(model, shift),
383
- })), M.exhaustive),
384
- ClickedPreviousMonthButton: () => {
385
- const next = Calendar.subtractMonths(Calendar.make(model.viewYear, model.viewMonth, 1), 1);
386
- return applyViewMonthChange(model, next.year, next.month, -1);
387
- },
388
- ClickedNextMonthButton: () => {
389
- const next = Calendar.addMonths(Calendar.make(model.viewYear, model.viewMonth, 1), 1);
390
- return applyViewMonthChange(model, next.year, next.month, 1);
391
- },
392
- ClickedHeading: () => M.value(model.viewMode).pipe(withUpdateReturn, M.when('Days', () => [
393
- evo(model, { viewMode: () => 'Months' }),
394
- [FocusGrid({ id: model.id })],
395
- Option.none(),
396
- ]), M.when('Months', () => [
397
- evo(model, { viewMode: () => 'Years' }),
398
- [FocusGrid({ id: model.id })],
399
- Option.none(),
400
- ]), M.when('Years', () => [model, [], Option.none()]), M.exhaustive),
401
- SelectedMonth: ({ month }) => {
402
- if (isMonthDisabled(model, model.viewYear, month)) {
403
- return [model, [], Option.none()];
404
- }
405
- else {
406
- const [nextModel, commands, maybeOutMessage] = applyViewMonthChange(model, model.viewYear, month, jumpDirection(model, model.viewYear, month));
407
- return [
408
- evo(nextModel, { viewMode: () => 'Days' }),
409
- [...commands, FocusGrid({ id: model.id })],
410
- maybeOutMessage,
411
- ];
412
- }
413
- },
414
- SelectedYear: ({ year }) => {
415
- if (isYearDisabled(model, year)) {
416
- return [model, [], Option.none()];
417
- }
418
- else {
419
- const [nextModel, commands, maybeOutMessage] = applyViewMonthChange(model, year, model.viewMonth, jumpDirection(model, year, model.viewMonth));
420
- return [
421
- evo(nextModel, { viewMode: () => 'Months' }),
422
- [...commands, FocusGrid({ id: model.id })],
423
- maybeOutMessage,
424
- ];
425
- }
426
- },
427
- PagedYears: ({ direction }) => applyYearsFocusShift(model, direction * YEARS_PAGE_SIZE),
428
- FocusedGrid: () => [
429
- evo(model, { isGridFocused: () => true }),
430
- [],
431
- Option.none(),
432
- ],
433
- BlurredGrid: () => [
434
- evo(model, { isGridFocused: () => false }),
435
- [],
436
- Option.none(),
437
- ],
438
- RefreshedToday: ({ today }) => [
439
- evo(model, { today: () => today }),
440
- [],
441
- Option.none(),
442
- ],
443
- CompletedFocusGrid: () => [model, [], Option.none()],
444
- }));
445
- // VIEW
446
- const headingId = (modelId) => `${modelId}-heading`;
447
- const dayCellId = (modelId, date) => `${modelId}-cell-${date.year}-${date.month}-${date.day}`;
448
- const monthCellId = (modelId, month) => `${modelId}-cell-month-${month}`;
449
- const yearCellId = (modelId, year) => `${modelId}-cell-year-${year}`;
450
- const DAY_NAMES_SUNDAY_FIRST = [
451
- 'Sunday',
452
- 'Monday',
453
- 'Tuesday',
454
- 'Wednesday',
455
- 'Thursday',
456
- 'Friday',
457
- 'Saturday',
458
- ];
459
- const DAY_OF_WEEK_INDEX = {
460
- Sunday: 0,
461
- Monday: 1,
462
- Tuesday: 2,
463
- Wednesday: 3,
464
- Thursday: 4,
465
- Friday: 5,
466
- Saturday: 6,
467
- };
468
- /** Rotates the Sunday-first day-name array so that `firstDayOfWeek` becomes
469
- * the first entry. Used to build column headers in locale-appropriate order. */
470
- const rotateDayNames = (names, firstDayOfWeek) => {
471
- const [front, back] = Array.splitAt(names, DAY_OF_WEEK_INDEX[firstDayOfWeek]);
472
- return [...back, ...front];
473
- };
474
- const WEEKS_IN_GRID = 6;
475
- const DAYS_IN_WEEK = 7;
476
- /** Builds the 6×7 grid of dates that a calendar view renders for a given
477
- * month. The grid always has 6 rows to keep height stable across months.
478
- * Returns the 2D grid alongside the starting date (top-left cell) so
479
- * callers can derive per-week positions without recomputing. */
480
- const buildGrid = (viewYear, viewMonth, firstDayOfWeek) => {
481
- const firstOfMonth = Calendar.make(viewYear, viewMonth, 1);
482
- const gridStart = Calendar.startOfWeek(firstOfMonth, firstDayOfWeek);
483
- const weeks = Array.makeBy(WEEKS_IN_GRID, weekIndex => Array.makeBy(DAYS_IN_WEEK, dayIndex => Calendar.addDays(gridStart, weekIndex * DAYS_IN_WEEK + dayIndex)));
484
- return { gridStart, weeks };
485
- };
486
- const NAV_KEYS = new Set([
487
- 'ArrowLeft',
488
- 'ArrowRight',
489
- 'ArrowUp',
490
- 'ArrowDown',
491
- 'Home',
492
- 'End',
493
- 'PageUp',
494
- 'PageDown',
495
- 'Enter',
496
- ' ',
497
- ]);
498
- const MONTHS_GRID_COLUMNS = 3;
499
- const YEARS_GRID_COLUMNS = 3;
500
- const MONTHS_IN_YEAR = 12;
501
- /** Returns the start year of the 12-year window the years grid renders. */
502
- const yearsPageStart = (viewYear) => Math.floor(viewYear / YEARS_PAGE_SIZE) * YEARS_PAGE_SIZE;
503
- /** A month range is fully disabled when its last day is below the minimum
504
- * or its first day is above the maximum. */
505
- const isMonthDisabled = (model, year, month) => {
506
- const monthStart = Calendar.make(year, month, 1);
507
- const monthEnd = Calendar.make(year, month, Calendar.daysInMonth(year, month));
508
- return (Option.exists(model.maybeMinDate, min => Calendar.isBefore(monthEnd, min)) ||
509
- Option.exists(model.maybeMaxDate, max => Calendar.isAfter(monthStart, max)));
510
- };
511
- /** A year is fully disabled when it falls entirely below the minimum date's
512
- * year or entirely above the maximum date's year. */
513
- const isYearDisabled = (model, year) => Option.exists(model.maybeMinDate, min => year < min.year) ||
514
- Option.exists(model.maybeMaxDate, max => year > max.year);
515
- const buildDaysAttributes = (model, viewInputs) => {
516
- const h = html();
517
- const { id, viewYear, viewMonth, maybeFocusedDate, maybeSelectedDate, today, locale, isGridFocused, } = model;
518
- const previousMonthLabel = viewInputs.previousMonthLabel ?? 'Previous month';
519
- const nextMonthLabel = viewInputs.nextMonthLabel ?? 'Next month';
520
- const headingButtonLabel = viewInputs.daysHeadingButtonLabel ?? 'Switch to month picker';
521
- const headingText = `${locale.monthNames[viewMonth - 1]} ${viewYear}`;
522
- const rotatedDayNames = rotateDayNames(DAY_NAMES_SUNDAY_FIRST, locale.firstDayOfWeek);
523
- const rotatedShortDayNames = rotateDayNames(locale.shortDayNames, locale.firstDayOfWeek);
524
- const { gridStart, weeks: weeksDates } = buildGrid(viewYear, viewMonth, locale.firstDayOfWeek);
525
- const rootAttributes = [h.Id(id), h.Key('Days')];
526
- const previousMonthButton = [
527
- h.Type('button'),
528
- h.AriaLabel(previousMonthLabel),
529
- h.OnClick(ClickedPreviousMonthButton()),
530
- ];
531
- const nextMonthButton = [
532
- h.Type('button'),
533
- h.AriaLabel(nextMonthLabel),
534
- h.OnClick(ClickedNextMonthButton()),
535
- ];
536
- const headingButton = [
537
- h.Type('button'),
538
- h.AriaLabel(headingButtonLabel),
539
- h.OnClick(ClickedHeading()),
540
- ];
541
- const handleKeyDown = (key, modifiers) => {
542
- if (!NAV_KEYS.has(key)) {
543
- return Option.none();
544
- }
545
- if (isCommitKey(key)) {
546
- const maybeCommit = pipe(maybeFocusedDate, Option.filter(date => !isDateDisabled(model, date)), Option.map(date => ClickedDay({ date })));
547
- if (Option.isSome(maybeCommit)) {
548
- return maybeCommit;
549
- }
550
- }
551
- return Option.some(PressedKeyOnGrid({ key, isShift: modifiers.shiftKey }));
552
- };
553
- const activeDescendantAttributes = pipe(maybeFocusedDate, Option.map(date => h.AriaActiveDescendant(dayCellId(id, date))), Option.toArray);
554
- const gridAttributes = [
555
- h.Id(gridId(id)),
556
- h.Role('grid'),
557
- h.AriaLabel(`Calendar, ${headingText}`),
558
- h.AriaRowcount(Number.increment(WEEKS_IN_GRID)),
559
- h.AriaColcount(DAYS_IN_WEEK),
560
- h.Tabindex(0),
561
- h.OnFocus(FocusedGrid()),
562
- h.OnBlur(BlurredGrid()),
563
- h.OnKeyDownPreventDefault(handleKeyDown),
564
- ...activeDescendantAttributes,
565
- ];
566
- const headerRowAttributes = [h.Role('row'), h.AriaRowindex(1)];
567
- const buildDayCell = (date, columnIndex) => {
568
- const isSelected = Option.exists(maybeSelectedDate, Calendar.isEqual(date));
569
- const isFocused = Option.exists(maybeFocusedDate, Calendar.isEqual(date));
570
- const isToday = Calendar.isEqual(today, date);
571
- const isInViewMonth = date.month === viewMonth && date.year === viewYear;
572
- const isDisabled = isDateDisabled(model, date);
573
- const stateDataAttributes = Array.getSomes([
574
- OptionExt.when(isToday, h.DataAttribute('today', '')),
575
- OptionExt.when(isSelected, h.DataAttribute('selected', '')),
576
- OptionExt.when(isFocused && isGridFocused, h.DataAttribute('focused', '')),
577
- OptionExt.when(!isInViewMonth, h.DataAttribute('outside-month', '')),
578
- OptionExt.when(isDisabled, h.DataAttribute('disabled', '')),
579
- ]);
580
- const cellAttributes = [
581
- h.Id(dayCellId(id, date)),
582
- h.Role('gridcell'),
583
- h.AriaSelected(isSelected),
584
- h.AriaColindex(Number.increment(columnIndex)),
585
- ...stateDataAttributes,
586
- ];
587
- const buttonAttributes = [
588
- h.Type('button'),
589
- h.Tabindex(-1),
590
- h.AriaLabel(Calendar.formatAriaLabel(date, locale)),
591
- h.AriaDisabled(isDisabled),
592
- ...(isDisabled ? [] : [h.OnClick(ClickedDay({ date }))]),
593
- ];
594
- return {
595
- date,
596
- label: String(date.day),
597
- cellAttributes: childAttributes(cellAttributes),
598
- buttonAttributes: childAttributes(buttonAttributes),
599
- isSelected,
600
- isFocused: isFocused && isGridFocused,
601
- isToday,
602
- isInViewMonth,
603
- isDisabled,
604
- };
605
- };
606
- const weeks = weeksDates.map((weekDates, weekIndex) => {
607
- const weekStart = Calendar.addDays(gridStart, weekIndex * DAYS_IN_WEEK);
608
- return {
609
- attributes: childAttributes([
610
- h.Role('row'),
611
- h.AriaRowindex(weekIndex + 2),
612
- h.AriaLabel(`Week of ${Calendar.formatLong(weekStart, locale)}`),
613
- ]),
614
- cells: weekDates.map(buildDayCell),
615
- };
616
- });
617
- const wrappedColumnHeaders = Array.zipWith(rotatedShortDayNames, rotatedDayNames, (name, fullName) => ({ name, fullName })).map(({ name, fullName }, columnIndex) => ({
618
- name,
619
- attributes: childAttributes([
620
- h.Role('columnheader'),
621
- h.AriaLabel(fullName),
622
- h.AriaColindex(Number.increment(columnIndex)),
623
- ]),
624
- }));
625
- return {
626
- _tag: 'Days',
627
- root: childAttributes(rootAttributes),
628
- previousMonthButton: childAttributes(previousMonthButton),
629
- nextMonthButton: childAttributes(nextMonthButton),
630
- headingButton: childAttributes(headingButton),
631
- heading: { id: headingId(id), text: headingText },
632
- grid: childAttributes(gridAttributes),
633
- headerRow: childAttributes(headerRowAttributes),
634
- columnHeaders: wrappedColumnHeaders,
635
- weeks,
636
- };
637
- };
638
- const buildMonthsAttributes = (model, viewInputs) => {
639
- const h = html();
640
- const { id, viewYear, viewMonth, maybeFocusedDate, today, locale, isGridFocused, } = model;
641
- const headingButtonLabel = viewInputs.monthsHeadingButtonLabel ?? 'Switch to year picker';
642
- const headingText = `${viewYear}`;
643
- const rootAttributes = [h.Id(id), h.Key('Months')];
644
- const headingButton = [
645
- h.Type('button'),
646
- h.AriaLabel(headingButtonLabel),
647
- h.OnClick(ClickedHeading()),
648
- ];
649
- const focusedMonth = Option.match(maybeFocusedDate, {
650
- onNone: () => viewMonth,
651
- onSome: date => (date.year === viewYear ? date.month : viewMonth),
652
- });
653
- const handleKeyDown = (key, modifiers) => {
654
- if (!NAV_KEYS.has(key)) {
655
- return Option.none();
656
- }
657
- else if (isCommitKey(key)) {
658
- return OptionExt.when(!isMonthDisabled(model, viewYear, focusedMonth), SelectedMonth({ month: focusedMonth }));
659
- }
660
- else {
661
- return Option.some(PressedKeyOnGrid({ key, isShift: modifiers.shiftKey }));
662
- }
663
- };
664
- const activeDescendantAttributes = [
665
- h.AriaActiveDescendant(monthCellId(id, focusedMonth)),
666
- ];
667
- const gridAttributes = [
668
- h.Id(gridId(id)),
669
- h.Role('grid'),
670
- h.AriaLabel(`Month picker, ${headingText}`),
671
- h.Tabindex(0),
672
- h.OnFocus(FocusedGrid()),
673
- h.OnBlur(BlurredGrid()),
674
- h.OnKeyDownPreventDefault(handleKeyDown),
675
- ...activeDescendantAttributes,
676
- ];
677
- const buildMonthCell = (month) => {
678
- const label = locale.monthNames[month - 1] ?? String(month);
679
- const shortLabel = locale.shortMonthNames[month - 1] ?? label;
680
- const isSelected = month === viewMonth;
681
- const isFocused = month === focusedMonth;
682
- const isCurrentMonth = today.year === viewYear && today.month === month;
683
- const isDisabled = isMonthDisabled(model, viewYear, month);
684
- const stateDataAttributes = Array.getSomes([
685
- OptionExt.when(isCurrentMonth, h.DataAttribute('today', '')),
686
- OptionExt.when(isSelected, h.DataAttribute('selected', '')),
687
- OptionExt.when(isFocused && isGridFocused, h.DataAttribute('focused', '')),
688
- OptionExt.when(isDisabled, h.DataAttribute('disabled', '')),
689
- ]);
690
- const cellAttributes = [
691
- h.Id(monthCellId(id, month)),
692
- h.Role('gridcell'),
693
- h.AriaSelected(isSelected),
694
- ...stateDataAttributes,
695
- ];
696
- const buttonAttributes = [
697
- h.Type('button'),
698
- h.Tabindex(-1),
699
- h.AriaLabel(`${label} ${viewYear}`),
700
- h.AriaDisabled(isDisabled),
701
- ...(isDisabled ? [] : [h.OnClick(SelectedMonth({ month }))]),
702
- ];
703
- return {
704
- month,
705
- label,
706
- shortLabel,
707
- cellAttributes: childAttributes(cellAttributes),
708
- buttonAttributes: childAttributes(buttonAttributes),
709
- isSelected,
710
- isFocused: isFocused && isGridFocused,
711
- isCurrentMonth,
712
- isDisabled,
713
- };
714
- };
715
- const cells = Array.makeBy(MONTHS_IN_YEAR, monthIndex => buildMonthCell(Number.increment(monthIndex)));
716
- return {
717
- _tag: 'Months',
718
- root: childAttributes(rootAttributes),
719
- headingButton: childAttributes(headingButton),
720
- heading: { id: headingId(id), text: headingText },
721
- grid: childAttributes(gridAttributes),
722
- cells,
723
- };
724
- };
725
- const buildYearsAttributes = (model, viewInputs) => {
726
- const h = html();
727
- const { id, viewYear, maybeFocusedDate, today, isGridFocused } = model;
728
- const previousYearsPageLabel = viewInputs.previousYearsPageLabel ?? 'Previous 12 years';
729
- const nextYearsPageLabel = viewInputs.nextYearsPageLabel ?? 'Next 12 years';
730
- const cursorYear = Option.match(maybeFocusedDate, {
731
- onNone: () => viewYear,
732
- onSome: date => date.year,
733
- });
734
- const pageStart = yearsPageStart(cursorYear);
735
- const pageEnd = pageStart + YEARS_PAGE_SIZE - 1;
736
- const headingText = `${pageStart}–${pageEnd}`;
737
- const rootAttributes = [h.Id(id), h.Key('Years')];
738
- const previousPageButton = [
739
- h.Type('button'),
740
- h.AriaLabel(previousYearsPageLabel),
741
- h.OnClick(PagedYears({ direction: -1 })),
742
- ];
743
- const nextPageButton = [
744
- h.Type('button'),
745
- h.AriaLabel(nextYearsPageLabel),
746
- h.OnClick(PagedYears({ direction: 1 })),
747
- ];
748
- const focusedYear = cursorYear;
749
- const handleKeyDown = (key, modifiers) => {
750
- if (!NAV_KEYS.has(key)) {
751
- return Option.none();
752
- }
753
- else if (isCommitKey(key)) {
754
- return OptionExt.when(!isYearDisabled(model, focusedYear), SelectedYear({ year: focusedYear }));
755
- }
756
- else {
757
- return Option.some(PressedKeyOnGrid({ key, isShift: modifiers.shiftKey }));
758
- }
759
- };
760
- const activeDescendantAttributes = [
761
- h.AriaActiveDescendant(yearCellId(id, focusedYear)),
762
- ];
763
- const gridAttributes = [
764
- h.Id(gridId(id)),
765
- h.Role('grid'),
766
- h.AriaLabel(`Year picker, ${headingText}`),
767
- h.Tabindex(0),
768
- h.OnFocus(FocusedGrid()),
769
- h.OnBlur(BlurredGrid()),
770
- h.OnKeyDownPreventDefault(handleKeyDown),
771
- ...activeDescendantAttributes,
772
- ];
773
- const buildYearCell = (year) => {
774
- const label = String(year);
775
- const isSelected = year === viewYear;
776
- const isFocused = year === focusedYear;
777
- const isCurrentYear = today.year === year;
778
- const isDisabled = isYearDisabled(model, year);
779
- const stateDataAttributes = Array.getSomes([
780
- OptionExt.when(isCurrentYear, h.DataAttribute('today', '')),
781
- OptionExt.when(isSelected, h.DataAttribute('selected', '')),
782
- OptionExt.when(isFocused && isGridFocused, h.DataAttribute('focused', '')),
783
- OptionExt.when(isDisabled, h.DataAttribute('disabled', '')),
784
- ]);
785
- const cellAttributes = [
786
- h.Id(yearCellId(id, year)),
787
- h.Role('gridcell'),
788
- h.AriaSelected(isSelected),
789
- ...stateDataAttributes,
790
- ];
791
- const buttonAttributes = [
792
- h.Type('button'),
793
- h.Tabindex(-1),
794
- h.AriaLabel(label),
795
- h.AriaDisabled(isDisabled),
796
- ...(isDisabled ? [] : [h.OnClick(SelectedYear({ year }))]),
797
- ];
798
- return {
799
- year,
800
- label,
801
- cellAttributes: childAttributes(cellAttributes),
802
- buttonAttributes: childAttributes(buttonAttributes),
803
- isSelected,
804
- isFocused: isFocused && isGridFocused,
805
- isCurrentYear,
806
- isDisabled,
807
- };
808
- };
809
- const cells = Array.makeBy(YEARS_PAGE_SIZE, offset => buildYearCell(pageStart + offset));
810
- return {
811
- _tag: 'Years',
812
- root: childAttributes(rootAttributes),
813
- previousPageButton: childAttributes(previousPageButton),
814
- nextPageButton: childAttributes(nextPageButton),
815
- heading: { id: headingId(id), text: headingText },
816
- grid: childAttributes(gridAttributes),
817
- cells,
818
- };
819
- };
820
- /** Renders an accessible calendar. Publishes mode-specific ARIA attribute
821
- * bundles + derived cell data, then delegates layout to the consumer's
822
- * `toView` callback. The variant of `CalendarAttributes` passed to
823
- * `toView` matches `model.viewMode`. */
824
- export const view = defineView((model, viewInputs) => viewInputs.toView(M.value(model.viewMode).pipe(M.withReturnType(), M.when('Days', () => buildDaysAttributes(model, viewInputs)), M.when('Months', () => buildMonthsAttributes(model, viewInputs)), M.when('Years', () => buildYearsAttributes(model, viewInputs)), M.exhaustive)));