forty-cdk 0.28.0 → 0.30.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 (178) hide show
  1. package/README.md +11 -8
  2. package/breadcrumbs/README.md +1 -1
  3. package/breakpoints/README.md +1 -1
  4. package/calendar/README.md +17 -17
  5. package/combobox/README.md +60 -15
  6. package/date-adapter/README.md +48 -0
  7. package/date-field/README.md +17 -15
  8. package/date-picker/README.md +74 -23
  9. package/defaults/README.md +100 -0
  10. package/drawer/README.md +1 -1
  11. package/fesm2022/forty-cdk-avatar.mjs +4 -26
  12. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  13. package/fesm2022/forty-cdk-breadcrumbs.mjs +4 -26
  14. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -1
  15. package/fesm2022/forty-cdk-breakpoints.mjs +7 -61
  16. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  17. package/fesm2022/forty-cdk-calendar.mjs +7 -159
  18. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  19. package/fesm2022/forty-cdk-carousel.mjs +4 -37
  20. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  21. package/fesm2022/forty-cdk-combobox.mjs +151 -50
  22. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-context-menu.mjs +4 -30
  24. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  25. package/fesm2022/forty-cdk-core-overlay.mjs +18 -129
  26. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  27. package/fesm2022/forty-cdk-core.mjs +124 -135
  28. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  29. package/fesm2022/forty-cdk-date-adapter.mjs +188 -0
  30. package/fesm2022/forty-cdk-date-adapter.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-date-field.mjs +78 -100
  32. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  33. package/fesm2022/forty-cdk-date-picker.mjs +192 -92
  34. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-defaults.mjs +1506 -0
  36. package/fesm2022/forty-cdk-defaults.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-dialog.mjs +6 -34
  38. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  39. package/fesm2022/forty-cdk-drag-drop.mjs +4 -32
  40. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  41. package/fesm2022/forty-cdk-drawer.mjs +6 -46
  42. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  43. package/fesm2022/forty-cdk-dropdown-menu.mjs +4 -30
  44. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  45. package/fesm2022/forty-cdk-field.mjs +95 -7
  46. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  47. package/fesm2022/forty-cdk-file-upload.mjs +14 -3
  48. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  49. package/fesm2022/forty-cdk-hover-card.mjs +6 -56
  50. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  51. package/fesm2022/forty-cdk-input.mjs +31 -6
  52. package/fesm2022/forty-cdk-input.mjs.map +1 -1
  53. package/fesm2022/forty-cdk-internationalized-date.mjs +19 -3
  54. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  55. package/fesm2022/forty-cdk-listbox.mjs +4 -36
  56. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  57. package/fesm2022/forty-cdk-menu.mjs +4 -33
  58. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  59. package/fesm2022/forty-cdk-menubar.mjs +4 -30
  60. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  61. package/fesm2022/forty-cdk-navigation-menu.mjs +5 -29
  62. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  63. package/fesm2022/forty-cdk-number-input.mjs +4 -26
  64. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  65. package/fesm2022/forty-cdk-otp-input.mjs +17 -3
  66. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  67. package/fesm2022/forty-cdk-pagination.mjs +4 -27
  68. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-popover.mjs +12 -32
  70. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  71. package/fesm2022/forty-cdk-progress.mjs +4 -27
  72. package/fesm2022/forty-cdk-progress.mjs.map +1 -1
  73. package/fesm2022/forty-cdk-radio-group.mjs +4 -26
  74. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  75. package/fesm2022/forty-cdk-scroll-area.mjs +4 -29
  76. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  77. package/fesm2022/forty-cdk-search.mjs +4 -26
  78. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  79. package/fesm2022/forty-cdk-select.mjs +13 -36
  80. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  81. package/fesm2022/forty-cdk-shared.mjs +2 -1
  82. package/fesm2022/forty-cdk-shared.mjs.map +1 -1
  83. package/fesm2022/forty-cdk-slider.mjs +4 -26
  84. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  85. package/fesm2022/forty-cdk-stepper.mjs +4 -29
  86. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  87. package/fesm2022/forty-cdk-table-virtualization.mjs +6 -0
  88. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  89. package/fesm2022/forty-cdk-table.mjs +166 -83
  90. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  91. package/fesm2022/forty-cdk-tabs.mjs +4 -27
  92. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  93. package/fesm2022/forty-cdk-testing.mjs +381 -0
  94. package/fesm2022/forty-cdk-testing.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-time-field.mjs +78 -94
  96. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  97. package/fesm2022/forty-cdk-time-picker.mjs +134 -38
  98. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  99. package/fesm2022/forty-cdk-toast.mjs +4 -36
  100. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  101. package/fesm2022/forty-cdk-toggle.mjs +18 -29
  102. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  103. package/fesm2022/forty-cdk-toolbar.mjs +4 -26
  104. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  105. package/fesm2022/forty-cdk-tooltip.mjs +6 -80
  106. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  107. package/fesm2022/forty-cdk-tree.mjs +71 -63
  108. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  109. package/field/README.md +40 -1
  110. package/file-upload/README.md +20 -12
  111. package/hover-card/README.md +1 -1
  112. package/input/README.md +50 -25
  113. package/internationalized-date/README.md +1 -1
  114. package/listbox/README.md +1 -1
  115. package/menu/README.md +2 -2
  116. package/otp-input/README.md +10 -4
  117. package/package.json +13 -1
  118. package/popover/README.md +1 -1
  119. package/select/README.md +2 -2
  120. package/shared/README.md +14 -14
  121. package/table/README.md +120 -30
  122. package/table-virtualization/README.md +24 -3
  123. package/testing/README.md +97 -0
  124. package/time-field/README.md +8 -6
  125. package/time-picker/README.md +76 -28
  126. package/toast/README.md +1 -1
  127. package/toggle/README.md +19 -18
  128. package/tooltip/README.md +1 -1
  129. package/tree/README.md +44 -23
  130. package/types/forty-cdk-avatar.d.ts +4 -31
  131. package/types/forty-cdk-breadcrumbs.d.ts +2 -31
  132. package/types/forty-cdk-breakpoints.d.ts +14 -64
  133. package/types/forty-cdk-calendar.d.ts +12 -107
  134. package/types/forty-cdk-carousel.d.ts +6 -92
  135. package/types/forty-cdk-combobox.d.ts +104 -89
  136. package/types/forty-cdk-context-menu.d.ts +7 -58
  137. package/types/forty-cdk-core-overlay.d.ts +29 -125
  138. package/types/forty-cdk-core.d.ts +232 -299
  139. package/types/forty-cdk-date-adapter.d.ts +296 -0
  140. package/types/forty-cdk-date-field.d.ts +31 -126
  141. package/types/forty-cdk-date-picker.d.ts +152 -117
  142. package/types/forty-cdk-defaults.d.ts +2125 -0
  143. package/types/forty-cdk-dialog.d.ts +3 -69
  144. package/types/forty-cdk-drag-drop.d.ts +5 -62
  145. package/types/forty-cdk-drawer.d.ts +6 -127
  146. package/types/forty-cdk-dropdown-menu.d.ts +6 -56
  147. package/types/forty-cdk-field.d.ts +82 -32
  148. package/types/forty-cdk-file-upload.d.ts +30 -20
  149. package/types/forty-cdk-hover-card.d.ts +8 -80
  150. package/types/forty-cdk-input.d.ts +12 -0
  151. package/types/forty-cdk-internationalized-date.d.ts +17 -5
  152. package/types/forty-cdk-listbox.d.ts +4 -51
  153. package/types/forty-cdk-menu.d.ts +5 -88
  154. package/types/forty-cdk-menubar.d.ts +7 -55
  155. package/types/forty-cdk-navigation-menu.d.ts +4 -33
  156. package/types/forty-cdk-number-input.d.ts +4 -29
  157. package/types/forty-cdk-otp-input.d.ts +12 -2
  158. package/types/forty-cdk-pagination.d.ts +4 -27
  159. package/types/forty-cdk-popover.d.ts +7 -55
  160. package/types/forty-cdk-progress.d.ts +4 -38
  161. package/types/forty-cdk-radio-group.d.ts +4 -29
  162. package/types/forty-cdk-scroll-area.d.ts +5 -53
  163. package/types/forty-cdk-search.d.ts +5 -31
  164. package/types/forty-cdk-select.d.ts +11 -54
  165. package/types/forty-cdk-shared.d.ts +2 -1
  166. package/types/forty-cdk-slider.d.ts +4 -29
  167. package/types/forty-cdk-stepper.d.ts +5 -50
  168. package/types/forty-cdk-table-virtualization.d.ts +3 -1
  169. package/types/forty-cdk-table.d.ts +96 -20
  170. package/types/forty-cdk-tabs.d.ts +5 -37
  171. package/types/forty-cdk-testing.d.ts +200 -0
  172. package/types/forty-cdk-time-field.d.ts +31 -126
  173. package/types/forty-cdk-time-picker.d.ts +91 -65
  174. package/types/forty-cdk-toast.d.ts +6 -86
  175. package/types/forty-cdk-toggle.d.ts +17 -34
  176. package/types/forty-cdk-toolbar.d.ts +4 -29
  177. package/types/forty-cdk-tooltip.d.ts +8 -117
  178. package/types/forty-cdk-tree.d.ts +57 -65
