forty-cdk 0.37.0 → 0.39.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 (127) hide show
  1. package/README.md +4 -4
  2. package/avatar/README.md +3 -1
  3. package/breadcrumbs/README.md +2 -2
  4. package/button/README.md +13 -7
  5. package/calendar/README.md +10 -4
  6. package/carousel/README.md +1 -1
  7. package/checkbox/README.md +3 -3
  8. package/combobox/README.md +23 -21
  9. package/date-field/README.md +9 -9
  10. package/date-picker/README.md +7 -8
  11. package/drag-drop/README.md +56 -8
  12. package/drawer/README.md +25 -20
  13. package/dropdown-menu/README.md +3 -1
  14. package/fesm2022/forty-cdk-button.mjs +5 -4
  15. package/fesm2022/forty-cdk-button.mjs.map +1 -1
  16. package/fesm2022/forty-cdk-carousel.mjs +2 -2
  17. package/fesm2022/forty-cdk-carousel.mjs.map +1 -1
  18. package/fesm2022/forty-cdk-combobox.mjs +232 -141
  19. package/fesm2022/forty-cdk-combobox.mjs.map +1 -1
  20. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -1
  21. package/fesm2022/forty-cdk-core-overlay.mjs +23 -6
  22. package/fesm2022/forty-cdk-core-overlay.mjs.map +1 -1
  23. package/fesm2022/forty-cdk-core.mjs +114 -50
  24. package/fesm2022/forty-cdk-core.mjs.map +1 -1
  25. package/fesm2022/forty-cdk-date-field.mjs.map +1 -1
  26. package/fesm2022/forty-cdk-date-picker.mjs +5 -5
  27. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -1
  28. package/fesm2022/forty-cdk-defaults.mjs.map +1 -1
  29. package/fesm2022/forty-cdk-dialog.mjs +2 -0
  30. package/fesm2022/forty-cdk-dialog.mjs.map +1 -1
  31. package/fesm2022/forty-cdk-drag-drop.mjs +293 -71
  32. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -1
  33. package/fesm2022/forty-cdk-drawer.mjs +129 -52
  34. package/fesm2022/forty-cdk-drawer.mjs.map +1 -1
  35. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -1
  36. package/fesm2022/forty-cdk-field.mjs +3 -3
  37. package/fesm2022/forty-cdk-field.mjs.map +1 -1
  38. package/fesm2022/forty-cdk-fieldset.mjs +2 -2
  39. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -1
  40. package/fesm2022/forty-cdk-file-upload.mjs +6 -3
  41. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -1
  42. package/fesm2022/forty-cdk-listbox.mjs +199 -135
  43. package/fesm2022/forty-cdk-listbox.mjs.map +1 -1
  44. package/fesm2022/forty-cdk-menu.mjs.map +1 -1
  45. package/fesm2022/forty-cdk-navigation-menu.mjs +37 -45
  46. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -1
  47. package/fesm2022/forty-cdk-number-input.mjs +18 -11
  48. package/fesm2022/forty-cdk-number-input.mjs.map +1 -1
  49. package/fesm2022/forty-cdk-pagination.mjs +8 -4
  50. package/fesm2022/forty-cdk-pagination.mjs.map +1 -1
  51. package/fesm2022/forty-cdk-pane-resizer.mjs +2 -2
  52. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -1
  53. package/fesm2022/forty-cdk-select.mjs +243 -159
  54. package/fesm2022/forty-cdk-select.mjs.map +1 -1
  55. package/fesm2022/forty-cdk-slider.mjs +2 -2
  56. package/fesm2022/forty-cdk-slider.mjs.map +1 -1
  57. package/fesm2022/forty-cdk-stepper.mjs.map +1 -1
  58. package/fesm2022/forty-cdk-table-virtualization.mjs.map +1 -1
  59. package/fesm2022/forty-cdk-table.mjs +8 -8
  60. package/fesm2022/forty-cdk-table.mjs.map +1 -1
  61. package/fesm2022/forty-cdk-time-field.mjs.map +1 -1
  62. package/fesm2022/forty-cdk-time-picker.mjs +17 -3
  63. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -1
  64. package/fesm2022/forty-cdk-toast.mjs +3 -3
  65. package/fesm2022/forty-cdk-toast.mjs.map +1 -1
  66. package/fesm2022/forty-cdk-toggle.mjs.map +1 -1
  67. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -1
  68. package/fesm2022/forty-cdk-tree.mjs +174 -111
  69. package/fesm2022/forty-cdk-tree.mjs.map +1 -1
  70. package/fesm2022/forty-cdk-virtual-reorder.mjs +2 -1
  71. package/fesm2022/forty-cdk-virtual-reorder.mjs.map +1 -1
  72. package/fesm2022/forty-cdk-virtualization.mjs +74 -13
  73. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  74. package/field/README.md +9 -9
  75. package/fieldset/README.md +1 -1
  76. package/file-upload/README.md +1 -1
  77. package/input/README.md +2 -2
  78. package/listbox/README.md +86 -98
  79. package/menubar/README.md +1 -1
  80. package/navigation-menu/README.md +39 -36
  81. package/number-input/README.md +20 -8
  82. package/otp-input/README.md +2 -2
  83. package/package.json +1 -1
  84. package/pagination/README.md +2 -1
  85. package/pane-resizer/README.md +2 -2
  86. package/progress/README.md +22 -5
  87. package/radio-group/README.md +6 -6
  88. package/select/README.md +18 -12
  89. package/stepper/README.md +9 -9
  90. package/switch/README.md +30 -17
  91. package/table/README.md +43 -37
  92. package/table-virtualization/README.md +1 -1
  93. package/time-field/README.md +8 -8
  94. package/time-picker/README.md +9 -3
  95. package/toggle/README.md +12 -3
  96. package/tree/README.md +25 -14
  97. package/types/forty-cdk-button.d.ts +7 -1
  98. package/types/forty-cdk-carousel.d.ts +2 -1
  99. package/types/forty-cdk-combobox.d.ts +176 -63
  100. package/types/forty-cdk-context-menu.d.ts +1 -1
  101. package/types/forty-cdk-core-overlay.d.ts +12 -1
  102. package/types/forty-cdk-core.d.ts +105 -70
  103. package/types/forty-cdk-date-field.d.ts +6 -2
  104. package/types/forty-cdk-defaults.d.ts +6 -5
  105. package/types/forty-cdk-drag-drop.d.ts +52 -8
  106. package/types/forty-cdk-drawer.d.ts +50 -29
  107. package/types/forty-cdk-dropdown-menu.d.ts +1 -1
  108. package/types/forty-cdk-field.d.ts +3 -4
  109. package/types/forty-cdk-file-upload.d.ts +4 -0
  110. package/types/forty-cdk-listbox.d.ts +180 -66
  111. package/types/forty-cdk-menu.d.ts +1 -1
  112. package/types/forty-cdk-navigation-menu.d.ts +39 -30
  113. package/types/forty-cdk-number-input.d.ts +15 -15
  114. package/types/forty-cdk-pagination.d.ts +8 -1
  115. package/types/forty-cdk-pane-resizer.d.ts +4 -3
  116. package/types/forty-cdk-select.d.ts +215 -91
  117. package/types/forty-cdk-stepper.d.ts +11 -7
  118. package/types/forty-cdk-table.d.ts +5 -4
  119. package/types/forty-cdk-time-field.d.ts +5 -4
  120. package/types/forty-cdk-time-picker.d.ts +21 -8
  121. package/types/forty-cdk-toggle.d.ts +4 -3
  122. package/types/forty-cdk-toolbar.d.ts +2 -2
  123. package/types/forty-cdk-tree.d.ts +303 -61
  124. package/types/forty-cdk-virtual-reorder.d.ts +5 -3
  125. package/types/forty-cdk-virtualization.d.ts +52 -17
  126. package/virtual-reorder/README.md +8 -2
  127. package/virtualization/README.md +190 -31
