forty-cdk 0.13.0 → 0.15.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 (177) hide show
  1. package/README.md +1 -1
  2. package/button/README.md +13 -4
  3. package/carousel/README.md +27 -9
  4. package/combobox/README.md +2 -1
  5. package/context-menu/README.md +22 -20
  6. package/date-field/README.md +12 -11
  7. package/date-range-field/README.md +12 -11
  8. package/drag-drop/README.md +34 -10
  9. package/dropdown-menu/README.md +10 -6
  10. package/fesm2022/forty-cdk-accordion.mjs +4 -3
  11. package/fesm2022/forty-cdk-accordion.mjs.map +1 -1
  12. package/fesm2022/forty-cdk-breadcrumbs.mjs +4 -3
  13. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -1
  14. package/fesm2022/forty-cdk-button.mjs +35 -16
  15. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-calendar.mjs +16 -12
  17. package/fesm2022/forty-cdk-calendar.mjs.map +1 -1
  18. package/fesm2022/forty-cdk-carousel.mjs +86 -35
  19. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-combobox.mjs +95 -28
  21. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  22. package/fesm2022/forty-cdk-context-menu.mjs +14 -1
  23. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  24. package/fesm2022/forty-cdk-core.mjs +3501 -4054
  25. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-date-field.mjs +9 -4
  27. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-date-picker.mjs +6 -4
  29. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  30. package/fesm2022/forty-cdk-date-range-field.mjs +22 -7
  31. package/fesm2022/forty-cdk-date-range-field.mjs.map +1 -1
  32. package/fesm2022/forty-cdk-drag-drop.mjs +409 -40
  33. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  34. package/fesm2022/forty-cdk-drawer.mjs +341 -3
  35. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-dropdown-menu.mjs +18 -10
  37. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-field.mjs +50 -19
  39. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-fieldset.mjs +6 -7
  41. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-file-upload.mjs +53 -31
  43. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-hover-card.mjs +6 -6
  45. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -1
  46. package/fesm2022/forty-cdk-listbox.mjs +15 -6
  47. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  48. package/fesm2022/forty-cdk-menu.mjs +117 -76
  49. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  50. package/fesm2022/forty-cdk-menubar.mjs +158 -159
  51. package/fesm2022/forty-cdk-menubar.mjs.map +1 -1
  52. package/fesm2022/forty-cdk-meter.mjs +4 -3
  53. package/fesm2022/forty-cdk-meter.mjs.map +1 -1
  54. package/fesm2022/forty-cdk-navigation-menu.mjs +104 -55
  55. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  56. package/fesm2022/forty-cdk-number-input.mjs +274 -30
  57. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  58. package/fesm2022/forty-cdk-otp-input.mjs +24 -5
  59. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -1
  60. package/fesm2022/forty-cdk-pagination.mjs +10 -7
  61. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  62. package/fesm2022/forty-cdk-pane-resizer.mjs +75 -17
  63. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-popover.mjs +8 -5
  65. package/fesm2022/forty-cdk-popover.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-progress.mjs +4 -3
  67. package/fesm2022/forty-cdk-progress.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-radio-group.mjs +8 -1
  69. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-scroll-area.mjs +407 -105
  71. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-search.mjs +58 -15
  73. package/fesm2022/forty-cdk-search.mjs.map +1 -1
  74. package/fesm2022/forty-cdk-select.mjs +95 -28
  75. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  76. package/fesm2022/forty-cdk-shared.mjs +6 -0
  77. package/fesm2022/forty-cdk-shared.mjs.map +1 -0
  78. package/fesm2022/forty-cdk-slider.mjs +120 -56
  79. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  80. package/fesm2022/forty-cdk-stepper.mjs +12 -8
  81. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  82. package/fesm2022/forty-cdk-table.mjs +324 -134
  83. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  84. package/fesm2022/forty-cdk-tabs.mjs +5 -5
  85. package/fesm2022/forty-cdk-tabs.mjs.map +1 -1
  86. package/fesm2022/forty-cdk-time-field.mjs +9 -4
  87. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  88. package/fesm2022/forty-cdk-time-picker.mjs +6 -4
  89. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  90. package/fesm2022/forty-cdk-time-range-field.mjs +22 -7
  91. package/fesm2022/forty-cdk-time-range-field.mjs.map +1 -1
  92. package/fesm2022/forty-cdk-toast.mjs +8 -9
  93. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  94. package/fesm2022/forty-cdk-toggle.mjs +13 -4
  95. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  96. package/fesm2022/forty-cdk-toolbar.mjs +55 -45
  97. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  98. package/fesm2022/forty-cdk-tooltip.mjs +12 -7
  99. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -1
  100. package/fesm2022/forty-cdk-tree.mjs +149 -3
  101. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  102. package/fesm2022/forty-cdk-virtualization.mjs +18 -9
  103. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  104. package/fesm2022/forty-cdk-visually-hidden.mjs +6 -0
  105. package/fesm2022/forty-cdk-visually-hidden.mjs.map +1 -0
  106. package/field/README.md +2 -0
  107. package/fieldset/README.md +4 -4
  108. package/file-upload/README.md +16 -4
  109. package/menu/README.md +19 -17
  110. package/menubar/README.md +43 -32
  111. package/navigation-menu/README.md +9 -1
  112. package/number-input/README.md +6 -2
  113. package/otp-input/README.md +1 -1
  114. package/package.json +9 -1
  115. package/pane-resizer/README.md +40 -25
  116. package/radio-group/README.md +4 -0
  117. package/scroll-area/README.md +72 -8
  118. package/search/README.md +50 -0
  119. package/select/README.md +2 -1
  120. package/separator/README.md +1 -0
  121. package/shared/README.md +84 -0
  122. package/slider/README.md +7 -2
  123. package/table/README.md +55 -9
  124. package/time-field/README.md +1 -0
  125. package/time-range-field/README.md +1 -0
  126. package/toggle/README.md +19 -17
  127. package/toolbar/README.md +3 -3
  128. package/types/forty-cdk-accordion.d.ts +1 -1
  129. package/types/forty-cdk-breadcrumbs.d.ts +1 -0
  130. package/types/forty-cdk-button.d.ts +16 -2
  131. package/types/forty-cdk-calendar.d.ts +5 -1
  132. package/types/forty-cdk-carousel.d.ts +68 -15
  133. package/types/forty-cdk-combobox.d.ts +146 -57
  134. package/types/forty-cdk-context-menu.d.ts +20 -6
  135. package/types/forty-cdk-core.d.ts +798 -940
  136. package/types/forty-cdk-date-field.d.ts +6 -1
  137. package/types/forty-cdk-date-picker.d.ts +2 -1
  138. package/types/forty-cdk-date-range-field.d.ts +17 -1
  139. package/types/forty-cdk-dialog.d.ts +0 -1
  140. package/types/forty-cdk-drag-drop.d.ts +36 -10
  141. package/types/forty-cdk-drawer.d.ts +1 -1
  142. package/types/forty-cdk-dropdown-menu.d.ts +16 -8
  143. package/types/forty-cdk-field.d.ts +26 -11
  144. package/types/forty-cdk-fieldset.d.ts +4 -4
  145. package/types/forty-cdk-file-upload.d.ts +61 -19
  146. package/types/forty-cdk-hover-card.d.ts +4 -5
  147. package/types/forty-cdk-internationalized-date.d.ts +0 -1
  148. package/types/forty-cdk-listbox.d.ts +2 -1
  149. package/types/forty-cdk-menu.d.ts +42 -18
  150. package/types/forty-cdk-menubar.d.ts +161 -73
  151. package/types/forty-cdk-meter.d.ts +1 -0
  152. package/types/forty-cdk-navigation-menu.d.ts +51 -8
  153. package/types/forty-cdk-number-input.d.ts +56 -17
  154. package/types/forty-cdk-otp-input.d.ts +1 -0
  155. package/types/forty-cdk-pagination.d.ts +3 -1
  156. package/types/forty-cdk-pane-resizer.d.ts +31 -8
  157. package/types/forty-cdk-popover.d.ts +3 -1
  158. package/types/forty-cdk-progress.d.ts +1 -0
  159. package/types/forty-cdk-radio-group.d.ts +6 -1
  160. package/types/forty-cdk-scroll-area.d.ts +158 -14
  161. package/types/forty-cdk-search.d.ts +34 -7
  162. package/types/forty-cdk-select.d.ts +92 -51
  163. package/types/forty-cdk-shared.d.ts +1 -0
  164. package/types/forty-cdk-slider.d.ts +55 -11
  165. package/types/forty-cdk-stepper.d.ts +6 -2
  166. package/types/forty-cdk-table.d.ts +74 -236
  167. package/types/forty-cdk-tabs.d.ts +1 -1
  168. package/types/forty-cdk-time-field.d.ts +6 -1
  169. package/types/forty-cdk-time-picker.d.ts +2 -1
  170. package/types/forty-cdk-time-range-field.d.ts +17 -1
  171. package/types/forty-cdk-toast.d.ts +1 -1
  172. package/types/forty-cdk-toggle.d.ts +10 -1
  173. package/types/forty-cdk-toolbar.d.ts +21 -9
  174. package/types/forty-cdk-tooltip.d.ts +8 -4
  175. package/types/forty-cdk-tree.d.ts +1 -1
  176. package/types/forty-cdk-visually-hidden.d.ts +1 -0
  177. package/visually-hidden/README.md +78 -0
