forty-cdk 0.2.0 → 0.3.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 (220) hide show
  1. package/accordion/README.md +122 -0
  2. package/aspect-ratio/README.md +76 -0
  3. package/avatar/README.md +100 -0
  4. package/breadcrumbs/README.md +49 -0
  5. package/breakpoints/README.md +81 -0
  6. package/button/README.md +49 -0
  7. package/calendar/README.md +458 -0
  8. package/carousel/README.md +358 -0
  9. package/checkbox/README.md +146 -0
  10. package/combobox/README.md +535 -0
  11. package/context-menu/README.md +139 -0
  12. package/date-field/README.md +184 -0
  13. package/date-picker/README.md +338 -0
  14. package/dialog/README.md +388 -0
  15. package/disclosure/README.md +114 -0
  16. package/drag-drop/README.md +359 -0
  17. package/drawer/README.md +560 -0
  18. package/dropdown-menu/README.md +176 -0
  19. package/fesm2022/forty-cdk-accordion.mjs +348 -0
  20. package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
  21. package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
  22. package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
  23. package/fesm2022/forty-cdk-avatar.mjs +308 -0
  24. package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
  25. package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
  26. package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
  27. package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
  28. package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
  29. package/fesm2022/forty-cdk-button.mjs +134 -0
  30. package/fesm2022/forty-cdk-button.mjs.map +1 -0
  31. package/fesm2022/forty-cdk-calendar.mjs +2034 -0
  32. package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
  33. package/fesm2022/forty-cdk-carousel.mjs +968 -0
  34. package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
  35. package/fesm2022/forty-cdk-checkbox.mjs +226 -0
  36. package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
  37. package/fesm2022/forty-cdk-combobox.mjs +2596 -0
  38. package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
  39. package/fesm2022/forty-cdk-context-menu.mjs +413 -0
  40. package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
  41. package/fesm2022/forty-cdk-core.mjs +9022 -0
  42. package/fesm2022/forty-cdk-core.mjs.map +1 -0
  43. package/fesm2022/forty-cdk-date-field.mjs +744 -0
  44. package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
  45. package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
  46. package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
  47. package/fesm2022/forty-cdk-dialog.mjs +707 -0
  48. package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
  49. package/fesm2022/forty-cdk-disclosure.mjs +190 -0
  50. package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
  51. package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
  52. package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
  53. package/fesm2022/forty-cdk-drawer.mjs +1641 -0
  54. package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
  55. package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
  56. package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
  57. package/fesm2022/forty-cdk-field.mjs +425 -0
  58. package/fesm2022/forty-cdk-field.mjs.map +1 -0
  59. package/fesm2022/forty-cdk-fieldset.mjs +164 -0
  60. package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
  61. package/fesm2022/forty-cdk-file-upload.mjs +221 -0
  62. package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
  63. package/fesm2022/forty-cdk-hover-card.mjs +496 -0
  64. package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
  65. package/fesm2022/forty-cdk-input.mjs +274 -0
  66. package/fesm2022/forty-cdk-input.mjs.map +1 -0
  67. package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
  68. package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
  69. package/fesm2022/forty-cdk-listbox.mjs +1279 -0
  70. package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
  71. package/fesm2022/forty-cdk-menu.mjs +1439 -0
  72. package/fesm2022/forty-cdk-menu.mjs.map +1 -0
  73. package/fesm2022/forty-cdk-menubar.mjs +787 -0
  74. package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
  75. package/fesm2022/forty-cdk-meter.mjs +211 -0
  76. package/fesm2022/forty-cdk-meter.mjs.map +1 -0
  77. package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
  78. package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
  79. package/fesm2022/forty-cdk-number-input.mjs +559 -0
  80. package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
  81. package/fesm2022/forty-cdk-otp-input.mjs +527 -0
  82. package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
  83. package/fesm2022/forty-cdk-pagination.mjs +323 -0
  84. package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
  85. package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
  86. package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
  87. package/fesm2022/forty-cdk-popover.mjs +698 -0
  88. package/fesm2022/forty-cdk-popover.mjs.map +1 -0
  89. package/fesm2022/forty-cdk-progress.mjs +226 -0
  90. package/fesm2022/forty-cdk-progress.mjs.map +1 -0
  91. package/fesm2022/forty-cdk-radio-group.mjs +378 -0
  92. package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
  93. package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
  94. package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
  95. package/fesm2022/forty-cdk-search.mjs +205 -0
  96. package/fesm2022/forty-cdk-search.mjs.map +1 -0
  97. package/fesm2022/forty-cdk-select.mjs +1661 -0
  98. package/fesm2022/forty-cdk-select.mjs.map +1 -0
  99. package/fesm2022/forty-cdk-separator.mjs +82 -0
  100. package/fesm2022/forty-cdk-separator.mjs.map +1 -0
  101. package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
  102. package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
  103. package/fesm2022/forty-cdk-slider.mjs +803 -0
  104. package/fesm2022/forty-cdk-slider.mjs.map +1 -0
  105. package/fesm2022/forty-cdk-stepper.mjs +886 -0
  106. package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
  107. package/fesm2022/forty-cdk-switch.mjs +137 -0
  108. package/fesm2022/forty-cdk-switch.mjs.map +1 -0
  109. package/fesm2022/forty-cdk-table.mjs +1518 -0
  110. package/fesm2022/forty-cdk-table.mjs.map +1 -0
  111. package/fesm2022/forty-cdk-tabs.mjs +400 -0
  112. package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
  113. package/fesm2022/forty-cdk-time-field.mjs +593 -0
  114. package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
  115. package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
  116. package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
  117. package/fesm2022/forty-cdk-toast.mjs +1153 -0
  118. package/fesm2022/forty-cdk-toast.mjs.map +1 -0
  119. package/fesm2022/forty-cdk-toggle.mjs +516 -0
  120. package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
  121. package/fesm2022/forty-cdk-toolbar.mjs +374 -0
  122. package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
  123. package/fesm2022/forty-cdk-tooltip.mjs +672 -0
  124. package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
  125. package/fesm2022/forty-cdk-tree.mjs +2007 -0
  126. package/fesm2022/forty-cdk-tree.mjs.map +1 -0
  127. package/fesm2022/forty-cdk-virtualization.mjs +1 -1
  128. package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
  129. package/fesm2022/forty-cdk.mjs +0 -43310
  130. package/fesm2022/forty-cdk.mjs.map +1 -1
  131. package/field/README.md +97 -0
  132. package/fieldset/README.md +86 -0
  133. package/file-upload/README.md +73 -0
  134. package/hover-card/README.md +171 -0
  135. package/input/README.md +156 -0
  136. package/listbox/README.md +424 -0
  137. package/menu/README.md +181 -0
  138. package/menubar/README.md +140 -0
  139. package/meter/README.md +128 -0
  140. package/navigation-menu/README.md +253 -0
  141. package/number-input/README.md +171 -0
  142. package/otp-input/README.md +198 -0
  143. package/package.json +213 -1
  144. package/pagination/README.md +61 -0
  145. package/pane-resizer/README.md +136 -0
  146. package/popover/README.md +262 -0
  147. package/progress/README.md +115 -0
  148. package/radio-group/README.md +129 -0
  149. package/scroll-area/README.md +184 -0
  150. package/search/README.md +42 -0
  151. package/select/README.md +488 -0
  152. package/separator/README.md +84 -0
  153. package/signal-forms/README.md +72 -0
  154. package/slider/README.md +152 -0
  155. package/stepper/README.md +292 -0
  156. package/switch/README.md +116 -0
  157. package/table/README.md +769 -0
  158. package/tabs/README.md +130 -0
  159. package/time-field/README.md +157 -0
  160. package/time-picker/README.md +172 -0
  161. package/toast/README.md +398 -0
  162. package/toggle/README.md +224 -0
  163. package/toolbar/README.md +109 -0
  164. package/tooltip/README.md +274 -0
  165. package/tree/README.md +708 -0
  166. package/types/forty-cdk-accordion.d.ts +242 -0
  167. package/types/forty-cdk-aspect-ratio.d.ts +59 -0
  168. package/types/forty-cdk-avatar.d.ts +133 -0
  169. package/types/forty-cdk-breadcrumbs.d.ts +92 -0
  170. package/types/forty-cdk-breakpoints.d.ts +141 -0
  171. package/types/forty-cdk-button.d.ts +80 -0
  172. package/types/forty-cdk-calendar.d.ts +914 -0
  173. package/types/forty-cdk-carousel.d.ts +530 -0
  174. package/types/forty-cdk-checkbox.d.ts +141 -0
  175. package/types/forty-cdk-combobox.d.ts +1259 -0
  176. package/types/forty-cdk-context-menu.d.ts +313 -0
  177. package/types/forty-cdk-core.d.ts +5774 -0
  178. package/types/forty-cdk-date-field.d.ts +307 -0
  179. package/types/forty-cdk-date-picker.d.ts +622 -0
  180. package/types/forty-cdk-dialog.d.ts +546 -0
  181. package/types/forty-cdk-disclosure.d.ts +127 -0
  182. package/types/forty-cdk-drag-drop.d.ts +456 -0
  183. package/types/forty-cdk-drawer.d.ts +871 -0
  184. package/types/forty-cdk-dropdown-menu.d.ts +242 -0
  185. package/types/forty-cdk-field.d.ts +236 -0
  186. package/types/forty-cdk-fieldset.d.ts +119 -0
  187. package/types/forty-cdk-file-upload.d.ts +124 -0
  188. package/types/forty-cdk-hover-card.d.ts +320 -0
  189. package/types/forty-cdk-input.d.ts +169 -0
  190. package/types/forty-cdk-internationalized-date.d.ts +1 -1
  191. package/types/forty-cdk-listbox.d.ts +513 -0
  192. package/types/forty-cdk-menu.d.ts +629 -0
  193. package/types/forty-cdk-menubar.d.ts +451 -0
  194. package/types/forty-cdk-meter.d.ts +122 -0
  195. package/types/forty-cdk-navigation-menu.d.ts +514 -0
  196. package/types/forty-cdk-number-input.d.ts +319 -0
  197. package/types/forty-cdk-otp-input.d.ts +248 -0
  198. package/types/forty-cdk-pagination.d.ts +214 -0
  199. package/types/forty-cdk-pane-resizer.d.ts +145 -0
  200. package/types/forty-cdk-popover.d.ts +509 -0
  201. package/types/forty-cdk-progress.d.ts +143 -0
  202. package/types/forty-cdk-radio-group.d.ts +222 -0
  203. package/types/forty-cdk-scroll-area.d.ts +258 -0
  204. package/types/forty-cdk-search.d.ts +142 -0
  205. package/types/forty-cdk-select.d.ts +899 -0
  206. package/types/forty-cdk-separator.d.ts +59 -0
  207. package/types/forty-cdk-signal-forms.d.ts +58 -0
  208. package/types/forty-cdk-slider.d.ts +379 -0
  209. package/types/forty-cdk-stepper.d.ts +650 -0
  210. package/types/forty-cdk-switch.d.ts +87 -0
  211. package/types/forty-cdk-table.d.ts +723 -0
  212. package/types/forty-cdk-tabs.d.ts +235 -0
  213. package/types/forty-cdk-time-field.d.ts +307 -0
  214. package/types/forty-cdk-time-picker.d.ts +578 -0
  215. package/types/forty-cdk-toast.d.ts +598 -0
  216. package/types/forty-cdk-toggle.d.ts +310 -0
  217. package/types/forty-cdk-toolbar.d.ts +217 -0
  218. package/types/forty-cdk-tooltip.d.ts +436 -0
  219. package/types/forty-cdk-tree.d.ts +688 -0
  220. package/types/forty-cdk.d.ts +1 -19743