package/README.md CHANGED
@@ -26,10 +26,10 @@ Required:
26
26
 
27
27
  Optional — install only if you use the matching entry point / primitives:
28
28
 
29
- | Peer | Needed by |
30
- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
- | `@angular/forms` `^22.1.0` | Form-control primitives (`Switch`, `Checkbox`, `RadioGroup`, `Listbox`, `Select`, `Slider`, `Combobox`, …). They implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so the published bundle never references the package — consumers using only non-form primitives can skip it. |
32
- | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/date-adapter` — install this peer only when you import that entry point. |
29
+ | Peer | Needed by |
30
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
+ | `@angular/forms` `^22.1.0` | The form-control entry points: `forty-cdk/checkbox`, `combobox`, `date-field`, `date-picker`, `input`, `listbox`, `number-input`, `otp-input`, `radio-group`, `search`, `select`, `slider`, `switch`, `time-field`, `time-picker` and `toggle`. Their controls implement `FormValueControl` / `FormCheckboxControl` from `@angular/forms/signals` for `[formField]` auto-wiring. The contract is type-only, so no published bundle references the package, but those entry points' type declarations do: a project that type-checks its dependencies (`skipLibCheck: false`) needs the peer as soon as it imports one of them. Every other entry point, `forty-cdk/field` and `forty-cdk/stepper` included, compiles without it. |
32
+ | `@internationalized/date` `^3.0.0` | The `forty-cdk/internationalized-date` entry point (`InternationalizedDateAdapter`, `InternationalizedDateTimeAdapter`). The date/time primitives themselves depend only on the abstract `DateAdapter` contract from `forty-cdk/date-adapter` — install this peer only when you import that entry point. |
33
33
 
34
34
  ### Regular dependencies
35
35
 
package/avatar/README.md CHANGED
@@ -22,7 +22,7 @@ It is headless and presentational: it tracks the load lifecycle of an `<img>` an
22
22
 
23
23
  ## Examples
24
24
 
25
- Let the image load, then break its URL: `data-status` moves between `loading`, `loaded` and `error`, and the fallback only appears once the delay has passed without an image.
25
+ The image loads, so `data-status` settles on `loaded` and the initials never render: `fallbackDelayMs` holds the fallback back for 500 ms, so a fast load never flashes it. Failed load below breaks the URL to show the `error` path.
26
26
 
27
27
  ```ts
28
28
  import { ChangeDetectionStrategy, Component } from '@angular/core';
@@ -100,6 +100,8 @@ When the image errors, the directive flips `shouldShowFallback()` and the initia
100
100
 
101
101
  The directive does not impose a `role`. Pair the avatar with visible name text or `aria-label` on the surrounding element when identity matters. Set `alt=""` on the `<img>` for purely decorative avatars next to a name, or provide a meaningful `alt` description if the avatar stands alone.
102
102
 
103
+ A meaningful `alt` names the avatar only while the image is shown: once it fails, the image is hidden and the fallback initials are all that is left to announce. When an avatar stands alone, name the root instead (`role="img"` with an `aria-label`) and give the `<img>` `alt=""`, so the fallback carries the same name as the loaded image.
104
+
103
105
  ## Styling
104
106
 
