forty-cdk 0.2.0 → 0.3.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 (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
@@ -0,0 +1,2034 @@
1
+ import * as i0 from '@angular/core';
2
+ import { computed, InjectionToken, inject, signal, Injector, model, input, booleanAttribute, numberAttribute, linkedSignal, effect, afterNextRender, Directive, ElementRef, Injectable } from '@angular/core';
3
+ import { compareDateOf, clampToBounds, Collection, createDefaults, IdGenerator, LiveAnnouncer, injectDateAdapter, injectTextDirection, adoptHostId, registerHandle, reflectDisabled, FOR_DATE_ADAPTER } from 'forty-cdk/core';
4
+ export { FOR_DATE_ADAPTER, assertTimeCapable, injectDateAdapter } from 'forty-cdk/core';
5
+
6
+ /**
7
+ * Owns every `[min, max]` bounds question a `ForCalendar` asks: whole-month /
8
+ * whole-year out-of-bounds checks, the prev / next-month disabled predicates,
9
+ * per-date availability, and the navigation clamp. Shared by the root and the
10
+ * day / month / year sub-grids so the bounds math lives in one place.
11
+ *
12
+ * Constructed directly (`new CalendarBounds(host)`); it holds no injection
13
+ * context, mirroring how `SegmentEditor` is lifted out of the date / time
14
+ * fields.
15
+ *
16
+ * @typeParam D The adapter's immutable date type.
17
+ */
18
+ class CalendarBounds {
19
+ #host;
20
+ /** Whether the previous-month button should be disabled (bounded by `min`). */
21
+ isPreviousMonthDisabled = computed(() => {
22
+ const min = this.#host.min();
23
+ if (min === null) {
24
+ return false;
25
+ }
26
+ const adapter = this.#host.adapter;
27
+ const previousMonthLastDay = adapter.addDays(this.#host.visibleMonth(), -1);
28
+ return compareDateOf(adapter, previousMonthLastDay, min) < 0;
29
+ }, /* @ts-ignore */
30
+ ...(ngDevMode ? [{ debugName: "isPreviousMonthDisabled" }] : /* istanbul ignore next */ []));
31
+ /** Whether the next-month button should be disabled (bounded by `max`). */
32
+ isNextMonthDisabled = computed(() => {
33
+ const max = this.#host.max();
34
+ if (max === null) {
35
+ return false;
36
+ }
37
+ const adapter = this.#host.adapter;
38
+ const nextMonthFirstDay = adapter.addMonths(this.#host.visibleMonth(), 1);
39
+ return compareDateOf(adapter, nextMonthFirstDay, max) > 0;
40
+ }, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "isNextMonthDisabled" }] : /* istanbul ignore next */ []));
42
+ constructor(host) {
43
+ this.#host = host;
44
+ }
45
+ /** Whether every day of `month` (**1-12**) in `year` falls outside `[min, max]`. */
46
+ isMonthOutOfBounds(year, month) {
47
+ const adapter = this.#host.adapter;
48
+ const firstDay = adapter.createDate(year, month, 1);
49
+ const lastDay = adapter.createDate(year, month, adapter.getDaysInMonth(firstDay));
50
+ return this.#outOfBounds(firstDay, lastDay);
51
+ }
52
+ /** Whether every day of `year` falls outside `[min, max]`. */
53
+ isYearDisabled(year) {
54
+ const adapter = this.#host.adapter;
55
+ return this.#outOfBounds(adapter.createDate(year, 1, 1), adapter.createDate(year, 12, 31));
56
+ }
57
+ /** Whether `date` cannot be selected (`disabled`, out of `[min, max]`, or unavailable). */
58
+ isUnavailable(date) {
59
+ if (this.#host.disabled()) {
60
+ return true;
61
+ }
62
+ const adapter = this.#host.adapter;
63
+ const min = this.#host.min();
64
+ if (min !== null && compareDateOf(adapter, date, min) < 0) {
65
+ return true;
66
+ }
67
+ const max = this.#host.max();
68
+ if (max !== null && compareDateOf(adapter, date, max) > 0) {
69
+ return true;
70
+ }
71
+ return this.#host.isDateUnavailable()(date);
72
+ }
73
+ /** Clamp `date` into `[min, max]`, returning the nearer bound when outside. */
74
+ clamp(date) {
75
+ const adapter = this.#host.adapter;
76
+ return clampToBounds(adapter, date, this.#host.min(), this.#host.max(), (a, b) => compareDateOf(adapter, a, b));
77
+ }
78
+ #outOfBounds(firstDay, lastDay) {
79
+ const adapter = this.#host.adapter;
80
+ const min = this.#host.min();
81
+ if (min !== null && compareDateOf(adapter, lastDay, min) < 0) {
82
+ return true;
83
+ }
84
+ const max = this.#host.max();
85
+ if (max !== null && compareDateOf(adapter, firstDay, max) > 0) {
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /** Injection token for {@link ForCalendarContext}, provided by `ForCalendar`. */
93
+ const FOR_CALENDAR_CONTEXT = new InjectionToken('FOR_CALENDAR_CONTEXT');
94
+ /**
95
+ * Injects the nearest {@link ForCalendarContext}, throwing a descriptive
96
+ * error when used outside a `[forCalendar]` element.
97
+ *
98
+ * @param piece Name of the calling directive, used in the error message.
99
+ */
100
+ function injectCalendarContext(piece) {
101
+ const ctx = inject(FOR_CALENDAR_CONTEXT, { optional: true });
102
+ if (!ctx) {
103
+ throw new Error(`[forty-cdk/calendar] ${piece} must be used inside a [forCalendar] element.`);
104
+ }
105
+ return ctx;
106
+ }
107
+
108
+ /**
109
+ * Builds the day grid for the month containing `monthAnchor`.
110
+ *
111
+ * Returns rows of 7 dates each, starting at `firstDayOfWeek`. Leading and
112
+ * trailing days from the adjacent months are included so every row is full;
113
+ * callers mark them with `data-outside-month`. The number of rows is the
114
+ * minimum needed to cover the month (4-6), so there is no empty trailing week.
115
+ *
116
+ * Pure and adapter-driven: the same `(adapter, anchor, firstDayOfWeek)` always
117
+ * yields the same matrix, and two adapters over the same calendar system
118
+ * produce day-identical grids.
119
+ *
120
+ * @param adapter The date adapter to compute with.
121
+ * @param monthAnchor Any date within the target month.
122
+ * @param firstDayOfWeek First column's day of week, **0-6** (`0` = Sunday).
123
+ */
124
+ function buildMonthMatrix(adapter, monthAnchor, firstDayOfWeek) {
125
+ const firstOfMonth = adapter.createDate(adapter.getYear(monthAnchor), adapter.getMonth(monthAnchor), 1);
126
+ const daysInMonth = adapter.getDaysInMonth(firstOfMonth);
127
+ const leading = (adapter.getDayOfWeek(firstOfMonth) - firstDayOfWeek + 7) % 7;
128
+ const numWeeks = Math.ceil((leading + daysInMonth) / 7);
129
+ const gridStart = adapter.addDays(firstOfMonth, -leading);
130
+ const weeks = [];
131
+ for (let week = 0; week < numWeeks; week++) {
132
+ const row = [];
133
+ for (let day = 0; day < 7; day++) {
134
+ row.push(adapter.addDays(gridStart, week * 7 + day));
135
+ }
136
+ weeks.push(row);
137
+ }
138
+ return weeks;
139
+ }
140
+
141
+ /**
142
+ * Resolve a calendar grid keydown into the target the move lands on, or `null`
143
+ * when the key is not a navigation key. `rtl` mirrors the horizontal arrows.
144
+ * Shared by all three sub-grids via their {@link CalendarGridMoves} mapping.
145
+ */
146
+ function resolveCalendarGridMove(event, rtl, moves) {
147
+ switch (event.key) {
148
+ case 'ArrowLeft':
149
+ return moves.horizontal(rtl ? 1 : -1);
150
+ case 'ArrowRight':
151
+ return moves.horizontal(rtl ? -1 : 1);
152
+ case 'ArrowUp':
153
+ return moves.vertical(-1);
154
+ case 'ArrowDown':
155
+ return moves.vertical(1);
156
+ case 'Home':
157
+ return moves.lineStart();
158
+ case 'End':
159
+ return moves.lineEnd();
160
+ case 'PageUp':
161
+ return moves.pageBackward(event.shiftKey);
162
+ case 'PageDown':
163
+ return moves.pageForward(event.shiftKey);
164
+ default:
165
+ return null;
166
+ }
167
+ }
168
+ /** Whether a keydown is the activation key (`Enter` / `Space`) every sub-grid selects on. */
169
+ function isCalendarActivationKey(event) {
170
+ return event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar';
171
+ }
172
+
173
+ /**
174
+ * The day grid view's state machine (`view="day"`): the 7-column date table.
175
+ * Owns its cell registry, the rendered `weekDays` / `weeks` data, its
176
+ * table-driven keyboard mover (the date-math counterpart to the month / year
177
+ * movers), the start-of-week helper, and the per-view label / disabled / paging
178
+ * / focus the root composes as a {@link CalendarViewStrategy}.
179
+ *
180
+ * Constructed directly by the root (`new CalendarDayNavigator(host)`); it holds
181
+ * no injection context, mirroring how `SegmentEditor` is lifted out of the date
182
+ * / time fields. The intended-day, clamp, and announce machinery stays a single
183
+ * source of truth on the root, reached through {@link CalendarDayNavigatorHost}.
184
+ *
185
+ * @typeParam D The adapter's immutable date type.
186
+ */
187
+ class CalendarDayNavigator {
188
+ #host;
189
+ #cells = new Collection();
190
+ #matrix = computed(() => buildMonthMatrix(this.#host.adapter, this.#host.visibleMonth(), this.#host.firstDayOfWeek()), /* @ts-ignore */
191
+ ...(ngDevMode ? [{ debugName: "#matrix" }] : /* istanbul ignore next */ []));
192
+ /** Weekday column headers for the visible month, starting at `firstDayOfWeek`. */
193
+ weekDays = computed(() => {
194
+ const adapter = this.#host.adapter;
195
+ const firstDay = this.#host.firstDayOfWeek();
196
+ return this.#matrix()[0].map((date, index) => ({
197
+ key: String((firstDay + index) % 7),
198
+ narrow: adapter.format(date, { weekday: 'narrow' }),
199
+ short: adapter.format(date, { weekday: 'short' }),
200
+ long: adapter.format(date, { weekday: 'long' }),
201
+ }));
202
+ }, /* @ts-ignore */
203
+ ...(ngDevMode ? [{ debugName: "weekDays" }] : /* istanbul ignore next */ []));
204
+ /** Week rows of the visible month, including outside-month padding days. */
205
+ weeks = computed(() => {
206
+ const adapter = this.#host.adapter;
207
+ return this.#matrix().map((row) => ({
208
+ key: this.#dateKey(row[0]),
209
+ days: row.map((date) => ({
210
+ key: this.#dateKey(date),
211
+ date,
212
+ label: String(adapter.getDate(date)),
213
+ dateLabel: this.#host.getDateLabel(date),
214
+ })),
215
+ }));
216
+ }, /* @ts-ignore */
217
+ ...(ngDevMode ? [{ debugName: "weeks" }] : /* istanbul ignore next */ []));
218
+ constructor(host) {
219
+ this.#host = host;
220
+ }
221
+ triggerLabel() {
222
+ return this.#host.visibleMonthLabel();
223
+ }
224
+ isPreviousDisabled() {
225
+ return this.#host.isPreviousMonthDisabled();
226
+ }
227
+ isNextDisabled() {
228
+ return this.#host.isNextMonthDisabled();
229
+ }
230
+ page(direction) {
231
+ this.#host.pageMonths(direction);
232
+ }
233
+ focusActive() {
234
+ return this.focusCell(this.#host.focusedDate());
235
+ }
236
+ handleKeydown(event, fromDate) {
237
+ if (isCalendarActivationKey(event)) {
238
+ event.preventDefault();
239
+ this.#host.selectDate(fromDate);
240
+ return;
241
+ }
242
+ const target = this.#resolveMove(event, fromDate);
243
+ if (target === null) {
244
+ return;
245
+ }
246
+ event.preventDefault();
247
+ const isPaging = event.key === 'PageUp' || event.key === 'PageDown';
248
+ this.#host.applyDayKeyMove(target, isPaging);
249
+ }
250
+ register(handle) {
251
+ this.#cells.register(handle);
252
+ }
253
+ unregister(handle) {
254
+ this.#cells.unregister(handle);
255
+ }
256
+ focusCell(target) {
257
+ const adapter = this.#host.adapter;
258
+ const handle = this.#cells.items().find((cell) => adapter.isSameDay(cell.date(), target));
259
+ if (!handle) {
260
+ return false;
261
+ }
262
+ handle.host.focus();
263
+ return true;
264
+ }
265
+ #resolveMove(event, fromDate) {
266
+ const adapter = this.#host.adapter;
267
+ return resolveCalendarGridMove(event, this.#host.dir() === 'rtl', {
268
+ horizontal: (step) => adapter.addDays(fromDate, step),
269
+ vertical: (step) => adapter.addDays(fromDate, step * 7),
270
+ lineStart: () => this.#startOfWeek(fromDate),
271
+ lineEnd: () => adapter.addDays(this.#startOfWeek(fromDate), 6),
272
+ pageBackward: (shiftKey) => shiftKey ? adapter.addYears(fromDate, -1) : adapter.addMonths(fromDate, -1),
273
+ pageForward: (shiftKey) => shiftKey ? adapter.addYears(fromDate, 1) : adapter.addMonths(fromDate, 1),
274
+ });
275
+ }
276
+ #startOfWeek(date) {
277
+ const adapter = this.#host.adapter;
278
+ const offset = (adapter.getDayOfWeek(date) - this.#host.firstDayOfWeek() + 7) % 7;
279
+ return adapter.addDays(date, -offset);
280
+ }
281
+ #dateKey(date) {
282
+ const adapter = this.#host.adapter;
283
+ return `${adapter.getYear(date)}-${adapter.getMonth(date)}-${adapter.getDate(date)}`;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Library fallback for calendar defaults, read at the root injector when no
289
+ * consumer has called `provideForCalendarDefaults`. Exported for the shared
290
+ * defaults contract spec; not re-exported from the primitive's public entry.
291
+ */
292
+ const FOR_CALENDAR_FALLBACK_DEFAULTS = {
293
+ firstDayOfWeek: null,
294
+ };
295
+ const { token, provideDefaults } = createDefaults('FOR_CALENDAR_DEFAULTS', FOR_CALENDAR_FALLBACK_DEFAULTS);
296
+ /** Token holding the resolved calendar defaults for the current scope. */
297
+ const FOR_CALENDAR_DEFAULTS = token;
298
+ /**
299
+ * Configures forty-cdk calendar defaults for this injector scope. Partial
300
+ * overrides inherit unspecified keys from the parent scope (or library
301
+ * defaults at the root).
302
+ */
303
+ function provideForCalendarDefaults(defaults = {}) {
304
+ return provideDefaults(defaults);
305
+ }
306
+
307
+ const GRID_COLUMNS$1 = 3;
308
+ /**
309
+ * The month picker view's state machine (`view="month"`): a 3-column grid of the
310
+ * twelve months of the visible year. Owns its cell registry, its `is*` quartet
311
+ * (selected / today / focused / disabled), its table-driven keyboard mover, and
312
+ * the per-view paging / focus the root composes as a {@link CalendarViewStrategy}.
313
+ *
314
+ * Constructed directly by the root (`new CalendarMonthNavigator(host, today)`);
315
+ * it holds no injection context, mirroring how `SegmentEditor` is lifted out of
316
+ * the date / time fields.
317
+ *
318
+ * @typeParam D The adapter's immutable date type.
319
+ */
320
+ class CalendarMonthNavigator {
321
+ #host;
322
+ #today;
323
+ #cells = new Collection();
324
+ constructor(host, today) {
325
+ this.#host = host;
326
+ this.#today = today;
327
+ }
328
+ /** Twelve localized, bounds-aware month options for the visible year. */
329
+ options() {
330
+ const adapter = this.#host.adapter;
331
+ const year = this.#host.visibleYear();
332
+ const options = [];
333
+ for (let month = 1; month <= 12; month++) {
334
+ options.push({
335
+ value: month,
336
+ label: adapter.format(adapter.createDate(year, month, 1), { month: 'long' }),
337
+ disabled: this.#host.isMonthOutOfBounds(year, month),
338
+ });
339
+ }
340
+ return options;
341
+ }
342
+ /** Rows of the month picker grid (3 columns) for the visible year. */
343
+ rows() {
344
+ const options = this.options();
345
+ const rows = [];
346
+ for (let i = 0; i < options.length; i += GRID_COLUMNS$1) {
347
+ rows.push({ key: `m-${i}`, months: options.slice(i, i + GRID_COLUMNS$1) });
348
+ }
349
+ return rows;
350
+ }
351
+ triggerLabel() {
352
+ return String(this.#host.visibleYear());
353
+ }
354
+ isPreviousDisabled() {
355
+ return this.#host.isYearDisabled(this.#host.visibleYear() - 1);
356
+ }
357
+ isNextDisabled() {
358
+ return this.#host.isYearDisabled(this.#host.visibleYear() + 1);
359
+ }
360
+ page(direction) {
361
+ this.#host.pageByYears(direction);
362
+ }
363
+ focusActive() {
364
+ return this.focusCell(this.#host.visibleMonthNumber());
365
+ }
366
+ isSelected(month) {
367
+ const adapter = this.#host.adapter;
368
+ const value = this.#host.value();
369
+ return (value !== null &&
370
+ adapter.getYear(value) === this.#host.visibleYear() &&
371
+ adapter.getMonth(value) === month);
372
+ }
373
+ isToday(month) {
374
+ const adapter = this.#host.adapter;
375
+ return (adapter.getYear(this.#today) === this.#host.visibleYear() &&
376
+ adapter.getMonth(this.#today) === month);
377
+ }
378
+ isFocused(month) {
379
+ return month === this.#host.visibleMonthNumber();
380
+ }
381
+ handleKeydown(event, fromMonth) {
382
+ if (isCalendarActivationKey(event)) {
383
+ event.preventDefault();
384
+ this.#host.selectMonth(fromMonth);
385
+ return;
386
+ }
387
+ const target = this.#resolveMove(event, fromMonth);
388
+ if (target === null) {
389
+ return;
390
+ }
391
+ event.preventDefault();
392
+ this.#host.setFocusedDate(this.#host.dateInMonth(target.year, target.month));
393
+ this.#host.scheduleFocus(() => this.focusCell(this.#host.visibleMonthNumber()));
394
+ }
395
+ register(handle) {
396
+ this.#cells.register(handle);
397
+ }
398
+ unregister(handle) {
399
+ this.#cells.unregister(handle);
400
+ }
401
+ focusCell(month) {
402
+ const handle = this.#cells.items().find((cell) => cell.month() === month);
403
+ if (!handle) {
404
+ return false;
405
+ }
406
+ handle.host.focus();
407
+ return true;
408
+ }
409
+ #resolveMove(event, fromMonth) {
410
+ const year = this.#host.visibleYear();
411
+ const base = year * 12 + (fromMonth - 1);
412
+ const index = resolveCalendarGridMove(event, this.#host.dir() === 'rtl', {
413
+ horizontal: (step) => base + step,
414
+ vertical: (step) => base + step * GRID_COLUMNS$1,
415
+ lineStart: () => year * 12,
416
+ lineEnd: () => year * 12 + 11,
417
+ pageBackward: () => base - 12,
418
+ pageForward: () => base + 12,
419
+ });
420
+ if (index === null) {
421
+ return null;
422
+ }
423
+ return { year: Math.floor(index / 12), month: (index % 12) + 1 };
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Tracks the day-of-month the user is conceptually navigating with so paging
429
+ * across months restores the original day when landing on a longer month,
430
+ * instead of cumulatively drifting back through a chain of short months.
431
+ *
432
+ * It also records the focused date each paging operation produced, letting the
433
+ * root detect a focus that moved for any other reason (explicit day move,
434
+ * selection, an external `value` write driving the `focusedDate` linkedSignal)
435
+ * and reset the intended day to the new day — all without an `effect`.
436
+ *
437
+ * Constructed directly (`new IntendedDayCursor(adapter, seed)`); it holds no
438
+ * injection context, mirroring how `SegmentEditor` is lifted out of the date /
439
+ * time fields.
440
+ *
441
+ * @typeParam D The adapter's immutable date type.
442
+ */
443
+ class IntendedDayCursor {
444
+ #adapter;
445
+ #intendedDay;
446
+ #pagedFocus = null;
447
+ constructor(adapter, seed) {
448
+ this.#adapter = adapter;
449
+ this.#intendedDay = adapter.getDate(seed);
450
+ }
451
+ /** Record `date` as the focus origin for a non-paging move and reset the intended day. */
452
+ setFocusedDay(date) {
453
+ this.#intendedDay = this.#adapter.getDate(date);
454
+ this.#pagedFocus = null;
455
+ }
456
+ /**
457
+ * Re-sync the intended day to `focused`'s actual day unless the focus is still
458
+ * where the last paging operation left it. Anything that moved the focus for
459
+ * another reason resets the intended day so a fresh paging run starts from the
460
+ * day the user can see.
461
+ */
462
+ sync(focused) {
463
+ if (this.#pagedFocus === null || !this.#adapter.isSameDay(this.#pagedFocus, focused)) {
464
+ this.#intendedDay = this.#adapter.getDate(focused);
465
+ }
466
+ }
467
+ /**
468
+ * Re-derive `date` so its day-of-month matches the user's intended day,
469
+ * clamped to the target month's length. Restores e.g. the 31st when a paging
470
+ * chain lands back on a 31-day month, instead of carrying forward the day a
471
+ * shorter intermediate month clamped it to.
472
+ */
473
+ apply(date) {
474
+ const adapter = this.#adapter;
475
+ const daysInMonth = adapter.getDaysInMonth(date);
476
+ const day = this.#intendedDay < daysInMonth ? this.#intendedDay : daysInMonth;
477
+ return adapter.createDate(adapter.getYear(date), adapter.getMonth(date), day);
478
+ }
479
+ /** Mark `date` as the focused date the last paging operation produced. */
480
+ markPaged(date) {
481
+ this.#pagedFocus = date;
482
+ }
483
+ }
484
+
485
+ /**
486
+ * The shared navigation / selection engine for `ForCalendar`: it owns the
487
+ * {@link IntendedDayCursor} and every focused-date move — single / range
488
+ * selection, month / year paging, direct `goTo`, and the resolved day keyboard
489
+ * move — keeping the intended-day, clamp, and announce machinery in one place
490
+ * instead of scattered across the root and the sub-grids.
491
+ *
492
+ * Constructed directly (`new CalendarNavigation(host, seed)`); it holds no
493
+ * injection context, mirroring how `SegmentEditor` is lifted out of the date /
494
+ * time fields.
495
+ *
496
+ * @typeParam D The adapter's immutable date type.
497
+ */
498
+ class CalendarNavigation {
499
+ #host;
500
+ #cursor;
501
+ constructor(host, seed) {
502
+ this.#host = host;
503
+ this.#cursor = new IntendedDayCursor(host.adapter, seed);
504
+ }
505
+ /** Select `date`, unless the calendar is disabled / read-only or the date is unavailable. */
506
+ selectDate(date) {
507
+ if (this.#host.disabled() || this.#host.readonly() || this.#host.bounds.isUnavailable(date)) {
508
+ return;
509
+ }
510
+ if (this.#host.selectionMode() === 'range') {
511
+ if (this.#host.range.select(date)) {
512
+ this.#cursor.setFocusedDay(date);
513
+ this.#host.focusedDate.set(date);
514
+ this.#host.focusDayCell(date);
515
+ }
516
+ return;
517
+ }
518
+ this.#cursor.setFocusedDay(date);
519
+ this.#host.value.set(this.#withPreservedTime(date));
520
+ this.#host.focusDayCell(date);
521
+ }
522
+ /** Page the visible month by `delta` (signed month count). Keeps DOM focus on the caller. */
523
+ pageMonths(delta) {
524
+ if (this.#host.disabled()) {
525
+ return;
526
+ }
527
+ this.#pageTo(this.#host.adapter.addMonths(this.#host.focusedDate(), delta));
528
+ }
529
+ /**
530
+ * Page the visible year by `step` (signed) — the shared month / year-view
531
+ * paging path. Reached only from the disabled-guarded `pagePrevious` /
532
+ * `pageNext`, so it carries no guard of its own. Keeps DOM focus on the caller.
533
+ */
534
+ pageByYears(step) {
535
+ this.#pageTo(this.#host.adapter.addYears(this.#host.focusedDate(), step));
536
+ }
537
+ /**
538
+ * Set the visible month to (`year`, `month`, **1-12**) without selecting,
539
+ * re-applying the intended day, clamping, and announcing a month change. A
540
+ * no-op while disabled.
541
+ */
542
+ goTo(year, month) {
543
+ if (this.#host.disabled()) {
544
+ return;
545
+ }
546
+ this.#pageTo(this.#host.adapter.createDate(year, month, 1));
547
+ }
548
+ /** Apply a resolved day keyboard move: paging re-applies the intended day; a plain move records the origin. */
549
+ applyDayKeyMove(target, isPaging) {
550
+ const previousMonth = this.#host.visibleMonth();
551
+ let next;
552
+ if (isPaging) {
553
+ this.#cursor.sync(this.#host.focusedDate());
554
+ next = this.#host.bounds.clamp(this.#cursor.apply(target));
555
+ this.#cursor.markPaged(next);
556
+ }
557
+ else {
558
+ next = target;
559
+ this.#cursor.setFocusedDay(next);
560
+ }
561
+ this.#host.focusedDate.set(next);
562
+ this.#host.focusDayCell(next);
563
+ this.#announceMonthChange(previousMonth);
564
+ }
565
+ /** A date in (`year`, `month`) keeping the focused day clamped to the month length. */
566
+ dateInMonth(year, month) {
567
+ const adapter = this.#host.adapter;
568
+ const daysInMonth = adapter.getDaysInMonth(adapter.createDate(year, month, 1));
569
+ const day = Math.min(adapter.getDate(this.#host.focusedDate()), daysInMonth);
570
+ return adapter.createDate(year, month, day);
571
+ }
572
+ #pageTo(target) {
573
+ const previousMonth = this.#host.visibleMonth();
574
+ this.#cursor.sync(this.#host.focusedDate());
575
+ const next = this.#host.bounds.clamp(this.#cursor.apply(target));
576
+ this.#host.focusedDate.set(next);
577
+ this.#cursor.markPaged(next);
578
+ this.#announceMonthChange(previousMonth);
579
+ }
580
+ #announceMonthChange(previousMonth) {
581
+ if (!this.#host.adapter.isSameDay(previousMonth, this.#host.visibleMonth())) {
582
+ this.#host.announce(this.#host.visibleMonthLabel());
583
+ }
584
+ }
585
+ #withPreservedTime(date) {
586
+ const adapter = this.#host.adapter;
587
+ const current = this.#host.value();
588
+ if (current === null ||
589
+ adapter.supportsTime?.() !== true ||
590
+ !adapter.getHours ||
591
+ !adapter.getMinutes ||
592
+ !adapter.getSeconds ||
593
+ !adapter.setTime) {
594
+ return date;
595
+ }
596
+ return adapter.setTime(date, adapter.getHours(current), adapter.getMinutes(current), adapter.getSeconds(current));
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Owns the two-click anchor → commit range-selection state machine for
602
+ * `ForCalendar` in `selectionMode="range"`: the pending `anchor`, the pointer
603
+ * `hovered` cursor, the derived `effectiveRange` (committed when idle, preview
604
+ * while selecting), and the `isRangeStart` / `isRangeEnd` / `isInRange` /
605
+ * `isRangePreview` predicates the day cells reflect as `data-*`.
606
+ *
607
+ * Constructed directly (`new CalendarRangeSelection(host)`); it holds no
608
+ * injection context, mirroring how `SegmentEditor` is lifted out of the date /
609
+ * time fields. The root keeps the focus orchestration around a click and only
610
+ * delegates the range bookkeeping here.
611
+ *
612
+ * @typeParam D The adapter's immutable date type.
613
+ */
614
+ class CalendarRangeSelection {
615
+ #host;
616
+ #anchor = signal(null, /* @ts-ignore */
617
+ ...(ngDevMode ? [{ debugName: "#anchor" }] : /* istanbul ignore next */ []));
618
+ #hovered = signal(null, /* @ts-ignore */
619
+ ...(ngDevMode ? [{ debugName: "#hovered" }] : /* istanbul ignore next */ []));
620
+ #effectiveRange = computed(() => {
621
+ const adapter = this.#host.adapter;
622
+ const anchor = this.#anchor();
623
+ if (anchor !== null) {
624
+ const cursor = this.#hovered() ?? this.#host.focusedDate();
625
+ const cmp = compareDateOf(adapter, cursor, anchor);
626
+ const [start, end] = cmp < 0 ? [cursor, anchor] : [anchor, cursor];
627
+ return { start, end, preview: true };
628
+ }
629
+ const committed = this.#host.range();
630
+ return committed === null
631
+ ? null
632
+ : { start: committed.start, end: committed.end, preview: false };
633
+ }, /* @ts-ignore */
634
+ ...(ngDevMode ? [{ debugName: "#effectiveRange" }] : /* istanbul ignore next */ []));
635
+ constructor(host) {
636
+ this.#host = host;
637
+ }
638
+ isRangeStart(date) {
639
+ const er = this.#effectiveRange();
640
+ return er !== null && this.#host.adapter.isSameDay(date, er.start);
641
+ }
642
+ isRangeEnd(date) {
643
+ const er = this.#effectiveRange();
644
+ return er !== null && this.#host.adapter.isSameDay(date, er.end);
645
+ }
646
+ isInRange(date) {
647
+ const er = this.#effectiveRange();
648
+ return er !== null && !er.preview && this.#withinInclusive(date, er);
649
+ }
650
+ isRangePreview(date) {
651
+ const er = this.#effectiveRange();
652
+ return er !== null && er.preview && this.#withinInclusive(date, er);
653
+ }
654
+ /** Whether `date` is within the effective range, used by the day cell's `isSelected`. */
655
+ contains(date) {
656
+ const er = this.#effectiveRange();
657
+ return er !== null && !er.preview && this.#withinInclusive(date, er);
658
+ }
659
+ setHovered(date) {
660
+ if (!this.#host.active()) {
661
+ return;
662
+ }
663
+ this.#hovered.set(date);
664
+ }
665
+ /**
666
+ * Apply a click on `date` to the anchor → commit flow. Returns `true` when the
667
+ * click moved the selection (so the root should move focus to `date`), `false`
668
+ * when it was rejected by the min / max length guard (anchor preserved, no
669
+ * focus move).
670
+ */
671
+ select(date) {
672
+ const adapter = this.#host.adapter;
673
+ const anchor = this.#anchor();
674
+ if (anchor === null) {
675
+ this.#host.range.set(null);
676
+ this.#anchor.set(date);
677
+ }
678
+ else if (compareDateOf(adapter, date, anchor) < 0) {
679
+ this.#anchor.set(date);
680
+ }
681
+ else {
682
+ if (!this.#rangeLengthSatisfied(anchor, date)) {
683
+ return false;
684
+ }
685
+ this.#host.range.set({ start: anchor, end: date });
686
+ this.#anchor.set(null);
687
+ }
688
+ return true;
689
+ }
690
+ #rangeLengthSatisfied(anchor, end) {
691
+ const adapter = this.#host.adapter;
692
+ const minLen = this.#host.minRangeLength();
693
+ const maxLen = this.#host.maxRangeLength();
694
+ if (minLen === null && maxLen === null) {
695
+ return true;
696
+ }
697
+ let len = 1;
698
+ let cursor = anchor;
699
+ while (compareDateOf(adapter, cursor, end) < 0) {
700
+ cursor = adapter.addDays(cursor, 1);
701
+ len++;
702
+ if (maxLen !== null && len > maxLen) {
703
+ return false;
704
+ }
705
+ }
706
+ if (minLen !== null && len < minLen) {
707
+ return false;
708
+ }
709
+ return true;
710
+ }
711
+ #withinInclusive(date, range) {
712
+ const adapter = this.#host.adapter;
713
+ return (compareDateOf(adapter, date, range.start) >= 0 && compareDateOf(adapter, date, range.end) <= 0);
714
+ }
715
+ }
716
+
717
+ const GRID_COLUMNS = 3;
718
+ /**
719
+ * The year picker view's state machine (`view="year"`): a 3-column grid of an
720
+ * aligned block of `yearBlockSize` years. Owns its cell registry, its `is*`
721
+ * quartet (selected / today / focused / disabled), its table-driven keyboard
722
+ * mover, and the per-view paging / focus the root composes as a
723
+ * {@link CalendarViewStrategy}.
724
+ *
725
+ * Constructed directly by the root (`new CalendarYearNavigator(host, today)`);
726
+ * it holds no injection context, mirroring how `SegmentEditor` is lifted out of
727
+ * the date / time fields.
728
+ *
729
+ * @typeParam D The adapter's immutable date type.
730
+ */
731
+ class CalendarYearNavigator {
732
+ #host;
733
+ #today;
734
+ #cells = new Collection();
735
+ constructor(host, today) {
736
+ this.#host = host;
737
+ this.#today = today;
738
+ }
739
+ /** Rows of the year picker grid (3 columns) for the aligned block containing the visible year. */
740
+ rows() {
741
+ const start = this.#host.yearBlockStart();
742
+ const size = this.#host.yearBlockSize();
743
+ const years = [];
744
+ for (let i = 0; i < size; i++) {
745
+ const value = start + i;
746
+ years.push({ value, disabled: this.#host.isYearDisabled(value) });
747
+ }
748
+ const rows = [];
749
+ for (let i = 0; i < years.length; i += GRID_COLUMNS) {
750
+ rows.push({ key: `y-${i}`, years: years.slice(i, i + GRID_COLUMNS) });
751
+ }
752
+ return rows;
753
+ }
754
+ triggerLabel() {
755
+ const start = this.#host.yearBlockStart();
756
+ return `${start} – ${start + this.#host.yearBlockSize() - 1}`;
757
+ }
758
+ isPreviousDisabled() {
759
+ return this.#host.isYearDisabled(this.#host.yearBlockStart() - 1);
760
+ }
761
+ isNextDisabled() {
762
+ return this.#host.isYearDisabled(this.#host.yearBlockStart() + this.#host.yearBlockSize());
763
+ }
764
+ page(direction) {
765
+ this.#host.pageByYears(direction * this.#host.yearBlockSize());
766
+ }
767
+ focusActive() {
768
+ return this.focusCell(this.#host.visibleYear());
769
+ }
770
+ isSelected(year) {
771
+ const value = this.#host.value();
772
+ return value !== null && this.#host.adapter.getYear(value) === year;
773
+ }
774
+ isToday(year) {
775
+ return this.#host.adapter.getYear(this.#today) === year;
776
+ }
777
+ isFocused(year) {
778
+ return year === this.#host.visibleYear();
779
+ }
780
+ handleKeydown(event, fromYear) {
781
+ if (isCalendarActivationKey(event)) {
782
+ event.preventDefault();
783
+ this.#host.selectYear(fromYear);
784
+ return;
785
+ }
786
+ const target = this.#resolveMove(event, fromYear);
787
+ if (target === null) {
788
+ return;
789
+ }
790
+ event.preventDefault();
791
+ this.#host.setFocusedDate(this.#host.dateInMonth(target, this.#host.visibleMonthNumber()));
792
+ this.#host.scheduleFocus(() => this.focusCell(this.#host.visibleYear()));
793
+ }
794
+ register(handle) {
795
+ this.#cells.register(handle);
796
+ }
797
+ unregister(handle) {
798
+ this.#cells.unregister(handle);
799
+ }
800
+ focusCell(year) {
801
+ const handle = this.#cells.items().find((cell) => cell.year() === year);
802
+ if (!handle) {
803
+ return false;
804
+ }
805
+ handle.host.focus();
806
+ return true;
807
+ }
808
+ #resolveMove(event, fromYear) {
809
+ const size = this.#host.yearBlockSize();
810
+ const start = this.#host.yearBlockStart();
811
+ return resolveCalendarGridMove(event, this.#host.dir() === 'rtl', {
812
+ horizontal: (step) => fromYear + step,
813
+ vertical: (step) => fromYear + step * GRID_COLUMNS,
814
+ lineStart: () => start,
815
+ lineEnd: () => start + size - 1,
816
+ pageBackward: () => fromYear - size,
817
+ pageForward: () => fromYear + size,
818
+ });
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Headless implementation of a single-date calendar grid following the
824
+ * [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/), the
825
+ * date table at the heart of the APG
826
+ * [Date Picker Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/)
827
+ * example.
828
+ *
829
+ * `ForCalendar` is the root: it owns the selected value, the focused date, and
830
+ * the visible month, and exposes them to the grid / header / cell / heading /
831
+ * navigation pieces through `FOR_CALENDAR_CONTEXT`. All date math goes through
832
+ * a pluggable {@link DateAdapter} (`provideInternationalizedDateAdapter()` or
833
+ * `provideNativeDateAdapter()`), so the library hard-depends on no date
834
+ * library.
835
+ *
836
+ * The grid uses roving tabindex: exactly one cell (the focused date) carries
837
+ * `tabindex="0"`. Arrow / `Home` / `End` / `PageUp` / `PageDown` /
838
+ * `Shift+PageUp` / `Shift+PageDown` move the focused date, re-paging the grid
839
+ * when crossing a month boundary; `Enter` / `Space` select.
840
+ *
841
+ * The day / month / year views are each backed by a sub-grid collaborator
842
+ * ({@link CalendarDayNavigator} / {@link CalendarMonthNavigator} /
843
+ * {@link CalendarYearNavigator}), and range selection by
844
+ * {@link CalendarRangeSelection}; the root wires them together and exposes the
845
+ * `[(value)]` / `[(view)]` / `[(range)]` models.
846
+ *
847
+ * `ForCalendar` is the grid widget, not a form value — it exposes `[(value)]`
848
+ * as a `model<D | null>` in `selectionMode="single"` (default), and
849
+ * `[(range)]` as a `model<CalendarDateRange<D> | null>` in
850
+ * `selectionMode="range"`. The `FormValueControl<D>` contract arrives with the
851
+ * follow-up `ForDatePicker` / `ForDateField`. See the primitive's README for a
852
+ * complete styleless usage example.
853
+ *
854
+ * @typeParam D The adapter's immutable date type.
855
+ */
856
+ class ForCalendar {
857
+ #defaults = inject(FOR_CALENDAR_DEFAULTS);
858
+ #idGen = inject(IdGenerator);
859
+ #injector = inject(Injector);
860
+ #announcer = inject(LiveAnnouncer);
861
+ /** The active date adapter, resolved from `FOR_DATE_ADAPTER`. */
862
+ adapter = injectDateAdapter('ForCalendar');
863
+ /**
864
+ * Two-way bindable selected date, or `null`. The `model()` change emitter
865
+ * (`(valueChange)`) fires only when the calendar itself updates the value
866
+ * (cell activation), never on consumer writes via `[(value)]`.
867
+ */
868
+ value = model(null, /* @ts-ignore */
869
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
870
+ /** Minimum selectable date (inclusive). Dates before it are unavailable. */
871
+ min = input(null, /* @ts-ignore */
872
+ ...(ngDevMode ? [{ debugName: "min" }] : /* istanbul ignore next */ []));
873
+ /** Maximum selectable date (inclusive). Dates after it are unavailable. */
874
+ max = input(null, /* @ts-ignore */
875
+ ...(ngDevMode ? [{ debugName: "max" }] : /* istanbul ignore next */ []));
876
+ /**
877
+ * Per-date predicate marking a date as unavailable (present in the grid but
878
+ * not selectable, reflected as `aria-disabled` / `data-disabled`).
879
+ */
880
+ isDateUnavailable = input(() => false, /* @ts-ignore */
881
+ ...(ngDevMode ? [{ debugName: "isDateUnavailable" }] : /* istanbul ignore next */ []));
882
+ /**
883
+ * Formats the full accessible date string each gridcell exposes as its
884
+ * `aria-label` (the visible content stays the bare day number). Defaults to
885
+ * the localized full date (e.g. `"Monday, June 15, 2026"`), with
886
+ * outside-month padding days suffixed (`" (outside month)"`) so assistive
887
+ * tech can tell them apart from the visible month. Override to localize that
888
+ * suffix or change the format entirely.
889
+ */
890
+ dateLabel = input((date, { adapter, outsideMonth }) => {
891
+ const formatted = adapter.format(date, {
892
+ weekday: 'long',
893
+ day: 'numeric',
894
+ month: 'long',
895
+ year: 'numeric',
896
+ });
897
+ return outsideMonth ? `${formatted} (outside month)` : formatted;
898
+ }, /* @ts-ignore */
899
+ ...(ngDevMode ? [{ debugName: "dateLabel" }] : /* istanbul ignore next */ []));
900
+ /** Disables the whole calendar: no focus movement, no selection. */
901
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
902
+ /** Makes the calendar read-only: dates stay focusable but selection is blocked. */
903
+ readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
904
+ /**
905
+ * Selection mode. `'single'` (default) keeps the single-date `[(value)]`
906
+ * behaviour unchanged. `'range'` switches to the two-click anchor → commit
907
+ * flow and exposes the result through `[(range)]`.
908
+ */
909
+ selectionMode = input('single', /* @ts-ignore */
910
+ ...(ngDevMode ? [{ debugName: "selectionMode" }] : /* istanbul ignore next */ []));
911
+ /**
912
+ * Active calendar view. `'day'` (default) shows the date grid; `'month'` and
913
+ * `'year'` show the month / year picker grids. Two-way bindable; the `model()`
914
+ * change emitter (`(viewChange)`) fires only when the calendar itself cycles
915
+ * or drills the view, never on consumer writes via `[(view)]`.
916
+ */
917
+ view = model('day', /* @ts-ignore */
918
+ ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
919
+ /**
920
+ * Number of years the year view shows, as an aligned block containing the
921
+ * visible year. Default `12`.
922
+ */
923
+ yearBlockSize = input(12, { ...(ngDevMode ? { debugName: "yearBlockSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
924
+ /**
925
+ * Two-way bindable committed date range, or `null`. Only used when
926
+ * `selectionMode="range"`. The `model()` change emitter (`(rangeChange)`)
927
+ * fires only when the calendar internally commits or clears a range, never
928
+ * on consumer writes via `[(range)]`.
929
+ */
930
+ range = model(null, /* @ts-ignore */
931
+ ...(ngDevMode ? [{ debugName: "range" }] : /* istanbul ignore next */ []));
932
+ /**
933
+ * Minimum inclusive day count for a range selection. A click that would
934
+ * commit a range shorter than this is a no-op (anchor is preserved).
935
+ * `null` (default) means no minimum. Only honoured in `selectionMode="range"`.
936
+ */
937
+ minRangeLength = input(null, /* @ts-ignore */
938
+ ...(ngDevMode ? [{ debugName: "minRangeLength" }] : /* istanbul ignore next */ []));
939
+ /**
940
+ * Maximum inclusive day count for a range selection. A click that would
941
+ * commit a range longer than this is a no-op (anchor is preserved).
942
+ * `null` (default) means no maximum. Only honoured in `selectionMode="range"`.
943
+ */
944
+ maxRangeLength = input(null, /* @ts-ignore */
945
+ ...(ngDevMode ? [{ debugName: "maxRangeLength" }] : /* istanbul ignore next */ []));
946
+ /**
947
+ * First day of the week as a **0-6** index (`0` = Sunday). When `null`
948
+ * (default, overridable via `provideForCalendarDefaults`), the adapter's
949
+ * `getFirstDayOfWeek()` is used.
950
+ */
951
+ firstDayOfWeek = input(this.#defaults.firstDayOfWeek, /* @ts-ignore */
952
+ ...(ngDevMode ? [{ debugName: "firstDayOfWeek" }] : /* istanbul ignore next */ []));
953
+ /**
954
+ * Writing direction. When unset (default `null`), the inherited ambient
955
+ * direction is resolved from the nearest ancestor carrying a `dir` attribute
956
+ * (or `<html dir>`), defaulting to `'ltr'`. An explicit `[dir]` always wins.
957
+ * The resolved value is reflected to the host `dir` attribute and swaps the
958
+ * `ArrowLeft` / `ArrowRight` semantics in RTL.
959
+ */
960
+ _dirInput = input(null, { ...(ngDevMode ? { debugName: "_dirInput" } : /* istanbul ignore next */ {}), alias: 'dir' });
961
+ dir = injectTextDirection(this._dirInput);
962
+ #today = this.adapter.today();
963
+ /** Internal focused date (the roving entry point), seeded from `value ?? range.start ?? today`. */
964
+ focusedDate = linkedSignal(() => this.value() ?? this.range()?.start ?? this.#today, /* @ts-ignore */
965
+ ...(ngDevMode ? [{ debugName: "focusedDate" }] : /* istanbul ignore next */ []));
966
+ #bounds = new CalendarBounds({
967
+ adapter: this.adapter,
968
+ min: this.min,
969
+ max: this.max,
970
+ visibleMonth: () => this.visibleMonth(),
971
+ isDateUnavailable: this.isDateUnavailable,
972
+ disabled: this.disabled,
973
+ });
974
+ #range = new CalendarRangeSelection({
975
+ adapter: this.adapter,
976
+ focusedDate: this.focusedDate,
977
+ range: this.range,
978
+ active: computed(() => this.selectionMode() === 'range'),
979
+ minRangeLength: this.minRangeLength,
980
+ maxRangeLength: this.maxRangeLength,
981
+ });
982
+ #nav = new CalendarNavigation({
983
+ adapter: this.adapter,
984
+ disabled: this.disabled,
985
+ readonly: this.readonly,
986
+ selectionMode: this.selectionMode,
987
+ focusedDate: this.focusedDate,
988
+ value: this.value,
989
+ visibleMonth: () => this.visibleMonth(),
990
+ visibleMonthLabel: () => this.visibleMonthLabel(),
991
+ visibleYear: () => this.visibleYear(),
992
+ visibleMonthNumber: () => this.visibleMonthNumber(),
993
+ bounds: this.#bounds,
994
+ range: this.#range,
995
+ announce: (label) => this.#announcer.announce(label, 'polite'),
996
+ scheduleFocus: (fn) => this.#scheduleFocus(fn),
997
+ focusDayCell: (target) => this.#focusDayCell(target),
998
+ }, this.value() ?? this.range()?.start ?? this.#today);
999
+ #dayNav = new CalendarDayNavigator({
1000
+ ...this.#subGridHost(),
1001
+ visibleMonth: () => this.visibleMonth(),
1002
+ firstDayOfWeek: () => this.#resolvedFirstDayOfWeek(),
1003
+ getDateLabel: (date) => this.getDateLabel(date),
1004
+ selectDate: (date) => this.#nav.selectDate(date),
1005
+ pageMonths: (delta) => this.#nav.pageMonths(delta),
1006
+ applyDayKeyMove: (target, isPaging) => this.#nav.applyDayKeyMove(target, isPaging),
1007
+ });
1008
+ #monthNav = new CalendarMonthNavigator({
1009
+ ...this.#subGridHost(),
1010
+ ...this.#pickerViewHost(),
1011
+ isMonthOutOfBounds: (year, month) => this.#bounds.isMonthOutOfBounds(year, month),
1012
+ selectMonth: (month) => this.selectMonth(month),
1013
+ }, this.#today);
1014
+ #yearNav = new CalendarYearNavigator({
1015
+ ...this.#subGridHost(),
1016
+ ...this.#pickerViewHost(),
1017
+ selectYear: (year) => this.selectYear(year),
1018
+ }, this.#today);
1019
+ #strategies = {
1020
+ day: this.#dayNav,
1021
+ month: this.#monthNav,
1022
+ year: this.#yearNav,
1023
+ };
1024
+ #strategyFor(view) {
1025
+ return this.#strategies[view];
1026
+ }
1027
+ /** The reactive root surface shared by all three sub-grid navigators. */
1028
+ #subGridHost() {
1029
+ return {
1030
+ adapter: this.adapter,
1031
+ dir: this.dir,
1032
+ yearBlockSize: () => this.yearBlockSize(),
1033
+ yearBlockStart: () => this.#yearBlockStart(),
1034
+ visibleMonthLabel: () => this.visibleMonthLabel(),
1035
+ visibleYear: () => this.visibleYear(),
1036
+ visibleMonthNumber: () => this.visibleMonthNumber(),
1037
+ focusedDate: this.focusedDate,
1038
+ isPreviousMonthDisabled: this.#bounds.isPreviousMonthDisabled,
1039
+ isNextMonthDisabled: this.#bounds.isNextMonthDisabled,
1040
+ isYearDisabled: (year) => this.#bounds.isYearDisabled(year),
1041
+ };
1042
+ }
1043
+ /** The navigation / focus surface shared by the month and year picker navigators. */
1044
+ #pickerViewHost() {
1045
+ return {
1046
+ value: () => this.value(),
1047
+ pageByYears: (step) => this.#nav.pageByYears(step),
1048
+ setFocusedDate: (date) => this.focusedDate.set(date),
1049
+ dateInMonth: (year, month) => this.#nav.dateInMonth(year, month),
1050
+ scheduleFocus: (fn) => this.#scheduleFocus(fn),
1051
+ };
1052
+ }
1053
+ #viewSwitched = false;
1054
+ #viewFocusEffect = effect(() => {
1055
+ const view = this.view();
1056
+ if (!this.#viewSwitched) {
1057
+ this.#viewSwitched = true;
1058
+ return;
1059
+ }
1060
+ afterNextRender(() => this.#strategyFor(view).focusActive(), { injector: this.#injector });
1061
+ }, /* @ts-ignore */
1062
+ ...(ngDevMode ? [{ debugName: "#viewFocusEffect" }] : /* istanbul ignore next */ []));
1063
+ #resolvedFirstDayOfWeek = computed(() => this.firstDayOfWeek() ?? this.adapter.getFirstDayOfWeek(), /* @ts-ignore */
1064
+ ...(ngDevMode ? [{ debugName: "#resolvedFirstDayOfWeek" }] : /* istanbul ignore next */ []));
1065
+ /** First day of the visible month, derived from {@link focusedDate}. */
1066
+ visibleMonth = computed(() => {
1067
+ const focused = this.focusedDate();
1068
+ return this.adapter.createDate(this.adapter.getYear(focused), this.adapter.getMonth(focused), 1);
1069
+ }, /* @ts-ignore */
1070
+ ...(ngDevMode ? [{ debugName: "visibleMonth" }] : /* istanbul ignore next */ []));
1071
+ #headingId = signal(this.#idGen.next('for-calendar-heading'), /* @ts-ignore */
1072
+ ...(ngDevMode ? [{ debugName: "#headingId" }] : /* istanbul ignore next */ []));
1073
+ headingId = this.#headingId.asReadonly();
1074
+ /**
1075
+ * Adopts a consumer-set static `id` on the `[forCalendarHeading]` host into
1076
+ * `headingId` (the grid's `aria-labelledby` resolves to it) instead of
1077
+ * letting the `[id]` host binding clobber it.
1078
+ */
1079
+ adoptHeadingId(el) {
1080
+ adoptHostId(el, this.#headingId);
1081
+ }
1082
+ visibleMonthLabel = computed(() => this.adapter.format(this.visibleMonth(), { month: 'long', year: 'numeric' }), /* @ts-ignore */
1083
+ ...(ngDevMode ? [{ debugName: "visibleMonthLabel" }] : /* istanbul ignore next */ []));
1084
+ /** The visible month's full year (e.g. `2026`). */
1085
+ visibleYear = computed(() => this.adapter.getYear(this.visibleMonth()), /* @ts-ignore */
1086
+ ...(ngDevMode ? [{ debugName: "visibleYear" }] : /* istanbul ignore next */ []));
1087
+ /** The visible month, **1-12**. */
1088
+ visibleMonthNumber = computed(() => this.adapter.getMonth(this.visibleMonth()), /* @ts-ignore */
1089
+ ...(ngDevMode ? [{ debugName: "visibleMonthNumber" }] : /* istanbul ignore next */ []));
1090
+ /**
1091
+ * Twelve entries for the visible year — each a localized month name plus
1092
+ * whether the whole month falls outside `[min, max]`. Tracks {@link visibleYear}.
1093
+ * Build native `<select>` month dropdowns from it.
1094
+ */
1095
+ monthOptions = computed(() => this.#monthNav.options(), /* @ts-ignore */
1096
+ ...(ngDevMode ? [{ debugName: "monthOptions" }] : /* istanbul ignore next */ []));
1097
+ #yearBlockStart = computed(() => {
1098
+ const size = this.yearBlockSize();
1099
+ return Math.floor(this.visibleYear() / size) * size;
1100
+ }, /* @ts-ignore */
1101
+ ...(ngDevMode ? [{ debugName: "#yearBlockStart" }] : /* istanbul ignore next */ []));
1102
+ /** Rows of the month picker grid (3 columns) for the visible year. */
1103
+ monthRows = computed(() => this.#monthNav.rows(), /* @ts-ignore */
1104
+ ...(ngDevMode ? [{ debugName: "monthRows" }] : /* istanbul ignore next */ []));
1105
+ /** Rows of the year picker grid (3 columns) for the aligned block containing the visible year. */
1106
+ yearRows = computed(() => this.#yearNav.rows(), /* @ts-ignore */
1107
+ ...(ngDevMode ? [{ debugName: "yearRows" }] : /* istanbul ignore next */ []));
1108
+ /** Label for the view trigger / heading, reflecting the active view. */
1109
+ viewTriggerLabel = computed(() => this.#strategyFor(this.view()).triggerLabel(), /* @ts-ignore */
1110
+ ...(ngDevMode ? [{ debugName: "viewTriggerLabel" }] : /* istanbul ignore next */ []));
1111
+ isPreviousDisabled = computed(() => this.#strategyFor(this.view()).isPreviousDisabled(), /* @ts-ignore */
1112
+ ...(ngDevMode ? [{ debugName: "isPreviousDisabled" }] : /* istanbul ignore next */ []));
1113
+ isNextDisabled = computed(() => this.#strategyFor(this.view()).isNextDisabled(), /* @ts-ignore */
1114
+ ...(ngDevMode ? [{ debugName: "isNextDisabled" }] : /* istanbul ignore next */ []));
1115
+ /** Weekday column headers for the visible month, starting at `firstDayOfWeek`. */
1116
+ weekDays = this.#dayNav.weekDays;
1117
+ /** Week rows of the visible month, including outside-month padding days. */
1118
+ weeks = this.#dayNav.weeks;
1119
+ /** Whether the previous-month button should be disabled (bounded by `min`). */
1120
+ isPreviousMonthDisabled = this.#bounds.isPreviousMonthDisabled;
1121
+ /** Whether the next-month button should be disabled (bounded by `max`). */
1122
+ isNextMonthDisabled = this.#bounds.isNextMonthDisabled;
1123
+ /**
1124
+ * Whether every day of `month` (**1-12**) in the visible year falls outside
1125
+ * `[min, max]`. Use it to disable a month in a custom dropdown.
1126
+ */
1127
+ isMonthDisabled(month) {
1128
+ return this.#bounds.isMonthOutOfBounds(this.visibleYear(), month);
1129
+ }
1130
+ /**
1131
+ * Whether every day of `year` falls outside `[min, max]`. Use it to disable a
1132
+ * year in a custom dropdown.
1133
+ */
1134
+ isYearDisabled(year) {
1135
+ return this.#bounds.isYearDisabled(year);
1136
+ }
1137
+ isMonthSelected(month) {
1138
+ return this.#monthNav.isSelected(month);
1139
+ }
1140
+ isMonthToday(month) {
1141
+ return this.#monthNav.isToday(month);
1142
+ }
1143
+ isMonthFocused(month) {
1144
+ return this.#monthNav.isFocused(month);
1145
+ }
1146
+ isYearSelected(year) {
1147
+ return this.#yearNav.isSelected(year);
1148
+ }
1149
+ isYearToday(year) {
1150
+ return this.#yearNav.isToday(year);
1151
+ }
1152
+ isYearFocused(year) {
1153
+ return this.#yearNav.isFocused(year);
1154
+ }
1155
+ isSelected(date) {
1156
+ if (this.selectionMode() === 'range') {
1157
+ return this.#range.contains(date);
1158
+ }
1159
+ const value = this.value();
1160
+ return value !== null && this.adapter.isSameDay(date, value);
1161
+ }
1162
+ isRangeStart(date) {
1163
+ return this.#range.isRangeStart(date);
1164
+ }
1165
+ isRangeEnd(date) {
1166
+ return this.#range.isRangeEnd(date);
1167
+ }
1168
+ isInRange(date) {
1169
+ return this.#range.isInRange(date);
1170
+ }
1171
+ isRangePreview(date) {
1172
+ return this.#range.isRangePreview(date);
1173
+ }
1174
+ setHovered(date) {
1175
+ this.#range.setHovered(date);
1176
+ }
1177
+ isToday(date) {
1178
+ return this.adapter.isSameDay(date, this.#today);
1179
+ }
1180
+ isFocused(date) {
1181
+ return this.adapter.isSameDay(date, this.focusedDate());
1182
+ }
1183
+ isOutsideMonth(date) {
1184
+ const visible = this.visibleMonth();
1185
+ return (this.adapter.getMonth(date) !== this.adapter.getMonth(visible) ||
1186
+ this.adapter.getYear(date) !== this.adapter.getYear(visible));
1187
+ }
1188
+ isUnavailable(date) {
1189
+ return this.#bounds.isUnavailable(date);
1190
+ }
1191
+ getDateLabel(date) {
1192
+ return this.dateLabel()(date, {
1193
+ adapter: this.adapter,
1194
+ outsideMonth: this.isOutsideMonth(date),
1195
+ });
1196
+ }
1197
+ selectDate(date) {
1198
+ this.#nav.selectDate(date);
1199
+ }
1200
+ pageMonths(delta) {
1201
+ this.#nav.pageMonths(delta);
1202
+ }
1203
+ /**
1204
+ * Set the visible month to (`year`, `month`) without selecting a date.
1205
+ * `month` is **1-12**. Re-applies the user's intended day-of-month (clamped to
1206
+ * the target month's length), clamps the result into `[min, max]`, and
1207
+ * announces the new period politely when the visible month changes. Keeps DOM
1208
+ * focus on the caller — it does not move focus into the grid. A no-op while the
1209
+ * calendar is disabled.
1210
+ */
1211
+ goTo(year, month) {
1212
+ this.#nav.goTo(year, month);
1213
+ }
1214
+ /** Set the visible month within the current visible year. `month` is **1-12**. */
1215
+ goToMonth(month) {
1216
+ this.goTo(this.visibleYear(), month);
1217
+ }
1218
+ /** Set the visible year, keeping the current visible month. */
1219
+ goToYear(year) {
1220
+ this.goTo(year, this.visibleMonthNumber());
1221
+ }
1222
+ handleCellKeydown(event, fromDate) {
1223
+ if (this.disabled()) {
1224
+ return;
1225
+ }
1226
+ this.#dayNav.handleKeydown(event, fromDate);
1227
+ }
1228
+ /**
1229
+ * Cycle the view one step coarser: `day → month → year`, clamped at `'year'`.
1230
+ * No-op when the whole calendar is disabled.
1231
+ */
1232
+ cycleView() {
1233
+ if (this.disabled()) {
1234
+ return;
1235
+ }
1236
+ const order = ['day', 'month', 'year'];
1237
+ const next = order[order.indexOf(this.view()) + 1];
1238
+ if (next) {
1239
+ this.view.set(next);
1240
+ }
1241
+ }
1242
+ /**
1243
+ * Select a whole month (1-12): navigate to it within the current visible year
1244
+ * and switch the view to `'day'`. No-op when the calendar is disabled /
1245
+ * read-only or the month is out of `[min, max]`.
1246
+ */
1247
+ selectMonth(month) {
1248
+ if (this.disabled() || this.readonly() || this.isMonthDisabled(month)) {
1249
+ return;
1250
+ }
1251
+ this.goToMonth(month);
1252
+ this.view.set('day');
1253
+ }
1254
+ /**
1255
+ * Select a whole year: navigate to it and switch the view to `'month'`. No-op
1256
+ * when the calendar is disabled / read-only or the year is out of `[min, max]`.
1257
+ */
1258
+ selectYear(year) {
1259
+ if (this.disabled() || this.readonly() || this.isYearDisabled(year)) {
1260
+ return;
1261
+ }
1262
+ this.goToYear(year);
1263
+ this.view.set('month');
1264
+ }
1265
+ /** Page backward by one month / year / block depending on the active view. */
1266
+ pagePrevious() {
1267
+ this.#page(-1);
1268
+ }
1269
+ /** Page forward by one month / year / block depending on the active view. */
1270
+ pageNext() {
1271
+ this.#page(1);
1272
+ }
1273
+ #page(direction) {
1274
+ if (this.disabled()) {
1275
+ return;
1276
+ }
1277
+ this.#strategyFor(this.view()).page(direction);
1278
+ }
1279
+ /**
1280
+ * Move DOM focus to the roving cell — the gridcell carrying the tab stop,
1281
+ * i.e. the one matching {@link focusedDate} (day view) or the highlighted
1282
+ * month / year cell (month / year view). Returns `false` when no matching
1283
+ * cell is currently rendered, so an overlay host wrapping the calendar can
1284
+ * fall back to its own focus logic.
1285
+ */
1286
+ focusActiveCell() {
1287
+ return this.#strategyFor(this.view()).focusActive();
1288
+ }
1289
+ registerCell(handle) {
1290
+ this.#dayNav.register(handle);
1291
+ }
1292
+ unregisterCell(handle) {
1293
+ this.#dayNav.unregister(handle);
1294
+ }
1295
+ handleMonthCellKeydown(event, fromMonth) {
1296
+ if (this.disabled()) {
1297
+ return;
1298
+ }
1299
+ this.#monthNav.handleKeydown(event, fromMonth);
1300
+ }
1301
+ handleYearCellKeydown(event, fromYear) {
1302
+ if (this.disabled()) {
1303
+ return;
1304
+ }
1305
+ this.#yearNav.handleKeydown(event, fromYear);
1306
+ }
1307
+ registerMonthCell(handle) {
1308
+ this.#monthNav.register(handle);
1309
+ }
1310
+ unregisterMonthCell(handle) {
1311
+ this.#monthNav.unregister(handle);
1312
+ }
1313
+ registerYearCell(handle) {
1314
+ this.#yearNav.register(handle);
1315
+ }
1316
+ unregisterYearCell(handle) {
1317
+ this.#yearNav.unregister(handle);
1318
+ }
1319
+ #scheduleFocus(fn) {
1320
+ afterNextRender(fn, { injector: this.#injector });
1321
+ }
1322
+ #focusDayCell(target) {
1323
+ afterNextRender(() => this.#dayNav.focusCell(target), { injector: this.#injector });
1324
+ }
1325
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendar, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1326
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendar, isStandalone: true, selector: "[forCalendar]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, isDateUnavailable: { classPropertyName: "isDateUnavailable", publicName: "isDateUnavailable", isSignal: true, isRequired: false, transformFunction: null }, dateLabel: { classPropertyName: "dateLabel", publicName: "dateLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, selectionMode: { classPropertyName: "selectionMode", publicName: "selectionMode", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, yearBlockSize: { classPropertyName: "yearBlockSize", publicName: "yearBlockSize", isSignal: true, isRequired: false, transformFunction: null }, range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, minRangeLength: { classPropertyName: "minRangeLength", publicName: "minRangeLength", isSignal: true, isRequired: false, transformFunction: null }, maxRangeLength: { classPropertyName: "maxRangeLength", publicName: "maxRangeLength", isSignal: true, isRequired: false, transformFunction: null }, firstDayOfWeek: { classPropertyName: "firstDayOfWeek", publicName: "firstDayOfWeek", isSignal: true, isRequired: false, transformFunction: null }, _dirInput: { classPropertyName: "_dirInput", publicName: "dir", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", view: "viewChange", range: "rangeChange" }, host: { properties: { "attr.dir": "dir()", "attr.data-disabled": "disabled() ? \"\" : null", "attr.data-readonly": "readonly() ? \"\" : null", "attr.data-view": "view()" } }, providers: [{ provide: FOR_CALENDAR_CONTEXT, useExisting: ForCalendar }], exportAs: ["forCalendar"], ngImport: i0 });
1327
+ }
1328
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendar, decorators: [{
1329
+ type: Directive,
1330
+ args: [{
1331
+ selector: '[forCalendar]',
1332
+ exportAs: 'forCalendar',
1333
+ host: {
1334
+ '[attr.dir]': 'dir()',
1335
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1336
+ '[attr.data-readonly]': 'readonly() ? "" : null',
1337
+ '[attr.data-view]': 'view()',
1338
+ },
1339
+ providers: [{ provide: FOR_CALENDAR_CONTEXT, useExisting: ForCalendar }],
1340
+ }]
1341
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], min: [{ type: i0.Input, args: [{ isSignal: true, alias: "min", required: false }] }], max: [{ type: i0.Input, args: [{ isSignal: true, alias: "max", required: false }] }], isDateUnavailable: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDateUnavailable", required: false }] }], dateLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "dateLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], selectionMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectionMode", required: false }] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }, { type: i0.Output, args: ["viewChange"] }], yearBlockSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "yearBlockSize", required: false }] }], range: [{ type: i0.Input, args: [{ isSignal: true, alias: "range", required: false }] }, { type: i0.Output, args: ["rangeChange"] }], minRangeLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "minRangeLength", required: false }] }], maxRangeLength: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxRangeLength", required: false }] }], firstDayOfWeek: [{ type: i0.Input, args: [{ isSignal: true, alias: "firstDayOfWeek", required: false }] }], _dirInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "dir", required: false }] }] } });
1342
+
1343
+ /**
1344
+ * The calendar's date table (`role="grid"`). Apply on a `<table>`. Labelled by
1345
+ * the `[forCalendarHeading]` via `aria-labelledby`, and reflects `aria-disabled`
1346
+ * / `aria-readonly` from the root.
1347
+ *
1348
+ * Exposes the month's `weekDays()` (column headers) and `weeks()` (day rows)
1349
+ * for the consumer to iterate — bind `cell.date` to each `[forCalendarCell]`.
1350
+ */
1351
+ class ForCalendarGrid {
1352
+ ctx = injectCalendarContext('ForCalendarGrid');
1353
+ /** Weekday column headers for the visible month, starting at `firstDayOfWeek`. */
1354
+ weekDays = this.ctx.weekDays;
1355
+ /** Week rows of the visible month, including outside-month padding days. */
1356
+ weeks = this.ctx.weeks;
1357
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarGrid, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1358
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarGrid, isStandalone: true, selector: "[forCalendarGrid]", host: { attributes: { "role": "grid" }, properties: { "attr.aria-labelledby": "ctx.headingId()", "attr.aria-disabled": "ctx.disabled() ? \"true\" : null", "attr.aria-readonly": "ctx.readonly() ? \"true\" : null" } }, exportAs: ["forCalendarGrid"], ngImport: i0 });
1359
+ }
1360
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarGrid, decorators: [{
1361
+ type: Directive,
1362
+ args: [{
1363
+ selector: '[forCalendarGrid]',
1364
+ exportAs: 'forCalendarGrid',
1365
+ host: {
1366
+ role: 'grid',
1367
+ '[attr.aria-labelledby]': 'ctx.headingId()',
1368
+ '[attr.aria-disabled]': 'ctx.disabled() ? "true" : null',
1369
+ '[attr.aria-readonly]': 'ctx.readonly() ? "true" : null',
1370
+ },
1371
+ }]
1372
+ }] });
1373
+
1374
+ /**
1375
+ * Marks the calendar grid's header rowgroup. Apply on a `<thead>` holding a
1376
+ * single row of `<th scope="col">` weekday headers.
1377
+ *
1378
+ * Reflects `role="rowgroup"` so the weekday header is exposed as a distinct
1379
+ * rowgroup inside the `role="grid"` table — assistive tech announces the
1380
+ * column headers as their own group rather than folding them into the day
1381
+ * rows. (A bare `<thead>` carries the implicit role, but the grid's explicit
1382
+ * `role="grid"` suppresses the table's native row/rowgroup semantics, so the
1383
+ * header must restate it.)
1384
+ *
1385
+ * Also exposes `weekDays()` so the header row can be iterated from this piece,
1386
+ * as an alternative to reading it off `[forCalendarGrid]`.
1387
+ */
1388
+ class ForCalendarGridHeader {
1389
+ ctx = injectCalendarContext('ForCalendarGridHeader');
1390
+ /** Weekday column headers for the visible month, starting at `firstDayOfWeek`. */
1391
+ weekDays = this.ctx.weekDays;
1392
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarGridHeader, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1393
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarGridHeader, isStandalone: true, selector: "[forCalendarGridHeader]", host: { attributes: { "role": "rowgroup" } }, exportAs: ["forCalendarGridHeader"], ngImport: i0 });
1394
+ }
1395
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarGridHeader, decorators: [{
1396
+ type: Directive,
1397
+ args: [{
1398
+ selector: '[forCalendarGridHeader]',
1399
+ exportAs: 'forCalendarGridHeader',
1400
+ host: {
1401
+ role: 'rowgroup',
1402
+ },
1403
+ }]
1404
+ }] });
1405
+
1406
+ /**
1407
+ * A single day cell (`role="gridcell"`). Apply on the `<td>` for each day in
1408
+ * the grid and bind `[date]` to the cell's date from `weeks()`.
1409
+ *
1410
+ * Carries the roving tab stop (`tabindex="0"` only on the focused date), the
1411
+ * full accessible date as `aria-label` (the bare day number stays the visible
1412
+ * content), `aria-selected` (always emitted), `aria-current="date"` on today,
1413
+ * `aria-disabled` on unavailable dates, and the boolean `data-*` styling hooks
1414
+ * (`data-selected`, `data-today`, `data-highlighted`, `data-disabled`,
1415
+ * `data-outside-month`). In `selectionMode="range"`, also emits
1416
+ * `data-range-start`, `data-range-end`, `data-in-range`, and
1417
+ * `data-range-preview` (all boolean present/absent). Click and `Enter` /
1418
+ * `Space` select; arrow / paging keys move the focused date.
1419
+ *
1420
+ * @typeParam D The adapter's date type (inferred from the bound `[date]`).
1421
+ */
1422
+ class ForCalendarCell {
1423
+ ctx = injectCalendarContext('ForCalendarCell');
1424
+ #host = inject(ElementRef);
1425
+ /** The date this cell represents. */
1426
+ date = input.required(/* @ts-ignore */
1427
+ ...(ngDevMode ? [{ debugName: "date" }] : /* istanbul ignore next */ []));
1428
+ selected = computed(() => this.ctx.isSelected(this.date()), /* @ts-ignore */
1429
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1430
+ isToday = computed(() => this.ctx.isToday(this.date()), /* @ts-ignore */
1431
+ ...(ngDevMode ? [{ debugName: "isToday" }] : /* istanbul ignore next */ []));
1432
+ focused = computed(() => this.ctx.isFocused(this.date()), /* @ts-ignore */
1433
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1434
+ unavailable = computed(() => this.ctx.isUnavailable(this.date()), /* @ts-ignore */
1435
+ ...(ngDevMode ? [{ debugName: "unavailable" }] : /* istanbul ignore next */ []));
1436
+ outsideMonth = computed(() => this.ctx.isOutsideMonth(this.date()), /* @ts-ignore */
1437
+ ...(ngDevMode ? [{ debugName: "outsideMonth" }] : /* istanbul ignore next */ []));
1438
+ dateLabel = computed(() => this.ctx.getDateLabel(this.date()), /* @ts-ignore */
1439
+ ...(ngDevMode ? [{ debugName: "dateLabel" }] : /* istanbul ignore next */ []));
1440
+ rangeStart = computed(() => this.ctx.isRangeStart(this.date()), /* @ts-ignore */
1441
+ ...(ngDevMode ? [{ debugName: "rangeStart" }] : /* istanbul ignore next */ []));
1442
+ rangeEnd = computed(() => this.ctx.isRangeEnd(this.date()), /* @ts-ignore */
1443
+ ...(ngDevMode ? [{ debugName: "rangeEnd" }] : /* istanbul ignore next */ []));
1444
+ inRange = computed(() => this.ctx.isInRange(this.date()), /* @ts-ignore */
1445
+ ...(ngDevMode ? [{ debugName: "inRange" }] : /* istanbul ignore next */ []));
1446
+ rangePreview = computed(() => this.ctx.isRangePreview(this.date()), /* @ts-ignore */
1447
+ ...(ngDevMode ? [{ debugName: "rangePreview" }] : /* istanbul ignore next */ []));
1448
+ constructor() {
1449
+ const handle = {
1450
+ host: this.#host.nativeElement,
1451
+ date: this.date,
1452
+ };
1453
+ registerHandle(handle, (h) => this.ctx.registerCell(h), (h) => this.ctx.unregisterCell(h));
1454
+ }
1455
+ onClick() {
1456
+ this.ctx.selectDate(this.date());
1457
+ }
1458
+ onKeyDown(event) {
1459
+ this.ctx.handleCellKeydown(event, this.date());
1460
+ }
1461
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1462
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendarCell, isStandalone: true, selector: "[forCalendarCell]", inputs: { date: { classPropertyName: "date", publicName: "date", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "gridcell" }, listeners: { "click": "onClick()", "keydown": "onKeyDown($event)", "pointerenter": "ctx.setHovered(date())", "pointerleave": "ctx.setHovered(null)" }, properties: { "attr.tabindex": "focused() ? 0 : -1", "attr.aria-label": "dateLabel()", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.aria-current": "isToday() ? \"date\" : null", "attr.aria-disabled": "unavailable() ? \"true\" : null", "attr.data-selected": "selected() ? \"\" : null", "attr.data-today": "isToday() ? \"\" : null", "attr.data-highlighted": "focused() ? \"\" : null", "attr.data-disabled": "unavailable() ? \"\" : null", "attr.data-outside-month": "outsideMonth() ? \"\" : null", "attr.data-range-start": "rangeStart() ? \"\" : null", "attr.data-range-end": "rangeEnd() ? \"\" : null", "attr.data-in-range": "inRange() ? \"\" : null", "attr.data-range-preview": "rangePreview() ? \"\" : null" } }, exportAs: ["forCalendarCell"], ngImport: i0 });
1463
+ }
1464
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarCell, decorators: [{
1465
+ type: Directive,
1466
+ args: [{
1467
+ selector: '[forCalendarCell]',
1468
+ exportAs: 'forCalendarCell',
1469
+ host: {
1470
+ role: 'gridcell',
1471
+ '[attr.tabindex]': 'focused() ? 0 : -1',
1472
+ '[attr.aria-label]': 'dateLabel()',
1473
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
1474
+ '[attr.aria-current]': 'isToday() ? "date" : null',
1475
+ '[attr.aria-disabled]': 'unavailable() ? "true" : null',
1476
+ '[attr.data-selected]': 'selected() ? "" : null',
1477
+ '[attr.data-today]': 'isToday() ? "" : null',
1478
+ '[attr.data-highlighted]': 'focused() ? "" : null',
1479
+ '[attr.data-disabled]': 'unavailable() ? "" : null',
1480
+ '[attr.data-outside-month]': 'outsideMonth() ? "" : null',
1481
+ '[attr.data-range-start]': 'rangeStart() ? "" : null',
1482
+ '[attr.data-range-end]': 'rangeEnd() ? "" : null',
1483
+ '[attr.data-in-range]': 'inRange() ? "" : null',
1484
+ '[attr.data-range-preview]': 'rangePreview() ? "" : null',
1485
+ '(click)': 'onClick()',
1486
+ '(keydown)': 'onKeyDown($event)',
1487
+ '(pointerenter)': 'ctx.setHovered(date())',
1488
+ '(pointerleave)': 'ctx.setHovered(null)',
1489
+ },
1490
+ }]
1491
+ }], ctorParameters: () => [], propDecorators: { date: [{ type: i0.Input, args: [{ isSignal: true, alias: "date", required: true }] }] } });
1492
+
1493
+ /**
1494
+ * The month/year heading. Apply on a heading element (e.g. `<h2>`) and render
1495
+ * its `label()` as the content: `<h2 forCalendarHeading #h="forCalendarHeading">{{ h.label() }}</h2>`.
1496
+ * The grid is `aria-labelledby` this element, so it names the visible period.
1497
+ *
1498
+ * It is **not** an `aria-live` region: paging the month announces the new
1499
+ * period through a dedicated off-screen live region owned by `[forCalendar]`,
1500
+ * so the grid's accessible name is re-read on demand without the heading
1501
+ * itself double-announcing on every render.
1502
+ */
1503
+ class ForCalendarHeading {
1504
+ ctx = injectCalendarContext('ForCalendarHeading');
1505
+ /** The visible month's accessible label, e.g. `"June 2026"`. */
1506
+ label = this.ctx.visibleMonthLabel;
1507
+ constructor() {
1508
+ this.ctx.adoptHeadingId(inject(ElementRef).nativeElement);
1509
+ }
1510
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarHeading, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1511
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarHeading, isStandalone: true, selector: "[forCalendarHeading]", host: { properties: { "id": "ctx.headingId()" } }, exportAs: ["forCalendarHeading"], ngImport: i0 });
1512
+ }
1513
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarHeading, decorators: [{
1514
+ type: Directive,
1515
+ args: [{
1516
+ selector: '[forCalendarHeading]',
1517
+ exportAs: 'forCalendarHeading',
1518
+ host: {
1519
+ '[id]': 'ctx.headingId()',
1520
+ },
1521
+ }]
1522
+ }], ctorParameters: () => [] });
1523
+
1524
+ /**
1525
+ * Pages the calendar backward. In `day` view pages by one month; in `month` view
1526
+ * pages by one year; in `year` view pages by one block. Apply on a `<button>`.
1527
+ * Keeps DOM focus on itself while paging; the `aria-live` heading announces the
1528
+ * new period. Auto-disabled at the view's bound, or when the whole calendar is
1529
+ * disabled.
1530
+ *
1531
+ * Provide an accessible name via the `[ariaLabel]` input (e.g.
1532
+ * `[ariaLabel]="'Previous'"`).
1533
+ */
1534
+ class ForCalendarPrevButton {
1535
+ ctx = injectCalendarContext('ForCalendarPrevButton');
1536
+ /** Accessible name for the button. A `null` (default) or empty value emits no attribute. */
1537
+ ariaLabel = input(null, /* @ts-ignore */
1538
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1539
+ disabled = computed(() => this.ctx.disabled() || this.ctx.isPreviousDisabled(), /* @ts-ignore */
1540
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1541
+ constructor() {
1542
+ reflectDisabled(this.disabled);
1543
+ }
1544
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarPrevButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1545
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendarPrevButton, isStandalone: true, selector: "[forCalendarPrevButton]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, listeners: { "click": "ctx.pagePrevious()" }, properties: { "attr.aria-label": "ariaLabel() || null", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-disabled": "disabled() ? \"\" : null" } }, exportAs: ["forCalendarPrevButton"], ngImport: i0 });
1546
+ }
1547
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarPrevButton, decorators: [{
1548
+ type: Directive,
1549
+ args: [{
1550
+ selector: '[forCalendarPrevButton]',
1551
+ exportAs: 'forCalendarPrevButton',
1552
+ host: {
1553
+ type: 'button',
1554
+ '[attr.aria-label]': 'ariaLabel() || null',
1555
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
1556
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1557
+ '(click)': 'ctx.pagePrevious()',
1558
+ },
1559
+ }]
1560
+ }], ctorParameters: () => [], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
1561
+
1562
+ /**
1563
+ * Pages the calendar forward. In `day` view pages by one month; in `month` view
1564
+ * pages by one year; in `year` view pages by one block. Apply on a `<button>`.
1565
+ * Keeps DOM focus on itself while paging; the `aria-live` heading announces the
1566
+ * new period. Auto-disabled at the view's bound, or when the whole calendar is
1567
+ * disabled.
1568
+ *
1569
+ * Provide an accessible name via the `[ariaLabel]` input (e.g.
1570
+ * `[ariaLabel]="'Next'"`).
1571
+ */
1572
+ class ForCalendarNextButton {
1573
+ ctx = injectCalendarContext('ForCalendarNextButton');
1574
+ /** Accessible name for the button. A `null` (default) or empty value emits no attribute. */
1575
+ ariaLabel = input(null, /* @ts-ignore */
1576
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
1577
+ disabled = computed(() => this.ctx.disabled() || this.ctx.isNextDisabled(), /* @ts-ignore */
1578
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1579
+ constructor() {
1580
+ reflectDisabled(this.disabled);
1581
+ }
1582
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarNextButton, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1583
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendarNextButton, isStandalone: true, selector: "[forCalendarNextButton]", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button" }, listeners: { "click": "ctx.pageNext()" }, properties: { "attr.aria-label": "ariaLabel() || null", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-disabled": "disabled() ? \"\" : null" } }, exportAs: ["forCalendarNextButton"], ngImport: i0 });
1584
+ }
1585
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarNextButton, decorators: [{
1586
+ type: Directive,
1587
+ args: [{
1588
+ selector: '[forCalendarNextButton]',
1589
+ exportAs: 'forCalendarNextButton',
1590
+ host: {
1591
+ type: 'button',
1592
+ '[attr.aria-label]': 'ariaLabel() || null',
1593
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
1594
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1595
+ '(click)': 'ctx.pageNext()',
1596
+ },
1597
+ }]
1598
+ }], ctorParameters: () => [], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
1599
+
1600
+ /**
1601
+ * The month picker grid (`role="grid"`). Apply on a `<table>`. Labelled by the
1602
+ * `[forCalendarHeading]` via `aria-labelledby`. Exposes `rows()` — iterate
1603
+ * rows then cells, binding each cell's `month` to `[forCalendarMonthCell]`.
1604
+ *
1605
+ * Shows the 12 months of the visible year in 3-column rows. Used when
1606
+ * `view === 'month'`.
1607
+ */
1608
+ class ForCalendarMonthGrid {
1609
+ ctx = injectCalendarContext('ForCalendarMonthGrid');
1610
+ /** Rows of the month picker grid (3 columns) for the visible year. */
1611
+ rows = this.ctx.monthRows;
1612
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarMonthGrid, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1613
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarMonthGrid, isStandalone: true, selector: "[forCalendarMonthGrid]", host: { attributes: { "role": "grid" }, properties: { "attr.aria-labelledby": "ctx.headingId()", "attr.aria-disabled": "ctx.disabled() ? \"true\" : null", "attr.aria-readonly": "ctx.readonly() ? \"true\" : null", "attr.data-view": "\"month\"" } }, exportAs: ["forCalendarMonthGrid"], ngImport: i0 });
1614
+ }
1615
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarMonthGrid, decorators: [{
1616
+ type: Directive,
1617
+ args: [{
1618
+ selector: '[forCalendarMonthGrid]',
1619
+ exportAs: 'forCalendarMonthGrid',
1620
+ host: {
1621
+ role: 'grid',
1622
+ '[attr.aria-labelledby]': 'ctx.headingId()',
1623
+ '[attr.aria-disabled]': 'ctx.disabled() ? "true" : null',
1624
+ '[attr.aria-readonly]': 'ctx.readonly() ? "true" : null',
1625
+ '[attr.data-view]': '"month"',
1626
+ },
1627
+ }]
1628
+ }] });
1629
+
1630
+ /**
1631
+ * A single month cell (`role="gridcell"`) in the month picker grid. Apply on
1632
+ * the `<td>` for each month and bind `[month]` to the month number (1-12) from
1633
+ * `rows()`.
1634
+ *
1635
+ * Carries the roving tab stop (`tabindex="0"` on the focused month), reflects
1636
+ * `aria-selected` (always emitted), `data-today`, `data-selected`,
1637
+ * `data-highlighted`, `data-disabled`. Click and `Enter` / `Space` drill down
1638
+ * to the day view for that month; arrow / paging keys move the roving cell.
1639
+ */
1640
+ class ForCalendarMonthCell {
1641
+ ctx = injectCalendarContext('ForCalendarMonthCell');
1642
+ #host = inject(ElementRef);
1643
+ /** The month this cell represents, **1-12**. */
1644
+ month = input.required(/* @ts-ignore */
1645
+ ...(ngDevMode ? [{ debugName: "month" }] : /* istanbul ignore next */ []));
1646
+ selected = computed(() => this.ctx.isMonthSelected(this.month()), /* @ts-ignore */
1647
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1648
+ today = computed(() => this.ctx.isMonthToday(this.month()), /* @ts-ignore */
1649
+ ...(ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []));
1650
+ focused = computed(() => this.ctx.isMonthFocused(this.month()), /* @ts-ignore */
1651
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1652
+ disabled = computed(() => this.ctx.isMonthDisabled(this.month()), /* @ts-ignore */
1653
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1654
+ constructor() {
1655
+ const handle = {
1656
+ host: this.#host.nativeElement,
1657
+ month: this.month,
1658
+ };
1659
+ registerHandle(handle, (h) => this.ctx.registerMonthCell(h), (h) => this.ctx.unregisterMonthCell(h));
1660
+ }
1661
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarMonthCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1662
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendarMonthCell, isStandalone: true, selector: "[forCalendarMonthCell]", inputs: { month: { classPropertyName: "month", publicName: "month", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "gridcell" }, listeners: { "click": "ctx.selectMonth(month())", "keydown": "ctx.handleMonthCellKeydown($event, month())" }, properties: { "attr.tabindex": "focused() ? 0 : -1", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-selected": "selected() ? \"\" : null", "attr.data-today": "today() ? \"\" : null", "attr.data-highlighted": "focused() ? \"\" : null", "attr.data-disabled": "disabled() ? \"\" : null" } }, exportAs: ["forCalendarMonthCell"], ngImport: i0 });
1663
+ }
1664
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarMonthCell, decorators: [{
1665
+ type: Directive,
1666
+ args: [{
1667
+ selector: '[forCalendarMonthCell]',
1668
+ exportAs: 'forCalendarMonthCell',
1669
+ host: {
1670
+ role: 'gridcell',
1671
+ '[attr.tabindex]': 'focused() ? 0 : -1',
1672
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
1673
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
1674
+ '[attr.data-selected]': 'selected() ? "" : null',
1675
+ '[attr.data-today]': 'today() ? "" : null',
1676
+ '[attr.data-highlighted]': 'focused() ? "" : null',
1677
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1678
+ '(click)': 'ctx.selectMonth(month())',
1679
+ '(keydown)': 'ctx.handleMonthCellKeydown($event, month())',
1680
+ },
1681
+ }]
1682
+ }], ctorParameters: () => [], propDecorators: { month: [{ type: i0.Input, args: [{ isSignal: true, alias: "month", required: true }] }] } });
1683
+
1684
+ /**
1685
+ * The year picker grid (`role="grid"`). Apply on a `<table>`. Labelled by the
1686
+ * `[forCalendarHeading]` via `aria-labelledby`. Exposes `rows()` — iterate
1687
+ * rows then cells, binding each cell's `year` to `[forCalendarYearCell]`.
1688
+ *
1689
+ * Shows an aligned block of `yearBlockSize` years in 3-column rows. Used when
1690
+ * `view === 'year'`.
1691
+ */
1692
+ class ForCalendarYearGrid {
1693
+ ctx = injectCalendarContext('ForCalendarYearGrid');
1694
+ /** Rows of the year picker grid (3 columns) for the aligned block containing the visible year. */
1695
+ rows = this.ctx.yearRows;
1696
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarYearGrid, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1697
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarYearGrid, isStandalone: true, selector: "[forCalendarYearGrid]", host: { attributes: { "role": "grid" }, properties: { "attr.aria-labelledby": "ctx.headingId()", "attr.aria-disabled": "ctx.disabled() ? \"true\" : null", "attr.aria-readonly": "ctx.readonly() ? \"true\" : null", "attr.data-view": "\"year\"" } }, exportAs: ["forCalendarYearGrid"], ngImport: i0 });
1698
+ }
1699
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarYearGrid, decorators: [{
1700
+ type: Directive,
1701
+ args: [{
1702
+ selector: '[forCalendarYearGrid]',
1703
+ exportAs: 'forCalendarYearGrid',
1704
+ host: {
1705
+ role: 'grid',
1706
+ '[attr.aria-labelledby]': 'ctx.headingId()',
1707
+ '[attr.aria-disabled]': 'ctx.disabled() ? "true" : null',
1708
+ '[attr.aria-readonly]': 'ctx.readonly() ? "true" : null',
1709
+ '[attr.data-view]': '"year"',
1710
+ },
1711
+ }]
1712
+ }] });
1713
+
1714
+ /**
1715
+ * A single year cell (`role="gridcell"`) in the year picker grid. Apply on the
1716
+ * `<td>` for each year and bind `[year]` to the year number from `rows()`.
1717
+ *
1718
+ * Carries the roving tab stop (`tabindex="0"` on the focused year), reflects
1719
+ * `aria-selected` (always emitted), `data-today`, `data-selected`,
1720
+ * `data-highlighted`, `data-disabled`. Click and `Enter` / `Space` drill down
1721
+ * to the month view for that year; arrow / paging keys move the roving cell.
1722
+ */
1723
+ class ForCalendarYearCell {
1724
+ ctx = injectCalendarContext('ForCalendarYearCell');
1725
+ #host = inject(ElementRef);
1726
+ /** The year this cell represents. */
1727
+ year = input.required(/* @ts-ignore */
1728
+ ...(ngDevMode ? [{ debugName: "year" }] : /* istanbul ignore next */ []));
1729
+ selected = computed(() => this.ctx.isYearSelected(this.year()), /* @ts-ignore */
1730
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
1731
+ today = computed(() => this.ctx.isYearToday(this.year()), /* @ts-ignore */
1732
+ ...(ngDevMode ? [{ debugName: "today" }] : /* istanbul ignore next */ []));
1733
+ focused = computed(() => this.ctx.isYearFocused(this.year()), /* @ts-ignore */
1734
+ ...(ngDevMode ? [{ debugName: "focused" }] : /* istanbul ignore next */ []));
1735
+ disabled = computed(() => this.ctx.isYearDisabled(this.year()), /* @ts-ignore */
1736
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
1737
+ constructor() {
1738
+ const handle = {
1739
+ host: this.#host.nativeElement,
1740
+ year: this.year,
1741
+ };
1742
+ registerHandle(handle, (h) => this.ctx.registerYearCell(h), (h) => this.ctx.unregisterYearCell(h));
1743
+ }
1744
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarYearCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1745
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendarYearCell, isStandalone: true, selector: "[forCalendarYearCell]", inputs: { year: { classPropertyName: "year", publicName: "year", isSignal: true, isRequired: true, transformFunction: null } }, host: { attributes: { "role": "gridcell" }, listeners: { "click": "ctx.selectYear(year())", "keydown": "ctx.handleYearCellKeydown($event, year())" }, properties: { "attr.tabindex": "focused() ? 0 : -1", "attr.aria-selected": "selected() ? \"true\" : \"false\"", "attr.aria-disabled": "disabled() ? \"true\" : null", "attr.data-selected": "selected() ? \"\" : null", "attr.data-today": "today() ? \"\" : null", "attr.data-highlighted": "focused() ? \"\" : null", "attr.data-disabled": "disabled() ? \"\" : null" } }, exportAs: ["forCalendarYearCell"], ngImport: i0 });
1746
+ }
1747
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarYearCell, decorators: [{
1748
+ type: Directive,
1749
+ args: [{
1750
+ selector: '[forCalendarYearCell]',
1751
+ exportAs: 'forCalendarYearCell',
1752
+ host: {
1753
+ role: 'gridcell',
1754
+ '[attr.tabindex]': 'focused() ? 0 : -1',
1755
+ '[attr.aria-selected]': 'selected() ? "true" : "false"',
1756
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
1757
+ '[attr.data-selected]': 'selected() ? "" : null',
1758
+ '[attr.data-today]': 'today() ? "" : null',
1759
+ '[attr.data-highlighted]': 'focused() ? "" : null',
1760
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1761
+ '(click)': 'ctx.selectYear(year())',
1762
+ '(keydown)': 'ctx.handleYearCellKeydown($event, year())',
1763
+ },
1764
+ }]
1765
+ }], ctorParameters: () => [], propDecorators: { year: [{ type: i0.Input, args: [{ isSignal: true, alias: "year", required: true }] }] } });
1766
+
1767
+ /**
1768
+ * The view-cycle button in the calendar header. Apply on a `<button>`. Each
1769
+ * click advances the view one step coarser (`day → month → year`), clamped at
1770
+ * `'year'`. Reflects `data-view` so consumers can style it per view. Disabled
1771
+ * when the whole calendar is disabled.
1772
+ *
1773
+ * Bind the `label` signal to the button text to show the active view label
1774
+ * (e.g. `"June 2026"` in day view, `"2026"` in month view, `"2024 – 2035"` in
1775
+ * year view).
1776
+ */
1777
+ class ForCalendarViewTrigger {
1778
+ ctx = injectCalendarContext('ForCalendarViewTrigger');
1779
+ /** Label for the active view, e.g. `"June 2026"` / `"2026"` / `"2016 – 2027"`. */
1780
+ label = this.ctx.viewTriggerLabel;
1781
+ constructor() {
1782
+ reflectDisabled(this.ctx.disabled);
1783
+ }
1784
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarViewTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1785
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarViewTrigger, isStandalone: true, selector: "[forCalendarViewTrigger]", host: { attributes: { "type": "button" }, listeners: { "click": "ctx.cycleView()" }, properties: { "attr.data-view": "ctx.view()", "attr.aria-disabled": "ctx.disabled() ? \"true\" : null" } }, exportAs: ["forCalendarViewTrigger"], ngImport: i0 });
1786
+ }
1787
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarViewTrigger, decorators: [{
1788
+ type: Directive,
1789
+ args: [{
1790
+ selector: '[forCalendarViewTrigger]',
1791
+ exportAs: 'forCalendarViewTrigger',
1792
+ host: {
1793
+ type: 'button',
1794
+ '[attr.data-view]': 'ctx.view()',
1795
+ '[attr.aria-disabled]': 'ctx.disabled() ? "true" : null',
1796
+ '(click)': 'ctx.cycleView()',
1797
+ },
1798
+ }]
1799
+ }], ctorParameters: () => [] });
1800
+
1801
+ /**
1802
+ * Wires a native `<select>` to a `ForCalendar`'s month navigation. Apply on a
1803
+ * `<select>` element inside `[forCalendar]`: the directive keeps the select
1804
+ * showing the visible month and navigates the calendar when the user picks a
1805
+ * different one, without mutating the selected date.
1806
+ *
1807
+ * Render the `<option>` elements yourself from {@link options} so you keep full
1808
+ * control over their markup, labels, and any placeholder:
1809
+ *
1810
+ * ```html
1811
+ * <select forCalendarMonthSelect #m="forCalendarMonthSelect">
1812
+ * @for (opt of m.options(); track opt.value) {
1813
+ * <option [value]="opt.value" [disabled]="opt.disabled">{{ opt.label }}</option>
1814
+ * }
1815
+ * </select>
1816
+ * ```
1817
+ */
1818
+ class ForCalendarMonthSelect {
1819
+ ctx = injectCalendarContext('ForCalendarMonthSelect');
1820
+ /** The twelve localized, bounds-aware month options for the visible year. */
1821
+ options = this.ctx.monthOptions;
1822
+ onChange(event) {
1823
+ this.ctx.goToMonth(Number(event.target.value));
1824
+ }
1825
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarMonthSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1826
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForCalendarMonthSelect, isStandalone: true, selector: "select[forCalendarMonthSelect]", host: { listeners: { "change": "onChange($event)" }, properties: { "value": "ctx.visibleMonthNumber()", "disabled": "ctx.disabled()" } }, exportAs: ["forCalendarMonthSelect"], ngImport: i0 });
1827
+ }
1828
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarMonthSelect, decorators: [{
1829
+ type: Directive,
1830
+ args: [{
1831
+ selector: 'select[forCalendarMonthSelect]',
1832
+ exportAs: 'forCalendarMonthSelect',
1833
+ host: {
1834
+ '[value]': 'ctx.visibleMonthNumber()',
1835
+ '[disabled]': 'ctx.disabled()',
1836
+ '(change)': 'onChange($event)',
1837
+ },
1838
+ }]
1839
+ }] });
1840
+
1841
+ /**
1842
+ * Wires a native `<select>` to a `ForCalendar`'s year navigation. Apply on a
1843
+ * `<select>` element inside `[forCalendar]`: the directive keeps the select
1844
+ * showing the visible year and navigates the calendar when the user picks a
1845
+ * different one, without mutating the selected date.
1846
+ *
1847
+ * Render the `<option>` elements yourself from {@link years} so you keep full
1848
+ * control over their markup:
1849
+ *
1850
+ * ```html
1851
+ * <select forCalendarYearSelect #y="forCalendarYearSelect" [minYear]="1900" [maxYear]="2100">
1852
+ * @for (opt of y.years(); track opt.value) {
1853
+ * <option [value]="opt.value" [disabled]="opt.disabled">{{ opt.value }}</option>
1854
+ * }
1855
+ * </select>
1856
+ * ```
1857
+ */
1858
+ class ForCalendarYearSelect {
1859
+ ctx = injectCalendarContext('ForCalendarYearSelect');
1860
+ /**
1861
+ * Lowest year listed in {@link years}. Defaults to 100 years before the
1862
+ * current year when `null`.
1863
+ */
1864
+ minYear = input(null, /* @ts-ignore */
1865
+ ...(ngDevMode ? [{ debugName: "minYear" }] : /* istanbul ignore next */ []));
1866
+ /**
1867
+ * Highest year listed in {@link years}. Defaults to 10 years after the
1868
+ * current year when `null`.
1869
+ */
1870
+ maxYear = input(null, /* @ts-ignore */
1871
+ ...(ngDevMode ? [{ debugName: "maxYear" }] : /* istanbul ignore next */ []));
1872
+ /**
1873
+ * The selectable years, from {@link minYear} to {@link maxYear} inclusive.
1874
+ * Each entry is `disabled` when the whole year falls outside the calendar's
1875
+ * `[min, max]`. The default window is anchored to the current year (not the
1876
+ * visible year), so navigating far away never drops the current year off the
1877
+ * list.
1878
+ */
1879
+ years = computed(() => {
1880
+ const currentYear = this.ctx.adapter.getYear(this.ctx.adapter.today());
1881
+ const min = this.minYear() ?? currentYear - 100;
1882
+ const max = this.maxYear() ?? currentYear + 10;
1883
+ const out = [];
1884
+ for (let year = min; year <= max; year++) {
1885
+ out.push({ value: year, disabled: this.ctx.isYearDisabled(year) });
1886
+ }
1887
+ return out;
1888
+ }, /* @ts-ignore */
1889
+ ...(ngDevMode ? [{ debugName: "years" }] : /* istanbul ignore next */ []));
1890
+ onChange(event) {
1891
+ this.ctx.goToYear(Number(event.target.value));
1892
+ }
1893
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarYearSelect, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1894
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForCalendarYearSelect, isStandalone: true, selector: "select[forCalendarYearSelect]", inputs: { minYear: { classPropertyName: "minYear", publicName: "minYear", isSignal: true, isRequired: false, transformFunction: null }, maxYear: { classPropertyName: "maxYear", publicName: "maxYear", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "change": "onChange($event)" }, properties: { "value": "ctx.visibleYear()", "disabled": "ctx.disabled()" } }, exportAs: ["forCalendarYearSelect"], ngImport: i0 });
1895
+ }
1896
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForCalendarYearSelect, decorators: [{
1897
+ type: Directive,
1898
+ args: [{
1899
+ selector: 'select[forCalendarYearSelect]',
1900
+ exportAs: 'forCalendarYearSelect',
1901
+ host: {
1902
+ '[value]': 'ctx.visibleYear()',
1903
+ '[disabled]': 'ctx.disabled()',
1904
+ '(change)': 'onChange($event)',
1905
+ },
1906
+ }]
1907
+ }], propDecorators: { minYear: [{ type: i0.Input, args: [{ isSignal: true, alias: "minYear", required: false }] }], maxYear: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxYear", required: false }] }] } });
1908
+
1909
+ function daysInMonth(year, month) {
1910
+ return new Date(year, month, 0).getDate();
1911
+ }
1912
+ function clampDay(year, month, day) {
1913
+ return new Date(year, month - 1, Math.min(day, daysInMonth(year, month)));
1914
+ }
1915
+ /**
1916
+ * Zero-dependency {@link DateAdapter} over the built-in `Date`. `today` and
1917
+ * `createDate` produce days at local midnight, and the calendar grid stays
1918
+ * day-granular. It is also **time-capable** — `Date` natively carries a
1919
+ * wall-clock time, so the optional `getHours` / `getMinutes` / `getSeconds` /
1920
+ * `setTime` accessors are implemented, letting `ForTimeField` use this adapter
1921
+ * directly. Consistently with that, `compare` orders by the full instant
1922
+ * (day *and* time), matching `InternationalizedDateTimeAdapter`, so a date-time
1923
+ * `minDate`/`maxDate` clamps identically on both.
1924
+ *
1925
+ * This is the fallback adapter. Prefer `provideInternationalizedDateAdapter()`
1926
+ * for correct date-math and locale-aware formatting (or
1927
+ * `provideInternationalizedDateTimeAdapter()` for a time-capable one); reach
1928
+ * for this one when adding a date library to the bundle is not worthwhile. Both
1929
+ * `Date` and the `@internationalized/date` adapters operate on the Gregorian
1930
+ * calendar today; non-Gregorian calendar systems are deferred to #354.
1931
+ */
1932
+ class NativeDateAdapter {
1933
+ /**
1934
+ * Today at local midnight in the runtime time zone. Subject to the
1935
+ * SSR/hydration caveat on {@link DateAdapter.today}.
1936
+ */
1937
+ today() {
1938
+ const now = new Date();
1939
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
1940
+ }
1941
+ createDate(year, month, day) {
1942
+ return clampDay(year, month, day);
1943
+ }
1944
+ getYear(date) {
1945
+ return date.getFullYear();
1946
+ }
1947
+ getMonth(date) {
1948
+ return date.getMonth() + 1;
1949
+ }
1950
+ getDate(date) {
1951
+ return date.getDate();
1952
+ }
1953
+ getDayOfWeek(date) {
1954
+ return date.getDay();
1955
+ }
1956
+ getDaysInMonth(date) {
1957
+ return daysInMonth(date.getFullYear(), date.getMonth() + 1);
1958
+ }
1959
+ getFirstDayOfWeek() {
1960
+ return 0;
1961
+ }
1962
+ addDays(date, n) {
1963
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + n);
1964
+ }
1965
+ addMonths(date, n) {
1966
+ const total = date.getMonth() + n;
1967
+ const year = date.getFullYear() + Math.floor(total / 12);
1968
+ const month = ((total % 12) + 12) % 12;
1969
+ return clampDay(year, month + 1, date.getDate());
1970
+ }
1971
+ addYears(date, n) {
1972
+ return clampDay(date.getFullYear() + n, date.getMonth() + 1, date.getDate());
1973
+ }
1974
+ compare(a, b) {
1975
+ return a.getTime() - b.getTime();
1976
+ }
1977
+ isSameDay(a, b) {
1978
+ return (a.getFullYear() === b.getFullYear() &&
1979
+ a.getMonth() === b.getMonth() &&
1980
+ a.getDate() === b.getDate());
1981
+ }
1982
+ isValid(date) {
1983
+ return date instanceof Date && !Number.isNaN(date.getTime());
1984
+ }
1985
+ /**
1986
+ * Formats through the runtime's default locale. Subject to the SSR/hydration
1987
+ * caveat on {@link DateAdapter.format}.
1988
+ */
1989
+ format(date, options) {
1990
+ return new Intl.DateTimeFormat(undefined, options).format(date);
1991
+ }
1992
+ supportsTime() {
1993
+ return true;
1994
+ }
1995
+ getHours(date) {
1996
+ return date.getHours();
1997
+ }
1998
+ getMinutes(date) {
1999
+ return date.getMinutes();
2000
+ }
2001
+ getSeconds(date) {
2002
+ return date.getSeconds();
2003
+ }
2004
+ setTime(date, hours, minutes, seconds) {
2005
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, minutes, seconds);
2006
+ }
2007
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NativeDateAdapter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2008
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NativeDateAdapter });
2009
+ }
2010
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: NativeDateAdapter, decorators: [{
2011
+ type: Injectable
2012
+ }] });
2013
+ /**
2014
+ * Provides the zero-dependency {@link NativeDateAdapter} as the active
2015
+ * {@link DateAdapter}. Add to your application or component providers when
2016
+ * `ForCalendar` should operate on native `Date` values.
2017
+ *
2018
+ * @example
2019
+ * ```ts
2020
+ * bootstrapApplication(App, {
2021
+ * providers: [provideNativeDateAdapter()],
2022
+ * });
2023
+ * ```
2024
+ */
2025
+ function provideNativeDateAdapter() {
2026
+ return [{ provide: FOR_DATE_ADAPTER, useClass: NativeDateAdapter }];
2027
+ }
2028
+
2029
+ /**
2030
+ * Generated bundle index. Do not edit.
2031
+ */
2032
+
2033
+ export { FOR_CALENDAR_CONTEXT, FOR_CALENDAR_DEFAULTS, ForCalendar, ForCalendarCell, ForCalendarGrid, ForCalendarGridHeader, ForCalendarHeading, ForCalendarMonthCell, ForCalendarMonthGrid, ForCalendarMonthSelect, ForCalendarNextButton, ForCalendarPrevButton, ForCalendarViewTrigger, ForCalendarYearCell, ForCalendarYearGrid, ForCalendarYearSelect, NativeDateAdapter, provideForCalendarDefaults, provideNativeDateAdapter };
2034
+ //# sourceMappingURL=forty-cdk-calendar.mjs.map