@@ -0,0 +1,769 @@
1
+ # ForTable
2
+
3
+ Headless table primitive that decorates either a native `<table>` or a `<div role>` CSS-grid structure with correct WAI-ARIA table semantics. Implements the [WAI-ARIA Table pattern](https://www.w3.org/WAI/ARIA/apg/patterns/table/) and the [WAI-ARIA Grid pattern](https://www.w3.org/WAI/ARIA/apg/patterns/grid/).
4
+
5
+ The library sets roles, `aria-label`, writing direction, `data-column`, sticky hooks, and (in grid mode) `aria-rowcount` / `aria-colcount` / `aria-rowindex` / `aria-colindex` and roving keyboard navigation. The consumer owns all styles.
6
+
7
+ ## Native `<table>` mode
8
+
9
+ ```html
10
+ <table forTable [ariaLabel]="caption">
11
+ <thead>
12
+ <tr forTableHeaderRow>
13
+ <th forTableHeaderCell name="name" sticky>Name</th>
14
+ <th forTableHeaderCell name="role">Role</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <tr forTableRow>
19
+ <td forTableCell name="name">Ada Lovelace</td>
20
+ <td forTableCell name="role">Engineer</td>
21
+ </tr>
22
+ </tbody>
23
+ </table>
24
+ ```
25
+
26
+ ## `<div>` mode (required for virtualization)
27
+
28
+ When you need virtual scrolling, use `<div role>` structure with `mode="grid"` on the root. The `<div>` mode is the only shape supported by virtualizers because native `<table>` cannot have its rows omitted from the DOM mid-body. All pieces accept any element.
29
+
30
+ ```html
31
+ <div
32
+ forTable
33
+ mode="grid"
34
+ ariaLabel="People"
35
+ style="display: grid; grid-template-columns: 1fr 1fr; overflow-y: auto; max-height: 400px;"
36
+ >
37
+ <div role="rowgroup">
38
+ <div forTableHeaderRow style="display: contents;">
39
+ <div forTableHeaderCell name="name" sticky style="position: sticky; top: 0;">Name</div>
40
+ <div forTableHeaderCell name="role" sticky style="position: sticky; top: 0;">Role</div>
41
+ </div>
42
+ </div>
43
+ <div role="rowgroup">
44
+ @for (row of rows(); track row.id) {
45
+ <div forTableRow style="display: contents;">
46
+ <div forTableCell name="name">{{ row.name }}</div>
47
+ <div forTableCell name="role">{{ row.role }}</div>
48
+ </div>
49
+ }
50
+ </div>
51
+ </div>
52
+ ```
53
+
54
+ ## Sticky header + CSS custom property
55
+
56
+ `ForTable` measures the header row height with `ResizeObserver` and exposes it as `--for-table-header-height` on the root host (the header row must generate a box — use `display: grid` / `flex` on `[forTableHeaderRow]`, not `display: contents`). Use it to keep data cells stuck below the header row without hard-coding a pixel offset that drifts when the header content wraps:
57
+
58
+ ```css
59
+ [forTableHeaderCell][data-sticky] {
60
+ position: sticky;
61
+ top: 0;
62
+ z-index: 1;
63
+ }
64
+
65
+ [forTableCell][data-sticky] {
66
+ position: sticky;
67
+ left: 0; /* start-edge sticky */
68
+ z-index: 1;
69
+ }
70
+
71
+ [forTableCell][data-sticky='end'] {
72
+ position: sticky;
73
+ right: 0; /* end-edge sticky */
74
+ }
75
+ ```
76
+
77
+ End-edge sticky cells use `sticky="end"` on the directive:
78
+
79
+ ```html
80
+ <th forTableHeaderCell name="actions" sticky="end">Actions</th>
81
+ <td forTableCell name="actions" sticky="end">…</td>
82
+ ```
83
+
84
+ ## Grid mode (keyboard navigation)
85
+
86
+ `mode="grid"` (or `mode="treegrid"`) turns the data cells into a single-tab-stop roving group with 2D keyboard navigation. Arrow keys move focus between cells; `Home` / `End` jump to the first / last cell of the current row; `Ctrl+Home` / `Ctrl+End` jump to the first / last cell of the entire grid; `PageUp` / `PageDown` jump to the first / last cell of the grid. All horizontal movement is RTL-mirrored when the resolved writing direction is `rtl`. Disabled cells (set via the cell's `disabled` input) are skipped during navigation.
87
+
88
+ The root emits `aria-rowcount` and `aria-colcount` (defaulting to the rendered data-row count and the column count of the first data row). Override them for server-paged or virtualized tables via the `rowCount` and `colCount` inputs. Data rows emit `aria-rowindex` (1-based). Data cells emit `aria-colindex` (1-based) and `data-highlighted` on the currently focused cell.
89
+
90
+ ```html
91
+ <div forTable mode="grid" ariaLabel="People" [rowCount]="totalRows">
92
+ <div role="rowgroup">
93
+ <div forTableHeaderRow>
94
+ <div forTableHeaderCell name="name">Name</div>
95
+ <div forTableHeaderCell name="role">Role</div>
96
+ </div>
97
+ </div>
98
+ <div role="rowgroup">
99
+ @for (row of rows(); track row.id) {
100
+ <div forTableRow>
101
+ <div forTableCell name="name" [disabled]="row.disabled">{{ row.name }}</div>
102
+ <div forTableCell name="role">{{ row.role }}</div>
103
+ </div>
104
+ }
105
+ </div>
106
+ </div>
107
+ ```
108
+
109
+ ```css
110
+ [forTableCell][data-highlighted] {
111
+ outline: 2px solid blue;
112
+ }
113
+
114
+ [forTableCell][data-disabled] {
115
+ opacity: 0.4;
116
+ }
117
+ ```
118
+
119
+ ## Treegrid mode (expandable hierarchical rows)
120
+
121
+ `mode="treegrid"` sets `role="treegrid"` on the root. Rows are a flat sibling list in the DOM; hierarchy is expressed through ARIA attributes, not DOM nesting.
122
+
123
+ - **`[level]`** — 1-based tree depth, reflected as `aria-level`. Default `1`.
124
+ - **`[expandable]`** — marks a row as a parent; emits `aria-expanded="true"|"false"` and `data-state="open"|"closed"`. Leaf rows emit neither.
125
+ - **`[(expanded)]`** — two-way bindable array of open parent-row values (keyed by row `[value]`). Use `compareWith` for object values.
126
+ - **`aria-posinset` / `aria-setsize`** — auto-recomputed from the rendered flat list on every expand/collapse.
127
+ - **ArrowRight** — expands a collapsed parent (RTL: collapses); if already expanded or the row is a leaf, falls through to grid cell navigation.
128
+ - **ArrowLeft** — collapses an expanded parent (RTL: expands); otherwise navigates left.
129
+ - Consumer mounts/unmounts child rows with `@if` driven by `expanded()`. A `#r="forTableRow"` template ref exposes `r.toggleExpanded()` for pointer-driven expand buttons.
130
+
131
+ ```html
132
+ <div forTable mode="treegrid" [(expanded)]="expanded">
133
+ <div role="rowgroup">
134
+ @for (row of visibleRows(); track row.id) {
135
+ <div
136
+ forTableRow
137
+ #r="forTableRow"
138
+ [value]="row.id"
139
+ [level]="row.level"
140
+ [expandable]="row.expandable"
141
+ >
142
+ <div forTableCell name="name">
143
+ @if (row.expandable) {
144
+ <button type="button" (click)="r.toggleExpanded()">▶</button>
145
+ } {{ row.name }}
146
+ </div>
147
+ </div>
148
+ }
149
+ </div>
150
+ </div>
151
+ ```
152
+
153
+ ```ts
154
+ readonly expanded = signal<readonly unknown[]>([]);
155
+ readonly visibleRows = computed(() => {
156
+ const openIds = this.expanded() as readonly string[];
157
+ return this.allRows.filter((row) => row.parentId === null || openIds.includes(row.parentId));
158
+ });
159
+ ```
160
+
161
+ Style hooks:
162
+
163
+ ```css
164
+ [forTableRow][data-state='open'] {
165
+ background: #f0fff0;
166
+ }
167
+ [forTableRow][data-state='closed'] {
168
+ background: #fff0f0;
169
+ }
170
+ [forTableRow][aria-level='2'] {
171
+ padding-left: 2rem;
172
+ }
173
+ ```
174
+
175
+ ## Row selection
176
+
177
+ Add `selectionMode` to `[forTable]` to enable row selection. Use `[forTableRowSelector]` for a per-row decorative affordance and `[forTableSelectAll]` in the header for a tri-state select-all checkbox.
178
+
179
+ ### `selectionMode`
180
+
181
+ - `'none'` (default) — selection is disabled. No `aria-selected` or `aria-multiselectable` is emitted.
182
+ - `'single'` — at most one row can be selected. Rows emit `aria-selected="true"` or `"false"`.
183
+ - `'multiple'` — any number of rows can be selected. The root emits `aria-multiselectable="true"`.
184
+
185
+ ### `selectionBehavior`
186
+
187
+ Controls how a row click (on the row or on a cell) mutates the selection:
188
+
189
+ - `'toggle'` (default) — clicking a row always flips its selected state.
190
+ - `'replace'` — clicking a row replaces the selection with that single row. Modifier keys in `'multiple'` mode: **Ctrl/Cmd-click** toggles the clicked row without clearing others; **Shift-click** extends a range from the last anchor to the clicked row.
191
+
192
+ ### `[(selection)]`
193
+
194
+ A two-way-bindable `model<readonly unknown[]>()`. Single mode keeps 0–1 entries. The implicit `selectionChange` output fires only on internal mutations (selector / row click / Space / select-all). Consumer writes to the bound signal are reflected on the next change-detection cycle.
195
+
196
+ ### `compareWith`
197
+
198
+ An equality comparator `(a: unknown, b: unknown) => boolean` used for membership checks. Defaults to `===`. Supply an id-based comparator when rows carry objects:
199
+
200
+ ```ts
201
+ protected readonly idEquals = (a: unknown, b: unknown) =>
202
+ (a as { id: number }).id === (b as { id: number }).id;
203
+ ```
204
+
205
+ ### Space-to-select on a focused grid cell
206
+
207
+ In `mode="grid"` with `selectionMode` set to `'single'` or `'multiple'`, pressing **Space** on a focused data cell (`event.target === cellHost`) toggles the enclosing row's selection and prevents the default scroll. Space originating from a nested element (e.g. a button inside the cell) is ignored.
208
+
209
+ ### `[forTableRowSelector]`
210
+
211
+ Decorative per-row affordance (place inside any cell of each `[forTableRow]`). The row owns the announced `aria-selected`, so the selector is `aria-hidden`. Clicking it calls `toggleRowSelection` on the row; a `stopPropagation` prevents the outer row-click handler from double-toggling. Give the row a `[value]` input to make it selectable.
212
+
213
+ ```html
214
+ <div forTableRow [value]="row.id">
215
+ <div forTableCell name="sel">
216
+ <span forTableRowSelector>☑</span>
217
+ </div>
218
+ <div forTableCell name="name">{{ row.name }}</div>
219
+ </div>
220
+ ```
221
+
222
+ ### `[forTableSelectAll]`
223
+
224
+ Interactive header checkbox with tri-state. Reflects `aria-checked` and `data-state` derived from the aggregate selection state across all selectable rows. Clicking (or pressing Space / Enter) selects all when none or some are selected, and clears when all are. No-op outside `'multiple'` mode. Apply on a focusable element:
225
+
226
+ ```html
227
+ <div forTableHeaderRow>
228
+ <div forTableHeaderCell name="sel">
229
+ <span forTableSelectAll ariaLabel="Select all rows"></span>
230
+ </div>
231
+ <div forTableHeaderCell name="name">Name</div>
232
+ </div>
233
+ ```
234
+
235
+ ### Total-aware aggregates under virtualization: `[selectableValues]`
236
+
237
+ By default the select-all tri-state, `toggleSelectAll`, and Shift-click range selection compute against the **registered (rendered)** rows. Under `[forTableVirtualized]` only the windowed rows are registered, so these aggregates would otherwise see only the visible slice — select-all would report "all" once every _visible_ row is selected, and a range could not span unmounted rows.
238
+
239
+ Supply the full ordered set of selectable values via `[selectableValues]` so the aggregates compute against the true dataset instead of the window:
240
+
241
+ - The select-all tri-state reflects `selection` vs. the full set, so it stays correct across scrolling.
242
+ - `toggleSelectAll()` selects / clears every supplied value.
243
+ - Shift-click range resolves against the supplied order, so a range can span rows that are not currently mounted.
244
+
245
+ Per-row selection (`[forTableRowSelector]`, row click, Space) is unaffected — it persists in the bound `[(selection)]` array regardless of mount state. Leave `[selectableValues]` unset (`null`, the default) for non-virtualized tables to keep the registered-rows behaviour.
246
+
247
+ ```html
248
+ <div
249
+ forTable
250
+ forTableVirtualized
251
+ mode="grid"
252
+ selectionMode="multiple"
253
+ [rowCount]="people().length"
254
+ [selectableValues]="peopleIds()"
255
+ [(selection)]="selection"
256
+ >
257
+ <!-- header with [forTableSelectAll], windowed rows with [forTableRowSelector] -->
258
+ </div>
259
+ ```
260
+
261
+ ```ts
262
+ protected readonly peopleIds = computed(() => this.people().map((p) => p.id));
263
+ ```
264
+
265
+ ### Minimal multiple-select example
266
+
267
+ ```html
268
+ <div
269
+ forTable
270
+ mode="grid"
271
+ selectionMode="multiple"
272
+ selectionBehavior="toggle"
273
+ [(selection)]="selection"
274
+ >
275
+ <div role="rowgroup">
276
+ <div forTableHeaderRow>
277
+ <div forTableHeaderCell name="sel">
278
+ <span forTableSelectAll ariaLabel="Select all rows"></span>
279
+ </div>
280
+ <div forTableHeaderCell name="name">Name</div>
281
+ </div>
282
+ </div>
283
+ <div role="rowgroup">
284
+ @for (row of rows(); track row.id) {
285
+ <div forTableRow [value]="row.id">
286
+ <div forTableCell name="sel">
287
+ <span forTableRowSelector></span>
288
+ </div>
289
+ <div forTableCell name="name">{{ row.name }}</div>
290
+ </div>
291
+ }
292
+ </div>
293
+ </div>
294
+ ```
295
+
296
+ ## Sortable headers
297
+
298
+ `[forTableSortHeader]` turns a `[forTableHeaderCell]` into a sortable affordance. It emits `aria-sort` and fires `sortChange` on activation (click, Enter, Space). **The directive never sorts data** — the consumer reorders their own rows from the `sortChange` payload.
299
+
300
+ The directive is self-contained: it owns only its own `direction` state. The "one sorted column at a time" guarantee is the consumer's responsibility — hold a single sort descriptor signal and derive each header's `direction` from it:
301
+
302
+ ```html
303
+ <div forTableHeaderRow>
304
+ <div
305
+ forTableHeaderCell
306
+ name="name"
307
+ forTableSortHeader
308
+ column="name"
309
+ [direction]="directionFor('name')"
310
+ (sortChange)="onSort($event)"
311
+ >
312
+ Name
313
+ </div>
314
+ </div>
315
+ ```
316
+
317
+ ```ts
318
+ protected readonly sort = signal<TableSortDescriptor>({ column: '', direction: 'none' });
319
+ protected directionFor(column: string): TableSortDirection {
320
+ return this.sort().column === column ? this.sort().direction : 'none';
321
+ }
322
+ protected onSort(descriptor: TableSortDescriptor): void {
323
+ this.sort.set(descriptor);
324
+ }
325
+ protected readonly sortedRows = computed(() => /* the consumer sorts rows() by this.sort() */);
326
+ ```
327
+
328
+ The direction cycles `none → ascending → descending → none`. Set `disableClear` to make the cycle skip `none`: `ascending ↔ descending`. Set `firstClickDirection="descending"` to make a freshly activated column start descending: `none → descending → ascending → none` (and with `disableClear`, `none → descending → ascending → descending` — the descending-first-with-toggle behavior a single always-active sort descriptor needs). When `sortable` is `false` the header is fully inert (no `tabindex`, no `aria-sort`, no-op handlers) — useful when sorting is conditionally enabled. Because the directive coordinates nothing across columns, the single-`sort` descriptor pattern above is what enforces that only one column is sorted at a time.
329
+
330
+ ## Column resizing
331
+
332
+ `[forTableColumnResizer]` turns a focusable element inside a `[forTableHeaderCell]` into a column-resize handle. It publishes the resolved width as the CSS custom property `--for-table-col-<name>-width` on the table root, so the consumer's layout can apply it. **The directive never lays out columns itself** — wiring `--for-table-col-<name>-width` into `grid-template-columns` (or a `<col>` / cell width in native `<table>` mode) is the consumer's job.
333
+
334
+ ```html
335
+ <div
336
+ forTableHeaderRow
337
+ style="grid-template-columns: var(--for-table-col-name-width, 200px) var(--for-table-col-role-width, 200px);"
338
+ >
339
+ <div forTableHeaderCell name="name">
340
+ Name
341
+ <button
342
+ forTableColumnResizer
343
+ column="name"
344
+ [(width)]="nameWidth"
345
+ (resizeCommit)="onResize($event)"
346
+ aria-label="Resize Name column"
347
+ ></button>
348
+ </div>
349
+ <div forTableHeaderCell name="role">
350
+ Role
351
+ <button
352
+ forTableColumnResizer
353
+ column="role"
354
+ [(width)]="roleWidth"
355
+ (resizeCommit)="onResize($event)"
356
+ aria-label="Resize Role column"
357
+ ></button>
358
+ </div>
359
+ </div>
360
+ ```
361
+
362
+ Seed the initial width through the bound signal (`nameWidth = signal(200)`); the directive applies pointer / keyboard deltas on top of it. The same `--for-table-col-<name>-width` variable can be applied to a `<col>` width or an individual cell width in native `<table>` mode.
363
+
364
+ `data-resizing` (empty string) is present on the handle element while a pointer drag is active — use it to style the resize cursor or highlight the column.
365
+
366
+ `resizeCommit` fires once per gesture (pointer-up after a drag, each arrow press) with a `{ column, width }` payload — bind it to persist the width. Live updates during a drag come through `[(width)]` / `widthChange`.
367
+
368
+ Arrow-key resize (`ArrowLeft` / `ArrowRight`) moves the width by `[step]` pixels per press, respecting `[min]` / `[max]`. In RTL, the directions are mirrored.
369
+
370
+ ## Column & row reordering
371
+
372
+ `[forTableColumnReorder]` and `[forTableRowReorder]` are opt-in companion directives that compose the **drag-drop** primitive to make table headers and data rows reorderable. Each wraps `[forDropList]` via `hostDirectives`, so every drag-drop capability — `[forDraggable]`, `[forDragHandle]`, `[forDragPreview]`, `[forDragPlaceholder]`, FLIP animations, live announce, keyboard and pointer drag — is available to the consumer exactly as with a standalone drop list. **The table never mutates the consumer's data.** Reorder handlers apply `moveItemInArray` to a local signal.
373
+
374
+ ### Column reordering
375
+
376
+ Apply `[forTableColumnReorder]` on the `[forTableHeaderRow]` element and add `[forDraggable] [dragData]="col"` to each header cell. The wrapped list defaults to `orientation="horizontal"` (a column reorder is always along the row axis), so no `orientation` binding is needed. Bind `orientation="vertical"` to override for the rare case.
377
+
378
+ `columnReorder` fires once per committed drop (pointer or keyboard) with `{ from, to, columns }`. Apply `columns` directly to your column-name signal, or use `from`/`to` with `moveItemInArray` for object-shaped column configs.
379
+
380
+ ```html
381
+ <div forTableHeaderRow forTableColumnReorder (columnReorder)="columns.set($event.columns)">
382
+ @for (col of columns(); track col) {
383
+ <div forTableHeaderCell [name]="col" forDraggable [dragData]="col">{{ col }}</div>
384
+ }
385
+ </div>
386
+ ```
387
+
388
+ ```ts
389
+ import { ForDraggable, moveItemInArray } from 'forty-cdk/drag-drop';
390
+ import { ForTableColumnReorder } from 'forty-cdk/table';
391
+ ```
392
+
393
+ ### Row reordering
394
+
395
+ Apply `[forTableRowReorder]` on the rowgroup element that wraps the data rows (`<div role="rowgroup">` in `<div>` mode, `<tbody>` in native `<table>` mode). The list orientation defaults to `vertical`. Add `[forDraggable] [dragData]="row.id"` to each `[forTableRow]`.
396
+
397
+ `rowReorder` fires with `{ from, to }` — apply with `moveItemInArray`.
398
+
399
+ ```html
400
+ <div role="rowgroup" forTableRowReorder (rowReorder)="onRowReorder($event)">
401
+ @for (row of rows(); track row.id) {
402
+ <div forTableRow [value]="row.id" forDraggable [dragData]="row.id">…</div>
403
+ }
404
+ </div>
405
+ ```
406
+
407
+ ```ts
408
+ import { ForDraggable, moveItemInArray } from 'forty-cdk/drag-drop';
409
+ import { ForTableRowReorder } from 'forty-cdk/table';
410
+
411
+ onRowReorder(d: TableRowReorderDescriptor): void {
412
+ this.rows.update((r) => moveItemInArray(r, d.from, d.to));
413
+ }
414
+ ```
415
+
416
+ ### Reordering under virtualization
417
+
418
+ `[forTableRowReorder]` composes with `[forTableVirtualized]`. When virtualization is active, the
419
+ drop list only sees the rows currently in the rendered window, so its raw `from` / `to` would be
420
+ **window-relative**. `[forTableRowReorder]` translates them to **absolute** dataset indices using
421
+ each rendered row's `[virtualIndex]`, so applying `moveItemInArray` to your **full** row array
422
+ moves the right row. A non-virtualized table is unaffected — it emits rendered-order indices as
423
+ before.
424
+
425
+ Supported today:
426
+
427
+ - **Pointer drag within the rendered window**, and **auto-scroll past the window edge** to reach
428
+ rows beyond it — the lifted row is pinned mounted for the duration of the drag so auto-scroll
429
+ cannot unmount it and desync the indices.
430
+ - **Keyboard reorder across the entire dataset.** Space lifts the focused row; Arrow keys step
431
+ the target one row at a time; Home / Ctrl+Home jumps to the dataset start (index 0); End /
432
+ Ctrl+End jumps to the dataset end (last absolute index); PageUp / PageDown jump by one rendered
433
+ window; Space drops and emits absolute `from` / `to`. As the target steps past the rendered
434
+ window the target row is scrolled into view and the lifted row stays pinned mounted throughout.
435
+ `rowReorder` always emits absolute `from` / `to`.
436
+
437
+ Deferred (a `[forTableVirtualized]` row is the only drag-drop composition with this gap):
438
+
439
+ - **Single-gesture free pointer drag to an arbitrary far row** that auto-scroll cannot reach.
440
+
441
+ ```html
442
+ <div
443
+ #scroll
444
+ forTable
445
+ forTableVirtualized
446
+ mode="grid"
447
+ ariaLabel="People"
448
+ [rowCount]="people().length"
449
+ [scrollElement]="scrollEl()"
450
+ #v="forTableVirtualized"
451
+ style="height: 400px; overflow: auto; position: relative;"
452
+ >
453
+ <div forTableHeaderRow style="position: sticky; top: 0;">
454
+ <div forTableHeaderCell name="name">Name</div>
455
+ </div>
456
+ <div
457
+ role="rowgroup"
458
+ forTableRowReorder
459
+ lockAxis="y"
460
+ [style.height.px]="v.totalSize()"
461
+ style="position: relative"
462
+ (rowReorder)="onReorder($event)"
463
+ >
464
+ @for (vrow of v.virtualRows(); track vrow.index) {
465
+ <div
466
+ forTableRow
467
+ [virtualIndex]="vrow.index"
468
+ forDraggable
469
+ [dragData]="vrow.index"
470
+ [style.transform]="'translateY(' + vrow.start + 'px)'"
471
+ style="position: absolute; left: 0; right: 0;"
472
+ >
473
+ <div forTableCell name="name">{{ people()[vrow.index]!.name }}</div>
474
+ </div>
475
+ }
476
+ </div>
477
+ </div>
478
+ ```
479
+
480
+ ```ts
481
+ onReorder(d: TableRowReorderDescriptor): void {
482
+ // d.from / d.to are absolute indices into the full people() array.
483
+ this.people.update((p) => moveItemInArray(p, d.from, d.to));
484
+ }
485
+ ```
486
+
487
+ ### Live-sort placeholder
488
+
489
+ Both companions forward `[liveSort]` to the wrapped `[forDropList]`. Combined with a
490
+ `[forDragPlaceholder]` template on each draggable header cell / row, `[liveSort]="true"` makes
491
+ the placeholder follow the **live resolved drop index** during a pointer drag, so the
492
+ surrounding cells / rows part to reveal where the item will land — instead of only marking the
493
+ dragged item's source slot. It has no effect without a `[forDragPlaceholder]` template, and none
494
+ on keyboard dragging. See the [drag-drop README](../drag-drop/README.md) for the full behaviour.
495
+
496
+ ```html
497
+ <div
498
+ forTableHeaderRow
499
+ forTableColumnReorder
500
+ [liveSort]="true"
501
+ (columnReorder)="columns.set($event.columns)"
502
+ >
503
+ @for (col of columns(); track col) {
504
+ <div forTableHeaderCell [name]="col" forDraggable [dragData]="col">
505
+ {{ col }}
506
+ <ng-template forDragPlaceholder>
507
+ <div class="placeholder"></div>
508
+ </ng-template>
509
+ </div>
510
+ }
511
+ </div>
512
+ ```
513
+
514
+ ### Boundary & axis lock passthrough
515
+
516
+ Both companions forward `[boundary]` and `[lockAxis]` to the wrapped `[forDropList]`, giving the
517
+ same opt-in visual constraint that standalone drop lists support.
518
+
519
+ Because Angular cannot fix a host-directive input to a constant
520
+ ([Angular #51691](https://github.com/angular/angular/issues/51691)), `lockAxis` must be set
521
+ **explicitly** on the companion element — `lockAxis="x"` for columns (horizontal drag), `lockAxis="y"` for rows (vertical drag).
522
+
523
+ ```html
524
+ <div
525
+ forTableHeaderRow
526
+ forTableColumnReorder
527
+ lockAxis="x"
528
+ [boundary]="tableEl"
529
+ (columnReorder)="columns.set($event.columns)"
530
+ >
531
+ …
532
+ </div>
533
+ ```
534
+
535
+ `[boundary]` also accepts a CSS selector string, resolved via `closest()` from the companion's
536
+ host element:
537
+
538
+ ```html
539
+ <div
540
+ forTableHeaderRow
541
+ forTableColumnReorder
542
+ lockAxis="x"
543
+ [boundary]="'[data-testid=\"table-root\"]'"
544
+ (columnReorder)="columns.set($event.columns)"
545
+ >
546
+ …
547
+ </div>
548
+ ```
549
+
550
+ ### Automatic ARIA reindexing
551
+
552
+ `aria-rowindex` and `aria-colindex` recompute automatically after you apply the move. `ForTable` tracks DOM document order reactively via a `MutationObserver`; when Angular re-renders the `@for` in the new order, the indices update with no extra table code.
553
+
554
+ ### Caveats
555
+
556
+ - `[forTableSortHeader]` and `[forDraggable]` (column reorder) **may** share the same header cell. When co-located, the draggable's roving tabindex owns the single tab stop and the sort header yields its own `[tabindex]`, so the two no longer collide on the host attribute; `aria-sort` / `data-sorted` stay on the cell and clicking it still cycles the sort. Because both directives also handle Enter / Space, activating a focused co-located cell from the keyboard cycles the sort **and** starts a keyboard drag-lift — keep keyboard sorting and keyboard reorder on separate cells if you need the two interactions distinct.
557
+ - Reorderable rows and cells must generate real boxes. Avoid `display: contents` on `[forTableRow]` or header cells used as drag targets — the drag-drop primitive needs a non-zero bounding box for pointer geometry.
558
+ - In `mode="grid"`, both 2D cell roving and keyboard row dragging are keyboard-interactive. Reordering is the consumer's composition choice; the library provides affordances, not opinions about whether both should coexist.
559
+ - For all drag-drop CSS hooks (`data-dragging`, `data-drag-over`, `[forDragHandle]`, `[data-drag-preview]`, `data-settling`) see the [drag-drop README](../drag-drop/README.md).
560
+
561
+ ### Inputs
562
+
563
+ | Directive | Input | Type | Default | Description |
564
+ | ------------------------- | ---------------- | ------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------- |
565
+ | `[forTableColumnReorder]` | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Passthrough to `[forDropList]`, defaulted to `'horizontal'` (column axis). Bind `'vertical'` to override. |
566
+ | `[forTableColumnReorder]` | `dir` | `'ltr' \| 'rtl' \| null` | `null` | Writing direction passthrough. |
567
+ | `[forTableColumnReorder]` | `disabled` | `boolean` | `false` | Disables the whole list passthrough. |
568
+ | `[forTableColumnReorder]` | `autoScroll` | `boolean` | `true` | Auto-scroll passthrough. |
569
+ | `[forTableColumnReorder]` | `animateReorder` | `boolean` | `false` | FLIP animation passthrough. |
570
+ | `[forTableColumnReorder]` | `liveSort` | `boolean` | `false` | Live-sort placeholder passthrough. |
571
+ | `[forTableColumnReorder]` | `boundary` | `HTMLElement \| string \| null` | `null` | Boundary element (or selector) passthrough. Confines the preview; no effect on drop index. |
572
+ | `[forTableColumnReorder]` | `lockAxis` | `'x' \| 'y' \| null` | `null` | Axis-lock passthrough. Set `'x'` for column drag (holds vertical position). Must be set explicitly — Angular #51691. |
573
+ | `[forTableRowReorder]` | `dir` | `'ltr' \| 'rtl' \| null` | `null` | Writing direction passthrough. |
574
+ | `[forTableRowReorder]` | `disabled` | `boolean` | `false` | Disables the whole list passthrough. |
575
+ | `[forTableRowReorder]` | `autoScroll` | `boolean` | `true` | Auto-scroll passthrough. |
576
+ | `[forTableRowReorder]` | `animateReorder` | `boolean` | `false` | FLIP animation passthrough. |
577
+ | `[forTableRowReorder]` | `liveSort` | `boolean` | `false` | Live-sort placeholder passthrough. |
578
+ | `[forTableRowReorder]` | `boundary` | `HTMLElement \| string \| null` | `null` | Boundary element (or selector) passthrough. Confines the preview; no effect on drop index. |
579
+ | `[forTableRowReorder]` | `lockAxis` | `'x' \| 'y' \| null` | `null` | Axis-lock passthrough. Set `'y'` for row drag (holds horizontal position). Must be set explicitly — Angular #51691. |
580
+
581
+ ### Outputs
582
+
583
+ | Directive | Output | Payload | Description |
584
+ | ------------------------- | --------------- | ------------------------------ | -------------------------------------------------------------------- |
585
+ | `[forTableColumnReorder]` | `columnReorder` | `TableColumnReorderDescriptor` | `{ from, to, columns }` — fired once per committed column drag-drop. |
586
+ | `[forTableRowReorder]` | `rowReorder` | `TableRowReorderDescriptor` | `{ from, to }` — fired once per committed row drag-drop. |
587
+
588
+ ## Virtualized rows
589
+
590
+ `[forTableVirtualized]` is opt-in and works only with `<div role>` grid mode. Native `<table>` cannot omit rows mid-body (the browser recalculates all column widths when any row is missing), so virtualization requires the `<div>` structure documented above.
591
+
592
+ Place `[forTableVirtualized]` on the same element as `[forTable]`. Set `[rowCount]` on `[forTable]` to the **true total** row count — this drives both `aria-rowcount` and the window size.
593
+
594
+ ```html
595
+ <div
596
+ #scroll
597
+ forTable
598
+ forTableVirtualized
599
+ mode="grid"
600
+ ariaLabel="Big table"
601
+ [rowCount]="10000"
602
+ [estimateRowSize]="44"
603
+ [scrollElement]="scrollEl()"
604
+ #v="forTableVirtualized"
605
+ style="height: 400px; overflow: auto; position: relative;"
606
+ >
607
+ <div forTableHeaderRow style="position: sticky; top: 0;">
608
+ <div forTableHeaderCell name="name">Name</div>
609
+ </div>
610
+ <div role="rowgroup" [style.height.px]="v.totalSize()" style="position: relative">
611
+ @for (vrow of v.virtualRows(); track vrow.index) {
612
+ <div
613
+ forTableRow
614
+ [virtualIndex]="vrow.index"
615
+ [style.transform]="'translateY(' + vrow.start + 'px)'"
616
+ style="position: absolute; left: 0; right: 0;"
617
+ >
618
+ <div forTableCell name="name">{{ data()[vrow.index]!.name }}</div>
619
+ </div>
620
+ }
621
+ </div>
622
+ </div>
623
+ ```
624
+
625
+ Key points:
626
+
627
+ - The sticky header rowgroup lives **outside** the absolutely-positioned body so it is not clipped by the scroll container's overflow.
628
+ - The body rowgroup is `position: relative` and sized to `v.totalSize()` — this creates the full scroll range.
629
+ - Each row is `position: absolute; transform: translateY(vrow.start + 'px')`. Do not use `top` — `transform` avoids layout thrashing.
630
+ - Bind `[virtualIndex]="vrow.index"` on each `[forTableRow]`. This is what drives the absolute 1-based `aria-rowindex` (`vrow.index + 1`) rather than the DOM-order index.
631
+ - The **focused row stays mounted** even when scrolled out of the window. The roving-focused `gridcell` is never unmounted; roving navigation is unchanged.
632
+ - For measured (variable) row heights, call `v.measureRow(el)` per rendered row in `afterEveryRender`.
633
+
634
+ ```ts
635
+ import { afterEveryRender } from '@angular/core';
636
+ import { ForTableVirtualized } from 'forty-cdk/virtualization';
637
+
638
+ afterEveryRender(() => {
639
+ for (const el of this.rowEls()) {
640
+ this.v.measureRow(el.nativeElement);
641
+ }
642
+ });
643
+ ```
644
+
645
+ ### Scroll container (table root vs. ancestor)
646
+
647
+ By default the **table root** is the scroll container — the element carrying `[forTableVirtualized]` scrolls its own rows (the `overflow: auto` element in the examples above), so `[scrollElement]` can be left unset.
648
+
649
+ When the element that actually scrolls is an **ancestor** of the table — e.g. an app-shell viewport that scrolls projected content — the table cannot inject a scroll container it does not own. Bind `[scrollElement]` to that ancestor by hand (a template reference variable is the simplest source):
650
+
651
+ ```html
652
+ <div #shell style="height: 100vh; overflow: auto;">
653
+ <!-- other app-shell content scrolls together with the table -->
654
+ <div
655
+ forTable
656
+ forTableVirtualized
657
+ mode="grid"
658
+ ariaLabel="Big table"
659
+ [rowCount]="10000"
660
+ [scrollElement]="shell"
661
+ #v="forTableVirtualized"
662
+ style="position: relative;"
663
+ >
664
+ <!-- header + windowed rows exactly as above -->
665
+ </div>
666
+ </div>
667
+ ```
668
+
669
+ #### Wrapping: re-exposing / renaming `scrollElement`
670
+
671
+ A design-system wrapper that re-exposes `ForTableVirtualized` through `hostDirectives` can surface `scrollElement` directly, or rename it, via input aliasing — no bridging `effect` is needed because the value flows straight through:
672
+
673
+ ```ts
674
+ @Component({
675
+ selector: 'app-data-grid',
676
+ hostDirectives: [
677
+ {
678
+ directive: ForTableVirtualized,
679
+ inputs: ['scrollElement: scrollContainer'],
680
+ },
681
+ ],
682
+ })
683
+ export class DataGrid {}
684
+ ```
685
+
686
+ Consumers of the wrapper then bind `[scrollContainer]="shell"`.
687
+
688
+ ### `[forTableVirtualized]` inputs
689
+
690
+ | Input | Type | Default | Description |
691
+ | ----------------- | --------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
692
+ | `estimateRowSize` | `number` | `44` | Estimated row height in px. Used as the fixed size in fixed-size mode and as the initial estimate in measured mode. |
693
+ | `scrollElement` | `HTMLElement \| null` | `null` | Explicit scroll container. Defaults to the table root element; bind to an ancestor when it owns the scroll. |
694
+
695
+ ### `[forTableVirtualized]` API (`#v="forTableVirtualized"`)
696
+
697
+ | Member | Type | Description |
698
+ | ------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
699
+ | `virtualRows()` | `Signal<readonly VirtualItem[]>` | The visible window plus overscan, always including the focused row. |
700
+ | `range()` | `Signal<readonly [number, number]>` | The rendered window as `[firstIndex, lastIndex + 1)`, sourced from the true virtualizer window (not the focus-augmented `virtualRows()`). Plugs straight into `injectInfiniteScroll`. |
701
+ | `totalSize()` | `Signal<number>` | Total scroll height of all rows in px. Bind to the body container height. |
702
+ | `scrollToRow(index, options?)` | method | Scroll the container so row `index` is in view. |
703
+ | `measureRow(el)` | method | Record a rendered row element's measured size (for dynamic row heights). |
704
+
705
+ ### Tree-shaking
706
+
707
+ `@tanstack/virtual-core` only loads when you import `ForTableVirtualized`. A plain `ForTable` never pulls in the virtualization core.
708
+
709
+ ## Inputs
710
+
711
+ | Directive | Input | Type | Default | Description |
712
+ | ------------------------- | --------------------- | --------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------- |
713
+ | `[forTable]` | `mode` | `'table' \| 'grid' \| 'treegrid'` | `'table'` | ARIA role emitted on the host. |
714
+ | `[forTable]` | `ariaLabel` | `string \| null` | `null` | Reactive accessible label. |
715
+ | `[forTable]` | `dir` | `'ltr' \| 'rtl' \| null` | `null` | Writing direction; resolves ambient when unset. |
716
+ | `[forTable]` | `rowCount` | `number` | rendered count | True total data-row count for `aria-rowcount`. Ignored in `table` mode. |
717
+ | `[forTable]` | `colCount` | `number` | rendered count | True total column count for `aria-colcount`. Ignored in `table` mode. |
718
+ | `[forTable]` | `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'none'` | Row selection mode. |
719
+ | `[forTable]` | `selectionBehavior` | `'toggle' \| 'replace'` | `'toggle'` | How a row click mutates selection (modifier-aware in `replace` mode). |
720
+ | `[forTable]` | `selection` | `model<readonly unknown[]>([])` | `[]` | Two-way bindable selected row values. |
721
+ | `[forTable]` | `compareWith` | `(a: unknown, b: unknown) => boolean` | `===` | Equality comparator for row values. Override for object rows. |
722
+ | `[forTable]` | `selectableValues` | `readonly unknown[] \| null` | `null` | Full ordered set of selectable values for total-aware aggregates under virtualization; `null` uses the rendered rows. |
723
+ | `[forTable]` | `expanded` | `model<readonly unknown[]>([])` | `[]` | Two-way bindable open parent-row values for `mode="treegrid"`. Ignored in other modes. |
724
+ | `[forTableHeaderCell]` | `name` | `string` (required) | — | Column identifier, reflected as `data-column`. |
725
+ | `[forTableHeaderCell]` | `sticky` | `boolean \| 'end'` | `false` | Sticky edge; reflected as `data-sticky`. |
726
+ | `[forTableCell]` | `name` | `string` (required) | — | Column identifier, reflected as `data-column`. |
727
+ | `[forTableCell]` | `sticky` | `boolean \| 'end'` | `false` | Sticky edge; reflected as `data-sticky`. |
728
+ | `[forTableCell]` | `disabled` | `boolean` | `false` | Skipped during navigation; reflects `aria-disabled` / `data-disabled`. |
729
+ | `[forTableRow]` | `value` | `unknown` | `undefined` | Selection identity for this row. Leave unset for non-selectable rows. |
730
+ | `[forTableRow]` | `level` | `number` | `1` | 1-based tree depth for `aria-level` in `mode="treegrid"`. Ignored in other modes. |
731
+ | `[forTableRow]` | `expandable` | `boolean` | `false` | Marks this row as an expandable parent; emits `aria-expanded` + `data-state`. |
732
+ | `[forTableSelectAll]` | `ariaLabel` | `string \| null` | `null` | Accessible label for the select-all checkbox (e.g. `"Select all rows"`). |
733
+ | `[forTableSortHeader]` | `column` | `string` (required) | — | Column identity included in the `sortChange` payload. |
734
+ | `[forTableSortHeader]` | `direction` | `'ascending' \| 'descending' \| 'none'` | `'none'` | Current sort direction (two-way bindable via `[(direction)]`). |
735
+ | `[forTableSortHeader]` | `disableClear` | `boolean` | `false` | Skip the `'none'` step: cycle becomes `ascending ↔ descending`. |
736
+ | `[forTableSortHeader]` | `firstClickDirection` | `'ascending' \| 'descending'` | `'ascending'` | Direction a previously-unsorted column enters on its first activation (the `'none' → ?` step). |
737
+ | `[forTableSortHeader]` | `sortable` | `boolean` | `true` | When `false`, the header is fully inert (no tabindex, no aria-sort). |
738
+ | `[forTableColumnResizer]` | `column` | `string` (required) | — | Column identity; included in the `resizeCommit` payload and the CSS var name. |
739
+ | `[forTableColumnResizer]` | `width` | `model<number>()` | `undefined` | Current column width in pixels. Two-way bindable via `[(width)]`. Fires `widthChange` on every live update. |
740
+ | `[forTableColumnResizer]` | `min` | `number` | `0` | Minimum width in pixels. |
741
+ | `[forTableColumnResizer]` | `max` | `number` | `Infinity` | Maximum width in pixels. No upper bound by default. |
742
+ | `[forTableColumnResizer]` | `step` | `number` | `10` | Pixels applied per `ArrowLeft` / `ArrowRight` press. |
743
+
744
+ ## CSS hooks
745
+
746
+ | Token / attribute | Emitted by | Description |
747
+ | ------------------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------------- |
748
+ | `--for-table-header-height` | `[forTable]` | Header row height in px. Updated on resize. |
749
+ | `data-mode` | `[forTable]` | `'table' \| 'grid' \| 'treegrid'` |
750
+ | `data-column` | header / data cell | Column name from the `name` input. |
751
+ | `data-sticky` | header / data cell | `''` (start-edge) or `'end'` when sticky; absent otherwise. |
752
+ | `data-highlighted` | `[forTableCell]` | Present on the currently roving-focused cell in grid / treegrid mode. |
753
+ | `aria-expanded` | `[forTableRow]` | `"true"` / `"false"` (always-emit) on expandable rows in `treegrid` mode; absent on leaves. |
754
+ | `data-state` | `[forTableRow]` | `"open"` / `"closed"` on expandable rows in `treegrid` mode; absent on leaves. |
755
+ | `aria-level` | `[forTableRow]` | 1-based depth in `treegrid` mode; absent otherwise. |
756
+ | `aria-posinset` | `[forTableRow]` | 1-based position among same-level siblings in `treegrid` mode; absent otherwise. |
757
+ | `aria-setsize` | `[forTableRow]` | Total same-level sibling count in `treegrid` mode; absent otherwise. |
758
+ | `aria-rowindex` | `[forTableRow]` | 1-based row index in the data row set. Absent in table mode. |
759
+ | `aria-colindex` | `[forTableCell]` | 1-based column index within the row. Absent in table mode. |
760
+ | `aria-selected` | `[forTableRow]` | `"true"` / `"false"` (always-emit) when `selectionMode` is not `'none'`. |
761
+ | `data-selected` | `[forTableRow]` | Present (`""`) when selected; absent when not. Boolean present/absent hook. |
762
+ | `aria-multiselectable` | `[forTable]` | `"true"` when `selectionMode="multiple"`; absent otherwise. |
763
+ | `data-state` | `[forTableRowSelector]` | `"checked"` or `"unchecked"`. The row owns `aria-selected`; this is decoration. |
764
+ | `aria-checked` | `[forTableSelectAll]` | `"true"` / `"false"` / `"mixed"` (tri-state). |
765
+ | `data-state` | `[forTableSelectAll]` | `"checked"` / `"unchecked"` / `"indeterminate"`. |
766
+ | `aria-sort` | `[forTableSortHeader]` | `"ascending"` or `"descending"` while sorted; absent (`null`) when unsorted. Truthy-only. |
767
+ | `data-sorted` | `[forTableSortHeader]` | Same value as `aria-sort` — a CSS styling hook (e.g. for a sort arrow glyph). |
768
+ | `--for-table-col-<name>-width` | `[forTable]` (set by `[forTableColumnResizer]`) | Resolved column width in px; apply it to your layout. |
769
+ | `data-resizing` | `[forTableColumnResizer]` | Present (`""`) while a pointer drag is active. |