105
107
  forty-cdk ships no styles: put your own class on each piece and key your CSS off the `data-*` attributes listed under [API](#api), not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
@@ -126,10 +126,10 @@ Implements the [WAI-ARIA Breadcrumb pattern](https://www.w3.org/WAI/ARIA/apg/pat
126
126
 
127
127
  ## Styling
128
128
 
129
- forty-cdk ships no styles. Style the current item via `[aria-current="page"]`.
129
+ forty-cdk ships no styles: put your own class on each piece and key your CSS off the `aria-current="page"` the current item reflects, not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
130
130
 
131
131
  ```css
132
- [forBreadcrumbItem][aria-current='page'] {
132
+ .bc-link[aria-current='page'] {
133
133
  font-weight: bold;
134
134
  color: inherit;
135
135
  text-decoration: none;
package/button/README.md CHANGED
@@ -5,7 +5,7 @@ archetype: [composable-ui]
5
5
  apgUrl: https://www.w3.org/WAI/ARIA/apg/patterns/button/
6
6
  ---
7
7
 
8
- # ForButton
8
+ # Button
9
9
 
10
10
  Turns any element (a native `<button>` or a custom host like `<div>` / `<span>`) into an accessible button with keyboard activation. Disabled stays focusable (aria-disabled, never the native attribute) and pressed / hovered / focus-visible are reflected as data-\* hooks.
11
11
 
@@ -58,20 +58,26 @@ Disabled buttons stay focusable so assistive technology can announce them. The n
58
58
  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.
59
59
 
60
60
  ```html
61
- <fieldset forFieldset [disabled]="locked()">
62
- <legend forFieldsetLegend>Account</legend>
61
+ <div forFieldset [disabled]="locked()">
62
+ <span forFieldsetLegend>Account</span>
63
63
  <button forButton (activate)="save()">Save</button>
64
- </fieldset>
64
+ </div>
65
65
  ```
66
66
 
67
+ On a native `<fieldset>` host, `[forFieldset]` sets the native `disabled` attribute, and HTML then disables every native `<button>` inside it: the button leaves the tab order and cannot be focused, whatever `forButton` does. The guarantee that a disabled button stays focusable holds for a non-native group host like the `<div>` above, and for a non-native button host (`<div forButton>`) in either group.
68
+
67
69
  ## Preserve consumer `type`
68
70
 
69
- A native `<button>` without an explicit `type` attribute defaults to `type="button"`. A consumer-set `type="submit"` is preserved:
71
+ A native `<button>` without an explicit `type` attribute defaults to `type="button"`. A consumer-set `type` is preserved, whether it is a static attribute or a binding, and later writes to a binding are followed:
70
72
 
71
73
  ```html
72
74
  <button type="submit" forButton>Submit form</button>
75
+
76
+ <button [attr.type]="kind()" forButton>Save</button>
73
77
  ```
74
78
 
79
+ A binding that resolves to `null` removes the attribute, so the button falls back to the HTML default, which is `submit` inside a form.
80
+
75
81
  ## API
76
82
 
77
83
  ### `ForButton`
@@ -118,12 +124,12 @@ Implements the [WAI-ARIA Button pattern](https://www.w3.org/WAI/ARIA/apg/pattern
118
124
  forty-cdk ships no styles: put your own class on each piece and key your CSS off the `data-*` attributes listed under [API](#api), not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
119
125
 
120
126
  ```css
121
- [forButton][data-disabled] {
127
+ .btn[data-disabled] {
122
128
  opacity: 0.4;
123
129
  pointer-events: none;
124
130
  }
125
131
 
126
- [forButton][data-pressed] {
132
+ .btn[data-pressed] {
127
133
  transform: scale(0.97);
128
134
  }
129
135
  ```
@@ -341,12 +341,18 @@ The recommended path is `[forCalendarMonthSelect]` and `[forCalendarYearSelect]`
341
341
  />
342
342
  </svg>
343
343
  </button>
344
- <select forCalendarMonthSelect #m="forCalendarMonthSelect">
344
+ <select forCalendarMonthSelect #m="forCalendarMonthSelect" aria-label="Month">
345
345
  @for (opt of m.options(); track opt.value) {
346
346
  <option [value]="opt.value" [disabled]="opt.disabled">{{ opt.label }}</option>
347
347
  }
348
348
  </select>
349
- <select forCalendarYearSelect #y="forCalendarYearSelect" [minYear]="1900" [maxYear]="2100">
349
+ <select
350
+ forCalendarYearSelect
351
+ #y="forCalendarYearSelect"
352
+ aria-label="Year"
353
+ [minYear]="1900"
354
+ [maxYear]="2100"
355
+ >
350
356
  @for (opt of y.years(); track opt.value) {
351
357
  <option [value]="opt.value" [disabled]="opt.disabled">{{ opt.value }}</option>
352
358
  }
@@ -575,7 +581,7 @@ Implements the [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/
575
581
  - **`aria-selected`** is always emitted (`"true"` / `"false"`); **`aria-current="date"`** marks today; **`aria-disabled`** marks unavailable dates (truthy-only).
576
582
  - **`aria-multiselectable="true"`** is set on the day grid in `selectionMode="range"`, where a committed range selects several cells, and is absent in single mode.
577
583
  - **Roving tabindex**: exactly one cell (the focused date) is tabbable. `Tab` enters and leaves the grid in one stop.
578
- - **Boolean `data-*`** on the cell, present when true and absent when false: `data-selected`, `data-today`, `data-highlighted` (the focused/roving cell), `data-disabled` and `data-outside-month`.
584
+ - **Boolean `data-*`** on the cell, present when true and absent when false: `data-selected`, `data-today`, `data-highlighted` (the roving tab-stop cell, kept after focus leaves the grid), `data-disabled` and `data-outside-month`.
579
585
 
580
586
  ## Styling
581
587
 
@@ -612,7 +618,7 @@ import { CalendarDate, today, getLocalTimeZone } from '@internationalized/date';
612
618
  @Component({
613
619
  selector: 'app-date',
614
620
  changeDetection: ChangeDetectionStrategy.OnPush,
615
- /* ... */
621
+ template: `<p>Today is {{ date()?.toString() }}</p>`,
616
622
  })
617
623
  export class DatePage {
618
624
  readonly date = signal<CalendarDate | null>(null);
@@ -5,7 +5,7 @@ archetype: [composable-ui]
5
5
  apgUrl: https://www.w3.org/WAI/ARIA/apg/patterns/carousel/
6
6
  ---
7
7
 
8
- # ForCarousel
8
+ # Carousel
9
9
 
10
10
  A slideshow of content panels with previous / next controls, an indicator group, optional looping and multi-slide views, and an accessible autoplay mode with a pause control.
11
11
 
@@ -134,12 +134,12 @@ One class and one directive, three states. `disabled` and `readonly` both keep t
134
134
 
135
135
  ```ts
136
136
  import { Component, signal } from '@angular/core';
137
- import { form, required } from '@angular/forms/signals';
137
+ import { form, FormField, required } from '@angular/forms/signals';
138
138
  import { ForCheckbox } from 'forty-cdk/checkbox';
139
139
 
140
140
  @Component({
141
141
  selector: 'demo-checkout',
142
- imports: [ForCheckbox /* , FormField from @angular/forms */],
142
+ imports: [ForCheckbox, FormField],
143
143
  template: `
144
144
  <button forCheckbox class="cb-row" [formField]="checkout.acceptTerms">
145
145
  <span class="cb">
@@ -166,7 +166,7 @@ export class DemoCheckout {
166
166
  | `disabled` / `readonly` / `required` / `invalid` / `pending` | `input<boolean>` | Reflected as the matching `aria-*` / `data-*` attributes. A disabled checkbox stays focusable (per APG), with `aria-disabled="true"` + `data-disabled` and no native `disabled`; click is a no-op.<br>**Default:** — |
167
167
  | `name` | `input<string>` | Reflected on `name` (empty string omits the attribute). A native reset of the owning `<form>` restores `checked` and `indeterminate` to their first-render values.<br>**Default:** `''` |
168
168
  | `errors` | `input<readonly ValidationError.WithOptionalFieldTree[]>` | Validation errors fed by `[formField]`.<br>**Default:** — |
169
- | `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** — |
169
+ | `touched` | `model<boolean>` | Set to `true` on blur.<br>**Default:** `false` |
170
170
 
171
171
  | Data attribute | Values |
172
172
  | --------------- | ------------------------------------------- |
@@ -115,17 +115,11 @@ const COUNTRIES = [
115
115
  ForComboboxClear,
116
116
  ],
117
117
  template: `
118
- <div
119
- forCombobox
120
- #combobox="forCombobox"
121
- class="combobox"
122
- [(query)]="query"
123
- [(value)]="value"
124
- ariaLabel="Country search"
125
- >
118
+ <div forCombobox #combobox="forCombobox" class="combobox" [(query)]="query" [(value)]="value">
126
119
  <div class="combobox-single">
127
120
  <input
128
121
  forComboboxInput
122
+ aria-label="Country search"
129
123
  class="combobox-input combobox-input--boxed"
130
124
  placeholder="Search countries…"
131
125
  />
@@ -192,7 +186,7 @@ Click the `[forComboboxTrigger]` button, type into the search field inside the p
192
186
 
193
187
  ### Virtualized (1,000 options)
194
188
 
195
- The primitive never owns the scroll container, so it virtualizes with any windowing strategy. The demo uses a dependency-free one. The consumer renders only the visible window and wires `[totalCount]`, `[visibleRange]` and `[forComboboxOption][posInSet]`; `(scrollToIndex)` fires when navigation targets a row outside the window.
189
+ The primitive never owns the scroll container, so it virtualizes with any windowing strategy. The demo uses a dependency-free one. The consumer renders only the visible window, adds `forComboboxVirtualized` to the combobox, and wires its `[totalCount]` and `[visibleRange]` plus `[forComboboxOption][posInSet]`; `(scrollToIndex)` fires when navigation targets a row outside the window.
196
190
 
197
191
  ## API
198
192
 
@@ -225,6 +219,9 @@ The content anchors to the field box when a `[forComboboxAnchor]` is present; se
225
219
  | `[forCombobox]` | `data-dirty` | present / absent |
226
220
  | `[forCombobox]` | `data-pending` | present / absent |
227
221
  | `[forCombobox]` | `data-invalid` | present / absent |
222
+ | `[forComboboxTrigger]` | `data-state` | `open` \| `closed` |
223
+ | `[forComboboxTrigger]` | `data-disabled` | present / absent |
224
+ | `[forComboboxTrigger]` | `data-readonly` | present / absent |
228
225
  | `[forComboboxInput]` | `data-state` | `open` \| `closed` |
229
226
  | `[forComboboxInput]` | `data-disabled` | present / absent |
230
227
  | `[forComboboxToggle]` | `data-state` | `open` \| `closed` |
@@ -299,7 +296,7 @@ The default (editable) anatomy is a text field that filters in place. A **picker
299
296
 
300
297
  Add two parts:
301
298
 
302
- - **`[forComboboxTrigger]`**: a real `<button>` outside the `@if (open())`. It opens the panel, becomes the default positioning anchor, and is where focus returns on close.
299
+ - **`[forComboboxTrigger]`**: a real `<button>` outside the `@if (open())`. It opens the panel, becomes the default positioning anchor, and is where focus returns on close. It carries `role="combobox"` and announces the form state (`aria-required`, `aria-invalid`, `aria-busy`, `aria-readonly`), because while the panel is closed it is the only focusable part of the field. Give it a label: a combobox is not named by its text.
303
300
  - **`[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.
304
301
 
305
302
  ```html
@@ -669,14 +666,17 @@ The same three inputs cover multi mode + chips: bind `<span forComboboxChip [val
669
666
 
670
667
  ## Virtualization
671
668
 
672
- For very large option sets (1k+) the consumer can render only the visible window and let the directive coordinate navigation across the full source. Three additive inputs / one output cover the wiring; non-virtualized usage is unchanged.
669
+ For very large option sets (1k+) the consumer can render only the visible window and let the directive coordinate navigation across the full source. The wiring lives on `ForComboboxVirtualized` (`[forComboboxVirtualized]`, `exportAs: 'forComboboxVirtualized'`), placed on the same element as `[forCombobox]` and imported from `forty-cdk/combobox` next to `ForCombobox`. Non-virtualized usage is unchanged, and a combobox without the directive never bundles the virtualized navigation engine.
673
670
 
674
- | Input / Output | Purpose |
675
- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
676
- | `[totalCount]: number \| undefined` | Length of the **filtered** source array. Drives `aria-setsize` and lets navigation walk past the rendered window. Leave `undefined` (default) for non-virtualized lists. |
677
- | `[visibleRange]: [start, end) \| undefined` | Inclusive-exclusive index range currently rendered in the DOM. Pulled from your virtualizer's `getVirtualItems()`. |
678
- | `[forComboboxOption][posInSet]` | Absolute index of this option in the source array. Required when virtualizing; the directive folds option data into a snapshot keyed by this index so it survives unmount. |
679
- | `(scrollToIndex)` | Emitted when arrow keys (or Home / End) need to land on an option whose absolute index falls outside `visibleRange()`. Wire to the virtualizer's `scrollToIndex(idx)`. |
671
+ | `ForComboboxVirtualized` member | Purpose |
672
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
673
+ | `[totalCount]: number \| undefined` | Length of the **filtered** source array. Drives `aria-setsize` and lets navigation walk past the rendered window. Leave `undefined` (default) for non-virtualized lists. |
674
+ | `[visibleRange]: [start, end) \| undefined` | Inclusive-exclusive index range currently rendered in the DOM. Pulled from your virtualizer's `getVirtualItems()`. |
675
+ | `[dataVersion]: unknown` | Bind any value that changes when the source is re-sorted or refreshed without its length changing, so navigation never lands on a stale off-window entry. |
676
+ | `(scrollToIndex)` | Emitted when arrow keys (or Home / End) need to land on an option whose absolute index falls outside `visibleRange()`. Wire to the virtualizer's `scrollToIndex(idx)`. |
677
+ | `invalidateSnapshot()` | The imperative twin of `[dataVersion]`, for a same-length refresh you cannot express through the input. No-op while `totalCount` is unset. |
678
+
679
+ Each rendered option also binds `[forComboboxOption][posInSet]`, its absolute index in the source array. It is required when virtualizing: the directive keeps option data keyed by this index so it survives unmount.
680
680
 
681
681
  How navigation flows when virtualizing:
682
682
 
@@ -692,6 +692,7 @@ Inline autocomplete matches against the most recently rendered window overlaid w
692
692
  ```html
693
693
  <div
694
694
  forCombobox
695
+ forComboboxVirtualized
695
696
  [(query)]="query"
696
697
  [(value)]="value"
697
698
  [(open)]="open"
@@ -742,11 +743,11 @@ This uses the library's own [`injectVirtualizer`](../virtualization/README.md) c
742
743
 
743
744
  `[totalCount]` is also the result count the message pieces read: `[forComboboxStatus]`'s `count()` reports it rather than the size of the rendered window, so `{{ status.count() }} results.` announces the full count once and stays put while the window scrolls, and `[forComboboxEmpty]` stays hidden while a window with results has not rendered yet.
744
745
 
745
- When `[totalCount]` is omitted, the directive falls back to `options().length` and behaves exactly as before: `aria-setsize` is left to the platform default and navigation never emits `(scrollToIndex)`.
746
+ When `[forComboboxVirtualized]` is absent or its `[totalCount]` is omitted, the combobox falls back to `options().length` and behaves exactly as before: `aria-setsize` is left to the platform default and navigation never emits `(scrollToIndex)`.
746
747
 
747
748
  > **Disabled options off-screen.** The directive learns an option's `disabled` only when it's been rendered at least once. While the consumer can pre-mark disabled rows with their own filter (most apps do), arrow nav cannot skip an off-screen disabled option it has never seen: it will land on it, the option will mount, and the next arrow press skips. Mark disabled rows in the source array if this matters.
748
749
 
749
- > **Listbox virtualization** ships the same contract: `[forListbox]` defaults to roving tabindex (DOM focus on the actual option element) and switches to the `aria-activedescendant` model when you set `[totalCount]`. See the [Listbox README "Virtualization"](../listbox/README.md#virtualization) section.
750
+ > **Listbox virtualization** ships the same contract: `[forListbox]` defaults to roving tabindex (DOM focus on the actual option element) and switches to the `aria-activedescendant` model when you add `[forListboxVirtualized]` with `[totalCount]`. See the [Listbox README "Virtualization"](../listbox/README.md#virtualization) section.
750
751
 
751
752
  ## Writing direction
752
753
 
@@ -838,10 +839,11 @@ Hovering an option also makes it the activedescendant, so mouse and keyboard int
838
839
 
839
840
  Implements the [WAI-ARIA Combobox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/).
840
841
 
842
+ - **Name the input.** `role="combobox"` takes its name from the author, and a placeholder is only a fallback that disappears once the user types. The root's `ariaLabel` names the listbox, not the input. Give `[forComboboxInput]` a `<label for>`, an `aria-label` or an `aria-labelledby`, or wrap the combobox in `[forField]` with a `[forLabel]`. In the picker anatomy, label `[forComboboxTrigger]` the same way, for example `aria-labelledby` pointing at a visible label and at the element showing the value.
841
843
  - 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"`.
842
844
  - `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.
843
845
  - `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.
844
- - `[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.
846
+ - `[forComboboxTrigger]` (picker anatomy) is a real `<button>` with `role="combobox"`, 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. It also reflects `aria-required`, `aria-invalid`, `aria-busy` and `aria-readonly` (truthy-only) from the root, so a closed picker announces a required, failing or validating field: the input carrying the same state only exists while the panel is open. It keeps the role while the dialog is open, next to the `role="combobox"` search input inside it. 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.
845
847
  - `[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.
846
848
  - `[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.
847
849
  - `[forComboboxClear]` has `tabindex="-1"` and a localizable `aria-label`. It cancels `mousedown` too, and is exempt from the outside-pointer dismissal layer, so clearing an open listbox leaves it open with focus in the input and the combobox untouched.
@@ -885,4 +887,4 @@ forty-cdk ships no styles: put your own class on each piece and key your CSS off
885
887
 
886
888
  ## Wrapping in a design system
887
889
 
888
- [Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS` / `FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing.
890
+ [Wrapping form primitives](../../../docs/wrapping-form-primitives.md) documents both supported wrapper patterns: `hostDirectives` with the exported `FOR_COMBOBOX_HOST_DIRECTIVE_INPUTS` / `FOR_COMBOBOX_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing. A virtualized wrapper adds `ForComboboxVirtualized` as a second `hostDirectives` entry and re-exposes its surface with `FOR_COMBOBOX_VIRTUALIZED_HOST_DIRECTIVE_INPUTS` / `FOR_COMBOBOX_VIRTUALIZED_HOST_DIRECTIVE_OUTPUTS`.
@@ -99,14 +99,14 @@ With a time-capable adapter, a `granularity` finer than `'day'` appends hour / m
99
99
 
100
100
  ```ts
101
101
  import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
102
- import { form } from '@angular/forms/signals';
102
+ import { form, FormField } from '@angular/forms/signals';
103
103
  import { CalendarDate } from '@internationalized/date';
104
104
  import { ForDateField, ForDateFieldLiteral, ForDateFieldSegment } from 'forty-cdk/date-field';
105
105
 
106
106
  @Component({
107
107
  selector: 'app-dob-form',
108
108
  changeDetection: ChangeDetectionStrategy.OnPush,
109
- imports: [ForDateField, ForDateFieldSegment, ForDateFieldLiteral],
109
+ imports: [ForDateField, ForDateFieldSegment, ForDateFieldLiteral, FormField],
110
110
  template: `
111
111
  <div
112
112
  forDateField
@@ -135,7 +135,7 @@ export class DobFormField {
135
135
 
136
136
  ### Date range
137
137
 
138
- `ForDateRangeField` is the range variant, shipped from this same entry point. Two labelled endpoint groups share locale, granularity and bounds; each is its own tab stop, so `Tab` steps start → end while the arrows move between segments inside one endpoint.
138
+ `ForDateRangeField` is the range variant, shipped from this same entry point. Two labelled endpoint groups share locale, granularity and bounds; each is its own tab stop, so `Tab` steps start → end while the arrows move between segments inside one endpoint. Type an end date before the start and a message under the field says so, read from `disordered()`, since the red border alone is a colour cue only.
139
139
 
140
140
  ### Date & time range
141
141
 
@@ -143,7 +143,7 @@ With a time-capable adapter, a `granularity` finer than `'day'` appends time seg
143
143
 
144
144
  ### Range in Signal Forms
145
145
 
146
- `ForDateRangeField` implements `FormValueControl<DateRange | null>`, so `[formField]` binds the committed range into the form and pulls validation back out. A half-entered or out-of-order range keeps `value()` null, so a `required` field stays invalid until both endpoints are filled and ordered.
146
+ `ForDateRangeField` implements `FormValueControl<DateRange | null>`, so `[formField]` binds the committed range into the form and pulls validation back out. A half-entered or out-of-order range keeps `value()` null, so a `required` field stays invalid until both endpoints are filled and ordered. The error message tells the two cases apart through `disordered()`.
147
147
 
148
148
  ## API
149
149
 
@@ -191,7 +191,7 @@ A native reset of the owning `<form>` returns `value` to what it held when the f
191
191
  | `[forDateFieldSegment]` | `data-disabled` | present \| absent |
192
192
  | `[forDateFieldSegment]` | `data-readonly` | present \| absent |
193
193
 
194
- `data-empty` marks the field only while **every** editable segment is empty (nothing has been entered); a partially-typed field is **not** empty. `data-placeholder` marks each individual segment that is still empty. `data-highlighted` is the current roving-tabindex segment and the only focus hook the consumer gets, shared with the other roving primitives. `[forDateFieldLiteral]` carries no data-\* attributes (it is `aria-hidden` and out of the tab order).
194
+ `data-empty` marks the field only while **every** editable segment is empty (nothing has been entered); a partially-typed field is **not** empty. `data-placeholder` marks each individual segment that is still empty. `data-highlighted` marks the roving tab-stop segment, the vocabulary shared with the other roving primitives. It stays on that segment after focus leaves the field, so a style that reads as focus is scoped to the field's `:focus-within`, and to each endpoint group's in a range field, where each endpoint keeps its own. `[forDateFieldLiteral]` carries no data-\* attributes (it is `aria-hidden` and out of the tab order).
195
195
 
196
196
  ## Date-time field
197
197
 
@@ -306,7 +306,7 @@ The endpoint groups each accept an `ariaLabel` input for their own group label,
306
306
 
307
307
  ### Ordering
308
308
 
309
- The two endpoints are typed independently, so order is not guaranteed by construction the way the picker's two-click flow guarantees it. The field preserves the `DateRange` `end >= start` invariant by **never emitting an out-of-order range**: when both endpoints are complete but `start > end`, the typed segments are kept (not silently rewritten), `value` stays `null`, and the root reflects `aria-invalid="true"` + `data-range-error` so the disorder is perceivable and stylable. Editing either endpoint back into order emits the range.
309
+ The two endpoints are typed independently, so order is not guaranteed by construction the way the picker's two-click flow guarantees it. The field preserves the `DateRange` `end >= start` invariant by **never emitting an out-of-order range**: when both endpoints are complete but `start > end`, the typed segments are kept (not silently rewritten), `value` stays `null`, and the root reflects `aria-invalid="true"` + `data-range-error` so the disorder is perceivable and stylable. Editing either endpoint back into order emits the range. The border colour alone is not an error message, so read the same state from `disordered()` (through `#range="forDateRangeField"`) and say it in words, as the Date range example does.
310
310
 
311
311
  ### Range keyboard and accessibility
312
312
 
@@ -339,13 +339,13 @@ Composes the [WAI-ARIA Spinbutton pattern](https://www.w3.org/WAI/ARIA/apg/patte
339
339
  - **`role="spinbutton"`** per segment, with `aria-valuemin` / `aria-valuemax` / `aria-valuenow` reflected; the month segment also exposes a localized `aria-valuetext` ("March"), so screen readers read the name rather than the number.
340
340
  - **Roving tabindex**: exactly one segment is tabbable, so `Tab` enters and leaves the whole field in one stop; arrows move between segments.
341
341
  - **Literals are `aria-hidden`** and never focusable, so assistive tech reads only the spinbutton segments.
342
- - **Boolean `data-*`** on each segment, present when true and absent when false: `data-highlighted` (focused/roving), `data-placeholder` (empty), `data-disabled`, `data-readonly`.
342
+ - **Boolean `data-*`** on each segment, present when true and absent when false: `data-highlighted` (the roving tab stop, kept after blur), `data-placeholder` (empty), `data-disabled`, `data-readonly`.
343
343
  - **`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.
344
344
  - **On-screen keyboards work.** A segment of a field that is neither disabled nor read-only is a `contenteditable="plaintext-only"` editing host with `inputmode="numeric"`, so tapping it on a touch device opens the numeric keypad (the AM/PM segment opens the text keyboard). What the keyboard or an input method types goes through the [Keyboard](#keyboard) map above and is never inserted into the segment, which keeps rendering `{{ seg.text }}`.
345
345
 
346
346
  ## Styling
347
347
 
348
- The library is styleless, so style the boolean `data-*` hooks yourself: `[data-highlighted]` (the focused/roving segment), `[data-placeholder]` (empty), `[data-disabled]` and `[data-readonly]` on the segments, and `[data-empty]` / `[data-disabled]` / `[data-readonly]` / `[data-required]` plus the form-state hooks `[data-touched]` / `[data-dirty]` / `[data-pending]` / `[data-invalid]` on the root group.
348
+ The library is styleless, so style the boolean `data-*` hooks yourself: `[data-highlighted]` (the roving tab-stop segment, kept after focus leaves the field), `[data-placeholder]` (empty), `[data-disabled]` and `[data-readonly]` on the segments, and `[data-empty]` / `[data-disabled]` / `[data-readonly]` / `[data-required]` plus the form-state hooks `[data-touched]` / `[data-dirty]` / `[data-pending]` / `[data-invalid]` on the root group.
349
349
 
350
350
  forty-cdk ships no styles: put your own class on each piece and key your CSS off the `data-*` attributes listed under [Data attributes](#data-attributes), not off the `for*` selectors ([Styling forty-cdk](../../../docs/styling.md) explains why).
351
351
 
@@ -355,7 +355,7 @@ A segment is an editing host, so the browser paints a text caret in the focused
355
355
  .date-field-segment[data-placeholder] {
356
356
  color: GrayText;
357
357
  }
358
- .date-field-segment[data-highlighted] {
358
+ .date-field:focus-within .date-field-segment[data-highlighted] {
359
359
  caret-color: transparent;
360
360
  background: Highlight;
361
361
  color: HighlightText;
@@ -94,14 +94,13 @@ import { provideInternationalizedDateAdapter } from 'forty-cdk/internationalized
94
94
  ],
95
95
  providers: [...provideInternationalizedDateAdapter()],
96
96
  template: `
97
- <div
98
- forDatePicker
99
- #picker="forDatePicker"
100
- [(value)]="date"
101
- placeholder="Pick a date"
102
- ariaLabel="Choose a date"
103
- >
104
- <button forDatePickerTrigger type="button" class="date-picker-trigger">
97
+ <div forDatePicker #picker="forDatePicker" [(value)]="date" placeholder="Pick a date">
98
+ <button
99
+ forDatePickerTrigger
100
+ aria-label="Choose a date"
101
+ type="button"
102
+ class="date-picker-trigger"
103
+ >
105
104
  <span forDatePickerValue class="date-picker-value"></span>
106
105
  <svg class="date-picker-chevron" viewBox="0 0 24 24" aria-hidden="true">
107
106
  <path
@@ -157,7 +157,10 @@ whole gesture (and past the drop, while a settle transition runs) it answers the
157
157
  selector (`[forDraggable]`, or a composed one such as `[forTableRow]`) and repeats its `data-index`.
158
158
  `id` and `data-testid` are stripped from the clone and its whole subtree, so a hook that identifies
159
159
  a single element stays unambiguous; anything that **enumerates** items by attribute selector during
160
- a drag must filter the preview out with `:not([data-for-drag-preview])`.
160
+ a drag must filter the preview out with `:not([data-for-drag-preview])`. The clone is also made
161
+ inert before it joins the document: `name`, `form`, `autofocus` and `autoplay` are stripped, and
162
+ every `iframe`, `object` and `embed` becomes an empty box of the same size, so a cloned radio never
163
+ unchecks the item's own and no cloned media plays or frame reloads.
161
164
 
162
165
  ## Keyboard
163
166
 
@@ -263,6 +266,19 @@ unaffected.
263
266
  </li>
264
267
  ```
265
268
 
269
+ A handle carries `touch-action: none`, so a touch drag on it is not taken by the page's
270
+ scrolling, and the item around it leaves touch scrolling alone. Both follow the item: a handle
271
+ mounted or removed after the first render moves the `touch-action: none` between the handle and
272
+ the item, and a disabled item or list drops it from both.
273
+
274
+ ### Native drag
275
+
276
+ Images, links and selected text are draggable by the browser itself, and its own drag starts
277
+ before a pointer drag arms. A `[forDraggable]` or `[forFreeDrag]` therefore cancels the native
278
+ `dragstart` of every press it would turn into a pointer drag, so pressing a thumbnail inside an
279
+ item reorders the item instead of dragging a copy of the image. Content outside the item's
280
+ handles, and every press while the item or its list is disabled, keeps its native drag.
281
+
266
282
  ### Nested controls
267
283
 
268
284
  A control inside a draggable item keeps its press from starting a drag by calling
@@ -286,6 +302,28 @@ the press stays with the control; lifting with the keyboard is unaffected.
286
302
  - A canceled `pointerdown` suppresses the compatibility mouse events of that press (`mousedown`,
287
303
  `mouseup`), so the control must not rely on them. `click` still fires.
288
304
 
305
+ A `[forDraggable]` or `[forFreeDrag]` nested inside another one needs no `preventDefault()`: every
306
+ press inside it is its own, and the item around it never starts a drag from there, even while the
307
+ nested one is disabled. On a board whose columns are the items of one list and hold a list of
308
+ cards each, pressing a card drags the card, and pressing a column outside its cards drags the
309
+ column. Each item lifts with its own `[forDragPreview]` / `[forDragPlaceholder]`, never with one
310
+ declared inside a nested item.
311
+
312
+ ```html
313
+ <div forDropList orientation="horizontal" (dragDrop)="moveColumn($event)">
314
+ @for (column of columns(); track column.id) {
315
+ <section forDraggable [dragData]="column">
316
+ <h2>{{ column.title }}</h2>
317
+ <ul forDropList (dragDrop)="moveCard(column, $event)">
318
+ @for (card of column.cards; track card.id) {
319
+ <li forDraggable [dragData]="card">{{ card.title }}</li>
320
+ }
321
+ </ul>
322
+ </section>
323
+ }
324
+ </div>
325
+ ```
326
+
289
327
  ### Custom preview & placeholder
290
328
 
291
329
  Place `<ng-template forDragPreview>` and/or `<ng-template forDragPlaceholder>` as direct
@@ -398,7 +436,9 @@ always provided by the consumer via CSS; the library imposes none.
398
436
  ```
399
437
 
400
438
  With no such CSS, `animateReorder` is a graceful no-op: transforms clear instantly and the
401
- preview is destroyed promptly.
439
+ preview is destroyed promptly. Otherwise the preview is destroyed when its `transform` transition
440
+ ends, and never before that transition's `transition-delay` plus duration have elapsed, wherever
441
+ `transform` sits in a multi-property `transition` list.
402
442
 
403
443
  ## Orientation
404
444
 
@@ -453,10 +493,14 @@ the CSS `translate` property. It is the standalone counterpart to a sortable lis
453
493
  pointer-driven way to move an element freely, with optional root-element retargeting, a confinement
454
494
  boundary, axis locking, and a controllable position (see the inputs below).
455
495
 
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`.
496
+ The offset composes with the `transform` and the `translate` the moved element already has, inline
497
+ or from a stylesheet, so a dialog centred with `transform: translate(-50%, -50%)` or
498
+ `translate: -50% -50%` stays centred and moves from there. The `translate` it composes with is read
499
+ when the element first leaves its rest position, and the element's own `translate` comes back once
500
+ `position` returns to `{ x: 0, y: 0 }`.
501
+
502
+ Scrolling the page or a scroll container during a drag keeps the element under the pointer, and the
503
+ `boundary` clamp follows the scrolled geometry.
460
504
 
461
505
  ```html
462
506
  <!-- move the whole dialog by its header -->
@@ -483,7 +527,7 @@ properties rather than `translate`.
483
527
  | Output | Payload | Fires |
484
528
  | ----------- | ---------- | ---------------------------------------------------------------------------------------------- |
485
529
  | `moveStart` | `{ x; y }` | When a pointer drag starts (the lift-time position). |
486
- | `moveMove` | `{ x; y }` | On every armed move, with the live position. |
530
+ | `moveMove` | `{ x; y }` | On every armed move, and when a scroll moves the element, with the live position. |
487
531
  | `moveEnd` | `{ x; y }` | When the drag ends (commit keeps the position; cancel/Escape restores the lift-time snapshot). |
488
532
 
489
533
  `[forDragHandle]` works inside `[forFreeDrag]` exactly as it does inside `[forDraggable]`: once any
@@ -533,8 +577,12 @@ other list, because their reorder output describes a move within one dataset.
533
577
  [virtualCount]="rows().length"
534
578
  forVirtualReorder
535
579
  (itemReorder)="onReorder($event)"
580
+ role="list"
581
+ aria-label="Rows"
536
582
  >
537
- <div *forVirtualFor="let row of rows()" forDraggable [dragData]="row.id">{{ row.label }}</div>
583
+ <div *forVirtualFor="let row of rows()" role="listitem" forDraggable [dragData]="row.id">
584
+ {{ row.label }}
585
+ </div>
538
586
  </div>
539
587
  ```
540
588