package/README.md CHANGED
@@ -36,7 +36,7 @@ Optional — install only if you use the matching entry point / primitives:
36
36
 
37
37
  ## Primitives
38
38
 
39
- Every primitive ships as its own **secondary entry point** — import `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on — backed by the shared `forty-cdk/core` entry point. Each lives in its own folder under `projects/forty-cdk/` with its own `README.md` documenting its anatomy, API, keyboard interaction and styling hooks. The `@internationalized/date` adapters live in a dedicated `forty-cdk/internationalized-date` entry point so that optional peer stays truly optional. The main `forty-cdk` barrel is **intentionally empty** (it exports no symbols): always import from the specific `forty-cdk/<primitive>` entry point. Standalone directives plus `"sideEffects": false` mean your bundle only ever includes the primitives you import.
39
+ Every primitive ships as its own **secondary entry point** — import `ForDialog` from `forty-cdk/dialog`, `ForAccordion` from `forty-cdk/accordion`, and so on — backed by the shared `forty-cdk/core` entry point. Each lives in its own folder under `projects/forty-cdk/` with its own `README.md` documenting its anatomy, API, keyboard interaction and styling hooks. The `@internationalized/date` adapters live in a dedicated `forty-cdk/internationalized-date` entry point so that optional peer stays truly optional. The cross-primitive contract types a primitive's public API references — `WritingDirection`, `VetoableEvent`, `DateAdapter`, `FloatingSide`, … — are published by [`forty-cdk/shared`](shared); the main `forty-cdk` barrel is **intentionally empty** (it exports no symbols), so always import primitives from the specific `forty-cdk/<primitive>` entry point. Standalone directives plus `"sideEffects": false` mean your bundle only ever includes the primitives you import.
40
40
 
41
41
  The tables below group the primitives by purpose. The link on each name opens that primitive's README — the canonical reference for which HTML element each directive belongs on, its inputs / outputs, `data-*` attributes and keyboard map.
42
42
 
package/button/README.md CHANGED
@@ -34,6 +34,15 @@ Disabled buttons stay focusable so assistive technology can announce them. The n
34
34
  <button forButton [disabled]="isSaving()" (activate)="save()">Save</button>
35
35
  ```
