forty-cdk 0.33.0 → 0.35.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 (232) hide show
  1. package/README.md +4 -3
  2. package/calendar/README.md +2 -2
  3. package/carousel/README.md +11 -6
  4. package/checkbox/README.md +18 -3
  5. package/combobox/README.md +11 -9
  6. package/date-field/README.md +12 -1
  7. package/date-picker/README.md +8 -3
  8. package/dialog/README.md +46 -25
  9. package/drag-drop/README.md +4 -2
  10. package/drawer/README.md +5 -5
  11. package/dropdown-menu/README.md +1 -1
  12. package/fesm2022/forty-cdk-accordion.mjs +502 -379
  13. package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
  14. package/fesm2022/forty-cdk-aspect-ratio.mjs +61 -69
  15. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-avatar.mjs +314 -350
  17. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  18. package/fesm2022/forty-cdk-breadcrumbs.mjs +152 -119
  19. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-breakpoints.mjs +44 -80
  21. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-button.mjs +235 -337
  23. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-calendar.mjs +2087 -1953
  25. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-carousel.mjs +1246 -1031
  27. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-checkbox.mjs +233 -239
  29. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-combobox.mjs +2862 -3119
  31. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-context-menu.mjs +532 -527
  33. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-core-overlay.mjs +3333 -4909
  35. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-core.mjs +4605 -7960
  37. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-date-adapter.mjs +129 -175
  39. package/fesm2022/forty-cdk-date-adapter.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-date-field.mjs +1114 -1020
  41. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-date-picker.mjs +1393 -1463
  43. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-defaults.mjs +537 -1343
  45. package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
  46. package/fesm2022/forty-cdk-dialog.mjs +901 -690
  47. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-disclosure.mjs +254 -182
  49. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-drag-drop.mjs +1776 -1760
  51. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  52. package/fesm2022/forty-cdk-drawer.mjs +1835 -2105
  53. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  54. package/fesm2022/forty-cdk-dropdown-menu.mjs +451 -371
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  56. package/fesm2022/forty-cdk-field.mjs +563 -658
  57. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  58. package/fesm2022/forty-cdk-fieldset.mjs +162 -205
  59. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  60. package/fesm2022/forty-cdk-file-upload.mjs +383 -316
  61. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  62. package/fesm2022/forty-cdk-hover-card.mjs +600 -629
  63. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-input.mjs +230 -277
  65. package/fesm2022/forty-cdk-input.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-internationalized-date.mjs +183 -247
  67. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-listbox.mjs +1486 -1590
  69. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-menu.mjs +1924 -1723
  71. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-menubar.mjs +789 -955
  73. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-meter.mjs +312 -210
  75. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  76. package/fesm2022/forty-cdk-navigation-menu.mjs +1231 -1321
  77. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  78. package/fesm2022/forty-cdk-number-input.mjs +694 -834
  79. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  80. package/fesm2022/forty-cdk-otp-input.mjs +556 -550
  81. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  82. package/fesm2022/forty-cdk-pagination.mjs +493 -362
  83. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  84. package/fesm2022/forty-cdk-pane-resizer.mjs +470 -416
  85. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  86. package/fesm2022/forty-cdk-popover.mjs +846 -728
  87. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  88. package/fesm2022/forty-cdk-progress.mjs +265 -223
  89. package/fesm2022/forty-cdk-progress.mjs.map +1 -1
  90. package/fesm2022/forty-cdk-radio-group.mjs +468 -430
  91. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  92. package/fesm2022/forty-cdk-scroll-area.mjs +860 -934
  93. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  94. package/fesm2022/forty-cdk-search.mjs +281 -328
  95. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  96. package/fesm2022/forty-cdk-select.mjs +1872 -1878
  97. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  98. package/fesm2022/forty-cdk-separator.mjs +92 -67
  99. package/fesm2022/forty-cdk-separator.mjs.map +1 -1
  100. package/fesm2022/forty-cdk-shared.mjs +4 -8
  101. package/fesm2022/forty-cdk-slider.mjs +881 -876
  102. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  103. package/fesm2022/forty-cdk-stepper.mjs +1160 -909
  104. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  105. package/fesm2022/forty-cdk-switch.mjs +140 -145
  106. package/fesm2022/forty-cdk-switch.mjs.map +1 -1
  107. package/fesm2022/forty-cdk-table-virtualization.mjs +258 -369
  108. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  109. package/fesm2022/forty-cdk-table.mjs +4719 -4785
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  111. package/fesm2022/forty-cdk-tabs.mjs +566 -404
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  113. package/fesm2022/forty-cdk-testing.mjs +235 -359
  114. package/fesm2022/forty-cdk-testing.mjs.map +1 -1
  115. package/fesm2022/forty-cdk-time-field.mjs +1141 -1054
  116. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  117. package/fesm2022/forty-cdk-time-picker.mjs +1304 -1146
  118. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  119. package/fesm2022/forty-cdk-toast.mjs +1473 -1564
  120. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  121. package/fesm2022/forty-cdk-toggle.mjs +555 -525
  122. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  123. package/fesm2022/forty-cdk-toolbar.mjs +504 -373
  124. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  125. package/fesm2022/forty-cdk-tooltip.mjs +602 -628
  126. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  127. package/fesm2022/forty-cdk-tree.mjs +2431 -2629
  128. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  129. package/fesm2022/forty-cdk-virtual-reorder.mjs +399 -470
  130. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  131. package/fesm2022/forty-cdk-virtualization.mjs +552 -598
  132. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  133. package/fesm2022/forty-cdk-visually-hidden.mjs +1 -6
  134. package/fesm2022/forty-cdk.mjs +0 -4
  135. package/field/README.md +10 -10
  136. package/fieldset/README.md +2 -2
  137. package/hover-card/README.md +6 -4
  138. package/input/README.md +2 -0
  139. package/listbox/README.md +16 -4
  140. package/menu/README.md +2 -2
  141. package/menubar/README.md +13 -10
  142. package/navigation-menu/README.md +20 -9
  143. package/number-input/README.md +3 -1
  144. package/otp-input/README.md +7 -5
  145. package/package.json +7 -6
  146. package/radio-group/README.md +1 -1
  147. package/search/README.md +1 -1
  148. package/select/README.md +9 -6
  149. package/separator/README.md +4 -4
  150. package/shared/README.md +92 -3
  151. package/slider/README.md +20 -13
  152. package/stepper/README.md +3 -3
  153. package/switch/README.md +1 -1
  154. package/table/README.md +46 -34
  155. package/table-virtualization/README.md +29 -7
  156. package/tabs/README.md +1 -1
  157. package/testing/README.md +11 -11
  158. package/time-field/README.md +13 -2
  159. package/time-picker/README.md +8 -2
  160. package/toast/README.md +3 -2
  161. package/toggle/README.md +2 -2
  162. package/toolbar/README.md +5 -5
  163. package/tooltip/README.md +2 -2
  164. package/tree/README.md +71 -36
  165. package/types/forty-cdk-accordion.d.ts +210 -167
  166. package/types/forty-cdk-aspect-ratio.d.ts +23 -20
  167. package/types/forty-cdk-avatar.d.ts +73 -58
  168. package/types/forty-cdk-breadcrumbs.d.ts +43 -35
  169. package/types/forty-cdk-breakpoints.d.ts +42 -47
  170. package/types/forty-cdk-button.d.ts +47 -42
  171. package/types/forty-cdk-calendar.d.ts +712 -590
  172. package/types/forty-cdk-carousel.d.ts +497 -392
  173. package/types/forty-cdk-checkbox.d.ts +55 -46
  174. package/types/forty-cdk-combobox.d.ts +1277 -1064
  175. package/types/forty-cdk-context-menu.d.ts +322 -255
  176. package/types/forty-cdk-core-overlay.d.ts +3079 -2610
  177. package/types/forty-cdk-core.d.ts +3117 -2713
  178. package/types/forty-cdk-date-adapter.d.ts +195 -200
  179. package/types/forty-cdk-date-field.d.ts +472 -306
  180. package/types/forty-cdk-date-picker.d.ts +655 -475
  181. package/types/forty-cdk-defaults.d.ts +1497 -1193
  182. package/types/forty-cdk-dialog.d.ts +377 -349
  183. package/types/forty-cdk-disclosure.d.ts +78 -61
  184. package/types/forty-cdk-drag-drop.d.ts +570 -482
  185. package/types/forty-cdk-drawer.d.ts +729 -645
  186. package/types/forty-cdk-dropdown-menu.d.ts +236 -173
  187. package/types/forty-cdk-field.d.ts +206 -181
  188. package/types/forty-cdk-fieldset.d.ts +86 -85
  189. package/types/forty-cdk-file-upload.d.ts +152 -126
  190. package/types/forty-cdk-hover-card.d.ts +248 -202
  191. package/types/forty-cdk-input.d.ts +36 -36
  192. package/types/forty-cdk-internationalized-date.d.ts +81 -85
  193. package/types/forty-cdk-listbox.d.ts +574 -486
  194. package/types/forty-cdk-menu.d.ts +699 -547
  195. package/types/forty-cdk-menubar.d.ts +536 -510
  196. package/types/forty-cdk-meter.d.ts +130 -88
  197. package/types/forty-cdk-navigation-menu.d.ts +453 -377
  198. package/types/forty-cdk-number-input.d.ts +243 -199
  199. package/types/forty-cdk-otp-input.d.ts +194 -123
  200. package/types/forty-cdk-pagination.d.ts +239 -168
  201. package/types/forty-cdk-pane-resizer.d.ts +195 -133
  202. package/types/forty-cdk-popover.d.ts +381 -331
  203. package/types/forty-cdk-progress.d.ts +114 -87
  204. package/types/forty-cdk-radio-group.d.ts +189 -135
  205. package/types/forty-cdk-scroll-area.d.ts +329 -297
  206. package/types/forty-cdk-search.d.ts +103 -99
  207. package/types/forty-cdk-select.d.ts +881 -704
  208. package/types/forty-cdk-separator.d.ts +30 -22
  209. package/types/forty-cdk-shared.d.ts +4 -3
  210. package/types/forty-cdk-slider.d.ts +335 -278
  211. package/types/forty-cdk-stepper.d.ts +592 -505
  212. package/types/forty-cdk-switch.d.ts +26 -22
  213. package/types/forty-cdk-table-virtualization.d.ts +88 -70
  214. package/types/forty-cdk-table.d.ts +1987 -1351
  215. package/types/forty-cdk-tabs.d.ts +245 -191
  216. package/types/forty-cdk-testing.d.ts +64 -63
  217. package/types/forty-cdk-time-field.d.ts +527 -334
  218. package/types/forty-cdk-time-picker.d.ts +575 -394
  219. package/types/forty-cdk-toast.d.ts +643 -479
  220. package/types/forty-cdk-toggle.d.ts +212 -164
  221. package/types/forty-cdk-toolbar.d.ts +204 -141
  222. package/types/forty-cdk-tooltip.d.ts +305 -257
  223. package/types/forty-cdk-tree.d.ts +803 -650
  224. package/types/forty-cdk-virtual-reorder.d.ts +36 -27
  225. package/types/forty-cdk-virtualization.d.ts +295 -212
  226. package/types/forty-cdk-visually-hidden.d.ts +2 -1
  227. package/types/forty-cdk.d.ts +1 -2
  228. package/virtual-reorder/README.md +1 -1
  229. package/virtualization/README.md +20 -11
  230. package/fesm2022/forty-cdk-shared.mjs.map +0 -1
  231. package/fesm2022/forty-cdk-visually-hidden.mjs.map +0 -1
  232. package/fesm2022/forty-cdk.mjs.map +0 -1
