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.
- package/accordion/README.md +122 -0
- package/aspect-ratio/README.md +76 -0
- package/avatar/README.md +100 -0
- package/breadcrumbs/README.md +49 -0
- package/breakpoints/README.md +81 -0
- package/button/README.md +49 -0
- package/calendar/README.md +458 -0
- package/carousel/README.md +358 -0
- package/checkbox/README.md +146 -0
- package/combobox/README.md +535 -0
- package/context-menu/README.md +139 -0
- package/date-field/README.md +184 -0
- package/date-picker/README.md +338 -0
- package/dialog/README.md +388 -0
- package/disclosure/README.md +114 -0
- package/drag-drop/README.md +359 -0
- package/drawer/README.md +560 -0
- package/dropdown-menu/README.md +176 -0
- package/fesm2022/forty-cdk-accordion.mjs +348 -0
- package/fesm2022/forty-cdk-accordion.mjs.map +1 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs +74 -0
- package/fesm2022/forty-cdk-aspect-ratio.mjs.map +1 -0
- package/fesm2022/forty-cdk-avatar.mjs +308 -0
- package/fesm2022/forty-cdk-avatar.mjs.map +1 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs +125 -0
- package/fesm2022/forty-cdk-breadcrumbs.mjs.map +1 -0
- package/fesm2022/forty-cdk-breakpoints.mjs +117 -0
- package/fesm2022/forty-cdk-breakpoints.mjs.map +1 -0
- package/fesm2022/forty-cdk-button.mjs +134 -0
- package/fesm2022/forty-cdk-button.mjs.map +1 -0
- package/fesm2022/forty-cdk-calendar.mjs +2034 -0
- package/fesm2022/forty-cdk-calendar.mjs.map +1 -0
- package/fesm2022/forty-cdk-carousel.mjs +968 -0
- package/fesm2022/forty-cdk-carousel.mjs.map +1 -0
- package/fesm2022/forty-cdk-checkbox.mjs +226 -0
- package/fesm2022/forty-cdk-checkbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-combobox.mjs +2596 -0
- package/fesm2022/forty-cdk-combobox.mjs.map +1 -0
- package/fesm2022/forty-cdk-context-menu.mjs +413 -0
- package/fesm2022/forty-cdk-context-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-core.mjs +9022 -0
- package/fesm2022/forty-cdk-core.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-field.mjs +744 -0
- package/fesm2022/forty-cdk-date-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-date-picker.mjs +1011 -0
- package/fesm2022/forty-cdk-date-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-dialog.mjs +707 -0
- package/fesm2022/forty-cdk-dialog.mjs.map +1 -0
- package/fesm2022/forty-cdk-disclosure.mjs +190 -0
- package/fesm2022/forty-cdk-disclosure.mjs.map +1 -0
- package/fesm2022/forty-cdk-drag-drop.mjs +1180 -0
- package/fesm2022/forty-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/forty-cdk-drawer.mjs +1641 -0
- package/fesm2022/forty-cdk-drawer.mjs.map +1 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs +350 -0
- package/fesm2022/forty-cdk-dropdown-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-field.mjs +425 -0
- package/fesm2022/forty-cdk-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-fieldset.mjs +164 -0
- package/fesm2022/forty-cdk-fieldset.mjs.map +1 -0
- package/fesm2022/forty-cdk-file-upload.mjs +221 -0
- package/fesm2022/forty-cdk-file-upload.mjs.map +1 -0
- package/fesm2022/forty-cdk-hover-card.mjs +496 -0
- package/fesm2022/forty-cdk-hover-card.mjs.map +1 -0
- package/fesm2022/forty-cdk-input.mjs +274 -0
- package/fesm2022/forty-cdk-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-internationalized-date.mjs +1 -1
- package/fesm2022/forty-cdk-internationalized-date.mjs.map +1 -1
- package/fesm2022/forty-cdk-listbox.mjs +1279 -0
- package/fesm2022/forty-cdk-listbox.mjs.map +1 -0
- package/fesm2022/forty-cdk-menu.mjs +1439 -0
- package/fesm2022/forty-cdk-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-menubar.mjs +787 -0
- package/fesm2022/forty-cdk-menubar.mjs.map +1 -0
- package/fesm2022/forty-cdk-meter.mjs +211 -0
- package/fesm2022/forty-cdk-meter.mjs.map +1 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs +1145 -0
- package/fesm2022/forty-cdk-navigation-menu.mjs.map +1 -0
- package/fesm2022/forty-cdk-number-input.mjs +559 -0
- package/fesm2022/forty-cdk-number-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-otp-input.mjs +527 -0
- package/fesm2022/forty-cdk-otp-input.mjs.map +1 -0
- package/fesm2022/forty-cdk-pagination.mjs +323 -0
- package/fesm2022/forty-cdk-pagination.mjs.map +1 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs +297 -0
- package/fesm2022/forty-cdk-pane-resizer.mjs.map +1 -0
- package/fesm2022/forty-cdk-popover.mjs +698 -0
- package/fesm2022/forty-cdk-popover.mjs.map +1 -0
- package/fesm2022/forty-cdk-progress.mjs +226 -0
- package/fesm2022/forty-cdk-progress.mjs.map +1 -0
- package/fesm2022/forty-cdk-radio-group.mjs +378 -0
- package/fesm2022/forty-cdk-radio-group.mjs.map +1 -0
- package/fesm2022/forty-cdk-scroll-area.mjs +640 -0
- package/fesm2022/forty-cdk-scroll-area.mjs.map +1 -0
- package/fesm2022/forty-cdk-search.mjs +205 -0
- package/fesm2022/forty-cdk-search.mjs.map +1 -0
- package/fesm2022/forty-cdk-select.mjs +1661 -0
- package/fesm2022/forty-cdk-select.mjs.map +1 -0
- package/fesm2022/forty-cdk-separator.mjs +82 -0
- package/fesm2022/forty-cdk-separator.mjs.map +1 -0
- package/fesm2022/forty-cdk-signal-forms.mjs +97 -0
- package/fesm2022/forty-cdk-signal-forms.mjs.map +1 -0
- package/fesm2022/forty-cdk-slider.mjs +803 -0
- package/fesm2022/forty-cdk-slider.mjs.map +1 -0
- package/fesm2022/forty-cdk-stepper.mjs +886 -0
- package/fesm2022/forty-cdk-stepper.mjs.map +1 -0
- package/fesm2022/forty-cdk-switch.mjs +137 -0
- package/fesm2022/forty-cdk-switch.mjs.map +1 -0
- package/fesm2022/forty-cdk-table.mjs +1518 -0
- package/fesm2022/forty-cdk-table.mjs.map +1 -0
- package/fesm2022/forty-cdk-tabs.mjs +400 -0
- package/fesm2022/forty-cdk-tabs.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-field.mjs +593 -0
- package/fesm2022/forty-cdk-time-field.mjs.map +1 -0
- package/fesm2022/forty-cdk-time-picker.mjs +1013 -0
- package/fesm2022/forty-cdk-time-picker.mjs.map +1 -0
- package/fesm2022/forty-cdk-toast.mjs +1153 -0
- package/fesm2022/forty-cdk-toast.mjs.map +1 -0
- package/fesm2022/forty-cdk-toggle.mjs +516 -0
- package/fesm2022/forty-cdk-toggle.mjs.map +1 -0
- package/fesm2022/forty-cdk-toolbar.mjs +374 -0
- package/fesm2022/forty-cdk-toolbar.mjs.map +1 -0
- package/fesm2022/forty-cdk-tooltip.mjs +672 -0
- package/fesm2022/forty-cdk-tooltip.mjs.map +1 -0
- package/fesm2022/forty-cdk-tree.mjs +2007 -0
- package/fesm2022/forty-cdk-tree.mjs.map +1 -0
- package/fesm2022/forty-cdk-virtualization.mjs +1 -1
- package/fesm2022/forty-cdk-virtualization.mjs.map +1 -1
- package/fesm2022/forty-cdk.mjs +0 -43310
- package/fesm2022/forty-cdk.mjs.map +1 -1
- package/field/README.md +97 -0
- package/fieldset/README.md +86 -0
- package/file-upload/README.md +73 -0
- package/hover-card/README.md +171 -0
- package/input/README.md +156 -0
- package/listbox/README.md +424 -0
- package/menu/README.md +181 -0
- package/menubar/README.md +140 -0
- package/meter/README.md +128 -0
- package/navigation-menu/README.md +253 -0
- package/number-input/README.md +171 -0
- package/otp-input/README.md +198 -0
- package/package.json +213 -1
- package/pagination/README.md +61 -0
- package/pane-resizer/README.md +136 -0
- package/popover/README.md +262 -0
- package/progress/README.md +115 -0
- package/radio-group/README.md +129 -0
- package/scroll-area/README.md +184 -0
- package/search/README.md +42 -0
- package/select/README.md +488 -0
- package/separator/README.md +84 -0
- package/signal-forms/README.md +72 -0
- package/slider/README.md +152 -0
- package/stepper/README.md +292 -0
- package/switch/README.md +116 -0
- package/table/README.md +769 -0
- package/tabs/README.md +130 -0
- package/time-field/README.md +157 -0
- package/time-picker/README.md +172 -0
- package/toast/README.md +398 -0
- package/toggle/README.md +224 -0
- package/toolbar/README.md +109 -0
- package/tooltip/README.md +274 -0
- package/tree/README.md +708 -0
- package/types/forty-cdk-accordion.d.ts +242 -0
- package/types/forty-cdk-aspect-ratio.d.ts +59 -0
- package/types/forty-cdk-avatar.d.ts +133 -0
- package/types/forty-cdk-breadcrumbs.d.ts +92 -0
- package/types/forty-cdk-breakpoints.d.ts +141 -0
- package/types/forty-cdk-button.d.ts +80 -0
- package/types/forty-cdk-calendar.d.ts +914 -0
- package/types/forty-cdk-carousel.d.ts +530 -0
- package/types/forty-cdk-checkbox.d.ts +141 -0
- package/types/forty-cdk-combobox.d.ts +1259 -0
- package/types/forty-cdk-context-menu.d.ts +313 -0
- package/types/forty-cdk-core.d.ts +5774 -0
- package/types/forty-cdk-date-field.d.ts +307 -0
- package/types/forty-cdk-date-picker.d.ts +622 -0
- package/types/forty-cdk-dialog.d.ts +546 -0
- package/types/forty-cdk-disclosure.d.ts +127 -0
- package/types/forty-cdk-drag-drop.d.ts +456 -0
- package/types/forty-cdk-drawer.d.ts +871 -0
- package/types/forty-cdk-dropdown-menu.d.ts +242 -0
- package/types/forty-cdk-field.d.ts +236 -0
- package/types/forty-cdk-fieldset.d.ts +119 -0
- package/types/forty-cdk-file-upload.d.ts +124 -0
- package/types/forty-cdk-hover-card.d.ts +320 -0
- package/types/forty-cdk-input.d.ts +169 -0
- package/types/forty-cdk-internationalized-date.d.ts +1 -1
- package/types/forty-cdk-listbox.d.ts +513 -0
- package/types/forty-cdk-menu.d.ts +629 -0
- package/types/forty-cdk-menubar.d.ts +451 -0
- package/types/forty-cdk-meter.d.ts +122 -0
- package/types/forty-cdk-navigation-menu.d.ts +514 -0
- package/types/forty-cdk-number-input.d.ts +319 -0
- package/types/forty-cdk-otp-input.d.ts +248 -0
- package/types/forty-cdk-pagination.d.ts +214 -0
- package/types/forty-cdk-pane-resizer.d.ts +145 -0
- package/types/forty-cdk-popover.d.ts +509 -0
- package/types/forty-cdk-progress.d.ts +143 -0
- package/types/forty-cdk-radio-group.d.ts +222 -0
- package/types/forty-cdk-scroll-area.d.ts +258 -0
- package/types/forty-cdk-search.d.ts +142 -0
- package/types/forty-cdk-select.d.ts +899 -0
- package/types/forty-cdk-separator.d.ts +59 -0
- package/types/forty-cdk-signal-forms.d.ts +58 -0
- package/types/forty-cdk-slider.d.ts +379 -0
- package/types/forty-cdk-stepper.d.ts +650 -0
- package/types/forty-cdk-switch.d.ts +87 -0
- package/types/forty-cdk-table.d.ts +723 -0
- package/types/forty-cdk-tabs.d.ts +235 -0
- package/types/forty-cdk-time-field.d.ts +307 -0
- package/types/forty-cdk-time-picker.d.ts +578 -0
- package/types/forty-cdk-toast.d.ts +598 -0
- package/types/forty-cdk-toggle.d.ts +310 -0
- package/types/forty-cdk-toolbar.d.ts +217 -0
- package/types/forty-cdk-tooltip.d.ts +436 -0
- package/types/forty-cdk-tree.d.ts +688 -0
- package/types/forty-cdk.d.ts +1 -19743
package/table/README.md
ADDED
|
@@ -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. |
|