forty-cdk 0.32.0 → 0.34.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 (186) hide show
  1. package/breakpoints/README.md +6 -2
  2. package/carousel/README.md +4 -3
  3. package/checkbox/README.md +1 -1
  4. package/combobox/README.md +11 -9
  5. package/date-field/README.md +14 -3
  6. package/date-picker/README.md +14 -8
  7. package/dialog/README.md +46 -25
  8. package/drag-drop/README.md +12 -5
  9. package/drawer/README.md +6 -5
  10. package/dropdown-menu/README.md +1 -1
  11. package/fesm2022/forty-cdk-accordion.mjs +1 -2
  12. package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
  13. package/fesm2022/forty-cdk-avatar.mjs +6 -5
  14. package/fesm2022/forty-cdk-avatar.mjs.map +1 -1
  15. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-calendar.mjs +5 -8
  17. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  18. package/fesm2022/forty-cdk-carousel.mjs +35 -28
  19. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-checkbox.mjs +3 -1
  21. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-combobox.mjs +118 -47
  23. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-context-menu.mjs +9 -6
  25. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-core-overlay.mjs +709 -351
  27. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-core.mjs +1274 -511
  29. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-date-field.mjs +71 -5
  31. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-date-picker.mjs +161 -67
  33. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-defaults.mjs +49 -9
  35. package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-dialog.mjs +14 -10
  37. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-drag-drop.mjs +16 -6
  39. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-drawer.mjs +104 -87
  41. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-dropdown-menu.mjs +7 -3
  43. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-field.mjs +23 -12
  45. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  46. package/fesm2022/forty-cdk-fieldset.mjs +4 -6
  47. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-file-upload.mjs +23 -11
  49. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-hover-card.mjs +49 -66
  51. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  52. package/fesm2022/forty-cdk-listbox.mjs +48 -52
  53. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  54. package/fesm2022/forty-cdk-menu.mjs +76 -55
  55. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  56. package/fesm2022/forty-cdk-menubar.mjs +16 -61
  57. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  58. package/fesm2022/forty-cdk-meter.mjs +2 -8
  59. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  60. package/fesm2022/forty-cdk-navigation-menu.mjs +66 -49
  61. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  62. package/fesm2022/forty-cdk-number-input.mjs +19 -10
  63. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-otp-input.mjs +34 -21
  65. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-pagination.mjs +28 -10
  67. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-pane-resizer.mjs +21 -5
  69. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-popover.mjs +9 -11
  71. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-radio-group.mjs +42 -16
  73. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-scroll-area.mjs +15 -6
  75. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  76. package/fesm2022/forty-cdk-select.mjs +75 -79
  77. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  78. package/fesm2022/forty-cdk-slider.mjs +16 -4
  79. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  80. package/fesm2022/forty-cdk-stepper.mjs +20 -10
  81. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  82. package/fesm2022/forty-cdk-switch.mjs +2 -1
  83. package/fesm2022/forty-cdk-switch.mjs.map +1 -1
  84. package/fesm2022/forty-cdk-table-virtualization.mjs +6 -0
  85. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  86. package/fesm2022/forty-cdk-table.mjs +211 -99
  87. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  88. package/fesm2022/forty-cdk-tabs.mjs +1 -2
  89. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  90. package/fesm2022/forty-cdk-testing.mjs +69 -35
  91. package/fesm2022/forty-cdk-testing.mjs.map +1 -1
  92. package/fesm2022/forty-cdk-time-field.mjs +95 -13
  93. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  94. package/fesm2022/forty-cdk-time-picker.mjs +95 -50
  95. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  96. package/fesm2022/forty-cdk-toast.mjs +11 -4
  97. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  98. package/fesm2022/forty-cdk-toggle.mjs +20 -9
  99. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  100. package/fesm2022/forty-cdk-toolbar.mjs +25 -9
  101. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  102. package/fesm2022/forty-cdk-tooltip.mjs +39 -67
  103. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  104. package/fesm2022/forty-cdk-tree.mjs +114 -95
  105. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  106. package/fesm2022/forty-cdk-virtual-reorder.mjs +19 -2
  107. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  108. package/fesm2022/forty-cdk-virtualization.mjs +19 -5
  109. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  110. package/field/README.md +11 -11
  111. package/fieldset/README.md +2 -2
  112. package/file-upload/README.md +1 -1
  113. package/hover-card/README.md +6 -4
  114. package/input/README.md +2 -0
  115. package/listbox/README.md +16 -4
  116. package/menu/README.md +2 -2
  117. package/menubar/README.md +13 -10
  118. package/meter/README.md +1 -1
  119. package/navigation-menu/README.md +3 -2
  120. package/number-input/README.md +2 -0
  121. package/otp-input/README.md +7 -5
  122. package/package.json +1 -1
  123. package/pagination/README.md +4 -5
  124. package/pane-resizer/README.md +2 -2
  125. package/popover/README.md +1 -1
  126. package/radio-group/README.md +16 -16
  127. package/scroll-area/README.md +4 -4
  128. package/search/README.md +1 -1
  129. package/select/README.md +9 -6
  130. package/shared/README.md +14 -3
  131. package/slider/README.md +4 -4
  132. package/stepper/README.md +3 -3
  133. package/switch/README.md +1 -1
  134. package/table/README.md +48 -33
  135. package/tabs/README.md +1 -1
  136. package/testing/README.md +11 -11
  137. package/time-field/README.md +14 -3
  138. package/time-picker/README.md +8 -2
  139. package/toast/README.md +3 -1
  140. package/toggle/README.md +2 -2
  141. package/toolbar/README.md +1 -1
  142. package/tooltip/README.md +2 -2
  143. package/tree/README.md +27 -13
  144. package/types/forty-cdk-breakpoints.d.ts +1 -1
  145. package/types/forty-cdk-carousel.d.ts +11 -6
  146. package/types/forty-cdk-combobox.d.ts +57 -39
  147. package/types/forty-cdk-context-menu.d.ts +3 -0
  148. package/types/forty-cdk-core-overlay.d.ts +595 -440
  149. package/types/forty-cdk-core.d.ts +413 -170
  150. package/types/forty-cdk-date-field.d.ts +29 -0
  151. package/types/forty-cdk-date-picker.d.ts +52 -12
  152. package/types/forty-cdk-defaults.d.ts +93 -25
  153. package/types/forty-cdk-dialog.d.ts +28 -29
  154. package/types/forty-cdk-drag-drop.d.ts +9 -6
  155. package/types/forty-cdk-drawer.d.ts +64 -50
  156. package/types/forty-cdk-dropdown-menu.d.ts +6 -2
  157. package/types/forty-cdk-field.d.ts +24 -15
  158. package/types/forty-cdk-fieldset.d.ts +2 -2
  159. package/types/forty-cdk-file-upload.d.ts +17 -3
  160. package/types/forty-cdk-hover-card.d.ts +17 -6
  161. package/types/forty-cdk-listbox.d.ts +6 -3
  162. package/types/forty-cdk-menu.d.ts +49 -27
  163. package/types/forty-cdk-menubar.d.ts +16 -57
  164. package/types/forty-cdk-meter.d.ts +1 -1
  165. package/types/forty-cdk-navigation-menu.d.ts +17 -22
  166. package/types/forty-cdk-otp-input.d.ts +9 -3
  167. package/types/forty-cdk-pagination.d.ts +16 -6
  168. package/types/forty-cdk-pane-resizer.d.ts +16 -2
  169. package/types/forty-cdk-popover.d.ts +15 -17
  170. package/types/forty-cdk-radio-group.d.ts +13 -5
  171. package/types/forty-cdk-scroll-area.d.ts +11 -5
  172. package/types/forty-cdk-select.d.ts +36 -17
  173. package/types/forty-cdk-slider.d.ts +6 -5
  174. package/types/forty-cdk-stepper.d.ts +14 -10
  175. package/types/forty-cdk-table.d.ts +47 -8
  176. package/types/forty-cdk-testing.d.ts +31 -19
  177. package/types/forty-cdk-time-field.d.ts +53 -2
  178. package/types/forty-cdk-time-picker.d.ts +69 -8
  179. package/types/forty-cdk-toast.d.ts +11 -4
  180. package/types/forty-cdk-toggle.d.ts +5 -2
  181. package/types/forty-cdk-toolbar.d.ts +19 -2
  182. package/types/forty-cdk-tooltip.d.ts +12 -8
  183. package/types/forty-cdk-tree.d.ts +14 -8
  184. package/types/forty-cdk-virtualization.d.ts +16 -6
  185. package/virtual-reorder/README.md +1 -1
  186. package/virtualization/README.md +8 -4