package/README.md CHANGED
@@ -20,14 +20,15 @@ npm install forty-cdk
20
20
 
21
21
  Required:
22
22
 
23
- - `@angular/common` `^22.0.1`
24
- - `@angular/core` `^22.0.1`
23
+ - `@angular/common` `^22.1.0`
24
+ - `@angular/core` `^22.1.0`
25
+ - `@angular/platform-browser` `^22.1.0`, already installed by every Angular browser or server app
25
26
 
26
27
  Optional — install only if you use the matching entry point / primitives:
27
28
 
28
29
  | Peer | Needed by |
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
+ | `@angular/forms` `^22.1.0` | 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
32
  | `@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
33
 
33
34
  ### Regular dependencies
@@ -446,7 +446,7 @@ The lower-level hooks (`visibleMonthNumber()`, `visibleYear()`, `monthOptions()`
446
446
  <h2 forCalendarHeading #h="forCalendarHeading" class="sr-only">{{ h.label() }}</h2>
447
447
  </header>
448
448
 
449
- @switch (cal.view()) { @case ('day') {
449
+ @let view = cal.view(); @switch (view) { @case ('day') {
450
450
  <table forCalendarGrid #grid="forCalendarGrid">
451
451
  <thead forCalendarGridHeader>
452
452
  <tr>
@@ -489,7 +489,7 @@ The lower-level hooks (`visibleMonthNumber()`, `visibleYear()`, `monthOptions()`
489
489
  }
490
490
  </tbody>
491
491
  </table>
492
- } }
492
+ } @default never; }
493
493
  </div>
494
494
  ```
495
495
 
@@ -57,7 +57,7 @@ Headless and styleless: it ships slide tracking, keyboard navigation, focus mana
57
57
 
58
58
  ## Examples
59
59
 
60
- Page through the slides with the buttons, the arrow keys or a drag. The root carries `data-orientation`, and `data-dragging` is on while a pointer holds the track.
60
+ Page through the slides with the buttons or with the arrow keys on the dots. The root carries `data-orientation`, and only the current dot is in the tab order however the slide was reached.
61
61
 
62
62
  ```ts
63
63
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@@ -144,7 +144,7 @@ interface Slide {
144
144
  <button
145
145
  forCarouselIndicator
146
146
  class="car-dot"
147
- [attr.aria-label]="'Go to slide ' + (i + 1)"
147
+ [ariaLabel]="'Go to slide ' + (i + 1)"
148
148
  ></button>
149
149
  }
150
150
  </div>
@@ -186,7 +186,7 @@ Set `slidesPerView` above `1` to show several slides at once. Each slide is `fle
186
186
 
187
187
  ### Autoplay with pause control
188
188
 