36
36
 
37
+ A surrounding disabled `[forFieldset]` disables the button too — its `disabled` input is OR'd with the group's, so `aria-disabled` / `data-disabled` are reflected and activation is suppressed. This matters most on a non-native host (`<div forButton>`), which a native `<fieldset disabled>` cannot reach.
38
+
39
+ ```html
40
+ <fieldset forFieldset [disabled]="locked()">
41
+ <legend forFieldsetLegend>Account</legend>
42
+ <button forButton (activate)="save()">Save</button>
43
+ </fieldset>
44
+ ```
45
+
37
46
  ### Preserve consumer `type`
38
47
 
39
48
  A native `<button>` without an explicit `type` attribute defaults to `type="button"`. A consumer-set `type="submit"` is preserved:
@@ -46,10 +55,10 @@ A native `<button>` without an explicit `type` attribute defaults to `type="butt
46
55
 
47
56
  ### `ForButton`
48
57
 
49
- | Property | Type | Description |
50
- | ---------- | ---------------- | -------------------------------------------------------------------------------------------------- |
51
- | `disabled` | `input<boolean>` | Suppresses activation and reflects `aria-disabled` + `data-disabled`.<br>**Default:** `false` |
52
- | `activate` | `output<void>` | Fires once per user activation (click, Enter, Space). Never fires when disabled.<br>**Default:** — |
58
+ | Property | Type | Description |
59
+ | ---------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
60
+ | `disabled` | `input<boolean>` | Suppresses activation and reflects `aria-disabled` + `data-disabled`. OR'd with a surrounding `[forFieldset]`'s disabled state.<br>**Default:** `false` |
61
+ | `activate` | `output<void>` | Fires once per user activation (click, Enter, Space). Never fires when disabled.<br>**Default:** — |
53
62
 
54
63
  The directive reflects boolean `data-*` attributes (present with an empty-string value when true, absent when false). There is no `data-state` — this primitive has no open/closed or checked/unchecked logical state.
55
64
 
@@ -110,6 +110,11 @@ the defaults with `startLabel` / `stopLabel` inputs:
110
110
  <button forCarouselRotationControl startLabel="Play slideshow" stopLabel="Pause slideshow"></button>
111
111
  ```
112
112
 
