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,388 @@
1
+ # Dialog
2
+
3
+ > New to overlays in forty-cdk? [Your first overlay](../../../../../docs/your-first-overlay.md) walks a Popover from empty markup to styled-and-animated and explains the `@if` / open-state model and the portal → global CSS rule.
4
+
5
+ Headless implementation of the [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) with focus trap, body scroll lock, Escape-to-close, portal rendering, and a programmatic `ForDialogManager.open()` API.
6
+
7
+ ## Two flows, one engine
8
+
9
+ The same focus trap, scroll lock, portal, and dismissable-layer behaviors run under both APIs. Pick the one that fits the call site.
10
+
11
+ ### Declarative — `[forDialog]`
12
+
13
+ The dialog is an overlay: **mount equals open**. The consumer's signal drives `@if`, and the directive emits `(dismiss)` when it wants to be unmounted (Escape, backdrop, outside-pointer, outside-focus, close button). There is no `[(open)]` two-way binding on `[forDialog]` — the directive never opens itself, only requests close.
14
+
15
+ For the open side, drop `[forDialogTrigger]` on a `<button>`. It two-way binds `[(open)]` to the same signal that gates the surrounding `@if`, and wires `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`, and `data-state` automatically.
16
+
17
+ ```ts
18
+ import { Component, signal } from '@angular/core';
19
+ import {
20
+ ForDialog,
21
+ ForDialogBackdrop,
22
+ ForDialogClose,
23
+ ForDialogDescription,
24
+ ForDialogTitle,
25
+ ForDialogTrigger,
26
+ } from 'forty-cdk/dialog';
27
+
28
+ @Component({
29
+ selector: 'demo-confirm',
30
+ imports: [
31
+ ForDialog,
32
+ ForDialogTrigger,
33
+ ForDialogTitle,
34
+ ForDialogDescription,
35
+ ForDialogClose,
36
+ ForDialogBackdrop,
37
+ ],
38
+ template: `
39
+ <button forDialogTrigger [(open)]="open" controls="confirm-delete">Delete account</button>
40
+
41
+ @if (open()) {
42
+ <div forDialog id="confirm-delete" (dismiss)="open.set(false)" animate.leave="fade-out">
43
+ <div forDialogBackdrop class="my-backdrop" animate.leave="fade-out"></div>
44
+ <h2 forDialogTitle>Delete account?</h2>
45
+ <p forDialogDescription>This permanently removes your data.</p>
46
+ <button forDialogClose>Cancel</button>
47
+ <button (click)="confirm()">Delete</button>
48
+ </div>
49
+ }
50
+ `,
51
+ })
52
+ export class DemoConfirm {
53
+ readonly open = signal(false);
54
+ confirm() {
55
+ /* ... */ this.open.set(false);
56
+ }
57
+ }
58
+ ```
59
+
60
+ Wrapping with `@if` is what makes Angular's native `animate.enter` / `animate.leave` work — they fire on real mount / unmount, not on attribute toggling.
61
+
62
+ ### The `(dismiss)` contract — consumer owns unmount
63
+
64
+ `(dismiss)` reports the dialog's **intent** to be unmounted; it does not flip the consumer's signal. The consumer must call `open.set(false)` (or equivalent) inside the handler. If the handler is omitted or does not update the signal, Escape, backdrop-click, and outside-pointer-down all emit `(dismiss)` but the dialog stays mounted.
65
+
66
+ ```html
67
+ <!-- correct: (dismiss) drives the @if gate -->
68
+ <div forDialog id="my-dialog" (dismiss)="open.set(false)">…</div>
69
+
70
+ <!-- broken: `(dismiss)` is missing — Escape fires but the dialog never unmounts -->
71
+ <div forDialog id="my-dialog">…</div>
72
+ ```
73
+
74
+ This is different from trigger-anchored overlays (Popover, DropdownMenu, etc.) where the wrapper directive owns `[(open)]` and round-trips it automatically on close. Dialog is **flat**: there is no wrapper, so the consumer's `@if` is the sole lifecycle gate.
75
+
76
+ The payload is a `ForDialogCloseReason` string (`'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`) — use it if you need to branch on why the dialog closed, for example to show a "save changes?" prompt before dismissing. Emitting `(dismiss)` without acting on it is always safe: you can call `preventDefault()` on the preceding dismiss outputs (`(escapeKeyDown)`, `(pointerDownOutside)`, `(focusOutside)`, `(interactOutside)`) to suppress the `(dismiss)` entirely.
77
+
78
+ > **Declarative vs. imperative naming asymmetry.** The declarative output is `(dismiss)`, but the imperative handle method stays `ForDialogRef.close()`, the `[forDialogClose]` directive selector is unchanged, and the `ForDialogCloseReason` type keeps its name. This is intentional: the output rename removes the native-event collision (see [#814](https://github.com/tutkli/forty-cdk/issues/814)) while the imperative surface follows the convention established before that rename.
79
+
80
+ ### Trigger / surface id wiring
81
+
82
+ The trigger (`[forDialogTrigger]`) and the dialog surface (`[forDialog]`) are **separate, unrelated elements**. They wire to each other via a shared id that the consumer keeps in sync:
83
+
84
+ ```html
85
+ <!-- trigger: controls="<id>" tells it which dialog it opens -->
86
+ <button forDialogTrigger [(open)]="open" controls="my-dialog">Open</button>
87
+
88
+ <!-- surface: id="<id>" must match controls above -->
89
+ @if (open()) {
90
+ <div forDialog id="my-dialog" (dismiss)="open.set(false)">…</div>
91
+ }
92
+ ```
93
+
94
+ `[forDialogTrigger]` always reflects `aria-haspopup="dialog"` and `aria-expanded` (`"true"` / `"false"`, from the trigger's own `open` state) — these do not depend on `controls`. The `controls` value is what gets reflected as `aria-controls="my-dialog"`, and only while the dialog is open; omit `controls` and the trigger never gets an `aria-controls`, silently breaking assistive technology that announces "opens dialog X".
95
+
96
+ > **Popover is different.** `[forPopover]` wraps both the trigger and content in a single parent directive, so ids are auto-generated and kept in sync internally. Dialog is flat — trigger and surface can live anywhere in the template — so the wiring is manual.
97
+
98
+ ### Programmatic — `ForDialogManager.open()`
99
+
100
+ ```ts
101
+ import { Component, inject } from '@angular/core';
102
+ import { ForDialogManager, ForDialogRef, injectDialogData } from 'forty-cdk/dialog';
103
+
104
+ @Component({
105
+ template: `
106
+ <p>{{ data?.message }}</p>
107
+ <button (click)="ref.close('cancel')">Cancel</button>
108
+ <button (click)="ref.close('confirm')">Confirm</button>
109
+ `,
110
+ })
111
+ class ConfirmDialog {
112
+ readonly data = injectDialogData<{ message: string }>();
113
+ readonly ref = inject(ForDialogRef) as ForDialogRef<'confirm' | 'cancel'>;
114
+ }
115
+
116
+ @Component({
117
+ selector: 'demo-host',
118
+ template: `<button (click)="askToDelete()">Delete</button>`,
119
+ })
120
+ export class DemoHost {
121
+ readonly dialogs = inject(ForDialogManager);
122
+
123
+ async askToDelete() {
124
+ const ref = this.dialogs.open<ConfirmDialog, 'confirm' | 'cancel', { message: string }>(
125
+ ConfirmDialog,
126
+ { data: { message: 'Are you sure?' } },
127
+ );
128
+ const result = await ref.closed; // 'confirm' | 'cancel' | undefined
129
+ if (result === 'confirm') {
130
+ /* ... */
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ `injectDialogData<T>()` is typed `T | null`: the manager provides `null` when `open()` is called without `data`, so guard (`data?.message`) before dereferencing the payload.
137
+
138
+ **Styling the programmatic overlay root.** Declaratively you write the surface yourself (`<div forDialog class="my-dialog">`), so the class lands on the same element that carries `data-state` / `role`. The manager creates that host for you and it is class-less, so pass `class` / `classList` to style it.
139
+
140
+ ```ts
141
+ this.dialogs.open(ConfirmDialog, { data, alert: true, class: 'my-dialog my-dialog--pop' });
142
+ ```
143
+
144
+ The tokens go on the real `[forDialog]` host alongside `data-state` / `role` / `aria-modal`, merged and de-duped, never clobbering those attributes.
145
+
146
+ **Enter / exit animations.** A programmatic dialog is portaled to `document.body` and torn down imperatively, so the consumer can't attach `animate.leave` to the host the way a declarative `@if` block can. Pass `animateEnter` / `animateLeave` (CSS class names) instead: the manager applies `animateEnter` on mount (via `animate.enter`) and, on `close()`, keeps the host mounted with `animateLeave` until its CSS animations / transitions finish before tearing down. `close()` still resolves its promise and flips `isClosed()` immediately — only the visual teardown waits. With no class (or under `prefers-reduced-motion`, if your CSS disables the animation) close is immediate.
147
+
148
+ ```ts
149
+ this.dialogs.open(ConfirmDialog, {
150
+ data,
151
+ class: 'my-dialog',
152
+ animateEnter: 'dialog-in',
153
+ animateLeave: 'dialog-out',
154
+ });
155
+ ```
156
+
157
+ ```css
158
+ .my-dialog {
159
+ opacity: 1;
160
+ transition: opacity 150ms ease-out;
161
+ }
162
+ .dialog-in {
163
+ animation: dialog-fade-in 150ms ease-out;
164
+ }
165
+ .my-dialog.dialog-out {
166
+ opacity: 0;
167
+ }
168
+ @keyframes dialog-fade-in {
169
+ from {
170
+ opacity: 0;
171
+ }
172
+ }
173
+ ```
174
+
175
+ Set them once for a scope with `provideForDialogDefaults({ animateEnter, animateLeave })`; a per-`open()` value always wins over the scope default.
176
+
177
+ ## Pieces (declarative)
178
+
179
+ | Class | Selector | Role |
180
+ | ---------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
181
+ | `ForDialog` | `[forDialog]` | The dialog box. Owns `dismissible`, `modal`, `alert`, focus, scroll lock. |
182
+ | `ForDialogTrigger` | `[forDialogTrigger]` | Optional. Button that toggles `[(open)]` and reflects `aria-haspopup`/`aria-expanded`/`aria-controls`/`data-state`. |
183
+ | `ForDialogTitle` | `[forDialogTitle]` | Generates an id and registers it as `aria-labelledby`. |
184
+ | `ForDialogDescription` | `[forDialogDescription]` | Same, for `aria-describedby`. |
185
+ | `ForDialogClose` | `[forDialogClose]` | Button that requests close with reason `'closeButton'`. Accepts `[closeWith]` for programmatic mode. |
186
+ | `ForDialogBackdrop` | `[forDialogBackdrop]` | Optional overlay portaled alongside the surface (to `container`, or `document.body` by default). Direct click requests close with reason `'backdrop'` when `dismissible`. |
187
+
188
+ ## Inputs (`ForDialog`)
189
+
190
+ | API | Default | Description |
191
+ | -------------- | --------- | --------------------------------------------------------------------------------------------------------------------- |
192
+ | `dismissible` | `true` | When `false`, Escape, backdrop, outside-pointer, and outside-focus do not request close. The close button still does. |
193
+ | `modal` | `true` | When `false`, no `aria-modal`, no scroll lock, no focus trap. |
194
+ | `alert` | `false` | Switches role to `alertdialog`. |
195
+ | `returnFocus` | `true` | Focus returns to the previously focused element on close. |
196
+ | `initialFocus` | `'first'` | `'first'` (first focusable inside) or `'container'` (the dialog host). |
197
+ | `ariaLabel` | `null` | Manual `aria-label` if no `[forDialogTitle]` is rendered. |
198
+
199
+ ## Outputs (`ForDialog`)
200
+
201
+ `(dismiss)` is the main signal — wire it to flip the `@if` gate. The four dismiss outputs are vetoable: each receives a `VetoableNativeEvent<E>` carrying the underlying DOM event. Call `preventDefault()` on the emitted veto to suppress the directive's default action; the original DOM event is on `.event`.
202
+
203
+ | Output | Payload | Fires on |
204
+ | -------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
205
+ | `close` | `ForDialogCloseReason` | Dialog wants to be unmounted. Reasons: `'escape'`, `'backdrop'`, `'pointerDownOutside'`, `'focusOutside'`, `'closeButton'`, `'programmatic'`. |
206
+ | `escapeKeyDown` | `VetoableNativeEvent<KeyboardEvent>` | Escape while this dialog is the topmost dismissable layer. |
207
+ | `pointerDownOutside` | `VetoableNativeEvent<PointerEvent>` | Pointer-down outside the dialog. |
208
+ | `focusOutside` | `VetoableNativeEvent<FocusEvent>` | Focus moves outside the dialog. |
209
+ | `interactOutside` | `VetoableNativeEvent<PointerEvent \| FocusEvent>` | Composite: fires alongside both of the above (and shares their veto state). |
210
+
211
+ ### Per-channel dismissal (Escape-only dialogs)
212
+
213
+ `dismissible` is **not** all-or-nothing. The four dismiss channels — Escape, pointer-down-outside, focus-outside, and the composite outside-interaction — are independently vetoable, so you can keep some live and suppress others. The canonical case is a **floater** (an update banner, a devtools panel) that should close on Escape but stay put when the user clicks elsewhere. Floaters are usually non-modal (`[modal]="false"`), so the rest of the page stays interactive.
214
+
215
+ **Declarative** — veto the outside channel, leave Escape alone:
216
+
217
+ ```html
218
+ @if (open()) {
219
+ <div
220
+ forDialog
221
+ [modal]="false"
222
+ (interactOutside)="$event.preventDefault()"
223
+ (dismiss)="open.set(false)"
224
+ >
225
+ …
226
+ </div>
227
+ }
228
+ ```
229
+
230
+ `(interactOutside)` fires for both pointer-down-outside and focus-outside and shares their veto, so one handler covers every outside interaction. Escape keeps closing because its channel was never vetoed — to suppress Escape instead, veto `(escapeKeyDown)`.
231
+
232
+ **Programmatic** — the same four channels are callbacks on the open config, mirroring the `autoFocusOn*` shape:
233
+
234
+ ```ts
235
+ this.dialogs.open(FloaterComponent, {
236
+ modal: false,
237
+ // dismissible: true is the default — Escape stays live.
238
+ interactOutside: (event) => event.preventDefault(), // ignore outside interaction
239
+ // escapeKeyDown / pointerDownOutside / focusOutside are available too.
240
+ });
241
+ ```
242
+
243
+ Keep `dismissible: true` (the default) so Escape still closes, and veto only the channels you want to keep open. `event.event` carries the originating DOM event for inspection.
244
+
245
+ ### Inputs — focus callbacks
246
+
247
+ The auto-focus pair is bound as **function references** (input callbacks), not as event listeners. Each callback receives a `VetoableEvent` whose `preventDefault()` suppresses the directive's default focus action. This shape mirrors `ForDialogManager`'s `config.autoFocusOn*` callbacks and guarantees the `autoFocusOnClose` callback fires reliably on every close path — including a direct `open.set(false)` that bypasses the `(dismiss)` output. See [CLAUDE.md › Auto-focus hook shape](../../../../../CLAUDE.md#auto-focus-hook-shape) for why Dialog uses callback-shape inputs while trigger-anchored overlays (Popover, DropdownMenu, ContextMenu, Menu sub, Select) use output-shape.
248
+
249
+ | Input | Payload | Fires on |
250
+ | ------------------ | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
251
+ | `autoFocusOnOpen` | `(event: VetoableEvent) => void` | Just before focus moves into the dialog on mount. Call `event.preventDefault()` to skip the imperative initial focus. |
252
+ | `autoFocusOnClose` | `(event: VetoableEvent) => void` | Just before focus returns to the trigger on unmount. Fires on every close path regardless of mode; in non-modal mode the directive doesn't move focus, so the veto is informational. Call `event.preventDefault()` to skip the modal return-focus. |
253
+
254
+ ### Open without stealing focus
255
+
256
+ ```html
257
+ <input #q type="search" placeholder="Search…" />
258
+
259
+ @if (open()) {
260
+ <div forDialog (dismiss)="open.set(false)" [autoFocusOnOpen]="keepSearchFocused">
261
+ <h2 forDialogTitle>Results</h2>
262
+ …
263
+ </div>
264
+ }
265
+ ```
266
+
267
+ ```ts
268
+ readonly keepSearchFocused = (event: VetoableEvent): void => {
269
+ event.preventDefault();
270
+ this.q().nativeElement.focus();
271
+ };
272
+ ```
273
+
274
+ The dialog still installs the focus trap (so Tab cycles inside once focus enters), but the imperative initial focus move is suppressed and the search input keeps focus.
275
+
276
+ ## Programmatic API
277
+
278
+ | Symbol | Description |
279
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
280
+ | `ForDialogManager` | Injectable. `open(component, config?)` returns a `ForDialogRef<R>`. |
281
+ | `ForDialogRef<R>` | `close(result?)`, `closed: Promise<R \| undefined>`, `result: Signal<R \| undefined>`, `isClosed: Signal<boolean>`. |
282
+ | `FOR_DIALOG_DATA` | Token for the `data` payload. Inject in the opened component. |
283
+ | `injectDialogData<T>()` | Typed accessor for `FOR_DIALOG_DATA`. Returns `T \| null` — `null` when `open()` got no `data`. |
284
+
285
+ ### `ForDialogOpenConfig`
286
+
287
+ | Field | Default | Description |
288
+ | -------------------- | --------- | -------------------------------------------------------------------------------------------------------- |
289
+ | `data` | — | Payload available as `injectDialogData<T>()`. |
290
+ | `dismissible` | `true` | Escape closes when `true`. |
291
+ | `modal` | `true` | Sets `aria-modal`, locks body scroll, traps focus. |
292
+ | `alert` | `false` | Use `role="alertdialog"` instead of `"dialog"`. |
293
+ | `returnFocus` | `true` | Focus returns to the previously focused element on close. |
294
+ | `initialFocus` | `'first'` | `'first'` finds first focusable; `'container'` focuses the host. |
295
+ | `ariaLabel` | — | Manual accessible name when no title element is rendered. |
296
+ | `animateEnter` | — | CSS class applied on mount (via `animate.enter`) to play an enter animation. |
297
+ | `animateLeave` | — | CSS class applied on close; the host stays mounted until its animation finishes, then tears down. |
298
+ | `class` | — | CSS class(es) applied to the overlay root (the `[forDialog]` host). Single or space-separated string. |
299
+ | `classList` | — | CSS class(es) applied to the overlay root, as an array or space-separated string. Merged with `class`. |
300
+ | `providers` | `[]` | Extra providers for the opened component's injector. |
301
+ | `autoFocusOnOpen` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the imperative initial focus move. |
302
+ | `autoFocusOnClose` | — | Callback. Receives a `VetoableEvent`; `event.preventDefault()` skips the return-focus on close. |
303
+ | `escapeKeyDown` | — | Callback. `VetoableNativeEvent<KeyboardEvent>`; `preventDefault()` suppresses the Escape close. |
304
+ | `pointerDownOutside` | — | Callback. `VetoableNativeEvent<PointerEvent>`; `preventDefault()` suppresses the outside-pointer close. |
305
+ | `focusOutside` | — | Callback. `VetoableNativeEvent<FocusEvent>`; `preventDefault()` suppresses the outside-focus close. |
306
+ | `interactOutside` | — | Callback. Composite `VetoableNativeEvent<PointerEvent \| FocusEvent>`; shares the veto of the two above. |
307
+
308
+ The four dismiss callbacks mirror the declarative `(escapeKeyDown)` / `(pointerDownOutside)` / `(focusOutside)` / `(interactOutside)` outputs exactly — same events, same veto semantics. See [Per-channel dismissal](#per-channel-dismissal-escape-only-dialogs).
309
+
310
+ ## Styling
311
+
312
+ 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.
313
+
314
+ ### Data attributes
315
+
316
+ | Piece | Attribute | Values |
317
+ | --------------------- | -------------------------- | ---------------------------------------------------------------------------------------- |
318
+ | `[forDialog]` | `data-state` | `open` (always — the host is only mounted while open, so it is never `closed`) |
319
+ | `[forDialogTrigger]` | `data-state` | `open` \| `closed` |
320
+ | `[forDialogTrigger]` | `data-disabled` | present / absent |
321
+ | `[forDialogBackdrop]` | `data-state` | `open` (always — mounted alongside the dialog) |
322
+ | `[forDialogBackdrop]` | `data-for-dialog-backdrop` | present (stable marker; portaled alongside the dialog, so use it to select the backdrop) |
323
+ | `[forDialogClose]` | `data-state` | `open` (always — mounted alongside the dialog) |
324
+
325
+ `[forDialog]`, `[forDialogBackdrop]`, and `[forDialogClose]` carry a static `data-state="open"`: because mount equals open (the host only exists inside `@if (open())`), the element is present iff the dialog is open, so the attribute can never be `closed`. Exit styling is the consumer's `animate.leave`, not a `[data-state="closed"]` selector. Only `[forDialogTrigger]`, which stays mounted, toggles `open` / `closed`.
326
+
327
+ > **This dialog portals to `document.body`.** CSS scoped to ancestors of `[forDialog]` (or `[forDialogBackdrop]`) will not apply once the surface is moved to the body. Style it with **global CSS** or a class. Declaratively you write the surface yourself, so add the class directly (`<div forDialog class="my-dialog">`); for programmatically opened instances pass `class` / `classList` on the `ForDialogManager.open()` config — they land on the same `[forDialog]` host that carries `data-state` / `role` / `aria-modal`, merged and never clobbering them.
328
+
329
+ ```css
330
+ .my-dialog {
331
+ position: fixed;
332
+ inset: 0;
333
+ margin: auto;
334
+ }
335
+
336
+ .my-backdrop[data-for-dialog-backdrop] {
337
+ position: fixed;
338
+ inset: 0;
339
+ background: rgb(0 0 0 / 0.5);
340
+ }
341
+
342
+ .my-trigger[data-state='open'] {
343
+ background: var(--accent);
344
+ }
345
+ ```
346
+
347
+ ## Keyboard
348
+
349
+ - **Escape** requests close (reason `'escape'`) when `dismissible`.
350
+ - **Tab / Shift+Tab** cycles focus inside the dialog (focus trap, only when `modal`).
351
+ - **Click** on `[forDialogBackdrop]` requests close (reason `'backdrop'`) when `dismissible`.
352
+
353
+ ## Behavior notes
354
+
355
+ - **Mount equals open**. The directive does not manage `[hidden]` or any visibility attribute. The consumer's `@if (open())` controls presence, and `animate.enter` / `animate.leave` handle the visual transition.
356
+ - **Portal**: the dialog box is moved to `document.body` on first render (or to `container` when set). The backdrop portals alongside the dialog (to the same `container`, `document.body` by default). CSS scoped to ancestors won't apply — use global styles or classes.
357
+ - **Body scroll lock** is refcounted: stacking dialogs (or a dialog + a future overlay using the same lock) only restore on the last unlock.
358
+ - **Focus trap** scopes Tab inside the dialog box while `modal`. It does NOT itself mark the rest of the page `inert` — that's the inert-siblings utility's job (next bullet).
359
+ - **Inert siblings**. When `modal`, every direct child of `document.body` other than the dialog box (and its backdrop) gets `inert` and `aria-hidden="true"` while open, and is restored on close. This is what `aria-modal="true"` alone is missing — Safari + VoiceOver and several other AT pairings still announce siblings of an aria-modal node otherwise. Stacking is order-safe: when a second modal opens on top, the first becomes inert; closing the top dialog re-activates the underlying one.
360
+ - **Vetoable dismissals**. Each of `(escapeKeyDown)`, `(pointerDownOutside)`, `(focusOutside)`, `(interactOutside)` fires before the corresponding `(dismiss)`. Call `preventDefault()` on the event to keep the dialog open (e.g. to ask "are you sure?" first).
361
+ - **The close button** (`[forDialogClose]`) always requests close, regardless of `dismissible`. Reason emitted is `'closeButton'`.
362
+ - **Both flows share the same engine** — the focus trap, scroll lock, dismissable layer, and portal in `ForDialogManager.open()` use the same `_internal/` utilities as the directive. Behavior is identical.
363
+
364
+ ## Scoped / contained dialog (`container`)
365
+
366
+ Pass `[container]` to portal the dialog surface into a specific element instead of `document.body`. Pair it with `[modal]="false"` for a dialog scoped to a region of the page.
367
+
368
+ ```html
369
+ <section #panel style="position: relative; height: 300px; overflow: hidden;">
370
+ @if (open()) {
371
+ <div forDialog [modal]="false" [container]="panel" (dismiss)="open.set(false)">
372
+ <h2 forDialogTitle>Details</h2>
373
+ <button forDialogClose>Close</button>
374
+ </div>
375
+ }
376
+ </section>
377
+ ```
378
+
379
+ **CSS contract.** The container must be positioned (`position: relative`); the dialog surface must use `position: absolute` (not `fixed`) so it is bounded to the container's box. `[forDialogBackdrop]` portals to the same container — use `position: absolute` on the backdrop too so it fills the container rather than the viewport.
380
+
381
+ **`[container]` + `[modal]="true"` — region-isolating modal.** When both are set, the dialog isolates **within the container**: focus trap stays scoped to the dialog surface; inert siblings are applied to the container's other children only (body-level siblings outside the container stay interactive); and scroll lock targets the container's own `overflow`, not `<body>`. Programmatically: `ForDialogManager.open(Cmp, { modal: true, container: panelEl })`.
382
+
383
+ ## Accessibility notes
384
+
385
+ - Always provide an accessible name: render a `[forDialogTitle]` (sets `aria-labelledby`) or pass `ariaLabel`.
386
+ - `[forDialogDescription]` is optional — use it for non-title supporting copy (the question of a confirm, the rationale of an alert).
387
+ - `alert: true` interrupts assistive tech aggressively — only for genuine alerts (lost connection, unsaved changes warning), not for general confirms.
388
+ - Don't put interactive overlays (popovers, menus) outside the focus trap while a modal dialog is open — they won't be reachable. For a non-modal floating surface anchored to a trigger, use `[forPopover]` instead.
@@ -0,0 +1,114 @@
1
+ # Disclosure
2
+
3
+ Headless implementation of the [WAI-ARIA Disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/).
4
+ A button toggles the visibility of a content region, wired with `aria-expanded` and `aria-controls`.
5
+
6
+ ## Pieces
7
+
8
+ | Class | Selector | Role |
9
+ | ---------------------- | ------------------------ | ---------------------------------------------------------------------- |
10
+ | `ForDisclosure` | `[forDisclosure]` | Root. Holds `open` / `disabled` state and provides the shared context. |
11
+ | `ForDisclosureTrigger` | `[forDisclosureTrigger]` | Button that toggles the state. |
12
+ | `ForDisclosureContent` | `[forDisclosureContent]` | Panel revealed when open. |
13
+
14
+ ## Inputs / outputs
15
+
16
+ ### `ForDisclosure`
17
+
18
+ | API | Type | Description |
19
+ | ---------- | ---------------- | --------------------------------------------------------------------------------- |
20
+ | `open` | `model<boolean>` | Two-way bindable. Defaults to `false`. |
21
+ | `disabled` | `input<boolean>` | When true, click on the trigger is ignored. Reflects `data-disabled` on the host. |
22
+
23
+ The host element gets `data-state="open" \| "closed"` for CSS hooks.
24
+
25
+ ### `ForDisclosureTrigger`
26
+
27
+ | API | Type | Description |
28
+ | ---------- | ---------------- | ------------------------------------------------------------------ |
29
+ | `disabled` | `input<boolean>` | Disables this trigger only — merged OR with the root's `disabled`. |
30
+
31
+ Reflects on its host: `id`, `aria-expanded`, `aria-controls`, `disabled`, `data-state`. Toggles the state on click. The disabled reflection (`disabled`, `aria-disabled`, `data-disabled`) and the click guard follow the effective state — the trigger's own `disabled` OR the root's.
32
+
33
+ `aria-controls` is emitted only while open — mirroring the overlay triggers' open-only gating — so the reference never dangles at an unmounted panel under the recommended `@if (open())` mount pattern.
34
+
35
+ Use a native `<button type="button">` so Enter / Space activation come for free. Other elements lose keyboard accessibility — that is on you.
36
+
37
+ ### `ForDisclosureContent`
38
+
39
+ Reflects on its host: `id`, `data-state`, `data-disabled`, `aria-hidden` (when closed), `inert` (when closed).
40
+
41
+ The directive does **not** apply `[hidden]` or otherwise control DOM presence. Two patterns work:
42
+
43
+ - **Mount/unmount with `@if (open())`** — the panel is absent from the DOM while closed; idiomatic for `animate.enter` / `animate.leave`.
44
+ - **Leave it mounted** — preserve scroll/input state or run CSS-only transitions off `data-state`. While closed, the directive sets `aria-hidden="true"` and `inert` on the host so the panel is removed from the accessibility tree and focus order. Add `display: none` (or your own collapse animation) keyed on `[data-state="closed"]` to also hide it visually.
45
+
46
+ If the panel is a semantic region, add `role="region"` and `aria-labelledby="..."` pointing to the trigger.
47
+
48
+ ## Example
49
+
50
+ ```ts
51
+ import { Component, signal } from '@angular/core';
52
+ import { ForDisclosure, ForDisclosureContent, ForDisclosureTrigger } from 'forty-cdk/disclosure';
53
+
54
+ @Component({
55
+ selector: 'demo-faq',
56
+ imports: [ForDisclosure, ForDisclosureTrigger, ForDisclosureContent],
57
+ template: `
58
+ <div forDisclosure class="disclosure" [(open)]="isOpen">
59
+ <button type="button" forDisclosureTrigger class="disclosure-trigger">
60
+ {{ isOpen() ? 'Hide' : 'Show' }} details
61
+ </button>
62
+ @if (isOpen()) {
63
+ <div forDisclosureContent class="disclosure-content">
64
+ <p>Hidden content goes here.</p>
65
+ </div>
66
+ }
67
+ </div>
68
+ `,
69
+ })
70
+ export class DemoFaq {
71
+ readonly isOpen = signal(false);
72
+ }
73
+ ```
74
+
75
+ The library ships no styles. Hide animations / transitions can be driven off `data-state` on the trigger and content:
76
+
77
+ ```css
78
+ .disclosure-content[data-state='closed'] {
79
+ /* … */
80
+ }
81
+ .disclosure-content[data-state='open'] {
82
+ /* … */
83
+ }
84
+ ```
85
+
86
+ ## Styling
87
+
88
+ 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.
89
+
90
+ ### Data attributes
91
+
92
+ | Piece | Attribute | Values |
93
+ | ------------------------ | --------------- | ------------------ |
94
+ | `[forDisclosure]` | `data-state` | `open` \| `closed` |
95
+ | `[forDisclosure]` | `data-disabled` | present \| absent |
96
+ | `[forDisclosureTrigger]` | `data-state` | `open` \| `closed` |
97
+ | `[forDisclosureTrigger]` | `data-disabled` | present \| absent |
98
+ | `[forDisclosureContent]` | `data-state` | `open` \| `closed` |
99
+ | `[forDisclosureContent]` | `data-disabled` | present \| absent |
100
+
101
+ ```css
102
+ .disclosure-trigger .chevron {
103
+ transition: transform 150ms ease;
104
+ }
105
+ .disclosure-trigger[data-state='open'] .chevron {
106
+ transform: rotate(180deg);
107
+ }
108
+ ```
109
+
110
+ ## Accessibility notes
111
+
112
+ - The library does not auto-add `role="button"` or keyboard handlers when the trigger is not a `<button>`. Always use a real button.
113
+ - The directive does not apply the native `hidden` attribute to the content. Either wrap it with `@if (open())` so it unmounts when closed, or leave it mounted and rely on the `aria-hidden="true"` + `inert` reflection that keeps the closed panel out of the accessibility tree and focus order. Visual hiding (and enter/leave transitions) are still on you — drive them off `[data-state]`.
114
+ - Disabled state sets the native `disabled` attribute on the trigger (effective on `<button>` elements). Click is also ignored at the directive level as a defensive measure. The trigger can be disabled from the root (`[forDisclosure] [disabled]`) or per trigger (`[forDisclosureTrigger] [disabled]`); either source disables it.