package/README.md CHANGED
@@ -28,7 +28,7 @@ Optional — install only if you use the matching entry point / primitives:
28
28
  | Peer | Needed by |
29
29
  | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
30
  | `@angular/forms` `^22.0.1` | Form-control primitives (`Switch`, `Checkbox`, `RadioGroup`, `Listbox`, `Select`, `Slider`, `Combobox`, …). They implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so the published bundle never references the package — consumers using only non-form primitives can skip it. |
31
- | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/shared` — install this peer only when you import that entry point. |
31
+ | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/date-adapter` — install this peer only when you import that entry point. |
32
32
 
33
33
  ### Regular dependencies
34
34
 
@@ -41,15 +41,18 @@ Two packages are regular dependencies, installed automatically and never declare
41
41
 
42
42
  ## Entry points
43
43
 
44
- **The package name itself exports nothing.** `import { … } from 'forty-cdk'` resolves to no symbol, and your editor will not auto-import anything under the bare package name — by design, so that every symbol has exactly one import path. There are three specifiers you do import from:
44
+ **The package name itself exports nothing.** `import { … } from 'forty-cdk'` resolves to no symbol, and your editor will not auto-import anything under the bare package name — by design, so that the bare name never becomes a second import path for a symbol. There are six specifiers you do import from:
45
45
 
46
- | Specifier | What it exports |
47
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
- | `forty-cdk/<primitive>` | The primitive's directives, components, context tokens and defaults provider — `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on for every entry in the tables below. |
49
- | [`forty-cdk/shared`](shared) | The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — declared once and published once. Eight ship from their own primitive instead; that README names them. |
50
- | `forty-cdk/internationalized-date` | The `@internationalized/date` adapters, kept apart so that optional peer stays genuinely optional. |
46
+ | Specifier | What it exports |
47
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `forty-cdk/<primitive>` | The primitive's directives, components, context tokens and defaults provider — `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on for every entry in the tables below. |
49
+ | [`forty-cdk/shared`](shared) | The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — declared once and published once. Eight ship from their own primitive instead; that README names them. |
50
+ | `forty-cdk/internationalized-date` | The `@internationalized/date` adapters, kept apart so that optional peer stays genuinely optional. |
51
+ | [`forty-cdk/defaults`](defaults) | Every `provideFor<Primitive>Defaults` with its token and interface. Each primitive re-exports its own pair too, but a root provider imported from the primitive loads the primitive at startup; imported from here, it does not. |
52
+ | [`forty-cdk/date-adapter`](date-adapter) | The `DateAdapter` contract, the `FOR_DATE_ADAPTER` token and `provideNativeDateAdapter`. `forty-cdk/shared` re-exports them, but a root provider imported from there loads the code every primitive shares at startup; imported from here, it does not. |
53
+ | [`forty-cdk/testing`](testing) | Spec helpers: keyboard and mouse events dispatched the way a browser dispatches them, stubs for the browser APIs jsdom lacks, and dialog and drawer refs for content mounted outside its manager. |
51
54
 
52
- `forty-cdk/core` and `forty-cdk/core-overlay` resolve too, but neither is **public**: together they hold the engines and DI singletons the library refactors freely, and they exist so every primitive resolves that shared implementation to one compiled module. They are two rather than one for a bundling reason you get for free: a published module is a bundler's chunk-splitting unit, so keeping the positioning engine (`@floating-ui/dom` and the overlay shells) in its own module means a lazy route that renders no overlay does not load it. Measured on a seven-lazy-route app, that is **41.7 kB raw / 12.1 kB transfer** a non-overlay route no longer pays. If a symbol you need is not exported by the three specifiers above, it is internal by design — [open an issue](https://github.com/tutkli/forty-cdk/issues) rather than importing from either.
55
+ `forty-cdk/core` and `forty-cdk/core-overlay` resolve too, but neither is **public**: together they hold the engines and DI singletons the library refactors freely, and they exist so every primitive resolves that shared implementation to one compiled module. They are two rather than one for a bundling reason you get for free: a published module is a bundler's chunk-splitting unit, so keeping the positioning engine (`@floating-ui/dom` and the overlay shells) in its own module means a lazy route that renders no overlay does not load it. Measured on a seven-lazy-route app, that is **41.7 kB raw / 12.1 kB transfer** a non-overlay route no longer pays. If a symbol you need is not exported by the six specifiers above, it is internal by design — [open an issue](https://github.com/tutkli/forty-cdk/issues) rather than importing from either.
53
56
 
54
57
  ## Errors
55
58
 
@@ -93,7 +93,7 @@ The primitive renders whatever items you give it, so collapsing a deep path is a
93
93
  <!-- snippet: fragment -->
94
94
 