113
+ Both defaults come from the scope's `rotationStartLabel` / `rotationStopLabel`
114
+ (see [Localizing the default labels](#localizing-the-default-labels)); set either
115
+ input to `null` when the button carries a visible text label and you don't want
116
+ an `aria-label` overriding it.
117
+
113
118
  **Programmatic control** via `exportAs`:
114
119
 
115
120
  ```html
@@ -189,16 +194,19 @@ captured, so page scrolling on the perpendicular axis is unaffected.
189
194
 
190
195
  ## Localizing the default labels
191
196
 
192
- Each slide's default `aria-label` is the positional `"N of M"` string, and each
193
- indicator's is `"Go to slide N"`. Localize both centrally with
194
- `provideForCarouselDefaults` instead of setting `ariaLabel` on every slide and
195
- indicator:
197
+ Each slide's default `aria-label` is the positional `"N of M"` string, each
198
+ indicator's is `"Go to slide N"`, and the rotation control's swaps between
199
+ `"Start automatic slide show"` and `"Stop automatic slide show"`. Localize them
200
+ all centrally with `provideForCarouselDefaults` instead of setting `ariaLabel` on
201
+ every slide and indicator:
196
202
 
197
203
  ```ts
198
204
  providers: [
199
205
  provideForCarouselDefaults({
200
206
  slideLabel: (position, total) => `Diapositiva ${position} de ${total}`,
201
207
  indicatorLabel: (position) => `Ir a la diapositiva ${position}`,
208
+ rotationStartLabel: 'Iniciar la presentación',
209
+ rotationStopLabel: 'Detener la presentación',
202
210
  }),
203
211
  ];
204
212
  ```
@@ -206,7 +214,8 @@ providers: [
206
214
  `position` is the 1-based slide index and `total` is the slide count. Overrides
207
215
  merge with the parent scope, so you can localize just the labels and inherit the
208
216
  rest of the defaults. A per-element `ariaLabel` on `[forCarouselSlide]` /
209
- `[forCarouselIndicator]` still takes precedence over the localized default.
217
+ `[forCarouselIndicator]` still takes precedence over the localized default, as do
218
+ `[startLabel]` / `[stopLabel]` on `[forCarouselRotationControl]`.
210
219
 
211
220
  ## API
212
221
 
@@ -229,8 +238,8 @@ All inputs are on `[forCarousel]` unless noted.
229
238
  | `ariaLabel` (on `[forCarouselIndicators]`) | `string \| null` | Label for the picker group.<br>**Default:** `null` |
230
239
  | `ariaLabel` (on `[forCarouselSlide]`) | `string \| null` | Override the positional "N of M" label.<br>**Default:** `null` |
231
240
  | `disabled` (on `[forCarouselIndicator]`) | `boolean` | Disable this indicator.<br>**Default:** `false` |
232
- | `startLabel` (on `[forCarouselRotationControl]`) | `string` | Accessible name while rotation is stopped.<br>**Default:** `'Start automatic slide show'` |
233
- | `stopLabel` (on `[forCarouselRotationControl]`) | `string` | Accessible name while rotation is playing.<br>**Default:** `'Stop automatic slide show'` |
241
+ | `startLabel` (on `[forCarouselRotationControl]`) | `string \| null` | Accessible name while rotation is stopped.<br>**Default:** scope `rotationStartLabel` (`'Start automatic slide show'`) |
242
+ | `stopLabel` (on `[forCarouselRotationControl]`) | `string \| null` | Accessible name while rotation is playing.<br>**Default:** scope `rotationStopLabel` (`'Stop automatic slide show'`) |
234
243
 
235
244
  Reflected on the `[forCarousel]` host:
236
245
 
@@ -298,8 +307,10 @@ Implements the [WAI-ARIA Carousel pattern](https://www.w3.org/WAI/ARIA/apg/patte
298
307
  accessibility tree and focus order.
299
308
  - The indicator group should be labelled (e.g. `ariaLabel="Choose slide to display"`).
300
309
  - The current indicator is marked with `aria-current="true"`.
301
- - Prev/next buttons use native `disabled` so they are removed from the tab order when
302
- at the boundary without loop.
310
+ - Prev/next buttons never use the native `disabled` attribute. At a boundary without `loop`
311
+ they reflect `aria-disabled="true"` + `data-disabled` and ignore activation, so a keyboard
312
+ user who reaches the last slide keeps focus on the button instead of being dropped to
313
+ `<body>`. Style the boundary state off `[data-disabled]`, never `:disabled`.
303
314
  - The viewport carries `aria-live` and `aria-atomic="false"`. While the carousel is actively
304
315
  auto-rotating, `aria-live` is `"off"` so advancing slides do not bombard the screen reader.
305
316
  When stopped or paused, it is `"polite"` so manual navigation announces. The per-slide
@@ -365,6 +376,13 @@ children, unless noted otherwise:
365
376
  | `data-rotating` | On `[forCarousel]` — actively rotating right now |
366
377
  | `data-autoplay` | On `[forCarousel]` — the `autoplay` input is `true` |
367
378
 
379
+ ### Boundary styling hooks
380
+
381
+ | Attribute | When present |
382
+ | --------------- | --------------------------------------------------------- |
383
+ | `data-disabled` | On `[forCarouselPrevious]` — at index 0 without `loop` |
384
+ | `data-disabled` | On `[forCarouselNext]` — at the last index without `loop` |
385
+
368
386
  ### Drag styling hooks
369
387
 
370
388
  | Attribute | Host | When present |
@@ -129,6 +129,7 @@ Input tables are not yet tabulated for this primitive. See the feature sections
129
129
  | `[forComboboxIndicator]` | `data-state` | `checked` \| `unchecked` (mirrors the parent option) |
130
130
  | `[forComboboxChip]` | `data-value` | the chip's serialized value (verbatim string, or `itemToFormValue`) |
131
131
  | `[forComboboxChip]` | `data-disabled` | present / absent |
132
+ | `[forComboboxSeparator]` | `data-orientation` | `horizontal` \| `vertical` |
132
133
 
133
134
  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.
134
135
 
@@ -608,7 +609,7 @@ Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patte
608
609
  - `data-state="checked" | "unchecked"` always reflects membership in `value()`, so consumers can paint a checkmark icon with pure CSS regardless of mode.
609
610
  - `data-highlighted=""` marks the option that is the current `aria-activedescendant`. Because focus stays on the `<input>`, there is no `:focus` on the option to style — `data-highlighted` is the canonical CSS hook.
610
611
  - Disabled options keep the host `aria-disabled="true"`. Click and hover (activedescendant pinning) are no-ops on disabled options.
611
- - `[forComboboxSeparator]` is decorative and never registers with the listbox's option collection — keyboard navigation skips it automatically.
612
+ - `[forComboboxSeparator]` never registers with the listbox's option collection — keyboard navigation skips it automatically. It carries `role="separator"` and emits `aria-orientation` only for `orientation="vertical"`, because `horizontal` is the ARIA default; `data-orientation` is always stamped for styling. Set `decorative` when the surrounding options already convey the split — it switches the line to `role="none"` and drops `aria-orientation`, matching the [shared separator emission policy](../separator/README.md#accessibility).
612
613
  - `[forComboboxGroup]` is purely advisory grouping — options inside still register flatly with the root, so navigation flows through groups without interruption.
613
614
  - `[forComboboxEmpty]` carries `role="status"` + `aria-live="polite"` so the empty-state message is announced when filtering removes all matches.
614
615
  - Non-option pieces (`[forComboboxAction]`, and ideally `[forComboboxEmpty]` / `[forComboboxStatus]`) belong inside `[forComboboxContent]` but **outside** `[forComboboxList]` — `role="listbox"` may only own `option` / `group` children (`aria-required-owned-elements`). Wrapping the options in a `[forComboboxList]` (the "editable + list" shape) makes those pieces siblings of the listbox. `[forComboboxAction]` **requires** a `[forComboboxList]` and throws `[forty-cdk/combobox]` without one; `[forComboboxEmpty]` / `[forComboboxStatus]` stay lenient in the bare editable anatomy (documented compromise) — see the [editable-anatomy caveat](#anatomy).
@@ -99,26 +99,26 @@ Angular resolves `ng-template` DI at the template's **declaration** site, not wh
99
99
 
100
100
  ### `ForContextMenu`
101
101
 
102
- | Property | Type | Description |
103
- | --------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
104
- | `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
105
- | `side` | `input<string>` | Anchor side relative to the pointer.<br>**Default:** `'bottom'` |
106
- | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
107
- | `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0` |
108
- | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
109
- | `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.<br>**Default:** `'none'` |
110
- | `loop` | `input<boolean>` | Whether arrow navigation wraps.<br>**Default:** `true` |
111
- | `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'` |
112
- | `disabled` | `input<boolean>` | When `true`, the contextmenu event falls through to the native browser menu.<br>**Default:** `false` |
113
- | `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
114
- | `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger element on close.<br>**Default:** `true` |
115
- | `ariaLabel` | `input<string \| null>` | Manual `aria-label` on `[forMenuContent]`.<br>**Default:** `null` |
116
- | `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissable layer.<br>**Default:** — |
117
- | `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** — |
118
- | `focusOutside` | `output<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside content + trigger.<br>**Default:** — |
119
- | `interactOutside` | `output<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite — fires alongside the two above (and shares their veto state).<br>**Default:** — |
120
- | `autoFocusOnOpen` | `output<VetoableEvent>` | Output. Just before the imperative focus move on mount.<br>**Default:** — |
121
- | `autoFocusOnClose` | `output<VetoableEvent>` | Output. Just before the imperative focus move on unmount.<br>**Default:** — |
102
+ | Property | Type | Description |
103
+ | --------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | `open` | `model<boolean>` | Two-way bindable. Whether the menu is shown.<br>**Default:** `false` |
105
+ | `side` | `input<string>` | Anchor side relative to the pointer.<br>**Default:** `'bottom'` |
106
+ | `align` | `input<string>` | Alignment along `side` (`'start'` / `'center'` / `'end'`).<br>**Default:** `'start'` |
107
+ | `sideOffset` | `input<number>` | Gap (px) between the pointer and the menu along the main axis.<br>**Default:** `0` |
108
+ | `alignOffset` | `input<number>` | Gap (px) along the cross axis (parallel to `side`).<br>**Default:** `0` |
109
+ | `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.<br>**Default:** `'none'` |
110
+ | `loop` | `input<boolean>` | Whether arrow navigation wraps.<br>**Default:** `true` |
111
+ | `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'` |
112
+ | `disabled` | `input<boolean>` | When `true`, the contextmenu event falls through to the native browser menu.<br>**Default:** `false` |
113
+ | `dismissible` | `input<boolean>` | When `false`, Escape and outside interactions don't close.<br>**Default:** `true` |
114
+ | `returnFocus` | `input<boolean>` | When `true`, focus returns to the trigger element on close.<br>**Default:** `true` |
115
+ | `ariaLabel` | `input<string \| null>` | Accessible name reflected as `aria-label` on `[forMenuContent]`. The root's only name hook for a context menu — the right-click region is never used as an `aria-labelledby` target.<br>**Default:** `null` |
116
+ | `escapeKeyDown` | `output<VetoableNativeEvent<KeyboardEvent>>` | Output. Escape pressed while the menu is the topmost dismissable layer.<br>**Default:** — |
117
+ | `pointerDownOutside` | `output<VetoableNativeEvent<PointerEvent>>` | Output. Pointer-down on a target outside content + trigger.<br>**Default:** — |
118
+ | `focusOutside` | `output<VetoableNativeEvent<FocusEvent>>` | Output. Focus moves outside content + trigger.<br>**Default:** — |
119
+ | `interactOutside` | `output<VetoableNativeEvent<PointerEvent \| FocusEvent>>` | Output. Composite — fires alongside the two above (and shares their veto state).<br>**Default:** — |
120
+ | `autoFocusOnOpen` | `output<VetoableEvent>` | Output. Just before the imperative focus move on mount.<br>**Default:** — |
121
+ | `autoFocusOnClose` | `output<VetoableEvent>` | Output. Just before the imperative focus move on unmount.<br>**Default:** — |
122
122
 
123
123
  Same vetoable dismiss API as DropdownMenu. Call `preventDefault()` on the emitted veto to suppress the directive's default action; the original DOM event, when present, is on `.event`.
124
124
 
@@ -137,6 +137,8 @@ Same vetoable dismiss API as DropdownMenu. Call `preventDefault()` on the emitte
137
137
 
138
138
  `[forContextMenu]` implements the [WAI-ARIA Menu pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menubar/). The trigger captures `contextmenu`, `Shift+F10`, and the `ContextMenu` key; the menu surface and item roles come from the shared [`menu/`](../menu/README.md) primitives. Pointer activations anchor at the cursor; keyboard activations anchor at the bounding rect of the focused element so keyboard-only users get the menu next to whatever they're working on.
139
139
 
140
+ - **Name the menu with `[ariaLabel]`.** Unlike `[forDropdownMenu]` / `[forMenubar]` / `[forMenuSub]`, the content surface emits **no** `aria-labelledby` fallback here: the trigger is the whole right-click region, so pointing the menu's name at it would make screen readers announce the entire row / card text as the menu name. With no `[ariaLabel]` the `role="menu"` surface simply has no accessible name. A consumer-set static `aria-labelledby` on `[forMenuContent]` is still preserved, so pointing at your own visible heading also works. Submenus nested inside a context menu are unaffected — a `[forMenuSubContent]` is still labelled by its `[forMenuSubTrigger]`.
141
+
140
142
  ## Styling
141
143
 
142
144
  forty-cdk ships no styles. Add your own class to each piece — the for\* selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected data-\* attributes listed under [Data attributes](#data-attributes).
@@ -118,17 +118,17 @@ export class DobFormField {
118
118
 
119
119
  ### `ForDateField`
120
120
 
121
- | Property | Type | Description |
122
- | ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
123
- | `value` | `model<D \| null>` | Two-way bindable entered date, or `null` while any segment is empty. The `FormValueControl` backing.<br>**Default:** `null` |
124
- | `minDate` | `input<D \| null>` | Minimum date (inclusive). A composed value below it is clamped up. Named `minDate` — see note below.<br>**Default:** `null` |
125
- | `maxDate` | `input<D \| null>` | Maximum date (inclusive). A composed value above it is clamped down.<br>**Default:** `null` |
126
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` is date-only; coarser-than-day appends time segments. See below.<br>**Default:** `'day'` |
127
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
128
- | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
129
- | `placeholder` | `input<Partial<Record<DateTimeSegmentType, string>>>` | Per-segment placeholder while empty. Unspecified parts fall back to `dd` / `mm` / `yyyy` / `hh` / `mm` / `ss` / `--`.<br>**Default:** `{}` |
130
- | `ariaLabel` | `input<string \| null>` | Accessible name for the group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
131
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
121
+ | Property | Type | Description |
122
+ | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
123
+ | `value` | `model<D \| null>` | Two-way bindable entered date, or `null` while any segment is empty. The `FormValueControl` backing.<br>**Default:** `null` |
124
+ | `minDate` | `input<D \| null>` | Minimum date (inclusive). A composed value below it is clamped up. Named `minDate` — see note below.<br>**Default:** `null` |
125
+ | `maxDate` | `input<D \| null>` | Maximum date (inclusive). A composed value above it is clamped down.<br>**Default:** `null` |
126
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision. `'day'` is date-only; coarser-than-day appends time segments. See below.<br>**Default:** `'day'` |
127
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
128
+ | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
129
+ | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty. Unspecified parts fall back to `dd` / `mm` / `yyyy` / `hh` / `mm` / `ss` / `--`.<br>**Default:** `{}` |
130
+ | `ariaLabel` | `input<string \| null>` | Accessible name for the group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
131
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
132
132
 
133
133
  Plus the shared `FormUiControl` members from `@angular/forms/signals`: `disabled`, `readonly`, `required`, `invalid`, `name`, `errors`, `touched` (bound automatically by `[formField]`).
134
134
 
@@ -212,6 +212,7 @@ Composes the [WAI-ARIA Spinbutton pattern](https://www.w3.org/WAI/ARIA/apg/patte
212
212
  - **Roving tabindex**: exactly one segment is tabbable, so `Tab` enters and leaves the whole field in one stop; arrows move between segments.
213
213
  - **Literals are `aria-hidden`** and never focusable — assistive tech reads only the spinbutton segments.
214
214
  - **Boolean `data-*`** on each segment — `data-highlighted` (focused/roving), `data-placeholder` (empty), `data-disabled`, `data-readonly` — present when true, absent when false.
215
+ - **`aria-readonly` belongs on the segments, not the group.** WAI-ARIA supports it on `role="spinbutton"` but not on `role="group"`, so each segment carries `aria-readonly="true"` while the group reflects the `data-readonly` styling hook only.
215
216
 
216
217
  ## Styling
217
218
 
@@ -158,17 +158,17 @@ export class StayFormField {
158
158
 
159
159
  ### `ForDateRangeField`
160
160
 
161
- | Property | Type | Description |
162
- | ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
163
- | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
164
- | `minDate` | `input<D \| null>` | Minimum date (inclusive) for both endpoints. A composed endpoint below it is clamped up. Named `minDate` — see note below.<br>**Default:** `null` |
165
- | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
166
- | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; coarser-than-day appends time segments.<br>**Default:** `'day'` |
167
- | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
168
- | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
169
- | `placeholder` | `input<Partial<Record<DateTimeSegmentType, string>>>` | Per-segment placeholder while empty, applied to both endpoints.<br>**Default:** `{}` |
170
- | `ariaLabel` | `input<string \| null>` | Accessible name for the whole range field group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
171
- | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
161
+ | Property | Type | Description |
162
+ | ------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
163
+ | `value` | `model<DateRange<D> \| null>` | Two-way bindable committed range, or `null` while incomplete or out of order. The `FormValueControl` backing.<br>**Default:** `null` |
164
+ | `minDate` | `input<D \| null>` | Minimum date (inclusive) for both endpoints. A composed endpoint below it is clamped up. Named `minDate` — see note below.<br>**Default:** `null` |
165
+ | `maxDate` | `input<D \| null>` | Maximum date (inclusive) for both endpoints. A composed endpoint above it is clamped down.<br>**Default:** `null` |
166
+ | `granularity` | `input<'day' \| 'hour' \| 'minute' \| 'second'>` | Date-time precision shared by both endpoints. `'day'` is date-only; coarser-than-day appends time segments.<br>**Default:** `'day'` |
167
+ | `hourCycle` | `input<12 \| 24 \| null>` | 12/24-hour cycle for the time segments. `null` → locale. 12-hour adds the AM/PM segment.<br>**Default:** `null` |
168
+ | `locale` | `input<string \| null>` | BCP 47 locale driving segment order, separators, and month name. `null` → runtime locale.<br>**Default:** `null` |
169
+ | `placeholder` | `input<Partial<Record<SegmentType, string>>>` | Per-segment placeholder while empty, applied to both endpoints.<br>**Default:** `{}` |
170
+ | `ariaLabel` | `input<string \| null>` | Accessible name for the whole range field group. Emits no `aria-label` while `null`.<br>**Default:** `null` |
171
+ | `dir` | `input<'ltr' \| 'rtl' \| null>` | Writing direction. `null` resolves the ambient direction; mirrors ArrowLeft / ArrowRight segment navigation.<br>**Default:** `null` |
172
172
 
173
173
  The endpoint groups (`[forDateRangeFieldStart]` / `[forDateRangeFieldEnd]`) each accept an `ariaLabel` input for their own group label, falling back to the scope defaults (`'Start date'` / `'End date'`).
174
174
 
@@ -242,6 +242,7 @@ Each segment implements the [WAI-ARIA Spinbutton pattern](https://www.w3.org/WAI
242
242
  - **Roving tabindex per endpoint**: exactly one segment per endpoint is tabbable, so `Tab` steps start group → end group; arrows move between segments within an endpoint.
243
243
  - **`aria-invalid="true"`** is reflected on the root when the form marks it invalid **or** when two complete endpoints are out of order.
244
244
  - **Literals are `aria-hidden`** and never focusable — assistive tech reads only the spinbutton segments.
245
+ - **`aria-readonly` belongs on the segments, not the groups.** WAI-ARIA supports it on `role="spinbutton"` but not on `role="group"`, so each segment carries `aria-readonly="true"` while the root and both endpoint groups reflect the `data-readonly` styling hook only.
245
246
 
246
247
  ## Styling
247
248
 
@@ -7,15 +7,15 @@ whole dialog around by its header — see [`[forFreeDrag]`](#free-drag).
7
7
 
8
8
  ## Keyboard
9
9
 
10
- | State | Key | Action |
11
- | ------ | ----------- | ------------------------------------------------- |
12
- | Idle | Arrow keys | Move roving focus between items |
13
- | Idle | Home / End | Jump to first / last item |
14
- | Idle | Space/Enter | **Lift** the focused item |
15
- | Lifted | Arrow keys | Step the logical drop position |
16
- | Lifted | Home / End | Jump the lifted item to the first / last position |
17
- | Lifted | Space/Enter | **Drop** (commits and emits `(dragDrop)`) |
18
- | Lifted | Escape | **Cancel** (no event, focus stays on item) |
10
+ | State | Key | Action |
11
+ | ------ | ----------- | -------------------------------------------------------------- |
12
+ | Idle | Arrow keys | Move roving focus between items |
13
+ | Idle | Home / End | Jump to first / last item |
14
+ | Idle | Space/Enter | **Lift** the focused item |
15
+ | Lifted | Arrow keys | Step the logical drop position |
16
+ | Lifted | Home / End | Jump the lifted item to the first / last position |
17
+ | Lifted | Space/Enter | **Drop** (commits, emits `(dragDrop)`, focus follows the item) |
18
+ | Lifted | Escape | **Cancel** (no event, focus stays on item) |
19
19
 
20
20
  Arrow direction follows the list's `orientation` and respects RTL via `dir`. In
21
21
  `orientation="mixed"` every arrow key steps the lifted item linearly in DOM order.
@@ -24,6 +24,21 @@ Arrow direction follows the list's `orientation` and respects RTL via `dir`. In
24
24
 
25
25
  Keyboard lifting, stepping, dropping, and cancellation are announced via ARIA live regions. Override the default messages at any injector scope via `provideForDragDropDefaults` (see Announcement customisation below). Free-drag is pointer-only — there is no WAI-ARIA pattern for "reposition an element", so `[forFreeDrag]` owns no role or ARIA state; the consumer is responsible for keeping the moved element fully usable at its default position.
26
26
 
27
+ ### Focus after a keyboard drop
28
+
29
+ Applying the move in `(dragDrop)` destroys or re-inserts the lifted element, which would otherwise
30
+ leave focus on `<body>`. So after a **keyboard** drop whose lifted item held focus, `[forDropList]`
31
+ restores focus to the item at `currentIndex` in the target container on the next render — the same
32
+ element the user just placed, whether it stayed in this list or transferred to a connected one.
33
+
34
+ Two escape hatches:
35
+
36
+ - **Focus something yourself** inside the `(dragDrop)` handler (a status region, a toolbar, the next
37
+ row). The restore only fires when focus has already fallen to `<body>`, so whatever you focus wins.
38
+ - **Pointer drops are untouched** — focus is never moved after a mouse / touch / pen drop.
39
+
40
+ If your handler leaves the data unchanged, nothing is detached and focus simply stays on the item.
41
+
27
42
  ## Pointer dragging
28
43
 
29
44
  When a pointer drag starts, the dragged item stays in place and reflects
@@ -119,7 +134,8 @@ stays in the dragged item's source slot.
119
134
  ```
120
135
 
121
136
  `[liveSort]` has no visible effect without a `[forDragPlaceholder]` template, and has no effect
122
- on keyboard dragging.
137
+ on keyboard dragging. The drop index is resolved from the geometry measured at lift, so
138
+ `[liveSort]` never changes which index a pointer path commits — it only moves the placeholder.
123
139
 
124
140
  ### Boundary & axis lock
125
141
 
@@ -198,6 +214,10 @@ preview is destroyed promptly.
198
214
  happens to render as a single row or single column resolves identically to `"horizontal"` /
199
215
  `"vertical"`.
200
216
 
217
+ In a cross-list transfer each container resolves the drop index on **its own** `orientation` and
218
+ `dir`, so a vertical list can be connected to a horizontal one (or to one in the opposite writing
219
+ direction) and the pointer index lands on the target's axis.
220
+
201
221
  ```html
202
222
  <ul class="grid" forDropList orientation="mixed" (dragDrop)="onDrop($event)">
203
223
  @for (item of items(); track item.id) {
@@ -413,3 +433,7 @@ providers: [
413
433
  }),
414
434
  ];
415
435
  ```
436
+
437
+ `total` is the number of valid drop positions in the list being announced: the item count for a
438
+ same-list reorder, and one more than the item count for a transfer into a connected list (the
439
+ append gap counts).
@@ -155,11 +155,13 @@ The menu items, content surface, radio groups, separators, and groups live in th
155
155
 
156
156
  ## Keyboard
157
157
 
158
- | Key | Behavior |
159
- | --------------------------- | ----------------------------------------------------------------- |
160
- | `Click` / `Enter` / `Space` | Toggles the menu. On open, focus moves to the first enabled item. |
161
- | `ArrowDown` | Opens the menu and focuses the first enabled item. |
162
- | `ArrowUp` | Opens the menu and focuses the last enabled item. |
158
+ | Key | Behavior |
159
+ | ------------------------------- | ---------------------------------------------------------------------------------------------- |
160
+ | `Click` | Toggles the menu. On open, focus moves to the first enabled item. |
161
+ | `Enter` / `Space` / `ArrowDown` | Opens the menu and focuses the first enabled item; on an already-open menu, moves focus there. |
162
+ | `ArrowUp` | Opens the menu and focuses the last enabled item; on an already-open menu, moves focus there. |
163
+
164
+ The open keys never close the menu — the APG menu-button pattern gives them no close semantics (that's `Escape`, or a pointer click on the trigger). Pressing one while the menu is already open moves focus into it, which is what makes them useful after an `(autoFocusOnOpen)`-vetoed open left focus on the trigger. A menu with no enabled item moves nothing.
163
165
 
164
166
  Once focus is in the menu, see [`menu/README.md`](../menu/README.md) for the in-menu keyboard.
165
167
 
@@ -167,6 +169,8 @@ Once focus is in the menu, see [`menu/README.md`](../menu/README.md) for the in-
167
169
 
168
170
  `[forDropdownMenu]` implements the [WAI-ARIA Menu Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/). The trigger wires `aria-haspopup="menu"`, `aria-expanded`, and `aria-controls`; the menu surface and item roles come from the shared [`menu/`](../menu/README.md) primitives.
169
171
 
172
+ A disabled trigger (its own `[disabled]`, or the root's) reflects through a **single channel**: the native `disabled` attribute plus the `data-disabled` styling hook. No `aria-disabled` is emitted — the trigger is a real single-purpose `<button>` and the native attribute already conveys the state to assistive technology, per the sanctioned native-`disabled` case in [rule #561](https://github.com/tutkli/forty-cdk/issues/561) (D2). Style the disabled trigger off `[disabled]` or `[data-disabled]`, never `[aria-disabled]`.
173
+
170
174
  ## Styling
171
175
 
172
176
  forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes listed under [Data attributes](#data-attributes).
@@ -186,5 +190,5 @@ forty-cdk ships no styles. Add your own class to each piece — the `for*` selec
186
190
 
187
191
  - **Mount equals open.** The directive does not toggle `[hidden]` — `@if (open())` controls presence so `animate.enter` / `animate.leave` fire on the natural mount cycle.
188
192
  - **Trigger is exempt** from outside-pointer / outside-focus checks. Without this, clicking the trigger to close would race with its own toggle handler and reopen immediately.
189
- - **Initial focus depends on the opening key.** Click / Space / Enter / ArrowDown focus the first enabled item; ArrowUp focuses the last enabled item.
193
+ - **Initial focus depends on the opening key.** Click / Space / Enter / ArrowDown focus the first enabled item; ArrowUp focuses the last enabled item. The same keys re-focus that item when the menu is already open.
190
194
  - **Selecting an item closes the menu** by default. To keep the menu open after activation (multi-select pattern), call `$event.preventDefault()` in the item's `(activate)` handler.
@@ -1,6 +1,6 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, inject, input, booleanAttribute, model, Directive, computed, signal, ElementRef } from '@angular/core';
3
- import { Collection, injectTextDirection, moveIndex, IdGenerator, adoptHostId, registerHandle, reflectDisabled, resolveListNavigation } from 'forty-cdk/core';
3
+ import { Collection, injectTextDirection, moveIndex, IdGenerator, adoptHostId, registerHandle, reflectDisabled, resolveListNavigation, hostLabelledBy } from 'forty-cdk/core';
4
4
 
5
5
  const FOR_ACCORDION_CONTEXT = new InjectionToken('FOR_ACCORDION_CONTEXT');
6
6
  const FOR_ACCORDION_ITEM_CONTEXT = new InjectionToken('FOR_ACCORDION_ITEM_CONTEXT');
@@ -324,11 +324,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
324
324
  class ForAccordionContent {
325
325
  parent = injectAccordionContext('ForAccordionContent');
326
326
  item = injectAccordionItemContext('ForAccordionContent');
327
+ labelledBy = hostLabelledBy(() => this.item.triggerId());
327
328
  constructor() {
328
329
  this.item.adoptContentId(inject(ElementRef).nativeElement);
329
330
  }
330
331
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAccordionContent, deps: [], target: i0.ɵɵFactoryTarget.Directive });
331
- 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": "item.triggerId()", "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 });
332
+ 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 });
332
333
  }
333
334
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForAccordionContent, decorators: [{
334
335
  type: Directive,
@@ -338,7 +339,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
338
339
  host: {
339
340
  '[id]': 'item.contentId()',
340
341
  '[attr.role]': '"region"',
341
- '[attr.aria-labelledby]': 'item.triggerId()',
342
+ '[attr.aria-labelledby]': 'labelledBy()',
342
343
  '[attr.aria-hidden]': 'item.expanded() ? null : "true"',
343
344
  '[attr.inert]': 'item.expanded() ? null : ""',
344
345
  '[attr.data-state]': 'item.expanded() ? "open" : "closed"',