@@ -109,6 +109,8 @@ The default map gives you fully-typed names out of the box (`up('md')` autocompl
109
109
 
110
110
  ```ts
111
111
  // breakpoints.ts
112
+ import { injectBreakpoints } from 'forty-cdk/breakpoints';
113
+
112
114
  export const appBreakpoints = {
113
115
  mobile: 0,
114
116
  tablet: 640,
@@ -116,12 +118,14 @@ export const appBreakpoints = {
116
118
  desktop: 1280,
117
119
  } as const;
118
120
 
119
- declare module 'forty-cdk' {
121
+ declare module 'forty-cdk/breakpoints' {
120
122
  interface BreakpointRegistry extends Record<keyof typeof appBreakpoints, true> {}
121
123
  }
124
+
125
+ export const injectIsDesktop = () => injectBreakpoints().up('desktop');
122
126
  ```
123
127
 
124
- Now `injectBreakpoints()` autocompletes `'mobile' | 'tablet' | 'laptop' | 'desktop'` across the whole app.
128
+ Now `injectBreakpoints()` autocompletes `'mobile' | 'tablet' | 'laptop' | 'desktop'` across the whole app, and `up('desktop')` compiles without a cast. Augment `'forty-cdk/breakpoints'`, the entry point that declares the registry: the bare `'forty-cdk'` package exports nothing, so a block naming it declares an unrelated interface and the names stay the Tailwind scale.
125
129
 
126
130
  ## API
127
131
 
@@ -483,8 +483,9 @@ Implements the [WAI-ARIA Carousel pattern](https://www.w3.org/WAI/ARIA/apg/patte
483
483
  or localize the default format and both role descriptions app-wide with
484
484
  `provideForCarouselDefaults` (see
485
485
  [Localizing the default labels](#localizing-the-default-labels)).
486
- - Off-view slides receive `aria-hidden="true"` and `inert` to remove them from the
487
- accessibility tree and focus order.
486
+ - Slides outside the viewport receive `aria-hidden="true"` and `inert` to remove them from
487
+ the accessibility tree and focus order. The visible set follows the laid-out offset, so
488
+ `align` and `containScroll` decide which slides stay reachable.
488
489
  - The indicator group should be labelled (e.g. `ariaLabel="Choose slide to display"`).
489
490
  - The current indicator is marked with `aria-current="true"`.
490
491
  - Prev/next buttons never use the native `disabled` attribute. At a boundary without `loop`
@@ -532,7 +533,7 @@ children, unless noted otherwise:
532
533
  | Property | Host | Value | Notes |
533
534
  | --------------------------------- | ----------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
534
535
  | `--for-carousel-offset` | `[forCarousel]` | e.g. `-100%` | Pure arithmetic from `activeIndex`, `slidesPerView`, `align`. |
535
- | `--for-carousel-active-index` | `[forCarousel]` | integer | Current `activeIndex`. |
536
+ | `--for-carousel-active-index` | `[forCarousel]` | integer | Current `activeIndex`, clamped to the registered slides. |
536
537
  | `--for-carousel-slide-count` | `[forCarousel]` | integer | Total registered slides. |
537
538
  | `--for-carousel-slides-per-view` | `[forCarousel]` | integer | From the `slidesPerView` input. |
538
539
  | `--for-carousel-viewport-width` | `[forCarousel]` | e.g. `640px` | Measured via `ResizeObserver`. Absent on the server and before first measurement. |
@@ -149,7 +149,7 @@ export class DemoCheckout {
149
149
  | `checked` | `model<boolean>` | Two-way bindable on/off. Required by `FormCheckboxControl`.<br>**Default:** — |
150
150
  | `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
151
  | `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:** `''` |
152
+ | `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
153
  | `errors` | `input<readonly ValidationError.WithOptionalFieldTree[]>` | Validation errors fed by `[formField]`.<br>**Default:** — |
154
154
  | `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** — |
155
155
 
@@ -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
 
@@ -220,7 +229,7 @@ providers: [
220
229
 
221
230
  `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
231
 
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]`.
232
+ `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. A nested `provideForDateFieldDefaults` merges both records entry by entry with its parent scope, so a feature overriding only `dayPeriod` keeps the labels and placeholders the application root localized. `provideForDateRangeFieldDefaults` takes the same keys for `[forDateRangeField]`.
224
233
 
225
234
  `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.
226
235
 
@@ -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.
@@ -313,7 +324,7 @@ Key behavior applies per segment. Horizontal arrows mirror under `dir="rtl"`.
313
324
  | **Backspace** | Delete the last entered digit; the value becomes `null` when the last digit is removed. |
314
325
  | **Delete** | Clear the whole segment (the value becomes `null` until refilled). |
315
326
 
316
- The day clamps to the current month's length (e.g. 31 → 28 in February), and a composed value is clamped into `[minDate, maxDate]`.
327
+ The day clamps to the current month's length (e.g. 31 → 28 in February), and a composed value is clamped into `[minDate, maxDate]`. While focus stays in the field its segments keep what was entered, so editing one part never rewrites the others; they show the clamped value once focus leaves. Stepping the hour skips a wall-clock hour the day does not have, such as a spring-forward DST gap.
317
328
 
318
329
  ## Accessibility
319
330
 
@@ -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,18 +437,19 @@ 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).
439
444
 
440
445
  ## Keyboard
441
446
 
442
- | Key | Behavior |
443
- | -------------------------------------------------- | ------------------------------------------------------------------------- |
444
- | **Enter / Space** on trigger | Open the surface (native button activation). |
445
- | **ArrowDown / Alt+ArrowDown / ArrowUp** on trigger | Open the surface and move focus to the calendar. |
446
- | **Escape** | Dismiss the surface and return focus to the trigger (when `dismissible`). |
447
+ | Key | Behavior |
448
+ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
449
+ | **Enter / Space** on trigger | Open the surface (native button activation). |
450
+ | **ArrowDown / Alt+ArrowDown / ArrowUp** on trigger | Open the surface and move focus to the calendar. |
451
+ | **Escape** | Dismiss the surface and return focus to the trigger (when `dismissible`). |
452
+ | **Tab / Shift+Tab** past either end of the surface | Non-modal: Tab moves on to the element after the trigger and Shift+Tab lands on the trigger, closing the surface when `dismissible`. Modal: focus stays trapped. |
447
453
 
448
454
  Inside the surface, the projected `ForCalendar` owns the full grid keyboard map (arrows / `Home` / `End` / `PageUp` / `PageDown` / `Enter` / `Space`). On open, focus lands on the calendar's focused cell (`value ?? today`, with today clamped into the calendar's `[min, max]`) in non-modal mode, or the first focusable element in modal mode.
449
455
 
@@ -455,7 +461,7 @@ Implements the [WAI-ARIA Date Picker Dialog pattern](https://www.w3.org/WAI/ARIA
455
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).
456
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`.
457
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.
458
- - **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.
459
465
  - **Dismissal**: Escape (`(escapeKeyDown)`) and outside-pointer (`(pointerDownOutside)` / `(interactOutside)`) close the surface, each vetoable.
460
466
 
461
467
  ## Styling
@@ -479,4 +485,4 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
479
485
 
480
486
  ## Wrapping in a design system
481
487
 
482
- [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
 
@@ -446,11 +448,16 @@ scope. `[animateReorder]` (FLIP) reflows by DOM order and needs no change in mix
446
448
  ## Free drag
447
449
 
448
450
  `[forFreeDrag]` repositions its host element (or a resolved `rootElement`) by pointer drag, with
449
- **no `[forDropList]` required**. It never commits a reorder; it just moves the element around via a
450
- CSS `transform: translate(...)`. It is the standalone counterpart to a sortable list item: a
451
+ **no `[forDropList]` required**. It never commits a reorder; it just moves the element around via
452
+ the CSS `translate` property. It is the standalone counterpart to a sortable list item: a
451
453
  pointer-driven way to move an element freely, with optional root-element retargeting, a confinement
452
454
  boundary, axis locking, and a controllable position (see the inputs below).
453
455
 
456
+ The offset composes with any `transform` the moved element already has, so a dialog centred with
457
+ `transform: translate(-50%, -50%)` stays centred and moves from there. It replaces an inline
458
+ `translate` property of the element's own, so position the element with `transform` or layout
459
+ properties rather than `translate`.
460
+
454
461
  ```html
455
462
  <!-- move the whole dialog by its header -->
456
463
  <div class="dialog">
@@ -467,7 +474,7 @@ boundary, axis locking, and a controllable position (see the inputs below).
467
474
 
468
475
  | Input | Type | Default | Meaning |
469
476
  | ------------- | ------------------------------- | ------------- | -------------------------------------------------------------------------------------------------- |
470
- | `disabled` | `boolean` | `false` | When true, the element can't be dragged (it stays focusable; the transform doesn't change). |
477
+ | `disabled` | `boolean` | `false` | When true, the element can't be dragged (it stays focusable; its position doesn't change). |
471
478
  | `rootElement` | `HTMLElement \| string \| null` | `null` | The element actually moved. A `closest()` selector resolves an ancestor. `null` moves the host. |
472
479
  | `boundary` | `HTMLElement \| string \| null` | `null` | Confine the moved element fully inside this element (or `closest()` selector). `null` = unbounded. |
473
480
  | `lockAxis` | `'x' \| 'y' \| null` | `null` | Constrain movement to one axis. `'x'` pins lift-time `y`, `'y'` pins lift-time `x`. |
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:** — |
@@ -517,14 +517,15 @@ For a seamless release, transition **both** `translate` and your snap-position p
517
517
  - Dismissal and dragging are separate. With `swipeToDismiss` or `dismissible` set to `false`, a drawer with `snapPoints` still drags between them and stops at the lowest snap, and `(swipeEnd)` reports `willClose: false`; a drawer without `snapPoints` arms no drag at all.
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
- - Gestures starting inside a scrollable element that hasn't reached its edge are NOT treated as swipes (the helper defers to inner scroll).
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 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.
521
522
  - **`prefers-reduced-motion: reduce`** disables the drag entirely, snap dragging included. Escape, backdrop, outside-pointer, and close button continue to work.
522
523
 
523
524
  ## Scale background
524
525
 
525
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:
526
527
 
527
- 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.
528
529
  2. Set `[scaleBackground]="true"` on the drawer that should drive the effect.
529
530
 
530
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` |
@@ -356,7 +356,7 @@ class ForAccordionContent {
356
356
  this.item.adoptContentId(inject(ElementRef).nativeElement);
357
357
  }
358
358
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAccordionContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
359
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForAccordionContent, isStandalone: true, selector: "[forAccordionContent]", host: { properties: { "id": "item.contentId()", "attr.role": "\"region\"", "attr.aria-labelledby": "labelledBy()", "attr.aria-hidden": "item.expanded() ? null : \"true\"", "attr.inert": "item.expanded() ? null : \"\"", "attr.data-state": "item.expanded() ? \"open\" : \"closed\"", "attr.data-disabled": "item.disabled() ? \"\" : null", "attr.data-orientation": "parent.orientation()" } }, exportAs: ["forAccordionContent"], ngImport: i0 });
359
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForAccordionContent, isStandalone: true, selector: "[forAccordionContent]", host: { properties: { "id": "item.contentId()", "attr.role": "\"region\"", "attr.aria-hidden": "item.expanded() ? null : \"true\"", "attr.inert": "item.expanded() ? null : \"\"", "attr.data-state": "item.expanded() ? \"open\" : \"closed\"", "attr.data-disabled": "item.disabled() ? \"\" : null", "attr.data-orientation": "parent.orientation()" } }, exportAs: ["forAccordionContent"], ngImport: i0 });
360
360
  }
361
361
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAccordionContent, decorators: [{
362
362
  type: Directive,
@@ -366,7 +366,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
366
366
  host: {
367
367
  '[id]': 'item.contentId()',
368
368
  '[attr.role]': '"region"',
369
- '[attr.aria-labelledby]': 'labelledBy()',
370
369
  '[attr.aria-hidden]': 'item.expanded() ? null : "true"',
371
370
  '[attr.inert]': 'item.expanded() ? null : ""',
372
371
  '[attr.data-state]': 'item.expanded() ? "open" : "closed"',