95
95
  ```ts
96
- import { provideForBreadcrumbsDefaults } from 'forty-cdk/breadcrumbs';
96
+ import { provideForBreadcrumbsDefaults } from 'forty-cdk/defaults';
97
97
 
98
98
  bootstrapApplication(App, {
99
99
  providers: [provideForBreadcrumbsDefaults({ label: 'Ruta de navegación' })],
@@ -16,7 +16,7 @@ Configuring is optional: without a provider the Tailwind scale (`sm` 640, `md` 7
16
16
 
17
17
  ```ts
18
18
  import { ApplicationConfig } from '@angular/core';
19
- import { provideForBreakpointsDefaults } from 'forty-cdk/breakpoints';
19
+ import { provideForBreakpointsDefaults } from 'forty-cdk/defaults';
20
20
 
21
21
  export const appConfig: ApplicationConfig = {
22
22
  providers: [
@@ -210,22 +210,22 @@ Click the heading button to cycle from day → month → year view. Click a mont
210
210
 
211
211
  ### `ForCalendar`
212
212
 
213
- | Property | Type | Description |
214
- | ------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
215
- | `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection.<br>**Default:** `null` |
216
- | `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`.<br>**Default:** `'single'` |
217
- | `range` | `model<DateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits/clears.<br>**Default:** `null` |
218
- | `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op.<br>**Default:** `null` (no minimum) |
219
- | `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op.<br>**Default:** `null` (no maximum) |
220
- | `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable.<br>**Default:** `null` |
221
- | `max` | `input<D \| null>` | Maximum selectable date (inclusive). Later dates are unavailable.<br>**Default:** `null` |
222
- | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate marking a date unavailable (present but not selectable).<br>**Default:** `() => false` |
223
- | `dateLabel` | `input<CalendarDateLabelFormatter<D>>` | Formats each gridcell's `aria-label` (full accessible date).<br>**Default:** localized full date, outside-month days through the scope's `outsideMonthLabel` |
224
- | `disabled` | `input<boolean>` | Disables the whole calendar (no focus movement, no selection). Reflected as `data-disabled`.<br>**Default:** — |
225
- | `readonly` | `input<boolean>` | Read-only: dates stay focusable, selection is blocked. Reflected as `data-readonly`.<br>**Default:** — |
226
- | `firstDayOfWeek` | `input<number \| null>` | First column's weekday, **0-6** (`0` = Sunday).<br>**Default:** `null` → the adapter's value (or `provideForCalendarDefaults`) |
227
- | `locale` | `input<string \| null>` | BCP 47 locale for the heading, weekday headers, month-picker options and cell `aria-label` names. The calendar system stays Gregorian.<br>**Default:** `null` → the runtime's default locale |
228
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` and mirrors horizontal arrows |
213
+ | Property | Type | Description |
214
+ | ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
215
+ | `value` | `model<D \| null>` | Two-way bindable selected date, or `null`. Used in `selectionMode="single"`. `(valueChange)` fires only on internal selection.<br>**Default:** `null` |
216
+ | `selectionMode` | `input<'single' \| 'range'>` | `'single'` (default) keeps the single-date `value` flow. `'range'` switches to anchor → commit and exposes `range`.<br>**Default:** `'single'` |
217
+ | `range` | `model<DateRange<D> \| null>` | Two-way bindable committed range. Only used in `selectionMode="range"`. `(rangeChange)` fires only on internal commits/clears.<br>**Default:** `null` |
218
+ | `minRangeLength` | `input<number \| null>` | Minimum inclusive day count. A commit shorter than this is a no-op.<br>**Default:** `null` (no minimum) |
219
+ | `maxRangeLength` | `input<number \| null>` | Maximum inclusive day count. A commit longer than this is a no-op.<br>**Default:** `null` (no maximum) |
220
+ | `min` | `input<D \| null>` | Minimum selectable date (inclusive). Earlier dates are unavailable.<br>**Default:** `null` |
221
+ | `max` | `input<D \| null>` | Maximum selectable date (inclusive). Later dates are unavailable.<br>**Default:** `null` |
222
+ | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate marking a date unavailable (present but not selectable).<br>**Default:** `() => false` |
223
+ | `dateLabel` | `input<CalendarDateLabelFormatter<D>>` | Formats each gridcell's `aria-label` (full accessible date).<br>**Default:** localized full date, outside-month days through the scope's `outsideMonthLabel` |
224
+ | `disabled` | `input<boolean>` | Disables the whole calendar (no focus movement, no selection). Reflected as `data-disabled`.<br>**Default:** — |
225
+ | `readonly` | `input<boolean>` | Read-only: dates stay focusable, selection is blocked. Reflected as `data-readonly`.<br>**Default:** — |
226
+ | `firstDayOfWeek` | `input<number \| null>` | First column's weekday, **0-6** (`0` = Sunday).<br>**Default:** `null` → the adapter's value (or `provideForCalendarDefaults`) |
227
+ | `locale` | `input<string \| null>` | BCP 47 locale for the heading, weekday headers, month-picker options and cell `aria-label` names. The calendar system stays Gregorian.<br>**Default:** `null` → the adapter's `locale()`, then the runtime locale |
228
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` and mirrors horizontal arrows |
229
229
 
230
230
  ### Data attributes
231
231
 
@@ -535,7 +535,7 @@ Auto-disabled when the entire previous / next page would be outside `[min, max]`
535
535
  ## Scoped defaults
536
536
 
537
537
  ```ts
538
- import { provideForCalendarDefaults } from 'forty-cdk/calendar';
538
+ import { provideForCalendarDefaults } from 'forty-cdk/defaults';
539
539
 
540
540
  // app config or a component's providers — Monday-first weeks for this scope
541
541
  providers: [
@@ -207,6 +207,8 @@ Input tables are not yet tabulated for this primitive. See the feature sections
207
207
  | `[forCombobox]` | `data-readonly` | present / absent |
208
208
  | `[forComboboxInput]` | `data-state` | `open` \| `closed` |
209
209
  | `[forComboboxInput]` | `data-disabled` | present / absent |
210
+ | `[forComboboxToggle]` | `data-state` | `open` \| `closed` |
211
+ | `[forComboboxToggle]` | `data-disabled` | present / absent |
210
212
  | `[forComboboxContent]` | `data-state` | `open` \| `closed` |
211
213
  | `[forComboboxOption]` | `data-state` | `checked` \| `unchecked` (membership in `value()`, both modes) |
212
214
  | `[forComboboxOption]` | `data-highlighted` | present / absent (the current `aria-activedescendant`) |
@@ -245,7 +247,31 @@ By default the listbox is positioned against `[forComboboxInput]`. When the inpu
245
247
  </div>
246
248
  ```
247
249
 
248
- `[forComboboxAnchor]` changes **only** positioning. The input keeps `aria-controls` / `aria-expanded` / `aria-activedescendant`, all keyboard interaction, and its exemption from outside-pointer dismissal. Without an anchor the listbox falls back to the input, so existing markup is unaffected. Each `[forCombobox]` takes at most one `[forComboboxAnchor]`, and a second one throws `[forty-cdk/combobox]`. In multi mode, wrap `[forComboboxChips]` (which already wraps the chips + input) to anchor against the full chip cluster.
250
+ `[forComboboxAnchor]` changes **only** positioning. The input keeps `aria-controls` / `aria-expanded` / `aria-activedescendant`, all keyboard interaction, and its exemption from outside-pointer dismissal. Without an anchor the listbox falls back to the input, so existing markup is unaffected. It wins over a surrounding field's [`[forFieldAnchor]`](../field/README.md#positioning-anchor). Each `[forCombobox]` takes one `[forComboboxAnchor]`, and a second one warns in dev mode. In multi mode, wrap `[forComboboxChips]` (which already wraps the chips + input) to anchor against the full chip cluster.
251
+
252
+ ## Toggle button
253
+
254
+ An editable combobox often carries a chevron button next to the input, as in the APG's [editable combobox examples](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-autocomplete-list/). Put `[forComboboxToggle]` on a real `<button>`:
255
+
256
+ ```html
257
+ <div forCombobox #combobox="forCombobox" [(query)]="query" [(value)]="value">
258
+ <div forComboboxAnchor class="field-box">
259
+ <input forComboboxInput placeholder="Search a fruit…" />
260
+ <button forComboboxToggle>▾</button>
261
+ </div>
262
+ @if (combobox.open()) {
263
+ <div forComboboxContent>
264
+ @for (it of filtered; track it.id) {
265
+ <div forComboboxOption [value]="it.id" [label]="it.label">{{ it.label }}</div>
266
+ }
267
+ </div>
268
+ }
269
+ </div>
270
+ ```
271
+
272
+ A press closes an open listbox, or opens a closed one with the committed selection highlighted (the first enabled option when nothing is selected) and moves focus into the input. The press never takes focus itself, so an input that already has focus keeps it and the combobox is not marked touched. The button is exempt from the listbox's outside-pointer dismissal, so one press is one `(openChange)`.
273
+
274
+ Unlike `[forComboboxTrigger]`, a toggle keeps the editable anatomy: `commitOnSelect` still copies the picked label into the input, and `query` survives a close. The button is out of the Tab sequence (`tabindex="-1"`) because the input already owns the keyboard, reflects `aria-expanded` and `aria-controls` (the listbox, while open), and carries native `disabled` from the combobox's effective disabled. Its accessible name defaults to `'Show options'`; override it per instance with `[ariaLabel]`, or for the scope with `provideForComboboxDefaults({ toggleAriaLabel })`.
249
275
 
250
276
  ## Picker anatomy
251
277
 
@@ -402,6 +428,7 @@ They diverge while the user types and resync on activation:
402
428
  - **Multi mode** → option's value is toggled in/out of `value`. If `commitOnSelect` is on (default), `query` is **cleared** so the user can search the next item. Listbox stays open.
403
429
  - Clear button → both reset.
404
430
  - `clearOnQueryChange` (off by default, **single mode only**): flip on to drop `value` automatically whenever the query is edited (useful when the user editing means "I'm picking a new one").
431
+ - `restoreQueryOnClose` (off by default, **single mode only**): flip on to put the selected label back into the input when the listbox closes without a pick. See [`restoreQueryOnClose`](#restorequeryonclose).
405
432
 
406
433
  ### `commitOnSelect`: single vs multi
407
434
 
@@ -429,6 +456,19 @@ Multi, commitOnSelect=false
429
456
 
430
457
  Disable `commitOnSelect` when your filter logic compares against `query` directly and the listbox should keep showing the just-narrowed set after activation, instead of resetting to "everything matches the picked label".
431
458
 
459
+ ### `restoreQueryOnClose`
460
+
461
+ In the editable anatomy, closing the listbox leaves `query` as the user left it, so typing "ap" over a committed "Banana" and pressing Escape keeps showing "ap". With `[restoreQueryOnClose]="true"`, a single-select combobox restores the selected option's label on every close that is not a pick (Escape, an outside press, Tab, a `[forComboboxToggle]` press, `closeOverlay()`), and clears the input when nothing is selected. The input keeps focus on Escape, and the restored text reaches it even while focused. A pick still follows `commitOnSelect`.
462
+
463
+ ```text
464
+ Single, restoreQueryOnClose=true
465
+ user activates "Banana" → query="Banana" value=["banana"]
466
+ user types "ap" → query="ap" value=["banana"]
467
+ user presses Escape → query="Banana" value=["banana"] ← label restored, listbox closes
468
+ ```
469
+
470
+ The label is the one `selected()` resolves: the option's own label once it has rendered, `[itemToStringLabel]` before that (a value bound before the listbox ever opened). Multi mode and the picker anatomy ignore the input. Enable it for the whole scope with `provideForComboboxDefaults({ restoreQueryOnClose: true })`.
471
+
432
472
  ## Multi mode
433
473
 
434
474
  Pass `multiple` and let the consumer render chips inside `[forComboboxChips]`. The primitive's `selected()` computed returns `{ value, label }` pairs ready for `@for`:
@@ -493,6 +533,10 @@ When the input is empty (no query) and the user presses Backspace, focus jumps t
493
533
  | Auto-highlight first option | `true` | `[autoHighlight]="false"` to require arrowing to an option first |
494
534
  | Commit label / clear query on select | `true` | `[commitOnSelect]="false"` |
495
535
  | Clear value on query edit (single only) | `false` | `[clearOnQueryChange]="true"` |
536
+ | Highlight the selection on open | `first` | `openHighlight="selected"` |
537
+ | Restore label on close (single only) | `false` | `[restoreQueryOnClose]="true"` |
538
+
539
+ `openHighlight` decides where the editable anatomy's highlight lands when the listbox opens from focus, click, ArrowDown / ArrowUp or `openOverlay()` without an argument. With `'selected'`, a single-select combobox showing "Spain" reopens on "Spain" rather than on the first option; with nothing selected, ArrowDown still lands on the first option and ArrowUp on the last. Opening from a typed query always highlights the first match, because the list is a filter result there, and the picker anatomy always opens on the selection. Both inputs take their default from the scope: `provideForComboboxDefaults({ openHighlight: 'selected', restoreQueryOnClose: true })`.
496
540
 
497
541
  ## Autocomplete modes
498
542
 
@@ -735,20 +779,20 @@ A single-select field is modeled as the same `readonly T[]`, kept at length ≤
735
779
 
736
780
  Focus stays in the input throughout: arrow keys move the listbox's _active descendant_ (the highlighted option), not DOM focus.
737
781
 
738
- | Key | Action |
739
- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
740
- | **ArrowDown** | Open listbox + move activedescendant to next enabled option (or first when none). |
741
- | **ArrowUp** | Open listbox + move activedescendant to previous enabled option (or last when none). |
742
- | **Home** _(open)_ | Move activedescendant to first enabled option. |
743
- | **End** _(open)_ | Move activedescendant to last enabled option. |
744
- | **PageUp** _(open)_ | Move activedescendant to first enabled option. |
745
- | **PageDown** _(open)_ | Move activedescendant to last enabled option. |
746
- | **Enter** _(open)_ | Activate the activedescendant (single: replace + close; multi: toggle + stay open). |
747
- | **Escape** _(open)_ | Close the listbox. Focus stays in the input. |
748
- | **Tab** _(open, no action)_ | Close the listbox and let Tab flow to the next focusable. |
749
- | **Tab / Shift+Tab** _(open, action present)_ | Move focus around the input↔actions ring without dismissing (see [Action items](#action-items)). |
750
- | **Backspace** _(empty input, multi only)_ | Focus the last chip; a second Backspace there removes it. |
751
- | Printable keys | Update `query`. With `'inline'` / `'both'` autocomplete, complete the rest of the first match into the input as selected text. |
782
+ | Key | Action |
783
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
784
+ | **ArrowDown** | Open listbox + move activedescendant to next enabled option (or first when none; the selection under `openHighlight="selected"`). |
785
+ | **ArrowUp** | Open listbox + move activedescendant to previous enabled option (or last when none; the selection under `openHighlight="selected"`). |
786
+ | **Home** _(open)_ | Move activedescendant to first enabled option. |
787
+ | **End** _(open)_ | Move activedescendant to last enabled option. |
788
+ | **PageUp** _(open)_ | Move activedescendant to first enabled option. |
789
+ | **PageDown** _(open)_ | Move activedescendant to last enabled option. |
790
+ | **Enter** _(open)_ | Activate the activedescendant (single: replace + close; multi: toggle + stay open). |
791
+ | **Escape** _(open)_ | Close the listbox. Focus stays in the input. |
792
+ | **Tab** _(open, no action)_ | Close the listbox and let Tab flow to the next focusable. |
793
+ | **Tab / Shift+Tab** _(open, action present)_ | Move focus around the input↔actions ring without dismissing (see [Action items](#action-items)). |
794
+ | **Backspace** _(empty input, multi only)_ | Focus the last chip; a second Backspace there removes it. |
795
+ | Printable keys | Update `query`. With `'inline'` / `'both'` autocomplete, complete the rest of the first match into the input as selected text. |
752
796
 
753
797
  Hovering an option also makes it the activedescendant, so mouse and keyboard intent stay synchronized.
754
798
 
@@ -760,6 +804,7 @@ Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patte
760
804
  - `role="listbox"` lives on `[forComboboxContent]` in the editable anatomy and on `[forComboboxList]` in the picker anatomy; the input's `aria-controls` targets whichever carries it. In the picker anatomy the popup surface (`[forComboboxContent]`) is role-less so it can hold the input next to the list without an `aria-required-owned-elements` violation.
761
805
  - `aria-multiselectable="true"` (multi mode) and the labelled role (`aria-label` / `aria-labelledby`, pointing at the input) sit on whichever element carries `role="listbox"`: content in the editable anatomy, the list in the picker anatomy.
762
806
  - `[forComboboxTrigger]` (picker anatomy) is a real `<button>` reflecting `aria-haspopup="listbox"`, `aria-expanded`, `aria-controls` (the popup surface, while open), and native `disabled` from the combobox's effective disabled. It is exempt from the popup's outside-pointer dismissal layer, like the input.
807
+ - `[forComboboxToggle]` (editable anatomy) is a real `<button>` with `tabindex="-1"`, a localizable `aria-label`, `aria-expanded`, `aria-controls` (the listbox, while open) and native `disabled`. It cancels `mousedown` so focus stays in the input, and it is exempt from the outside-pointer dismissal layer.
763
808
  - In single mode, `aria-selected="true"` follows the activedescendant (the option Enter would activate). In multi mode it follows membership in `value()`, so every selected option carries `aria-selected="true"` simultaneously.
764
809
  - `data-state="checked" | "unchecked"` always reflects membership in `value()`, so consumers can paint a checkmark icon with pure CSS regardless of mode.
765
810
  - `data-highlighted=""` marks the option that is the current `aria-activedescendant`. Because focus stays on the `<input>`, there is no `:focus` on the option to style. `data-highlighted` is the canonical CSS hook.
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: Date adapter
3
+ group: utilities
4
+ archetype: [headless-utility]
5
+ ---
6
+
7
+ # Date adapter
8
+
9
+ The `DateAdapter` contract, the `FOR_DATE_ADAPTER` token that provides one, and the zero-dependency `NativeDateAdapter`, imported from `forty-cdk/date-adapter` so that providing an adapter at the application root does not load the date and time primitives, or the code they share, before bootstrap.
10
+
11
+ Every date and time primitive (Calendar, Date Field, Date Picker, Time Field, Time Picker) delegates its arithmetic and formatting to the adapter in scope, and none ships a default. Provide one where the [date adapters guide](../../../docs/date-adapters.md) says to, usually the application config:
12
+
13
+ ```ts
14
+ import { type ApplicationConfig } from '@angular/core';
15
+ import { provideNativeDateAdapter } from 'forty-cdk/date-adapter';
16
+
17
+ export const appConfig: ApplicationConfig = {
18
+ providers: [provideNativeDateAdapter()],
19
+ };
20
+ ```
21
+
22
+ `forty-cdk/shared` re-exports the same objects, and `forty-cdk/calendar` re-exports the native pair, so an import from either keeps compiling and binds the same token. Only the import from this entry point keeps the root provider cheap.
23
+
24
+ ## Why this exists
25
+
26
+ A bundler splits code into chunks one module at a time, and each forty-cdk entry point is published as a single module. A root `providers` array is in the static graph of your `main` bundle, so the module it imports from is loaded before the application bootstraps, on every route. This entry point imports nothing but `@angular/core`, so the code the primitives share stays in the lazy chunks of the routes that render them. `provideInternationalizedDateAdapter()` and `provideInternationalizedDateTimeAdapter()` read the token and the formatter cache from here too.
27
+
28
+ Measured on a production build of an app with four lazy routes (Tooltip, Calendar, Combobox, Menu) and `provideForTooltipDefaults`, `provideForCalendarDefaults`, `provideForComboboxDefaults` and `provideForMenuDefaults` from [`forty-cdk/defaults`](../defaults) at the root:
29
+
30
+ | Also at the root | `main` size (raw / gzip) | Shared chunks the lazy routes load |
31
+ | -------------------------------------------------------------------------- | ------------------------ | ---------------------------------- |
32
+ | No date adapter | 209,552 B / 63,580 B | two |
33
+ | A `NativeDateAdapter` subclass, imported from `forty-cdk/date-adapter` | 212,012 B / 64,263 B | two |
34
+ | A hand-written adapter on `FOR_DATE_ADAPTER` from `forty-cdk/date-adapter` | 209,723 B / 63,610 B | two |
35
+ | The same subclass, imported from `forty-cdk/shared` | 246,013 B / 74,209 B | none: they moved into `main` |
36
+
37
+ ## API
38
+
39
+ | Export | Kind | Contract |
40
+ | ---------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `DateAdapter<D>` | interface | The date-library seam every date and time primitive calls. Its members, and the Gregorian and locale limits, are in the [date adapters guide](../../../docs/date-adapters.md#writing-your-own-adapter). |
42
+ | `TimeCapableDateAdapter<D>` | type | A `DateAdapter<D>` that implements the optional `getHours` / `getMinutes` / `getSeconds` / `setTime`. The time primitives require one. |
43
+ | `FOR_DATE_ADAPTER` | token | Holds the active adapter. A date or time primitive with no adapter in scope throws `FORCDK-CORE-002`. |
44
+ | `NativeDateAdapter` | class | A time-capable adapter over the built-in `Date`. Subclass it and override `locale()` to format in the app's language. |
45
+ | `provideNativeDateAdapter()` | provider | Binds `NativeDateAdapter` to `FOR_DATE_ADAPTER`. |
46
+ | `createFormatterCache()` | function | Returns a function that hands back one `Intl.DateTimeFormat` per distinct `(locale, options)` pair, for an adapter of your own to format through. |
47
+
48
+ `injectDateAdapter` and `assertTimeCapable` are not here: they throw through the library's error helpers, which live with the primitives, and ship from [`forty-cdk/shared`](../shared).
@@ -155,8 +155,8 @@ With a time-capable adapter, a `granularity` coarser than `'day'` appends time s
155
155
  | `minDate` | `input<D \| null>` | Minimum date (inclusive). A composed value below it is clamped up. Named `minDate` (see note below).<br>**Default:** `null` |
156
156
  | `maxDate` | `input<D \| null>` | Maximum date (inclusive). A composed value above it is clamped down.<br>**Default:** `null` |
157
157
  | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` is date-only; coarser-than-day appends time segments. See below.<br>**Default:** `'day'` |
158
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
159
- | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
158
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → the scope's `hourCycle`, then the locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
159
+ | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → the adapter's `locale()`, then the runtime locale.<br>**Default:** `null` |
160
160
  | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty. Unspecified parts fall back to the scope's `placeholder`, then to `dd` / `mm` / `yyyy` / `hh` / `mm` / `ss` / `--`.<br>**Default:** `{}` |
161
161
  | `ariaLabel` | `input<string \| null>` | Accessible name for the group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
162
162
  | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
@@ -205,7 +205,7 @@ On the AM/PM segment, `a` / `p` set the period and ArrowUp / ArrowDown toggle it
205
205
  ## Scoped defaults
206
206
 
207
207
  ```ts
208
- import { provideForDateFieldDefaults } from 'forty-cdk/date-field';
208
+ import { provideForDateFieldDefaults } from 'forty-cdk/defaults';
209
209
 
210
210
  // app config or a component's providers — localize segment labels, the
211
211
  // empty-segment announcement and the placeholders for every nested [forDateField].
@@ -220,7 +220,9 @@ providers: [
220
220
 
221
221
  `segmentLabels` supplies each segment's default `aria-label`, keyed by part type. Unset keys keep the library default (the part name, and `'AM/PM'` for the `dayPeriod` segment), so overriding a single key never wipes the rest. A segment's own `[ariaLabel]` still wins over the scope default.
222
222
 
223
- `placeholder` is the text an empty segment shows, keyed the same way. A field's own `[placeholder]` wins for the parts it names and only those, and a part neither names keeps the letter-repeat default. `provideForDateRangeFieldDefaults` takes the same three keys for `[forDateRangeField]`.
223
+ `placeholder` is the text an empty segment shows, keyed the same way. A field's own `[placeholder]` wins for the parts it names and only those, and a part neither names keeps the letter-repeat default. `provideForDateRangeFieldDefaults` takes the same keys for `[forDateRangeField]`.
224
+
225
+ `hourCycle` sets the 12- or 24-hour cycle of every field that doesn't bind `[hourCycle]`, so a product on a 24-hour clock sets it once instead of on each field. Its fallback `null` derives the cycle from the locale.
224
226
 
225
227
  For a language the app sets or switches after bootstrap, pass the text keys as functions and the overrides as a factory, as [Localizing default text](../shared/README.md#localizing-default-text) shows.
226
228
 
@@ -268,17 +270,17 @@ readonly booking = form(this.model);
268
270
 
269
271
  ### `ForDateRangeField` API
270
272
 
271
- | Property | Type | Description |
272
- | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
273
- | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
274
- | `minDate` | `input<D \| null>` | Minimum date (inclusive) for both endpoints. A composed endpoint below it is clamped up. Named `minDate` (see note below).<br>**Default:** `null` |
275
- | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
276
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; coarser-than-day appends time segments.<br>**Default:** `'day'` |
277
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
278
- | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
279
- | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty, applied to both endpoints. Unspecified parts fall back to the scope's `placeholder`.<br>**Default:** `{}` |
280
- | `ariaLabel` | `input<string \| null>` | Accessible name for the whole range field group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
281
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
273
+ | Property | Type | Description |
274
+ | ------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
275
+ | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
276
+ | `minDate` | `input<D \| null>` | Minimum date (inclusive) for both endpoints. A composed endpoint below it is clamped up. Named `minDate` (see note below).<br>**Default:** `null` |
277
+ | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
278
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; coarser-than-day appends time segments.<br>**Default:** `'day'` |
279
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → the scope's `hourCycle`, then the locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
280
+ | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → the adapter's `locale()`, then the runtime locale.<br>**Default:** `null` |
281
+ | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty, applied to both endpoints. Unspecified parts fall back to the scope's `placeholder`.<br>**Default:** `{}` |
282
+ | `ariaLabel` | `input<string \| null>` | Accessible name for the whole range field group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
283
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
282
284
 
283
285
  The endpoint groups each accept an `ariaLabel` input for their own group label, falling back to the scope defaults (`'Start date'` / `'End date'`). Plus the shared `FormUiControl` members bound automatically by `[formField]`.
284
286
 
@@ -18,6 +18,7 @@ Reinterpreted idiomatically for modern Angular: a focusable trigger that opens a
18
18
  - **Date Picker**: a trigger plus a floating [Calendar](../calendar/README.md), and the form value itself (`FormValueControl<D | null>`). Choose it when the date is found by looking and the grid should stay out of the way until asked for.
19
19
  - **[Calendar](../calendar/README.md)**: the same grid inline and always visible. It exposes `[(value)]` as a model but implements no form-control contract, so a form binds the picker rather than the calendar.
20
20
  - **[Date Field](../date-field/README.md)**: segmented keyboard entry with no grid and no popup. Choose it when the user already knows the date and typing is the fast path.
21
+ - **Both at once**: a date field the user types into, with a calendar button beside it. That is this picker's [field anatomy](#field-anatomy).
21
22
 
22
23
  ## Date adapter
23
24
 
@@ -196,24 +197,26 @@ Open the picker and click a first day: the trigger keeps its placeholder, becaus
196
197
 
197
198
  ### `ForDatePicker`
198
199
 
199
- | Property | Type | Description |
200
- | ------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
201
- | `value` | `model<D \| null>` | Two-way bindable selected date. `(valueChange)` fires only on internal commits.<br>**Default:** `null` |
202
- | `open` | `model<boolean>` | Two-way bindable surface visibility. `(openChange)` fires only on internal transitions.<br>**Default:** `false` |
203
- | `minDate` | `input<D \| null>` | Minimum selectable date (inclusive). Forward to the projected calendar's `[min]`.<br>**Default:** `null` |
204
- | `maxDate` | `input<D \| null>` | Maximum selectable date (inclusive). Forward to the projected calendar's `[max]`.<br>**Default:** `null` |
205
- | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`.<br>**Default:** `() => false` |
206
- | `closeOnSelect` | `input<boolean>` | Close the surface after a date is picked. Honoured only at `granularity="day"`.<br>**Default:** `true` |
207
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is a pure date picker; coarser-than-day off composes a time field.<br>**Default:** `'day'` |
208
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the value display (and typically the projected `[forTimeField]`).<br>**Default:** `null` → locale |
209
- | `modal` | `input<boolean>` | Trap focus + inert background + scroll lock (centered dialog) instead of an anchored popover.<br>**Default:** `false` |
210
- | `dismissible` | `input<boolean>` | Escape / outside-pointer dismiss the surface.<br>**Default:** `true` |
211
- | `returnFocus` | `input<boolean>` | Return focus to the trigger on close.<br>**Default:** `true` |
212
- | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
213
- | `locale` | `input<string \| null>` | BCP 47 locale for the text rendered by `[forDatePickerValue]`. Not forwarded to the projected calendar, so bind its `[locale]` too.<br>**Default:** `null` → runtime locale |
214
- | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
215
- | `side` / `align` | `input` | Anchored placement (popover mode only). Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `'bottom'` / `'start'` |
216
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
200
+ | Property | Type | Description |
201
+ | ------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
202
+ | `value` | `model<D \| null>` | Two-way bindable selected date. `(valueChange)` fires only on internal commits.<br>**Default:** `null` |
203
+ | `open` | `model<boolean>` | Two-way bindable surface visibility. `(openChange)` fires only on internal transitions.<br>**Default:** `false` |
204
+ | `minDate` | `input<D \| null>` | Minimum selectable date (inclusive). Forward to the projected calendar's `[min]`.<br>**Default:** `null` |
205
+ | `maxDate` | `input<D \| null>` | Maximum selectable date (inclusive). Forward to the projected calendar's `[max]`.<br>**Default:** `null` |
206
+ | `isDateUnavailable` | `input<(date: D) => boolean>` | Per-date predicate. Forward to the projected calendar's `[isDateUnavailable]`.<br>**Default:** `() => false` |
207
+ | `closeOnSelect` | `input<boolean>` | Close the surface after a date is picked. Honoured at `granularity="day"`, and at any granularity with `anatomy="field"`.<br>**Default:** `true` |
208
+ | `anatomy` | `input<'trigger' \| 'field'>` | Which piece is the control. `'field'` makes a projected `[forDateField]` the control and the trigger a plain button; see [Field anatomy](#field-anatomy).<br>**Default:** `'trigger'` |
209
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` (default) is a pure date picker; coarser-than-day off composes a time field.<br>**Default:** `'day'` |
210
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the value display; a projected time field takes `resolvedHourCycle()`.<br>**Default:** `null` → the scope's `hourCycle` (`provideForDatePickerDefaults`), then the locale |
211
+ | `resolvedHourCycle` | `Signal<12 \| 24 \| null>` | The effective cycle: `hourCycle`, then the scope's, or `null` for the locale. Bind it to a projected time field's `[hourCycle]`.<br>**Default:** — |
212
+ | `modal` | `input<boolean>` | Trap focus + inert background + scroll lock (centered dialog) instead of an anchored popover.<br>**Default:** `false` |
213
+ | `dismissible` | `input<boolean>` | Escape / outside-pointer dismiss the surface.<br>**Default:** `true` |
214
+ | `returnFocus` | `input<boolean>` | Return focus to the trigger on close.<br>**Default:** `true` |
215
+ | `formatOptions` | `input<Intl.DateTimeFormatOptions>` | Options for the text rendered by `[forDatePickerValue]`.<br>**Default:** `{ year: 'numeric', month: 'long', day: 'numeric' }` |
216
+ | `locale` | `input<string \| null>` | BCP 47 locale for the text rendered by `[forDatePickerValue]`. Not forwarded to the projected calendar, so bind its `[locale]` too.<br>**Default:** `null` → the adapter's `locale()`, then the runtime locale |
217
+ | `placeholder` | `input<string>` | Fallback text for `[forDatePickerValue]` when empty.<br>**Default:** `''` |
218
+ | `side` / `align` | `input` | Anchored placement (popover mode only). Defaults from `provideForDatePickerDefaults` / `provideForDateRangePickerDefaults`.<br>**Default:** `'bottom'` / `'start'` |
219
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction.<br>**Default:** `null` resolves the ambient direction; reflected to the host `dir` |
217
220
 
218
221
  Plus the shared `FormUiControl` inputs from the base (`disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `name`, `errors`, and the `touched` model) and the floating tunables (`sideOffset`, `alignOffset`, `avoidCollisions`, `collisionPadding`, `sticky`, `hideWhenDetached`).
219
222
 
@@ -274,7 +277,7 @@ By default the surface is positioned against `[forDatePickerTrigger]`. When the
274
277
  </div>
275
278
  ```
276
279
 
277
- `[forDatePickerAnchor]` changes **only** positioning. The trigger keeps `aria-haspopup` / `aria-expanded` / `aria-controls`, the click toggle, focus return on close, and its exemption from outside-pointer dismissal. Without an anchor the surface falls back to the trigger, so existing markup is unaffected. Each `[forDatePicker]` accepts at most one `[forDatePickerAnchor]`, and a second one throws `[forty-cdk/date-picker]`. (A calendar has its own intrinsic width and ignores `--for-floating-anchor-width`, so the anchor mainly affects start / side alignment to the box edge.)
280
+ `[forDatePickerAnchor]` changes **only** positioning. The trigger keeps `aria-haspopup` / `aria-expanded` / `aria-controls`, the click toggle, focus return on close, and its exemption from outside-pointer dismissal. Without an anchor the surface falls back to the trigger, so existing markup is unaffected. It wins over a surrounding field's [`[forFieldAnchor]`](../field/README.md#positioning-anchor). Each `[forDatePicker]` accepts one `[forDatePickerAnchor]`, and a second one warns in dev mode. (A calendar has its own intrinsic width and ignores `--for-floating-anchor-width`, so the anchor mainly affects start / side alignment to the box edge.)
278
281
 
279
282
  ## Modal vs non-modal
280
283
 
@@ -312,7 +315,7 @@ The content is a field boundary. Inside a [`[forField]`](../field/README.md#how-
312
315
  <div
313
316
  forTimeField
314
317
  [value]="picker.value()"
315
- [hourCycle]="picker.hourCycle()"
318
+ [hourCycle]="picker.resolvedHourCycle()"
316
319
  #field="forTimeField"
317
320
  >
318
321
  @for (seg of field.segments(); track seg.id) { @if (seg.isLiteral) {
@@ -326,7 +329,55 @@ The content is a field boundary. Inside a [`[forField]`](../field/README.md#how-
326
329
  </div>
327
330
  ```
328
331
 
329
- The value display (`[forDatePickerValue]`) automatically appends the time to its formatting when `granularity > 'day'` and you haven't set time fields in `formatOptions`.
332
+ The value display (`[forDatePickerValue]`) automatically appends a two-digit hour and minute to its formatting when `granularity > 'day'` and you haven't set time fields in `formatOptions`, so it matches the projected `[forTimeField]`. The resolved `hourCycle` applies to that default and to time fields you set yourself, unless `formatOptions` sets `hour12` or `hourCycle`.
333
+
334
+ Bind the time field's `[hourCycle]` to `picker.resolvedHourCycle()`, not `picker.hourCycle()`. The raw input is `null` when the cycle comes from `provideForDatePickerDefaults`, and the time field would then fall back to its own defaults and the locale, so an `en-US` browser would show `14:30` on the trigger and `2:30 PM` in the panel. The same applies to a projected `[forTimePicker]`.
335
+
336
+ ## Field anatomy
337
+
338
+ The default anatomy makes the trigger the control: it shows the value and takes the label. The common form-field composite is the other way round, and so is the APG example: an editable field that holds the value, plus a separate button that opens the calendar. Set `anatomy="field"` and project a [`[forDateField]`](../date-field/README.md) inside the picker to get that shape.
339
+
340
+ ```html
341
+ <div forField>
342
+ <label forLabel>Appointment</label>
343
+ <div
344
+ forDatePicker
345
+ anatomy="field"
346
+ [formField]="form.when"
347
+ granularity="minute"
348
+ [minDate]="min"
349
+ [maxDate]="max"
350
+ #picker="forDatePicker"
351
+ >
352
+ <div forDateField #field="forDateField">
353
+ @for (seg of field.segments(); track seg.id) { @if (seg.isLiteral) {
354
+ <span forDateFieldLiteral>{{ seg.text }}</span>
355
+ } @else {
356
+ <span forDateFieldSegment [segment]="seg.type!">{{ seg.text }}</span>
357
+ } }
358
+ </div>
359
+ <button forDatePickerTrigger aria-label="Open calendar">…icon…</button>
360
+
361
+ @if (picker.open()) {
362
+ <div forDatePickerContent>
363
+ <div forCalendar [value]="picker.value()" [min]="picker.minDate()" [max]="picker.maxDate()">
364
+ <!-- …calendar header + grid… -->
365
+ </div>
366
+ </div>
367
+ }
368
+ </div>
369
+ </div>
370
+ ```
371
+
372
+ With the field adopted:
373
+
374
+ - **One form control.** The picker stays the `FormValueControl`, so `[formField]` binds once, on the picker. A typed date and a calendar pick both write the picker's value, and both mark it dirty; a pick marks it touched, and so does focus leaving the field.
375
+ - **State is set once.** `minDate`, `maxDate`, `disabled`, `readonly`, `granularity`, `hourCycle` and `locale` on the picker apply to the field. A read-only picker blocks typing and picking alike.
376
+ - **The field is the control.** A surrounding `[forField]` names and describes the field's `role="group"`, `aria-invalid` lands there, a label press focuses its first segment, and the picker's `focus()` goes there too.
377
+ - **The trigger is a plain button.** It drops `role="combobox"` and the form-control `aria-*` state, and keeps `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`. Give it a name of its own.
378
+ - **A pick closes the surface at any granularity.** The time is typed in the field rather than in the surface, so a picked day keeps the field's time and the surface closes (unless `closeOnSelect` is off).
379
+
380
+ The anatomy is explicit, so a date field placed inside the surface of a trigger-anatomy picker changes nothing. With `anatomy="field"` and no projected `[forDateField]`, opening the calendar or calling `focus()` throws `FORCDK-DATE-PICKER-007` in dev mode.
330
381
 
331
382
  ## Range selection — `ForDateRangePicker`
332
383
 
@@ -396,10 +447,10 @@ Inside the surface, the projected `ForCalendar` owns the full grid keyboard map
396
447
 
397
448
  Implements the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/).
398
449
 
399
- - **`role="combobox"`** on the trigger with **`aria-haspopup="dialog"`**, `aria-expanded` reflecting `open()`, and `aria-controls` pointing at the surface while open. This is the same shape `[forSelectTrigger]` / `[forTimePickerTrigger]` ship, with the `dialog` popup token ARIA 1.2 allows for a combobox surface. The role is also what makes the form-control ARIA below legal: `role="button"` supports neither `aria-readonly` nor `aria-required`.
450
+ - **`role="combobox"`** on the trigger with **`aria-haspopup="dialog"`**, `aria-expanded` reflecting `open()`, and `aria-controls` pointing at the surface while open. This is the same shape `[forSelectTrigger]` / `[forTimePickerTrigger]` ship, with the `dialog` popup token ARIA 1.2 allows for a combobox surface. The role is also what makes the form-control ARIA below legal: `role="button"` supports neither `aria-readonly` nor `aria-required`. In the [field anatomy](#field-anatomy) the trigger is a plain button with the same three popup attributes and none of the form-control state, which the date field carries instead.
400
451
  - **`role="dialog"`** on the surface, named by `[ariaLabel]` (or `aria-labelledby` the trigger when no label is set). `aria-modal="true"` only in modal mode (truthy-only).
401
452
  - **Form-control ARIA** (`aria-readonly` / `aria-required` / `aria-invalid` / `aria-busy`) is reflected on the focusable trigger so assistive tech announces validity on the element that takes focus, alongside the `data-readonly` styling hook. The disabled state is the exception. It reflects through one channel only: the native `disabled` attribute (plus `data-disabled`), never `aria-disabled`.
402
- - **Inside a `[forField]` the labelled element is the trigger**, not the `[forDatePicker]` / `[forDateRangePicker]` wrapper: the field's `controlId` and its `aria-labelledby` / `aria-describedby` / `aria-errormessage` land on `[forDatePickerTrigger]`, so `[forLabel]`'s `for` points at the element that takes focus, clicking a non-`<label>` `[forLabel]` opens the surface, and Signal Forms' focus-on-error reaches the trigger. `role="combobox"` takes its name from the author, so this is the channel that names the control. The root's `[ariaLabel]` names the `role="dialog"` surface instead.
453
+ - **Inside a `[forField]` the labelled element is the trigger**, not the `[forDatePicker]` / `[forDateRangePicker]` wrapper: the field's `controlId` and its `aria-labelledby` / `aria-describedby` / `aria-errormessage` land on `[forDatePickerTrigger]`, so `[forLabel]`'s `for` points at the element that takes focus, clicking a non-`<label>` `[forLabel]` opens the surface, and Signal Forms' focus-on-error reaches the trigger. `role="combobox"` takes its name from the author, so this is the channel that names the control. The root's `[ariaLabel]` names the `role="dialog"` surface instead. In the [field anatomy](#field-anatomy) the same association lands on the date field's `role="group"` instead, and a label press focuses its first segment.
403
454
  - **Focus management**: focus enters the surface on open (the calendar's roving cell in non-modal mode) and returns to the trigger on close, both vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)`.
404
455
  - **Dismissal**: Escape (`(escapeKeyDown)`) and outside-pointer (`(pointerDownOutside)` / `(interactOutside)`) close the surface, each vetoable.
405
456