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,398 @@
1
+ # Toast
2
+
3
+ Headless toast notifications. The visible toast renders with `role="status"` (`'info'` / `'success'` / `'warning'`) or `role="alert"` (`'error'`) plus `aria-live` so screen readers announce updates without forcing focus, following the [WAI-ARIA Alert pattern](https://www.w3.org/WAI/ARIA/apg/patterns/alert/).
4
+
5
+ Two ways to use the same primitive:
6
+
7
+ - **Programmatic** (the common path): inject `ForToastManager` and call `show({ title, … })` from anywhere.
8
+ - **Declarative**: drop `<div forToast>` directly in any template, controlling mount/unmount with `@if`.
9
+
10
+ ## Pieces
11
+
12
+ | Class | Selector | Role |
13
+ | --------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
14
+ | `ForToastManager` | injectable | Programmatic stack. `show()`, `dismiss(id)`, `dismissAll()`, reactive `toasts()` / `count()`. |
15
+ | `ForToastViewport` | `[forToastViewport]` / `<for-toast-viewport>` | Mount once per region near the app root. `role="region"`, renders the toasts of its `[region]`. The F6 hotkey is coordinated once by the manager, so it never double-fires across viewports. |
16
+ | `ForToast` | `[forToast]` | One toast. `role="status"` / `role="alert"` per variant, timer, hover/focus pause, Escape-to-close. |
17
+ | `ForToastTitle` | `[forToastTitle]` | Wires `aria-labelledby`. |
18
+ | `ForToastDescription` | `[forToastDescription]` | Wires `aria-describedby`. |
19
+ | `ForToastAction` | `[forToastAction]` | Action button — emits `(dismiss)` with reason `'action'` after invoking your `(click)` handler. Accepts `[altText]` for WCAG 2.2.1 announcements. |
20
+ | `ForToastClose` | `[forToastClose]` | Close button — emits `(dismiss)` with reason `'manual'`. Carries `aria-label="Close"`. |
21
+ | `ForToastRef<R>` | handle | Per-toast: `dismiss(reason, value)`, `update(patch)`, `closed: Promise`, signals of state. |
22
+
23
+ ## Mount the viewport once
24
+
25
+ In your `app.html`:
26
+
27
+ ```html
28
+ <for-toast-viewport [maxVisible]="5" />
29
+ ```
30
+
31
+ Position it from CSS — the directive doesn't impose layout:
32
+
33
+ ```css
34
+ [forToastViewport] {
35
+ position: fixed;
36
+ bottom: 1rem;
37
+ right: 1rem;
38
+ display: grid;
39
+ gap: 0.5rem;
40
+ pointer-events: none;
41
+ }
42
+ [forToast] {
43
+ pointer-events: auto;
44
+ }
45
+ ```
46
+
47
+ ## Multiple regions
48
+
49
+ A viewport renders only the toasts whose `region` matches its `[region]` input. Omit `region` everywhere and everything flows through the default region — that's the single-viewport setup above. To run independent regions (e.g. system notifications top-right, action confirmations bottom-center) mount one viewport per region and tag each `show()`:
50
+
51
+ ```html
52
+ <for-toast-viewport region="system" />
53
+ <!-- styled top-right -->
54
+ <for-toast-viewport region="confirmations" />
55
+ <!-- styled bottom-center -->
56
+ ```
57
+
58
+ ```ts
59
+ this.toasts.show({ region: 'system', title: 'New version available' });
60
+ this.toasts.show({ region: 'confirmations', title: 'Saved' });
61
+ ```
62
+
63
+ Each region resolves to the host `data-region` attribute, so you can position / theme regions purely from CSS:
64
+
65
+ ```css
66
+ [forToastViewport][data-region='system'] {
67
+ top: 1rem;
68
+ right: 1rem;
69
+ }
70
+ [forToastViewport][data-region='confirmations'] {
71
+ bottom: 1rem;
72
+ left: 50%;
73
+ transform: translateX(-50%);
74
+ }
75
+ ```
76
+
77
+ If two viewports share the same region, only the first one mounted renders it; the rest stay inactive (and warn in dev) so a stray second viewport — a lazy route, a shared layout — never silently duplicates toasts. A single `show()` always produces exactly one toast node.
78
+
79
+ ## Programmatic API
80
+
81
+ ```ts
82
+ import { ForToastManager } from 'forty-cdk/toast';
83
+
84
+ @Component(/* … */)
85
+ class SomeComponent {
86
+ readonly toasts = inject(ForToastManager);
87
+
88
+ save() {
89
+ const ref = this.toasts.show({ title: 'Saving…', duration: 0 });
90
+ api.save().then(
91
+ () => ref.update({ title: 'Saved', variant: 'success', duration: 3000 }),
92
+ () => ref.update({ title: 'Save failed', variant: 'error' }),
93
+ );
94
+ }
95
+
96
+ undoableDelete(item: Item) {
97
+ this.toasts.show({
98
+ title: 'Item deleted',
99
+ action: { label: 'Undo', onClick: () => restore(item) },
100
+ duration: 6000,
101
+ });
102
+ }
103
+ }
104
+ ```
105
+
106
+ `show()` returns a `ForToastRef`:
107
+
108
+ - `ref.dismiss(reason?, result?)` — close imperatively.
109
+ - `ref.update(patch)` — mutate config in place (text, duration, variant). `id` and `region` are fixed at `show()` and ignored here — `id` is the toast's identity, and `region` decides which viewport renders it, so changing it would silently remount the toast (resetting its timer and announcement). Dismiss and re-`show()` to move a toast between regions.
110
+ - `ref.closed` — `Promise<{ reason, result }>` resolved on first dismiss.
111
+ - `ref.isClosed()` — reactive boolean.
112
+ - `ref.config()` — reactive config snapshot.
113
+
114
+ ## Composition + styling model
115
+
116
+ Toast is "bring your own markup + classes", like every other primitive — even on the programmatic path where the viewport renders the markup for you. There are two levers:
117
+
118
+ ### Per-toast classes
119
+
120
+ Pass `class` (a single token or a space-separated string) or `classList` (a string or an array of tokens) in the `show()` config. They are applied to the rendered toast root (the `[forToast]` element), merged with the directive's own host attributes — they never clobber `data-state` / `data-variant` / the swipe CSS hooks.
121
+
122
+ ```ts
123
+ this.toasts.show({ title: 'Saved', variant: 'success', class: 'toast toast--success' });
124
+ this.toasts.show({ title: 'Failed', classList: ['toast', 'toast--error'] });
125
+ ```
126
+
127
+ ```css
128
+ /* Now you can target your own class instead of the [forToast] attribute. */
129
+ .toast {
130
+ display: grid;
131
+ gap: 0.25rem;
132
+ padding: 0.75rem 1rem;
133
+ border-radius: 0.5rem;
134
+ }
135
+ .toast--success {
136
+ border-inline-start: 4px solid green;
137
+ }
138
+ ```
139
+
140
+ Declarative toasts (`<div forToast class="toast">`) take consumer classes the native way — `class` in `show()` is the programmatic equivalent. Styling by the `[forToast]` / `[forToastTitle]` / … attribute selectors still works and remains a valid choice; the class hook just unblocks design-system class names.
141
+
142
+ ### Exit / enter animations (programmatic)
143
+
144
+ On the programmatic path the toast root is rendered for you inside `<for-toast-viewport>`'s `@for`, so — unlike a declarative `<div forToast>` — you have no node to put `animate.leave` on. Pass `animateLeave` (and, for symmetry, `animateEnter`) in the `show()` config instead, or set a viewport-wide default with `[animateLeave]` / `[animateEnter]`. The viewport binds them through Angular's native `animate.leave` / `animate.enter` on the rendered toast, so the toast stays mounted until its exit animation settles before it leaves the DOM:
145
+
146
+ ```ts
147
+ this.toasts.show({ title: 'Saved', variant: 'success', animateLeave: 'toast-out' });
148
+ ```
149
+
150
+ ```css
151
+ @keyframes toast-out {
152
+ to {
153
+ opacity: 0;
154
+ transform: translateX(100%);
155
+ }
156
+ }
157
+ [forToast].toast-out {
158
+ animation: toast-out 200ms ease forwards;
159
+ }
160
+ @media (prefers-reduced-motion: reduce) {
161
+ [forToast].toast-out {
162
+ animation-duration: 1ms;
163
+ }
164
+ }
165
+ ```
166
+
167
+ A viewport-level default applies to every toast that omits its own; a per-toast `animateLeave` always wins:
168
+
169
+ ```html
170
+ <for-toast-viewport animateLeave="toast-out" animateEnter="toast-in" />
171
+ ```
172
+
173
+ `animateLeave` is the load-bearing hook — a pure-CSS exit cannot defer the `@for` unmount, so it is the only way to play an exit animation on the programmatic path. `animateEnter` is optional: a plain `@keyframes` on `[forToast]` already plays on mount without it (this is why programmatic toasts have always animated _in_). Leaving `animateLeave` unset keeps the existing synchronous unmount on dismiss. `prefers-reduced-motion` is honored by your own CSS exactly as with the swipe / enter hooks — the directive adds no animation of its own.
174
+
175
+ The **declarative** path is unchanged — write `animate.leave` directly on your `<div forToast>` (see [Declarative usage](#declarative-usage)).
176
+
177
+ ### Custom rendering with a `template`
178
+
179
+ If the default title / description / action / close shape isn't enough, pass a `template`:
180
+
181
+ ```html
182
+ <ng-template #toastTpl let-toast let-data="data">
183
+ <span>{{ data.user.name }} liked your post</span>
184
+ <button type="button" (click)="goToPost(data.post); toast.dismiss()">View</button>
185
+ </ng-template>
186
+ ```
187
+
188
+ ```ts
189
+ this.toasts.show({ template: this.toastTpl, data: { user, post } });
190
+ ```
191
+
192
+ The template context is `{ $implicit: ForToastInstance, data: T }`. Use `toast.dismiss()` to close from inside the template.
193
+
194
+ **The helper directives work inside a custom `template`.** The viewport renders the template with the `[forToast]` injection context in scope, so `[forToastTitle]`, `[forToastDescription]`, `[forToastAction]`, and `[forToastClose]` keep their automatic `aria-labelledby` / `aria-describedby` / close-reason wiring — exactly as in the default shape. Just import the directives into the component that declares the `<ng-template>`:
195
+
196
+ ```ts
197
+ @Component({
198
+ imports: [ForToastViewport, ForToastTitle, ForToastDescription, ForToastAction, ForToastClose],
199
+ // …
200
+ })
201
+ ```
202
+
203
+ ```html
204
+ <ng-template #toastTpl let-toast let-data="data">
205
+ <div forToastTitle class="toast__title">{{ data.title }}</div>
206
+ <div forToastDescription class="toast__desc">{{ data.body }}</div>
207
+ <button forToastAction altText="Undo (Cmd+Z)" (click)="restore(data.item)">Undo</button>
208
+ <button forToastClose class="toast__close" aria-label="Dismiss">×</button>
209
+ </ng-template>
210
+ ```
211
+
212
+ `[forToastAction]` / `[forToastClose]` emit `(dismiss)` (reason `'action'` / `'manual'`) through the same context as the default shape — no need to call `toast.dismiss()` manually for those. (`toast.dismiss()` from `$implicit` is still available for arbitrary buttons that aren't action / close.) This combines with per-toast `class`: add a `class` for the root and your own classes on the helper elements.
213
+
214
+ ## Declarative usage
215
+
216
+ For a toast driven by component state (e.g. an offline banner):
217
+
218
+ ```html
219
+ @if (offline()) {
220
+ <div
221
+ forToast
222
+ variant="warning"
223
+ [duration]="0"
224
+ (dismiss)="offline.set(false)"
225
+ animate.leave="fade-out"
226
+ >
227
+ <div forToastTitle>Network unavailable</div>
228
+ <div forToastDescription>Reconnecting…</div>
229
+ </div>
230
+ }
231
+ ```
232
+
233
+ The directive doesn't manage its own visibility — `@if` does. The directive emits `(dismiss)` when the timer / Escape / action / close button want it gone; the consumer reacts by flipping the gate.
234
+
235
+ ## Swipe-to-dismiss
236
+
237
+ Optional, opt-in. Set `[swipeDirection]` on a declarative toast (or via `swipeDirection` in the programmatic config / on the viewport) to let the user drag the toast off-screen with a touch or mouse pointer. The gesture uses pointer events, so it works on every input device.
238
+
239
+ ```html
240
+ <div forToast swipeDirection="right" [swipeThreshold]="60" (dismiss)="dismiss()">…</div>
241
+ ```
242
+
243
+ `swipeDirection` accepts a single direction (`'left' | 'right' | 'up' | 'down'`) or an array of directions. The dominant axis of the user's drag picks which one wins; gestures perpendicular to every allowed direction are dropped. The dismiss commits when pointer-up happens past `swipeThreshold` pixels of pointer travel along the active direction (default `50`).
244
+
245
+ While the gesture is live the host carries:
246
+
247
+ | Attribute / variable | Values | Purpose |
248
+ | ----------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
249
+ | `data-swipe` | `"start" \| "move" \| "cancel" \| "end"` | Lifecycle marker — `"end"` means "about to fire `(dismiss)` with reason `swipe`". `"cancel"` is parked (with the released movement vars) so your CSS can spring the toast back, then cleared on the next `pointerdown`. |
250
+ | `data-swipe-direction` | `"left" \| "right" \| "up" \| "down"` | Direction the gesture armed in. |
251
+ | `--for-toast-swipe-movement-x` (px) | continuous | Horizontal pointer travel, clamped to the half-line of the active direction. |
252
+ | `--for-toast-swipe-movement-y` (px) | continuous | Vertical pointer travel, clamped to the half-line of the active direction. |
253
+
254
+ The directive does NOT animate anything — the consumer's CSS transitions / `animate.leave` drive the visual feedback:
255
+
256
+ ```css
257
+ [forToast] {
258
+ transition: transform 200ms ease-out;
259
+ transform: translate3d(
260
+ var(--for-toast-swipe-movement-x, 0px),
261
+ var(--for-toast-swipe-movement-y, 0px),
262
+ 0
263
+ );
264
+ }
265
+ [forToast][data-swipe='move'] {
266
+ transition: none;
267
+ }
268
+ [forToast][data-swipe='cancel'] {
269
+ transform: translate3d(0, 0, 0);
270
+ }
271
+ ```
272
+
273
+ Spring the toast back by resetting **`transform`** on `[data-swipe='cancel']` — not by zeroing the `--for-toast-swipe-movement-*` variables. The directive publishes those variables as **inline styles** on the host and holds them at the released delta through the cancel, so a stylesheet rule that tries to set them to `0` loses to the inline value and has no effect (the toast would stay stuck at the release position). Overriding `transform` sidesteps the inline variables entirely. After a cancel the host keeps `data-swipe="cancel"` and the parked movement vars so the transition above can run on its own timeline; the directive then clears that parked state — `data-swipe` removed, movement vars zeroed — on the next `pointerdown`, so a stale `cancel` never bleeds into the next gesture or lingers after a CSS-less consumer. A re-armed swipe overwrites it anyway.
274
+
275
+ Outputs:
276
+
277
+ - `(swipeStart)` — armed; emitted once with `{ direction, delta, originalEvent }`.
278
+ - `(swipeMove)` — every pointer move while active.
279
+ - `(swipeEnd)` — released past threshold (immediately followed by `(dismiss)` with reason `'swipe'`).
280
+ - `(swipeCancel)` — released before threshold, or `pointercancel`.
281
+
282
+ `closable=false` disables swipe entirely — a sticky / forced-action toast cannot be user-dismissed.
283
+
284
+ ## Auto-dismiss + pause-on-hover
285
+
286
+ - Timer starts on mount and fires `(dismiss)` with reason `'auto'` after `duration` ms.
287
+ - Hovering or focusing inside the toast pauses the timer; leaving / blurring resumes with the remaining time.
288
+ - The timer also pauses while `document.visibilityState !== 'visible'` (tab backgrounded, window hidden) and resumes when the page becomes visible again, so toasts don't silently expire while the user is not looking. The `visibilitychange` listener is shared across all live toasts (refcounted) — one document-level handler regardless of stack depth.
289
+ - A hover/focus/visibility pause captures the **remaining** time and resumes with it. A `ref.update()` that re-renders the toast while it is paused does not reset that captured time — resume always continues the countdown, never restarts it at the full duration.
290
+ - `duration: 0` keeps the toast sticky — only manual / action / programmatic close ends it.
291
+
292
+ > **`maxVisible` parks overflow, it does not expire it.** A toast pushed out of the visible window by `[maxVisible]` is unmounted, so its auto-dismiss timer is not running while it waits. When a newer toast is dismissed it re-enters the window and its `duration` countdown restarts from full (a fresh `[forToast]` mounts). If you need overflow toasts to clear on a deadline, dismiss them explicitly (`ForToastRef.dismiss()` / `dismissAll()` / the action / close button) rather than relying on the timer.
293
+
294
+ ## Styling
295
+
296
+ forty-cdk ships no styles. Add your own class to each piece — the `for*` selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected `data-*` attributes below.
297
+
298
+ Toast pieces (`[forToast]`, `[forToastTitle]`, `[forToastDescription]`, `[forToastAction]`, `[forToastClose]`) are rendered _inside_ the library's `<for-toast-viewport>` component on the programmatic path, so they cannot take a consumer class directly — style them with **global attribute selectors** (e.g. `[forToast][data-variant='error']`). The exception is per-toast `class` / `classList` in the `show()` config, which the viewport applies to the `[forToast]` root for you (see [Per-toast classes](#per-toast-classes)). Only `<for-toast-viewport>` itself lives in the consumer's own template, so it is the one element that can take an ordinary `class`. Declarative toasts (`<div forToast class="…">`) take consumer classes the native way.
299
+
300
+ ### Data attributes
301
+
302
+ | Piece | Attribute | Values |
303
+ | --------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
304
+ | `[forToast]` | `data-state` | `open` (always present while mounted; the consumer unmounts on close, so there is no `closed` state) |
305
+ | `[forToast]` | `data-variant` | `info` &#124; `success` &#124; `warning` &#124; `error` |
306
+ | `[forToast]` | `data-paused` | present / absent (the auto-dismiss timer is paused) |
307
+ | `[forToast]` | `data-swipe` | `start` &#124; `move` &#124; `cancel` &#124; `end` (absent until a swipe gesture begins) |
308
+ | `[forToast]` | `data-swipe-direction` | `left` &#124; `right` &#124; `up` &#124; `down` (absent until a swipe arms) |
309
+ | `[forToast]` | `data-front-stack-index` | `0`-based index in the visible stack (set by the viewport on the programmatic path; `0` is the front toast) |
310
+ | `[forToastViewport]` / `<for-toast-viewport>` | `data-region` | the viewport's region name (default `default`) |
311
+ | `[forToastViewport]` / `<for-toast-viewport>` | `data-toast-count` | number of toasts currently rendered |
312
+
313
+ ### CSS custom properties
314
+
315
+ Written on the `[forToast]` host while a swipe gesture is live, so the consumer can drive a transform-based animation entirely from CSS.
316
+
317
+ | Property | Meaning |
318
+ | ------------------------------ | ----------------------------------------------------------------------- |
319
+ | `--for-toast-swipe-movement-x` | Horizontal pointer travel in px, clamped to the active swipe direction. |
320
+ | `--for-toast-swipe-movement-y` | Vertical pointer travel in px, clamped to the active swipe direction. |
321
+
322
+ ```css
323
+ [forToast] {
324
+ transition: transform 200ms ease-out;
325
+ transform: translate3d(
326
+ var(--for-toast-swipe-movement-x, 0px),
327
+ var(--for-toast-swipe-movement-y, 0px),
328
+ 0
329
+ );
330
+ }
331
+ [forToast][data-swipe='move'] {
332
+ transition: none;
333
+ }
334
+ [forToast][data-swipe='cancel'] {
335
+ transform: translate3d(0, 0, 0);
336
+ }
337
+ [forToast][data-variant='error'] {
338
+ border-inline-start: 4px solid red;
339
+ }
340
+ ```
341
+
342
+ ## Keyboard
343
+
344
+ - Toast announcements never steal focus. The user keeps typing.
345
+ - The configured **hotkey** (default `F6`) anywhere in the document focuses the first visible toast.
346
+ - Inside a toast: **Tab** cycles between action / close buttons; **Escape** dismisses (when `closable`); **Shift+Tab** returns out.
347
+
348
+ ## Variants
349
+
350
+ | Variant | Role | aria-live | Use for |
351
+ | ------------------ | -------- | ----------- | ---------------------------------------- |
352
+ | `info` _(default)_ | `status` | `polite` | Neutral notifications. |
353
+ | `success` | `status` | `polite` | Confirmations of completed actions. |
354
+ | `warning` | `status` | `polite` | Non-blocking warnings. |
355
+ | `error` | `alert` | `assertive` | Failures that interrupt the user's task. |
356
+
357
+ `data-variant` is reflected on the host so consumers can paint per-variant icons / colors purely from CSS.
358
+
359
+ ## Global defaults
360
+
361
+ ```ts
362
+ import { provideForToastDefaults } from 'forty-cdk/toast';
363
+
364
+ bootstrapApplication(App, {
365
+ providers: [provideForToastDefaults({ duration: 4000, hotkey: 'F6', maxVisible: 5 })],
366
+ });
367
+ ```
368
+
369
+ Per-viewport overrides take precedence: `<for-toast-viewport [maxVisible]="3" hotkey="F8" />`.
370
+
371
+ ## Accessibility notes
372
+
373
+ - `aria-atomic="true"` on the toast means that when the host's own `aria-live` region announces, the screen reader reads the **whole** toast rather than only the changed node. It does **not** by itself guarantee a re-announcement on a `ref.update()` text change, and it is irrelevant on the silenced (`altText`) path where `aria-live` is `off`. Re-announcement on update is driven explicitly — see [Live updates and announcements](#live-updates-and-announcements) below.
374
+ - `aria-labelledby` and `aria-describedby` wire automatically from `[forToastTitle]` / `[forToastDescription]`. Multiple titles / descriptions concatenate ids.
375
+ - `role="alert"` (variant `error`) interrupts the screen reader queue; reserve it for genuinely interrupting messages.
376
+ - The viewport's `role="region"` with `aria-label` makes it discoverable in landmark navigation; the `F6` hotkey is the standard "jump to notifications" shortcut.
377
+ - Pause on hover / focus is mandated by [WCAG 2.1 SC 2.2.1](https://www.w3.org/WAI/WCAG21/Understanding/timing-adjustable.html) for time-limited content.
378
+ - Action buttons should set `[altText]` whenever the visible label (e.g. `"Undo"`) wouldn't tell a user how to recover the action after the toast disappears. When at least one `[forToastAction]` carries a non-empty `altText`, the toast silences its host `aria-live` and routes a synthesized announcement (`title. description. altText`) through the shared `LiveAnnouncer` — meeting [WCAG SC 2.2.1](https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html) for non-recoverable, time-limited actions.
379
+
380
+ ```html
381
+ <button forToastAction altText="Undo (Cmd+Z)" (click)="restore()">Undo</button>
382
+ ```
383
+
384
+ ### Live updates and announcements
385
+
386
+ A toast announces on two paths, picked automatically:
387
+
388
+ - **Host `aria-live` (default).** With no `altText` anywhere in the toast, the host carries `aria-live="polite"` (or `assertive` for `error`). The screen reader reads the toast when it mounts and, thanks to `aria-atomic`, re-reads the whole toast when the host region's text changes.
389
+ - **`LiveAnnouncer` (silenced path).** As soon as any `[forToastAction]` carries a non-empty `altText`, the host `aria-live` is set to `off` and the toast composes its message (`title. description. altText`) from the **rendered title / description / altText** and pushes it through the shared off-screen `LiveAnnouncer`.
390
+
391
+ Both paths are reactive. A late-bound `altText` (set after first render) and any `ref.update()` that changes the title, description, or `altText` re-announces — the composed message is tracked, and an unchanged message never re-fires. This is why the contract is "drive announcements explicitly", not "trust `aria-atomic`": `aria-atomic` does nothing on the silenced path, so the directive owns the re-announce.
392
+
393
+ ```ts
394
+ const ref = this.toasts.show({ title: 'Saving…', duration: 0 });
395
+ // Re-announced automatically when the text changes:
396
+ await api.save();
397
+ ref.update({ title: 'Saved', variant: 'success', duration: 3000 });
398
+ ```
@@ -0,0 +1,224 @@
1
+ # Toggle / ToggleGroup
2
+
3
+ Two related primitives in a single folder:
4
+
5
+ - **`[forToggle]`** — a standalone two-state button implementing the [WAI-ARIA Toggle Button pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/) (`<button aria-pressed>`).
6
+ - **`[forToggleGroup]` + `[forToggleGroupItem]`** — a [Toolbar](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) of toggle buttons with single / multiple selection, roving tabindex, and arrow-key navigation.
7
+
8
+ For exclusive selection where one option is always required, use `[forRadioGroup]` instead — Radio guarantees one-of-N, ToggleGroup in single mode lets the user clear the selection.
9
+
10
+ ## Standalone Toggle
11
+
12
+ ```ts
13
+ import { Component, signal } from '@angular/core';
14
+ import { ForToggle } from 'forty-cdk/toggle';
15
+
16
+ @Component({
17
+ selector: 'demo-toggle',
18
+ imports: [ForToggle],
19
+ template: ` <button forToggle [(checked)]="bold">B</button> `,
20
+ })
21
+ export class DemoToggle {
22
+ readonly bold = signal(false);
23
+ }
24
+ ```
25
+
26
+ ### Inputs
27
+
28
+ | API | Default | Description |
29
+ | ---------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | `checked` | `false` | Two-way bindable on/off state. Required by `FormCheckboxControl`; what `[formField]` binds. The host reflects it via `aria-pressed` and `data-state`. |
31
+ | `disabled` | `false` | When `true`, click is ignored; reflects `aria-disabled="true"` + `data-disabled`. Stays focusable (per APG) — no native `disabled`. |
32
+ | `readonly` | `false` | When `true`, click is ignored but the host stays focusable. Reflected as `aria-readonly`. |
33
+ | `required` | `false` | Reflected as `aria-required`. |
34
+ | `invalid` | `false` | Reflected as `aria-invalid` and `data-invalid`. |
35
+ | `pending` | `false` | Reflected as `aria-busy` and `data-pending`. |
36
+ | `dirty` | `false` | Reflected as `data-dirty`. |
37
+ | `name` | `''` | When non-empty, a hidden input is mounted for native form submission (`name=on` while checked). |
38
+ | `errors` | `[]` | Validation errors surfaced by Signal Forms. |
39
+ | `touched` | `false` | Two-way bindable. Set to `true` on blur. |
40
+
41
+ ## Signal Forms usage (single Toggle)
42
+
43
+ `ForToggle` implements `FormCheckboxControl`, so a single `aria-pressed` toggle auto-wires with `[formField]` from `@angular/forms/signals` — the natural home for bold / italic, mute, or favourite buttons. The schema's `disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `errors`, and `touched` flow into the matching inputs without consumer glue.
44
+
45
+ ```ts
46
+ import { Component, signal } from '@angular/core';
47
+ import { form, FormField, required } from '@angular/forms/signals';
48
+ import { ForToggle } from 'forty-cdk/toggle';
49
+
50
+ @Component({
51
+ selector: 'demo-bold',
52
+ imports: [ForToggle, FormField],
53
+ template: ` <button forToggle [formField]="prefs.bold" aria-label="Bold">B</button> `,
54
+ })
55
+ export class DemoBold {
56
+ readonly model = signal({ bold: false });
57
+ readonly prefs = form(this.model, (s) => required(s.bold));
58
+ }
59
+ ```
60
+
61
+ When `[name]` is set (typically through `[formField]`), the directive mounts an `<input type="hidden" value="on">` sibling while checked so the surrounding `<form>` picks it up during native submission.
62
+
63
+ For a set of related toggles, `ForToggleGroup` implements `FormValueControl<readonly string[]>` instead — its `value` is `readonly string[]`, and a single-mode group simply carries `[]` or `[selected]`.
64
+
65
+ ## ToggleGroup
66
+
67
+ ```ts
68
+ import { Component, signal } from '@angular/core';
69
+ import { ForToggleGroup, ForToggleGroupItem } from 'forty-cdk/toggle';
70
+
71
+ @Component({
72
+ selector: 'demo-alignment',
73
+ imports: [ForToggleGroup, ForToggleGroupItem],
74
+ template: `
75
+ <!-- single (default): one alignment at a time, click again to clear -->
76
+ <div forToggleGroup [(value)]="alignment">
77
+ <button forToggleGroupItem value="left">Left</button>
78
+ <button forToggleGroupItem value="center">Center</button>
79
+ <button forToggleGroupItem value="right">Right</button>
80
+ </div>
81
+
82
+ <!-- multiple: independent toggles -->
83
+ <div forToggleGroup [(value)]="format" multiple>
84
+ <button forToggleGroupItem value="bold">B</button>
85
+ <button forToggleGroupItem value="italic">I</button>
86
+ <button forToggleGroupItem value="underline">U</button>
87
+ </div>
88
+ `,
89
+ })
90
+ export class DemoAlignment {
91
+ readonly alignment = signal<readonly string[]>([]);
92
+ readonly format = signal<readonly string[]>([]);
93
+ }
94
+ ```
95
+
96
+ `value` is always `readonly string[]`. In single mode it carries 0 or 1 entries; this lets consumers flip `multiple` without re-typing their state.
97
+
98
+ ### Pieces
99
+
100
+ | Class | Selector | Role |
101
+ | -------------------- | ---------------------- | ------------------------------------------------------------------------- |
102
+ | `ForToggleGroup` | `[forToggleGroup]` | Root. Owns `value`, `multiple`, `disabled`, `orientation`, `dir`, `loop`. |
103
+ | `ForToggleGroupItem` | `[forToggleGroupItem]` | One toggle button. Pressed state derives from the group's `value`. |
104
+
105
+ ### Inputs (`ForToggleGroup`)
106
+
107
+ | API | Default | Description |
108
+ | ------------- | -------------- | --------------------------------------------------------------------------------------------------------- |
109
+ | `value` | `[]` | Two-way bindable. Selected values, in arbitrary order. Required by `FormValueControl<readonly string[]>`. |
110
+ | `multiple` | `false` | When `true`, items toggle independently. When `false`, single mode (clicking the pressed item clears). |
111
+ | `disabled` | `false` | Disables every item regardless of per-item state. |
112
+ | `readonly` | `false` | Click is ignored, items remain focusable. Reflected as `aria-readonly`. |
113
+ | `required` | `false` | Reflected as `aria-required`. |
114
+ | `invalid` | `false` | Reflected as `aria-invalid` and `data-invalid`. |
115
+ | `pending` | `false` | Reflected as `aria-busy` and `data-pending`. |
116
+ | `dirty` | `false` | Reflected as `data-dirty`. |
117
+ | `name` | `''` | When non-empty, hidden inputs are mounted for native form submission (one per selected value). |
118
+ | `errors` | `[]` | Validation errors surfaced by Signal Forms. |
119
+ | `touched` | `false` | Two-way bindable. Set on focusout outside the group. |
120
+ | `orientation` | `'horizontal'` | Layout direction for keyboard navigation. |
121
+ | `dir` | `'ltr'` | Reading direction. RTL swaps ArrowLeft / ArrowRight. |
122
+ | `loop` | `true` | When `true`, arrow nav wraps at the ends. The default is read from `provideForToggleDefaults`. |
123
+
124
+ ### Inputs (`ForToggleGroupItem`)
125
+
126
+ | API | Default | Description |
127
+ | ---------- | -------- | --------------------------------------------------------- |
128
+ | `value` | required | Identifier added to / removed from the group's `value`. |
129
+ | `disabled` | `false` | Per-item disabled, in addition to the group's `disabled`. |
130
+
131
+ ## Styling
132
+
133
+ forty-cdk ships no styles. Add your own class to each piece — the for\* selectors are the behavior API, not a styling contract (see [Styling forty-cdk](../../../../../docs/styling.md)). Key your CSS off the reflected data-\* attributes below.
134
+
135
+ ### Data attributes
136
+
137
+ | Piece | Attribute | Values |
138
+ | ---------------------- | ------------------ | -------------------------- |
139
+ | `[forToggle]` | `data-state` | `checked` \| `unchecked` |
140
+ | `[forToggle]` | `data-disabled` | present \| absent |
141
+ | `[forToggle]` | `data-readonly` | present \| absent |
142
+ | `[forToggleGroup]` | `data-orientation` | `horizontal` \| `vertical` |
143
+ | `[forToggleGroup]` | `data-disabled` | present \| absent |
144
+ | `[forToggleGroupItem]` | `data-state` | `checked` \| `unchecked` |
145
+ | `[forToggleGroupItem]` | `data-disabled` | present \| absent |
146
+ | `[forToggleGroupItem]` | `data-orientation` | `horizontal` \| `vertical` |
147
+
148
+ ```css
149
+ .toggle {
150
+ background: var(--surface);
151
+ }
152
+
153
+ .toggle[data-state='checked'] {
154
+ background: var(--accent);
155
+ }
156
+
157
+ .toggle[data-disabled] {
158
+ opacity: 0.5;
159
+ cursor: not-allowed;
160
+ }
161
+ ```
162
+
163
+ ## Keyboard
164
+
165
+ - **Enter / Space** on the focused item toggles it (native button behavior).
166
+ - **ArrowLeft / ArrowRight** moves focus in horizontal mode (or vertical, swapped via `orientation`). Disabled items are skipped.
167
+ - **ArrowUp / ArrowDown** moves focus in vertical mode.
168
+ - **Home / End** jump to the first / last enabled item.
169
+ - **Tab** enters and exits the group at the entry-point item. Before any interaction that is the first selected item, or the first enabled item when no value is selected; once you move focus with the arrows (or Home / End), the tab stop follows the last focused item, so Shift+Tab back into the group restores it.
170
+
171
+ Arrow keys move focus only — selection requires an explicit click or Space / Enter. There is no selection-on-focus, unlike `[forRadioGroup]`.
172
+
173
+ ## Behavior notes
174
+
175
+ - **`data-state`** uses the form-control vocabulary `"checked" | "unchecked"` (per `CLAUDE.md` cross-primitive convention), even though ARIA uses `aria-pressed` — the data attribute mirrors the logical "is this option active" state, not the ARIA term.
176
+ - **Single mode** lets the user reach the `[]` state by clicking the currently pressed item again. Use `[forRadioGroup]` if you need to enforce one-of-N.
177
+ - **Roving tabindex** follows focus: once any item is focused, that item becomes the tab stop so re-entry restores it. Before any focus, the entry point is computed from the group's value — with at least one selection the first selected item, otherwise the first enabled item in DOM order. The consumer never sets `tabindex` manually.
178
+
179
+ ## Signal Forms usage
180
+
181
+ `ForToggleGroup` implements `FormValueControl<readonly string[]>`, so it auto-wires with `[formField]` from `@angular/forms/signals`. The schema's `disabled`, `readonly`, `required`, `invalid`, `pending`, `dirty`, `errors`, `touched`, and `name` flow into the matching inputs without consumer glue.
182
+
183
+ ```ts
184
+ import { Component, signal } from '@angular/core';
185
+ import { form, required, requiredError, validate } from '@angular/forms/signals';
186
+ import { ForToggleGroup, ForToggleGroupItem } from 'forty-cdk/toggle';
187
+
188
+ @Component({
189
+ selector: 'demo-formats',
190
+ imports: [ForToggleGroup, ForToggleGroupItem /* , FormField from @angular/forms */],
191
+ template: `
192
+ <div forToggleGroup multiple [formField]="prefs.formats" aria-label="Formats">
193
+ <button forToggleGroupItem value="bold">B</button>
194
+ <button forToggleGroupItem value="italic">I</button>
195
+ <button forToggleGroupItem value="underline">U</button>
196
+ </div>
197
+ `,
198
+ })
199
+ export class DemoFormats {
200
+ readonly model = signal({ formats: [] as string[] });
201
+ readonly prefs = form(this.model, (s) => {
202
+ required(s.formats);
203
+ validate(s.formats, ({ value }) =>
204
+ value().length === 0 ? requiredError({ message: 'Pick at least one format' }) : undefined,
205
+ );
206
+ });
207
+ }
208
+ ```
209
+
210
+ > **Requiring a non-empty selection.** `ForToggleGroup`'s value is a `readonly string[]`, and Angular's
211
+ > `required()` treats only `''`, `false`, `null`, and `NaN` as empty — an empty array `[]` counts as
212
+ > _present_, so `required(s.formats)` reflects `aria-required="true"` but never makes the form invalid
213
+ > on its own. Enforce "at least one" with the explicit `validate(...)` length rule above, or with
214
+ > Angular's `minLength(s.formats, 1)` (which emits a `minLengthError` instead of a `requiredError`).
215
+ > The single-`boolean` `ForToggle` above is unaffected: `required(s.bold)` works because `false` is
216
+ > treated as empty.
217
+
218
+ When `[name]` is set (typically through `[formField]`), the directive mounts one `<input type="hidden">` sibling per selected value so the surrounding `<form>` picks the values up during native submission.
219
+
220
+ Choose between the two form-control shapes by the value you need: `ForToggle` (`FormCheckboxControl`, a single `boolean`) for one independent on/off button, `ForToggleGroup` (`FormValueControl<readonly string[]>`) for a set of related toggles whose selection is a list.
221
+
222
+ ## Wrapping in a design system
223
+
224
+ Both supported wrapper patterns — `hostDirectives` with the exported `FOR_TOGGLE_HOST_DIRECTIVE_INPUTS` / `FOR_TOGGLE_HOST_DIRECTIVE_OUTPUTS` and `FOR_TOGGLE_GROUP_HOST_DIRECTIVE_INPUTS` / `FOR_TOGGLE_GROUP_HOST_DIRECTIVE_OUTPUTS` name tuples, and subclassing — are documented in [Wrapping form primitives](../../../../../docs/wrapping-form-primitives.md).