189
- With `autoplay` set, the slides advance every three seconds, unless your system asks for reduced motion, in which case they wait for the rotation control. Hover the carousel or move focus into it and rotation pauses until you leave; stop it with the control and it stays stopped through hover and focus until you start it again. [Autoplay](#autoplay) has the full pause rules and why the control must be the first focusable child.
189
+ With `autoplay` set, the slides advance every three seconds, unless your system asks for reduced motion, in which case they wait for the rotation control. Hover the carousel or move focus into it and rotation pauses until you leave; stop it with the control and it stays stopped through hover and focus until you start it again, and a Start from the control rotates straight away, with focus and the pointer still on it. [Autoplay](#autoplay) has the full pause rules and why the control must be the first focusable child.
190
190
 
191
191
  ### Pointer drag & touch swipe
192
192
 
@@ -238,8 +238,12 @@ focusable child** of the carousel to enable automatic slide rotation.
238
238
 
239
239
  **Behaviour:**
240
240
 
241
- - Rotation pauses on hover, on keyboard focus anywhere inside the carousel, and
242
- while the browser tab is backgrounded. It resumes when none of those hold.
241
+ - Rotation that started on its own (`autoplay`) pauses on hover, on keyboard
242
+ focus anywhere inside the carousel, and while the browser tab is backgrounded.
243
+ It resumes when none of those hold.
244
+ - An explicit user Start (clicking the rotation control or calling `play()`)
245
+ rotates even while focus or the pointer is inside the carousel, which is where
246
+ the user is when they press it. Only a backgrounded tab still pauses it.
243
247
  - An explicit user stop (clicking the rotation control or calling `pause()`) is
244
248
  **sticky**: hover-in/out and focus-in/out will not restart it. Only an explicit
245
249
  Start does.
@@ -461,7 +465,8 @@ Reflected on the `[forCarouselViewport]` host this directive is applied to:
461
465
  ## Keyboard
462
466
 
463
467
  Keyboard navigation lives on the indicator group. Only the current
464
- indicator is in the tab order. Arrow keys move focus and activate the target slide
468
+ indicator is in the tab order, including after prev / next, autoplay or a drag
469
+ moved the slide. Arrow keys move focus and activate the target slide
465
470
  automatically.
466
471
 
467
472
  | Key | Action |
@@ -63,6 +63,12 @@ A parent checkbox reflects `indeterminate` when only some children are selected.
63
63
  import { Component, computed, signal } from '@angular/core';
64
64
  import { ForCheckbox } from 'forty-cdk/checkbox';
65
65
 
66
+ interface Topping {
67
+ readonly id: number;
68
+ readonly label: string;
69
+ readonly selected: boolean;
70
+ }
71
+
66
72
  @Component({
67
73
  selector: 'demo-select-all',
68
74
  imports: [ForCheckbox],
@@ -80,7 +86,12 @@ import { ForCheckbox } from 'forty-cdk/checkbox';
80
86
  Select all toppings
81
87
  </button>
82
88
  @for (item of items(); track item.id) {
83
- <button forCheckbox class="cb-row" [(checked)]="item.selected">
89
+ <button
90
+ forCheckbox
91
+ class="cb-row"
92
+ [checked]="item.selected"
93
+ (checkedChange)="setItem(item.id, $event)"
94
+ >
84
95
  <span class="cb">
85
96
  <span class="cb-check" aria-hidden="true"></span>
86
97
  </span>
@@ -90,7 +101,7 @@ import { ForCheckbox } from 'forty-cdk/checkbox';
90
101
  `,
91
102
  })
92
103
  export class DemoSelectAll {
93
- readonly items = signal([
104
+ readonly items = signal<readonly Topping[]>([
94
105
  { id: 1, label: 'Mozzarella', selected: false },
95
106
  { id: 2, label: 'Mushrooms', selected: true },
96
107
  { id: 3, label: 'Pepperoni', selected: false },
@@ -106,6 +117,10 @@ export class DemoSelectAll {
106
117
  const next = !this.allChecked();
107
118
  this.items.update((list) => list.map((i) => ({ ...i, selected: next })));
108
119
  }
120
+
121
+ setItem(id: number, selected: boolean): void {
122
+ this.items.update((list) => list.map((i) => (i.id === id ? { ...i, selected } : i)));
123
+ }
109
124
  }
110
125
  ```
111
126
 
@@ -149,7 +164,7 @@ export class DemoCheckout {
149
164
  | `checked` | `model<boolean>` | Two-way bindable on/off. Required by `FormCheckboxControl`.<br>**Default:** — |
150
165
  | `indeterminate` | `model<boolean>` | Two-way bindable. When true, `aria-checked="mixed"` regardless of `checked`. Click clears it. UI-only and not part of the form value.<br>**Default:** — |
151
166
  | `disabled` / `readonly` / `required` / `invalid` / `pending` | `input<boolean>` | Reflected as the matching `aria-*` / `data-*` attributes. A disabled checkbox stays focusable (per APG), with `aria-disabled="true"` + `data-disabled` and no native `disabled`; click is a no-op.<br>**Default:** — |
152
- | `name` | `input<string>` | Reflected on `name` (empty string omits the attribute).<br>**Default:** `''` |
167
+ | `name` | `input<string>` | Reflected on `name` (empty string omits the attribute). A native reset of the owning `<form>` restores `checked` and `indeterminate` to their first-render values.<br>**Default:** `''` |
153
168
  | `errors` | `input<readonly ValidationError.WithOptionalFieldTree[]>` | Validation errors fed by `[formField]`.<br>**Default:** — |
154
169
  | `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** — |
155
170
 
@@ -220,7 +220,7 @@ Input tables are not yet tabulated for this primitive. See the feature sections
220
220
  | `[forComboboxChip]` | `data-disabled` | present / absent |
221
221
  | `[forComboboxSeparator]` | `data-orientation` | `horizontal` \| `vertical` |
222
222
 
223
- Focus stays on the `<input>` the whole time the listbox is open, so options never get `:focus`. `data-highlighted` is the canonical hook for styling the keyboard-active option. A mouse press anywhere on the popup that lands on no focusable element (padding, a group label, an empty or status row) does not move focus either, so typing and the arrow keys keep working after it; the trade-off is that popup text cannot be selected with the mouse, as in a native combobox. A press on a focusable element inside the popup, such as a `[forComboboxAction]` or your own `<button>`, still focuses it.
223
+ Focus stays on the `<input>` the whole time the listbox is open, so options never get `:focus`. `data-highlighted` is the canonical hook for styling the keyboard-active option. A mouse press anywhere on the popup that lands on no focusable element (padding, a group label, an empty or status row) does not move focus either, so typing and the arrow keys keep working after it; the trade-off is that popup text cannot be selected with the mouse, as in a native combobox. A press on a `[forComboboxAction]` still focuses it in every browser. A press on your own `<button>` inside the popup is left to the browser, and macOS Safari and Firefox do not focus a `<button>` on a press.
224
224
 
225
225
  ## Mount/visibility convention
226
226
 
@@ -280,7 +280,7 @@ The default (editable) anatomy is a text field that filters in place. A **picker
280
280
  Add two parts:
281
281
 
282
282
  - **`[forComboboxTrigger]`**: a real `<button>` outside the `@if (open())`. It opens the panel, becomes the default positioning anchor, and is where focus returns on close.
283
- - **`[forComboboxList]`**: the `role="listbox"` element nested inside `[forComboboxContent]`, next to the input. It owns the options and the labelled role; `[forComboboxContent]` becomes a neutral popup surface.
283
+ - **`[forComboboxList]`**: the `role="listbox"` element nested inside `[forComboboxContent]`, next to the input. It owns the options and the labelled role; `[forComboboxContent]` becomes the `role="dialog"` the trigger opens, named by the trigger.
284
284
 
285
285
  ```html
286
286
  <div forCombobox #combobox="forCombobox" [(value)]="value" [(query)]="query" [(open)]="open">
@@ -292,20 +292,20 @@ Add two parts:
292
292
  @for (item of filtered(); track item.id) {
293
293
  <div forComboboxOption [value]="item.id" [label]="item.label">{{ item.label }}</div>
294
294
  }
295
- <div forComboboxEmpty>No matches.</div>
296
295
  </div>
296
+ <div forComboboxEmpty>No matches.</div>
297
297
  </div>
298
298
  }
299
299
  </div>
300
300
  ```
301
301
 
302
- Why the list part is required, not optional: a `role="listbox"` may only own `option` / `group` children (`aria-required-owned-elements`). Nesting the input inside a listbox would be invalid, so the listbox role moves to `[forComboboxList]` and the input sits beside it under the neutral popup surface. Put non-option chrome (`[forComboboxInput]`, `[forComboboxEmpty]`, `[forComboboxStatus]`) inside `[forComboboxContent]` but **outside** `[forComboboxList]`.
302
+ Why the list part is required, not optional: a `role="listbox"` may only own `option` / `group` children (`aria-required-owned-elements`). Nesting the input inside a listbox would be invalid, so the listbox role moves to `[forComboboxList]` and the input sits beside it inside the dialog surface. Put non-option chrome (`[forComboboxInput]`, `[forComboboxEmpty]`, `[forComboboxStatus]`) inside `[forComboboxContent]` but **outside** `[forComboboxList]`.
303
303
 
304
304
  **Focus hand-off.** Registering a trigger opts the combobox into the standard trigger-anchored focus model: on open, focus moves into the input (the search field inside the panel); on close, focus returns to the trigger. Both moves are vetoable via `(autoFocusOnOpen)` / `(autoFocusOnClose)` on `[forCombobox]`, and the return is gated by `[returnFocus]` (default `true`). Escape stays owned by the input. See [Focus & the `(autoFocusOnOpen)` / `(autoFocusOnClose)` hooks](#focus--the-autofocusonopen--autofocusonclose-hooks).
305
305
 
306
306
  **A trigger that registers late still owns the hand-off.** The trigger does not have to be declared before `[forComboboxContent]`, and does not have to exist when the panel first mounts. You can project it through `<ng-content>` from a wrapper, put it under a `@defer`, or gate it on an `@if` over loaded data. One caveat: focus moving _into_ the panel is a mount-time event, so a trigger that arrives while the panel is **already open** does not retroactively pull focus out of wherever you left it. From that moment on it does own the return focus, `(autoFocusOnClose)` and the Escape fallback, and the next open moves focus into the input as usual.
307
307
 
308
- **Trigger keyboard.** Click / Enter / Space toggle (open moves focus into the input). ArrowDown opens with the first enabled option highlighted; ArrowUp opens with the last.
308
+ **Trigger keyboard.** Click / Enter / Space toggle (open moves focus into the input). ArrowDown and ArrowUp open with the committed selection highlighted; with nothing selected, ArrowDown lands on the first enabled option and ArrowUp on the last.
309
309
 
310
310
  **Anchor preference.** With a trigger present the panel anchors to it by default. An explicit `[forComboboxAnchor]` still wins (explicit anchor → trigger → input), so you can wrap a decorated trigger box and anchor against it.
311
311
 
@@ -536,7 +536,7 @@ When the input is empty (no query) and the user presses Backspace, focus jumps t
536
536
  | Highlight the selection on open | `first` | `openHighlight="selected"` |
537
537
  | Restore label on close (single only) | `false` | `[restoreQueryOnClose]="true"` |
538
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 })`.
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. With [virtualization](#virtualization) the selection is not honoured: an open that would land on it highlights the first rendered enabled option instead. Both inputs take their default from the scope: `provideForComboboxDefaults({ openHighlight: 'selected', restoreQueryOnClose: true })`.
540
540
 
541
541
  ## Autocomplete modes
542
542
 
@@ -663,6 +663,8 @@ How navigation flows when virtualizing:
663
663
  3. Your virtualizer scrolls; the directive's `@for` mounts the option for index 999.
664
664
  4. As soon as that option registers (at the matching `posInSet`), the directive seeds `aria-activedescendant` to its id.
665
665
 
666
+ Opening seeds the highlight from the rendered window only: the first rendered enabled option, or the last for an ArrowUp with nothing selected. An open that would land on the committed selection (`openHighlight="selected"`, the picker anatomy, `[forComboboxToggle]`) lands on the first rendered enabled option too, because the selection may sit outside the window and the directive cannot know its index.
667
+
666
668
  Inline autocomplete matches against the most recently rendered window overlaid with the position snapshot, so completion against off-screen labels still works. `selected().label` reads a separate, selection-keyed cache instead. That cache is bounded by the selection, so the label of a selected option survives close / re-open and any number of query rebuilds, but it is **not** resolved from the position snapshot: a value that enters the selection while its option is outside the rendered window (a `[(value)]` write restoring a saved selection, say) falls back to `[itemToStringLabel]` until that option renders once. Supply `[itemToStringLabel]` whenever the selection can be seeded from outside the list.
667
669
 
668
670
  ```html
@@ -782,7 +784,7 @@ Static and `@for`-rendered options share the same registry, navigation order (DO
782
784
  </div>
783
785
  ```
784
786
 
785
- For a legacy `<form action="…">` flow, set `[name]`, and the directive mirrors `[(value)]` into N `<input type="hidden">` siblings (one per array entry; zero when empty). String values land verbatim in the hidden input; object values default to `JSON.stringify` (override via `[itemToFormValue]`, see below).
787
+ For a legacy `<form action="…">` flow, set `[name]`, and the directive mirrors `[(value)]` into N `<input type="hidden">` siblings (one per array entry; zero when empty). String values land verbatim in the hidden input; object values default to `JSON.stringify` (override via `[itemToFormValue]`, see below). A native reset of the owning `<form>` returns `value` and `query` to what they held when the combobox first rendered, and the input shows that query again.
786
788
 
787
789
  A single-select field is modeled as the same `readonly T[]`, kept at length ≤ 1, and bound with `[formField]` directly, so single mode needs no adapter. A `FieldTree<T | null>` cannot bind here; map to that shape at the edge that needs it. See [the selection value-type contract](../../../docs/selection-value-type-contract.md).
788
790
 
@@ -815,9 +817,9 @@ Hovering an option also makes it the activedescendant, so mouse and keyboard int
815
817
  Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
816
818
 
817
819
  - Apply the input directive to an actual `<input>`: the browser's caret and selection semantics are what make inline autocomplete work, and screen readers expect a real text field for `role="combobox"`.
818
- - `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.
820
+ - `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 a `role="dialog"` named by the trigger (`aria-labelledby`; a static `aria-label` or `aria-labelledby` on it wins), so it can hold the input next to the list without an `aria-required-owned-elements` violation. The "editable + list" shape, with no trigger, leaves it role-less.
819
821
  - `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.
820
- - `[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.
822
+ - `[forComboboxTrigger]` (picker anatomy) is a real `<button>` reflecting `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls` (the dialog surface, while open), an `id` (a static one is kept), and native `disabled` from the combobox's effective disabled. The popup is a dialog rather than a listbox because focus moves into the search input inside it. It is exempt from the popup's outside-pointer dismissal layer, like the input.
821
823
  - `[forComboboxContent]` and `[forComboboxList]` cancel a `mousedown` that would otherwise move focus onto the surface or blur the input, so a press between options leaves focus in the input. A press on a focusable descendant is let through.
822
824
  - `[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.
823
825
  - 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.
@@ -163,6 +163,15 @@ With a time-capable adapter, a `granularity` coarser than `'day'` appends time s
163
163
 
164
164
  Plus the shared `FormUiControl` members from `@angular/forms/signals`: `disabled`, `readonly`, `required`, `invalid`, `name`, `errors`, `touched` (bound automatically by `[formField]`).
165
165
 
166
+ | Method | Description |
167
+ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
168
+ | `clear()` | Sets `value` to `null` and empties every segment, a partly typed entry included. Call it from a Clear button. |
169
+ | `reset()` | Drops a partly typed entry and shows the current `value`. Signal Forms calls it on `form().reset()`, so a form reset clears the segments with no extra wiring. |
170
+
171
+ A partly typed date already reports `value()` as `null`, so writing `null` through `[(value)]` changes nothing and leaves the typed digits on screen. Clear the field with `clear()` instead, which you can reach through the `#field="forDateField"` template reference.
172
+
173
+ A native reset of the owning `<form>` returns `value` to what it held when the field first rendered and runs `reset()`, so a partly typed entry goes too and the hidden input submits that value again.
174
+
166
175
  > **Why `minDate` / `maxDate`, not `min` / `max`?** `FormUiControl.min` / `max` are reserved members typed `number | undefined` for numeric validators bound by `[formField]`. A date-typed `min` / `max` would break the `FormValueControl` contract, so the date bounds use distinct names.
167
176
 
168
177
  ### Data attributes
@@ -200,7 +209,7 @@ Set `granularity` to `'hour'`, `'minute'`, or `'second'` (`granularity > 'day'`)
200
209
  </div>
201
210
  ```
202
211
 
203
- On the AM/PM segment, `a` / `p` set the period and ArrowUp / ArrowDown toggle it; the period is derived from the entered hour, so clearing it is a no-op (clear or step the hour instead). The value stays `null` until every visible segment (date **and** time) is filled.
212
+ On the AM/PM segment, `a` / `p` set the period and ArrowUp / ArrowDown toggle it (on an empty segment ArrowUp sets PM and ArrowDown sets AM). The period is never derived from the entered hour: it stays empty until you set it, and Delete / Backspace leave it as it is. In a locale whose 12-hour clock counts 0–11 (`ja-JP`), the hour segment does too. The value stays `null` until every visible segment (date **and** time) is filled.
204
213
 
205
214
  ## Scoped defaults
206
215
 
@@ -284,6 +293,8 @@ readonly booking = form(this.model);
284
293
 
285
294
  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]`.
286
295
 
296
+ `ForDateRangeField` exposes the same `clear()` and `reset()` as `ForDateField`. They drop a half-entered range and an out-of-order one, its `data-range-error` included, which writing `null` through `[(value)]` cannot do: such a range already reports `value()` as `null`. Without them, finishing the end endpoint later would commit a range built from a start the user thought was gone. A native reset of the owning `<form>` returns the range to what it held when the field first rendered and runs `reset()`.
297
+
287
298
  > **Why `minDate` / `maxDate`, not `min` / `max`?** Beyond the reason above, `FormUiControl.min` / `max` are additionally typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
288
299
 
289
300
  `[forDateRangeField]` reflects the same `data-disabled` / `data-readonly` / `data-empty` hooks as `[forDateField]`, plus `data-range-error`; `[forDateRangeFieldSegment]` reflects the same four segment hooks. `data-empty` marks the field only while **both** endpoints are entirely empty; a partially-filled or complete-but-disordered range is **not** empty.
@@ -220,6 +220,8 @@ Open the picker and click a first day: the trigger keeps its placeholder, becaus
220
220
 
221
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`).
222
222
 
223
+ A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered. In the [field anatomy](#field-anatomy) it also drops a partly typed entry in the projected field.
224
+
223
225
  > **`locale` only styles the trigger display.** It drives the text rendered by `[forDatePickerValue]` (both here and on `ForDateRangePicker`); it is **not** forwarded to the projected `ForCalendar`. Bind the calendar's own `[locale]` to localize its heading / weekday / cell labels, exactly as you forward `[min]` / `[max]`.
224
226
 
225
227
  > **Why `minDate` / `maxDate`, not `min` / `max`?** `ForDatePicker` is a `FormValueControl`, and `FormUiControl` reserves `min` / `max` for numeric validators (`InputSignal<number | undefined>`). A date-typed `min` / `max` would break that contract, so the date bounds use the `*Date` suffix. (`ForCalendar` is not a form control, so it keeps `min` / `max`.)
@@ -294,6 +296,8 @@ Set `granularity` to `'hour'`, `'minute'`, or `'second'` to turn the picker into
294
296
 
295
297
  Bind the calendar **and** the time field **one-way** to `picker.value()` (not `[(value)]`). The picker is the single source of truth: when one-way bound to a timed value the calendar preserves the time-of-day on its own selection, and the picker re-grafts the previously entered time as a defensive fallback for the case where the calendar value was null or midnight (reading its own value, which the one-way children never clobber); a time-field edit emits a full date-time the picker mirrors in. A date-time picker never closes on a calendar selection, so the user can go on to set the time.
296
298
 
299
+ Like the calendar, the projected time field or time picker takes the picker's `readonly` and `disabled` without a binding, so a read-only or disabled picker's time source accepts no edit and shows only the committed time. A typed time the picker clamps into `[minDate, maxDate]` behaves as in a standalone time field: while focus stays in the field its segments keep what was entered, and they show the clamped value once focus leaves.
300
+
297
301
  The content is a field boundary. Inside a [`[forField]`](../field/README.md#how-the-control-connects) the time field does not register with the field, which keeps reflecting the picker (its label target, `invalid` state and errors) while the panel is open.
298
302
 
299
303
  ```html
@@ -379,6 +383,7 @@ With the field adopted:
379
383
  - **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.
380
384
  - **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.
381
385
  - **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).
386
+ - **A press on a segment closes the surface.** Only the trigger and the field's labels are exempt from outside dismissal, so pressing a segment to start typing closes the calendar and leaves focus on that segment.
382
387
 
383
388
  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.
384
389
 
@@ -432,7 +437,7 @@ readonly booking = form(this.model, (p) => required(p.stay));
432
437
 
433
438
  - **Form value.** The committed `DateRange<D> | null` is the `value` model. `null` is the empty state. Pair it with `required(p.stay)` so `invalid()` flips when the form demands a range and none is committed. `touched` fires on commit and on close, exactly like the single-date picker.
434
439
  - **Validity.** `start <= end` is guaranteed by construction and is never an error. Forward `minDate` / `maxDate` to the calendar's `[min]` / `[max]` so out-of-range days are disabled in the grid; a range picked in a calendar without them is clamped into `[minDate, maxDate]` on commit. Forward `minRangeLength` / `maxRangeLength` to the calendar's `[minRangeLength]` / `[maxRangeLength]` (a too-short / too-long range is rejected as a no-op by the calendar's two-click flow).
435
- - **Native submission.** When `name` is set, two hidden inputs `<name>-start` / `<name>-end` mirror the committed endpoints as ISO `YYYY-MM-DD` for native `<form>` posts.
440
+ - **Native submission.** When `name` is set, two hidden inputs `<name>-start` / `<name>-end` mirror the committed endpoints as ISO `YYYY-MM-DD` for native `<form>` posts. A native reset of the owning `<form>` returns `value` to what it held when the picker first rendered.
436
441
  - **Bounds naming.** `minDate` / `maxDate` (not `min` / `max`) for the same reason as `ForDatePicker`, and additionally because `FormUiControl.min` / `max` are typed `NonNullable<TValue>` (the range object itself), which is meaningless as a bound.
437
442
 
438
443
  Defaults are configured with `provideForDateRangePickerDefaults` (`side` / `align` / `sideOffset` / `collisionPadding`), and both wrapper patterns work via the exported `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_RANGE_PICKER_HOST_DIRECTIVE_OUTPUTS` tuples. See [Wrapping form primitives](../../../docs/wrapping-form-primitives.md).
@@ -456,7 +461,7 @@ Implements the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA
456
461
  - **`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).
457
462
  - **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`.
458
463
  - **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.
459
- - **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)`.
464
+ - **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)`. The one exception in non-modal mode is an **outside-interaction close**, meaning a pointer-down or focus-out that lands outside the surface. After such a close, focus stays where the interaction moved it instead of snapping back to the trigger, so a click on the next form control keeps the user's typing there. Escape, a pick and programmatic closes still return focus.
460
465
  - **Dismissal**: Escape (`(escapeKeyDown)`) and outside-pointer (`(pointerDownOutside)` / `(interactOutside)`) close the surface, each vetoable.
461
466
 
462
467
  ## Styling
@@ -480,4 +485,4 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
480
485
 
481
486
  ## Wrapping in a design system
482
487
 
483
- [Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing.
488
+ [Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_DATE_PICKER_HOST_DIRECTIVE_INPUTS` / `FOR_DATE_PICKER_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing. A subclass spreads [`provideForDatePicker(MyDatePicker)`](../../../docs/wrapping-form-primitives.md#date-and-time-roots-spread-their-provider-helper) or `provideForDateRangePicker(MyDateRangePicker)` as its `providers`, because the root also provides tokens its projected calendar, field and time control resolve that cannot be imported.
package/dialog/README.md CHANGED
@@ -157,9 +157,9 @@ Open a component imperatively and await its result. The manager mounts it under
157
157
  | Property | Type | Description |
158
158
  | -------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
159
  | `dismissible` | — | When `false`, Escape, backdrop, outside-pointer, and outside-focus do not request close. The close button still does.<br>**Default:** `true` |
160
- | `modal` | — | When `false`, no `aria-modal`, no scroll lock, no focus trap.<br>**Default:** `true` |
160
+ | `modal` | — | When `false`, no `aria-modal`, no scroll lock, no focus trap, no inert siblings, and no return focus on close.<br>**Default:** `true` |
161
161
  | `alert` | — | Switches role to `alertdialog`.<br>**Default:** `false` |
162
- | `returnFocus` | — | Focus returns to the previously focused element on close.<br>**Default:** `true` |
162
+ | `returnFocus` | — | Modal mode only: focus returns to the previously focused element on close. A non-modal dialog never moves focus on close.<br>**Default:** `true` |
163
163
  | `initialFocus` | — | `'first'` (first focusable inside) or `'container'` (the dialog host).<br>**Default:** `'first'` |
164
164
  | `ariaLabel` | — | Manual `aria-label` if no `[forDialogTitle]` is rendered.<br>**Default:** `null` |
165
165
  | `dismiss` | `OutputEmitterRef<ForDialogCloseReason>` | Output. Dialog wants to be unmounted. Reasons: `'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`. Spelled `dismiss` rather than `close` on purpose (see [Behavior notes](#behavior-notes)).<br>**Default:** — |
@@ -298,7 +298,22 @@ this.dialogs.open(ConfirmDialog, {
298
298
  }
299
299
  ```
300
300
 
301
- Set them once for a scope with `provideForDialogDefaults({ animateEnter, animateLeave })`; a per-`open()` value always wins over the scope default.
301
+ A template `animate.leave` inside the opened component never runs, because Angular does not run leave animations across the outlet the manager mounts it through. The exit animation is configured with `animateLeave` for the host and `backdropAnimateLeave` for a `[forDialogBackdrop]`, whose enter stays an `animate.enter` on the element itself.
302
+
303
+ Set them once with `provideForDialogDefaults({ animateEnter, animateLeave, backdropAnimateLeave })`; a per-`open()` value always wins over the default. `ForDialogManager` is provided in root and reads the defaults from the root injector, so a `provideForDialogDefaults` on a lazy route or in a component's `providers` reaches a programmatic dialog only when that scope's injector is passed as `injector`. The opened component is then parented on it too, so the scope's other providers reach it as well.
304
+
305
+ <!-- snippet: fragment -->
306
+
307
+ ```ts
308
+ class ScopedHost {
309
+ readonly #dialogs = inject(ForDialogManager);
310
+ readonly #injector = inject(Injector);
311
+
312
+ confirm(): void {
313
+ this.#dialogs.open(ConfirmDialog, { injector: this.#injector });
314
+ }
315
+ }
316
+ ```
302
317
 
303
318
  | Symbol | Description |
304
319
  | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -309,26 +324,30 @@ Set them once for a scope with `provideForDialogDefaults({ animateEnter, animate
309
324
 
310
325
  ### `ForDialogOpenConfig`
311
326
 
312
- | Field | Default | Description |
313
- | -------------------- | --------- | -------------------------------------------------------------------------------------------------------- |
314
- | `data` | — | Payload available as `injectDialogData<T>()`. |
315
- | `dismissible` | `true` | Escape closes when `true`. |
316
- | `modal` | `true` | Sets `aria-modal`, locks body scroll, traps focus. |
317
- | `alert` | `false` | Use `role="alertdialog"` instead of `"dialog"`. |
318
- | `returnFocus` | `true` | Focus returns to the previously focused element on close. |
319
- | `initialFocus` | `'first'` | `'first'` finds first focusable; `'container'` focuses the host. |
320
- | `ariaLabel` | — | Manual accessible name when no title element is rendered. |
321
- | `animateEnter` | — | CSS class applied on mount (via `animate.enter`) to play an enter animation. |
322
- | `animateLeave` | — | CSS class applied on close; the host stays mounted until its animation finishes, then tears down. |
323
- | `class` | — | CSS class(es) applied to the overlay root (the `[forDialog]` host). Single or space-separated string. |
324
- | `classList` | — | CSS class(es) applied to the overlay root, as an array or space-separated string. Merged with `class`. |
325
- | `providers` | `[]` | Extra providers for the opened component's injector. |
326
- | `autoFocusOnOpen` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the imperative initial focus move. |
327
- | `autoFocusOnClose` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the return-focus on close. |
328
- | `escapeKeyDown` | — | Callback. `VetoableNativeEvent<KeyboardEvent>`; `preventDefault()` suppresses the Escape close. |
329
- | `pointerDownOutside` | — | Callback. `VetoableNativeEvent<PointerEvent>`; `preventDefault()` suppresses the outside-pointer close. |
330
- | `focusOutside` | — | Callback. `VetoableNativeEvent<FocusEvent>`; `preventDefault()` suppresses the outside-focus close. |
331
- | `interactOutside` | — | Callback. Composite `VetoableNativeEvent<PointerEvent \| FocusEvent>`; shares the veto of the two above. |
327
+ | Field | Default | Description |
328
+ | ---------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- |
329
+ | `data` | — | Payload available as `injectDialogData<T>()`. |
330
+ | `dismissible` | `true` | When `false`, Escape, backdrop, outside-pointer and outside-focus do not close the dialog. |
331
+ | `modal` | `true` | Sets `aria-modal`, locks body scroll, traps focus, inerts siblings and returns focus on close. |
332
+ | `alert` | `false` | Use `role="alertdialog"` instead of `"dialog"`. |
333
+ | `returnFocus` | `true` | Modal mode only: focus returns to the previously focused element on close. |
334
+ | `returnFocusTarget` | — | Element focus returns to. Omitted, the trigger that opened the modal chain; `null`, the element focused at mount. |
335
+ | `initialFocus` | `'first'` | `'first'` finds first focusable; `'container'` focuses the host. |
336
+ | `ariaLabel` | — | Manual accessible name when no title element is rendered. |
337
+ | `container` | — | Portal target for the surface and its backdrop, `document.body` when omitted. Pair with `modal: false`. |
338
+ | `animateEnter` | — | CSS class applied on mount (via `animate.enter`) to play an enter animation. |
339
+ | `animateLeave` | — | CSS class applied on close; the host stays mounted until its animation finishes, then tears down. |
340
+ | `backdropAnimateLeave` | — | CSS class applied to the `[forDialogBackdrop]` on close; it stays mounted until its animation finishes. |
341
+ | `class` | — | CSS class(es) applied to the overlay root (the `[forDialog]` host). Single or space-separated string. |
342
+ | `classList` | — | CSS class(es) applied to the overlay root, as an array or space-separated string. Merged with `class`. |
343
+ | `providers` | `[]` | Extra providers for the opened component's injector. |
344
+ | `injector` | — | Caller scope: scoped `provideForDialogDefaults` and providers are read from it instead of the root. |
345
+ | `autoFocusOnOpen` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the imperative initial focus move. |
346
+ | `autoFocusOnClose` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the return-focus on close. |
347
+ | `escapeKeyDown` | — | Callback. `VetoableNativeEvent<KeyboardEvent>`; `preventDefault()` suppresses the Escape close. |
348
+ | `pointerDownOutside` | — | Callback. `VetoableNativeEvent<PointerEvent>`; `preventDefault()` suppresses the outside-pointer close. |
349
+ | `focusOutside` | — | Callback. `VetoableNativeEvent<FocusEvent>`; `preventDefault()` suppresses the outside-focus close. |
350
+ | `interactOutside` | — | Callback. Composite `VetoableNativeEvent<PointerEvent \| FocusEvent>`; shares the veto of the two above. |
332
351
 
333
352
  The four dismiss callbacks mirror the declarative `(escapeKeyDown)` / `(pointerDownOutside)` / `(focusOutside)` / `(interactOutside)` outputs exactly, with the same events and the same veto semantics. See [Per-channel dismissal](#per-channel-dismissal-escape-only-dialogs).
334
353
 
@@ -372,6 +391,8 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
372
391
  }
373
392
  ```
374
393
 
394
+ The backdrop is portaled immediately before its own dialog, so with no `z-index` on either the dialog paints above it. Set one only when other positioned content on the page has to sit beneath the pair.
395
+
375
396
  **Stacked dialogs.** Each surface and its backdrop publish their stacking position as `data-depth` and `--for-dialog-depth` (`0` for the first dialog, `1` above it, …), shared by declarative and managed dialogs. One rule then covers every level, with each backdrop one step below its own dialog and above every dialog underneath it:
376
397
 
377
398
  ```css
@@ -406,14 +427,14 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
406
427
 
407
428
  - **The declarative and imperative surfaces spell this differently, on purpose.** The output is `(dismiss)`, because an output named `close` would collide with the native DOM event and break any wrapper re-exposing it through `hostDirectives`. Nothing else changes name: the imperative handle method is `ForDialogRef.close()`, the directive selector is `[forDialogClose]`, and the payload type is `ForDialogCloseReason`.
408
429
  - **Mount equals open**. The directive does not manage `[hidden]` or any visibility attribute. The consumer's `@if (open())` controls presence, and `animate.enter` / `animate.leave` handle the visual transition. That is why `[forDialog]`, `[forDialogBackdrop]`, and `[forDialogClose]` carry a static `data-state="open"`: because the host only exists inside `@if (open())`, the element is present iff the dialog is open, so the attribute can never be `closed`. Exit styling is the consumer's `animate.leave`, not a `[data-state="closed"]` selector. Only `[forDialogTrigger]`, which stays mounted, toggles `open` / `closed`.
409
- - **Portal**: the dialog box is moved to `document.body` on first render (or to `container` when set). The backdrop portals alongside the dialog (to the same `container`, `document.body` by default). CSS scoped to ancestors won't apply, so use global styles or classes.
430
+ - **Portal**: the dialog box is moved to `document.body` on first render (or to `container` when set). The backdrop portals to the same `container`, `document.body` by default, immediately before its dialog. CSS scoped to ancestors won't apply, so use global styles or classes.
410
431
  - **Body scroll lock** is refcounted: stacking dialogs (or a dialog + a future overlay using the same lock) only restore on the last unlock.
411
432
  - **Focus trap** scopes Tab inside the dialog box while `modal`. It does NOT itself mark the rest of the page `inert`. That is the inert-siblings utility's job (next bullet).
412
433
  - **Inert siblings**. When `modal`, every direct child of `document.body` other than the dialog box (and its backdrop) gets `inert` and `aria-hidden="true"` while open, and is restored on close. This is what `aria-modal="true"` alone is missing, since Safari + VoiceOver and several other AT pairings otherwise still announce siblings of an aria-modal node. Stacking is order-safe: when a second modal opens on top, the first becomes inert; closing the top dialog re-activates the underlying one.
413
434
  - **Vetoable dismissals**. Each of `(escapeKeyDown)`, `(pointerDownOutside)`, `(focusOutside)`, `(interactOutside)` fires before the corresponding `(dismiss)`. Call `preventDefault()` on the event to keep the dialog open (e.g. to ask "are you sure?" first).
414
435
  - **Focus callbacks are inputs, not outputs.** The `autoFocusOnOpen` / `autoFocusOnClose` shape mirrors `ForDialogManager`'s `config.autoFocusOn*` callbacks and guarantees the `autoFocusOnClose` callback fires reliably on every close path, including a direct `open.set(false)` that bypasses the `(dismiss)` output. The trigger-anchored overlays (Popover, DropdownMenu, Select, …) expose the same pair as **outputs** instead, because every close transition there runs through their own `[(open)]` model, so no path can bypass the emitter.
415
436
  - **The close button** (`[forDialogClose]`) always requests close, regardless of `dismissible`. Reason emitted is `'closeButton'`.
416
- - **Both flows share the same engine**: the focus trap, scroll lock, dismissible layer, and portal in `ForDialogManager.open()` use the same `_internal/` utilities as the directive. Behavior is identical.
437
+ - **Both flows share the same engine**: `ForDialogManager.open()` renders the same `[forDialog]` directive, so the focus trap, scroll lock, dismissible layer and portal are the directive's own. Behavior is identical.
417
438
 
418
439
  ### Per-channel dismissal (Escape-only dialogs)
419
440
 
@@ -199,8 +199,10 @@ name and leave "sortable" to `aria-sort`.
199
199
 
200
200
  Applying the move in `(dragDrop)` destroys or re-inserts the lifted element, which would otherwise
201
201
  leave focus on `<body>`. So after a **keyboard** drop whose lifted item held focus, `[forDropList]`
202
- restores focus to the item at `currentIndex` in the target container on the next render. That item is
203
- the same element the user just placed, whether it stayed in this list or transferred to a connected one.
202
+ restores focus on the next render to the item whose `[dragData]` is the dropped item, falling back to the
203
+ item at `currentIndex` in the target container when no item carries it any more. That is the item the user
204
+ just placed, whether it stayed in this list or transferred to a connected one, and whether the `@for`
205
+ tracks by item identity or by `$index`.
204
206
 
205
207
  Two escape hatches:
206
208
 
package/drawer/README.md CHANGED
@@ -171,7 +171,7 @@ this.#drawers.open(ConfirmDrawer, { data, side: 'bottom', class: 'my-drawer' });
171
171
  }
172
172
  ```
173
173
 
174
- **Enter / exit animations.** A programmatic drawer is portaled to `document.body` and torn down imperatively, so the consumer can't attach `animate.leave` to the host the way a declarative `@if` block can. Pass `animateEnter` / `animateLeave` (CSS class names) instead: the manager applies `animateEnter` on mount (via `animate.enter`) and, on `close()`, keeps the host mounted with `animateLeave` until its CSS animations / transitions finish before tearing down. `close()` still resolves its promise and flips `isClosed()` immediately. Only the visual teardown waits. Set them once for a scope with `provideForDrawerDefaults({ animateEnter, animateLeave })`; a per-`open()` value wins over the scope default.
174
+ **Enter / exit animations.** A programmatic drawer is portaled to `document.body` and torn down imperatively, so the consumer can't attach `animate.leave` to the host the way a declarative `@if` block can. Pass `animateEnter` / `animateLeave` (CSS class names) instead: the manager applies `animateEnter` on mount (via `animate.enter`) and, on `close()`, keeps the host mounted with `animateLeave` until its CSS animations / transitions finish before tearing down. `close()` still resolves its promise and flips `isClosed()` immediately. Only the visual teardown waits. A template `animate.leave` inside the opened component never runs, because Angular does not run leave animations across the outlet the manager mounts it through; a `[forDrawerBackdrop]` takes its exit class from `backdropAnimateLeave`. Set them once with `provideForDrawerDefaults({ animateEnter, animateLeave, backdropAnimateLeave })`; a per-`open()` value wins over the default. `ForDrawerManager` is provided in root and reads the defaults from the root injector, so a `provideForDrawerDefaults` on a lazy route or in a component's `providers` reaches a programmatic drawer only when that scope's injector is passed as `injector` on the `open()` config.
175
175
 
176
176
  <!-- snippet: fragment -->
177
177
 
@@ -362,10 +362,10 @@ Press the button: the manager opens the confirmation as a bottom drawer, and the
362
362
  | Property | Type | Description |
363
363
  | --------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
364
364
  | `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | Anchored edge. Drives swipe direction and `data-side`.<br>**Default:** `'bottom'` |
365
- | `modal` | `boolean` | `aria-modal`, scroll lock, focus trap, inert siblings.<br>**Default:** `true` |
365
+ | `modal` | `boolean` | `aria-modal`, scroll lock, focus trap, inert siblings, return focus on close.<br>**Default:** `true` |
366
366
  | `dismissible` | `boolean` | Whether Escape / backdrop / outside / swipe close. When `false`, no swipe dismisses; `snapPoints` still drag between snaps.<br>**Default:** `true` |
367
367
  | `alert` | `boolean` | `role="alertdialog"`.<br>**Default:** `false` |
368
- | `returnFocus` | `boolean` | Restore focus on close.<br>**Default:** `true` |
368
+ | `returnFocus` | `boolean` | Modal mode only: restore focus on close. A non-modal drawer never moves focus on close.<br>**Default:** `true` |
369
369
  | `initialFocus` | `'first' \| 'container'` | **Default:** `'first'` |
370
370
  | `ariaLabel` | `string \| null` | Use when no visible title is rendered.<br>**Default:** `null` |
371
371
  | `autoFocusOnOpen` | `(e: VetoableEvent) => void` \| `undefined` | `event.preventDefault()` skips the imperative focus move.<br>**Default:** — |
@@ -518,14 +518,14 @@ For a seamless release, transition **both** `translate` and your snap-position p
518
518
  - `closeThreshold` (default `0.25`) is the fraction past which a release from the lowest snap dismisses. It is measured against that snap's own extent (not the full dimension), so a small "peek" snap stays dismissible without dragging it off-screen.
519
519
  - `handleOnly: true` confines the gesture to a registered `[forDrawerHandle]`, leaving the rest of the surface free for content scroll.
520
520
  - On touch the browser would otherwise claim the swipe as a scroll. The handle always carries `touch-action: none`, and while the whole surface arms the drag (`handleOnly` off, and a drag the drawer can arm) the surface carries an inline `touch-action` that leaves the browser only the other axis: `pan-x` for a `top` / `bottom` drawer, `pan-y` for `left` / `right`. A scroller inside the surface keeps its own native scrolling, so your CSS needs no `touch-action` rule of its own.
521
- - Gestures starting inside a scrollable element that hasn't reached its edge are NOT treated as swipes (the helper defers to inner scroll).
521
+ - Gestures whose press lands inside a scrollable element that hasn't reached its edge are NOT treated as swipes (the helper defers to inner scroll). The edge is read in the scroller's own writing direction, with a 1 px tolerance for fractional scroll offsets.
522
522
  - **`prefers-reduced-motion: reduce`** disables the drag entirely, snap dragging included. Escape, backdrop, outside-pointer, and close button continue to work.
523
523
 
524
524
  ## Scale background
525
525
 
526
526
  Opt in to the "viewport recedes behind the drawer" effect: when the drawer opens, the rest of the app shrinks slightly and rounds its corners to read as a layered surface. Two pieces required:
527
527
 
528
- 1. Apply `[forDrawerWrapper]` on the element that wraps the rest of the app (typically the root shell). Only one wrapper may be registered at a time.
528
+ 1. Apply `[forDrawerWrapper]` on the element that wraps the rest of the app (typically the root shell). Keep one wrapper per page: the most recently mounted one is scaled, and a second one still mounted once the render settles warns in dev mode.
529
529
  2. Set `[scaleBackground]="true"` on the drawer that should drive the effect.
530
530
 
531
531
  ```html
@@ -100,7 +100,7 @@ A settings-style dropdown built from the full menu vocabulary: `forMenuGroup` wi
100
100
  | `fallbackAxisSideDirection` | `input<'none' \| 'start' \| 'end'>` | When both sides of the preferred axis overflow, lets `flip` drop the menu to a perpendicular side instead of clipping. `'none'` keeps only the opposite same-axis placement. The default is read from `provideForDropdownMenuDefaults` for the surrounding scope, so set it once for the whole app rather than per call site.<br>**Default:** `'none'` |
101
101
  | `loop` | `input<boolean>` | Whether arrow navigation wraps at the ends.<br>**Default:** `true` |
102
102
  | `dir` | `input<string>` | Writing direction. In RTL, ArrowLeft opens submenus and ArrowRight closes them. The swap is automatic. Inherited by every nested `[forMenuSub]` underneath unless overridden.<br>**Default:** `'ltr'` |
103
- | `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored.<br>**Default:** `false` |
103
+ | `disabled` | `input<boolean>` | When `true`, trigger interactions are ignored and every item of the menu and its submenus is disabled. An open menu stays open, and keyboard navigation, Tab and Escape keep working.<br>**Default:** `false` |
104
104
  | `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
105
105
  | `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger on close.<br>**Default:** `true` |
106
106
  | `ariaLabel` | `input<string \| null>` | Manual `aria-label` on `[forMenuContent]` if the trigger isn't a meaningful name.<br>**Default:** `null` |