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,1641 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, inject, ElementRef, ErrorHandler, signal, computed, effect, DestroyRef, input, booleanAttribute, model, output, afterNextRender, Directive, ChangeDetectionStrategy, Component, createComponent, Injectable } from '@angular/core';
3
+ import { createDefaults, injectPrefersReducedMotion, isScrollableAtEdge, resolveSnapTarget, attachSwipeDismiss, ForDrawerScaleCoordinator, ForDrawerStack, injectModalShell, reflectDisabled, injectPortal, registerA11yName, registerA11yDescription, OverlayRef, OverlayContextInjector, OverlayManagerCore, resolveConfigClass } from 'forty-cdk/core';
4
+ import { NgComponentOutlet } from '@angular/common';
5
+
6
+ const FOR_DRAWER_CONTEXT = new InjectionToken('FOR_DRAWER_CONTEXT');
7
+ /**
8
+ * @internal Per-instance drawer id, provided by `ForDrawerManager` into the
9
+ * opened component's injector so the portaled `[forDrawerBackdrop]` can
10
+ * reflect `data-for-drawer-id` and be paired with its drawer for the
11
+ * manager's exit-animation orchestration. Absent in the declarative path,
12
+ * where the backdrop's own `animate.leave` runs normally.
13
+ */
14
+ const FOR_DRAWER_INSTANCE_ID = new InjectionToken('FOR_DRAWER_INSTANCE_ID');
15
+ function injectDrawerContext(piece) {
16
+ const ctx = inject(FOR_DRAWER_CONTEXT, { optional: true });
17
+ if (!ctx) {
18
+ throw new Error(`[forty-cdk/drawer] ${piece} must be used inside a [forDrawer] element.`);
19
+ }
20
+ return ctx;
21
+ }
22
+
23
+ const FALLBACK = {
24
+ side: 'bottom',
25
+ modal: true,
26
+ dismissible: true,
27
+ swipeToDismiss: true,
28
+ closeThreshold: 0.25,
29
+ initialFocus: 'first',
30
+ returnFocus: true,
31
+ handleOnly: false,
32
+ scaleBackground: false,
33
+ setBackgroundColorOnScale: true,
34
+ scaleAmount: 0.95,
35
+ scaleTranslateYpx: 14,
36
+ scaleBorderRadiusPx: 8,
37
+ scaleBackgroundColor: 'black',
38
+ nestedScaleAmount: 0.93,
39
+ nestedTranslateYpx: 8,
40
+ };
41
+ const { token, provideDefaults } = createDefaults('FOR_DRAWER_DEFAULTS', FALLBACK);
42
+ /** Token holding the resolved drawer defaults for the current scope. */
43
+ const FOR_DRAWER_DEFAULTS = token;
44
+ /**
45
+ * Configures forty-cdk drawer defaults for this injector scope. Partial
46
+ * overrides inherit unspecified keys from the parent scope (or library
47
+ * defaults at the root).
48
+ */
49
+ function provideForDrawerDefaults(defaults = {}) {
50
+ return provideDefaults(defaults);
51
+ }
52
+
53
+ const SNAP_POINT_PERCENT_RE = /^(-?\d+(?:\.\d+)?)%$/;
54
+ const SNAP_POINT_PX_RE = /^(-?\d+(?:\.\d+)?)px$/;
55
+ function classifySnapPoint(p) {
56
+ // Numbers and `NN%` strings both resolve as fractions of the dimension —
57
+ // their cross-comparison is dimension-independent. `NNpx` strings only
58
+ // become comparable once we know the dimension.
59
+ if (typeof p === 'number') {
60
+ return 'fraction';
61
+ }
62
+ return SNAP_POINT_PX_RE.test(p) ? 'px' : 'fraction';
63
+ }
64
+ /**
65
+ * Convert a snap point to its fractional position along the dismissal axis.
66
+ * Returns `[0, 1]` (or beyond, for fractional values > 1, allowed
67
+ * as overshoot). Throws on malformed strings or values that aren't finite.
68
+ */
69
+ function snapPointToFraction(p, dimension) {
70
+ if (typeof p === 'number') {
71
+ if (!Number.isFinite(p)) {
72
+ throw new Error(`[forty-cdk/drawer] Snap point must be a finite number, got ${p}.`);
73
+ }
74
+ return p;
75
+ }
76
+ const pctMatch = SNAP_POINT_PERCENT_RE.exec(p);
77
+ if (pctMatch) {
78
+ const n = Number.parseFloat(pctMatch[1]);
79
+ return n / 100;
80
+ }
81
+ const pxMatch = SNAP_POINT_PX_RE.exec(p);
82
+ if (pxMatch) {
83
+ const n = Number.parseFloat(pxMatch[1]);
84
+ return dimension === 0 ? 0 : n / dimension;
85
+ }
86
+ throw new Error(`[forty-cdk/drawer] Snap point must be a number, "NN%", or "NNpx" string. Got: ${String(p)}.`);
87
+ }
88
+ /**
89
+ * Mount-time **shape** check. Validates each snap point in isolation
90
+ * (rejects `NaN`, malformed strings, non-numbers) and only enforces strict
91
+ * monotonicity when every point shares the same unit family — pure
92
+ * numbers/percent strings are dimension-independent and can be compared
93
+ * directly, as can pure-px arrays. Mixed `'NNpx'` + fraction inputs are
94
+ * deferred to {@link validateSnapPositions}, which runs once the live
95
+ * dimension is known.
96
+ *
97
+ * Throws an `Error` with a `[forty-cdk/drawer]` prefix on the first failure.
98
+ */
99
+ function validateSnapPointsShape(snapPoints) {
100
+ // Sanity-check every entry; `snapPointToFraction` throws on NaN /
101
+ // malformed strings. Use `1` as a placeholder dimension — the px branch
102
+ // would otherwise return `0` which is a valid fraction. Discard the
103
+ // result; we only care about the throw.
104
+ for (const p of snapPoints) {
105
+ snapPointToFraction(p, 1);
106
+ }
107
+ // Monotonicity is dimension-independent only when every entry classifies
108
+ // the same way. Pure-fraction (numbers + percent strings) sorts directly;
109
+ // pure-px sorts by raw px value. A mixed array (e.g. `['200px', 0.5]`)
110
+ // cannot be checked here because the cross-comparison depends on the
111
+ // actual surface size — that runs in `validateSnapPositions` on first
112
+ // measurement.
113
+ const kind = classifySnapPoint(snapPoints[0]);
114
+ const homogeneous = snapPoints.every((p) => classifySnapPoint(p) === kind);
115
+ if (!homogeneous) {
116
+ return;
117
+ }
118
+ const positions = snapPoints.map((p) => {
119
+ if (kind === 'fraction') {
120
+ return snapPointToFraction(p, 1);
121
+ }
122
+ return Number.parseFloat(SNAP_POINT_PX_RE.exec(p)[1]);
123
+ });
124
+ for (let i = 1; i < positions.length; i++) {
125
+ if (positions[i] <= positions[i - 1]) {
126
+ throw new Error('[forty-cdk/drawer] snapPoints must be strictly increasing (closest-to-edge first).');
127
+ }
128
+ }
129
+ }
130
+ /**
131
+ * Returns the position of each snap point along the dismissal axis (in CSS
132
+ * pixels measured from the anchored edge). **Does not validate** — pair with
133
+ * {@link validateSnapPositions} when you need the strict-increase guarantee
134
+ * against a live dimension.
135
+ */
136
+ function computeSnapPositions(snapPoints, dimension) {
137
+ return snapPoints.map((p) => snapPointToFraction(p, dimension) * dimension);
138
+ }
139
+ /**
140
+ * First-measurement **monotonicity** check. Compares the resolved px
141
+ * positions against the live drawer dimension. Mixed `'NNpx'` + fraction
142
+ * inputs that look monotonic at one dimension and non-monotonic at another
143
+ * fail here — the error message names the offending point so the consumer
144
+ * doesn't have to manually back-out the conversion.
145
+ */
146
+ function validateSnapPositions(snapPoints, positions, dimension) {
147
+ for (let i = 1; i < positions.length; i++) {
148
+ const cur = positions[i];
149
+ const prev = positions[i - 1];
150
+ if (cur <= prev) {
151
+ const curConfig = JSON.stringify(snapPoints[i]);
152
+ const prevConfig = JSON.stringify(snapPoints[i - 1]);
153
+ throw new Error(`[forty-cdk/drawer] snap point ${curConfig} at index ${i} resolves to ${cur}px which is <= snap point ${prevConfig} at ${prev}px (drawer dimension ${dimension}px).`);
154
+ }
155
+ }
156
+ }
157
+
158
+ function sideToDirections(side) {
159
+ switch (side) {
160
+ case 'bottom':
161
+ return ['down'];
162
+ case 'top':
163
+ return ['up'];
164
+ case 'right':
165
+ return ['right'];
166
+ case 'left':
167
+ return ['left'];
168
+ }
169
+ }
170
+ /** Axis on which the drawer's dimension lives. */
171
+ function sideAxis(side) {
172
+ return side === 'left' || side === 'right' ? 'x' : 'y';
173
+ }
174
+ /** Sign of a positive (toward-edge) drag offset along the host's axis. */
175
+ function sideSign(side) {
176
+ return side === 'bottom' || side === 'right' ? 1 : -1;
177
+ }
178
+ /**
179
+ * Allowed swipe directions for the drag gesture. Without snap points the
180
+ * drawer only drags *toward* its anchored edge (dismiss). With snap points
181
+ * the surface must also grow on a drag *away* from the edge to reach a
182
+ * larger snap, so both directions along the dismissal axis arm the gesture.
183
+ */
184
+ function dragDirections(side, hasSnapPoints) {
185
+ if (!hasSnapPoints) {
186
+ return sideToDirections(side);
187
+ }
188
+ return sideAxis(side) === 'y' ? ['down', 'up'] : ['right', 'left'];
189
+ }
190
+ /**
191
+ * Pointer / swipe / snap gesture engine for `[forDrawer]`, extracted from the
192
+ * directive so the velocity tracking, per-gesture offset bounds,
193
+ * dimension-keyed snap-position cache, and runtime monotonicity re-validation
194
+ * live in one unit-testable collaborator. The directive keeps its inputs /
195
+ * outputs, label / description registration, drawer-stack + scale-coordinator
196
+ * registration, and `injectModalShell`.
197
+ *
198
+ * Must be called from an injection context (the directive constructor). It
199
+ * registers the drag-translate publisher, the runtime snap-rebind validator,
200
+ * and the swipe-gate effect immediately, but defers the two ordering-sensitive
201
+ * steps to handle methods the directive drives from its own `afterNextRender`s:
202
+ * {@link DrawerDragHandle.validateOnMount} (mount-time snap validation, called
203
+ * after the drawer-stack push so it lands at the original point in the mount
204
+ * sequence) and {@link DrawerDragHandle.arm} (wires the pointer listeners onto
205
+ * a host already portaled to `document.body` by the shell).
206
+ *
207
+ * Forwards the host's `ElementRef` and the injected `ErrorHandler` — the
208
+ * caller never has to touch them. Pointer listeners are detached on destroy
209
+ * via the engine's own `DestroyRef.onDestroy` hook.
210
+ */
211
+ function injectDrawerDrag(config) {
212
+ const host = inject(ElementRef);
213
+ const errorHandler = inject(ErrorHandler);
214
+ const prefersReducedMotion = injectPrefersReducedMotion();
215
+ const side = config.side;
216
+ const snapPoints = config.snapPoints;
217
+ const closeThreshold = config.closeThreshold;
218
+ const fadeFromIndex = config.fadeFromIndex;
219
+ const activeSnapPoint = config.activeSnapPoint;
220
+ const dragOffset = signal(0, /* @ts-ignore */
221
+ ...(ngDevMode ? [{ debugName: "dragOffset" }] : /* istanbul ignore next */ [])); // px translated along the dismissal axis (positive = away from edge)
222
+ const dragging = signal(false, /* @ts-ignore */
223
+ ...(ngDevMode ? [{ debugName: "dragging" }] : /* istanbul ignore next */ []));
224
+ const dragProgress = signal(0, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "dragProgress" }] : /* istanbul ignore next */ [])); // [0, 1] progress toward the anchored edge (dismiss direction)
226
+ const dragTranslate = computed(() => {
227
+ const offset = dragOffset();
228
+ if (offset === 0) {
229
+ return '0px 0px';
230
+ }
231
+ const px = sideSign(side()) * offset;
232
+ return sideAxis(side()) === 'y' ? `0px ${px}px` : `${px}px 0px`;
233
+ }, /* @ts-ignore */
234
+ ...(ngDevMode ? [{ debugName: "dragTranslate" }] : /* istanbul ignore next */ []));
235
+ // Pointer state for velocity tracking.
236
+ let pointerStartTime = 0;
237
+ let pointerLastY = 0;
238
+ let pointerLastX = 0;
239
+ let pointerLastTime = 0;
240
+ let pointerVelocity = 0;
241
+ let dimensionAtStart = 0;
242
+ // Per-gesture lower bound for the drag offset (px toward the edge),
243
+ // resolved on `onSwipeStart`. A positive offset moves the surface toward
244
+ // the edge (shrink / dismiss) and is never capped — dragging past the edge
245
+ // is how a dismiss arms. A negative offset moves it away from the edge to
246
+ // grow. Without snap points the floor stays 0 (the surface can only shrink
247
+ // toward the edge); with snap points the floor reaches the largest snap so
248
+ // an upward drag can expand the surface.
249
+ let dragMinOffset = 0;
250
+ // Snap positions cache, keyed by BOTH the dimension they were resolved
251
+ // against AND the `snapPoints` array identity. First-measurement validation
252
+ // populates this; `onSwipeStart` refreshes it when the surface has resized
253
+ // between gestures, and a runtime `[snapPoints]` rebind (same dimension, new
254
+ // array reference) is a cache miss so positions are recomputed and
255
+ // re-validated. Always pre-validated, so `onSwipeRelease` can read it
256
+ // without re-running monotonicity checks.
257
+ let snapPositionsCache = null;
258
+ // Flipped true once the mount-time `afterNextRender` has validated the
259
+ // initial snap config. Gates the runtime-rebind effect so its first
260
+ // (pre-render) run is a no-op and only genuine post-mount changes
261
+ // re-validate.
262
+ let snapConfigMounted = false;
263
+ // Signal flipped true by `arm()` in the directive's post-shell
264
+ // `afterNextRender`. Gates the swipe gate effect so the gesture arms on a
265
+ // host already wired by the shell; being a signal, flipping it re-runs the
266
+ // effect for the first real attach.
267
+ const swipeReady = signal(false, /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "swipeReady" }] : /* istanbul ignore next */ []));
269
+ let swipeCleanup = null;
270
+ /**
271
+ * Dimension-independent snap-point validation: the per-point shape /
272
+ * strict-increase check (`validateSnapPointsShape`) plus the
273
+ * `fadeFromIndex` range check. Throws with a `[forty-cdk/drawer]`-prefixed
274
+ * message on the first failure. A `null` / empty array is a valid "no snap
275
+ * points" config and skips both checks. Shared by the mount-time
276
+ * `afterNextRender` and the runtime-rebind effect so the two paths stay in
277
+ * lockstep.
278
+ */
279
+ function validateSnapPointConfig(points) {
280
+ if (!points || points.length === 0) {
281
+ return;
282
+ }
283
+ validateSnapPointsShape(points);
284
+ const idx = fadeFromIndex();
285
+ if (idx !== undefined && (idx < 0 || idx >= points.length)) {
286
+ throw new Error(`[forty-cdk/drawer] fadeFromIndex (${idx}) is out of range for snapPoints (length ${points.length}).`);
287
+ }
288
+ }
289
+ /**
290
+ * Resolve and validate snap positions against the host's current
291
+ * dimension. No-op if the cached positions already match BOTH the live
292
+ * dimension and the current `snapPoints` array. Throws (with the
293
+ * offending-point error message) when the live dimension flips a mixed
294
+ * `'NNpx'` + fraction array out of monotonic order.
295
+ *
296
+ * Called from `afterNextRender` (first measurement), from `onSwipeStart`
297
+ * (resize between gestures, or a runtime `[snapPoints]` rebind), and from
298
+ * the runtime-rebind effect. Never from `onSwipeRelease` — by the time
299
+ * release fires, the cache is already populated for this gesture's
300
+ * dimension and array.
301
+ */
302
+ function refreshSnapPositions(points) {
303
+ const rect = host.nativeElement.getBoundingClientRect();
304
+ const dim = sideAxis(side()) === 'y' ? rect.height : rect.width;
305
+ if (dim <= 0) {
306
+ // No layout yet (jsdom, or display: none). Defer; the next call with
307
+ // a real dimension will do the work.
308
+ return;
309
+ }
310
+ const cached = snapPositionsCache;
311
+ if (cached && cached.dimension === dim && cached.snapPoints === points) {
312
+ return;
313
+ }
314
+ const positions = computeSnapPositions(points, dim);
315
+ validateSnapPositions(points, positions, dim);
316
+ snapPositionsCache = { dimension: dim, snapPoints: points, positions };
317
+ }
318
+ /**
319
+ * Pixel position (from the anchored edge) of the currently-active snap
320
+ * point within `snapPositions`. Falls back to the closest-to-edge entry
321
+ * when the active point is unset or not found in `snapPoints`.
322
+ */
323
+ function activeSnapPositionPx(points, snapPositions) {
324
+ const active = activeSnapPoint();
325
+ if (active == null) {
326
+ return snapPositions[0];
327
+ }
328
+ const idx = points.indexOf(active);
329
+ return idx >= 0 ? snapPositions[idx] : snapPositions[0];
330
+ }
331
+ function onSwipeStart(detail) {
332
+ // Bail out conditions that the swipe-dismiss helper can't know about:
333
+ // (a) `handleOnly` and the gesture didn't start on the registered handle,
334
+ // (b) the gesture started inside a scrollable element that hasn't reached
335
+ // its edge along the dismissal direction (don't steal scroll).
336
+ const target = detail.originalEvent.target;
337
+ const handle = config.handleEl();
338
+ if (config.handleOnly() && (!handle || !target || !handle.contains(target))) {
339
+ dragging.set(false);
340
+ return;
341
+ }
342
+ if (target && isScrollableAtEdge(target, detail.direction, host.nativeElement)) {
343
+ dragging.set(false);
344
+ return;
345
+ }
346
+ dragging.set(true);
347
+ dragProgress.set(0);
348
+ pointerStartTime = detail.originalEvent.timeStamp || performance.now();
349
+ pointerLastTime = pointerStartTime;
350
+ pointerLastX = detail.originalEvent.clientX;
351
+ pointerLastY = detail.originalEvent.clientY;
352
+ pointerVelocity = 0;
353
+ const rect = host.nativeElement.getBoundingClientRect();
354
+ dimensionAtStart = sideAxis(side()) === 'y' ? rect.height : rect.width;
355
+ // Refresh & validate snap positions for this gesture's dimension. If
356
+ // mount-time first-measurement saw a non-zero dimension equal to the
357
+ // current one, this is a cache hit and no work runs. Validation throws
358
+ // here (pre-drag) rather than from the release handler.
359
+ const points = snapPoints();
360
+ if (points && points.length > 0) {
361
+ refreshSnapPositions(points);
362
+ const cached = snapPositionsCache;
363
+ const positions = cached && cached.snapPoints === points
364
+ ? cached.positions
365
+ : computeSnapPositions(points, dimensionAtStart);
366
+ const activePos = activeSnapPositionPx(points, positions);
367
+ const highestPos = positions[positions.length - 1] ?? activePos;
368
+ dragMinOffset = activePos - highestPos;
369
+ }
370
+ else {
371
+ dragMinOffset = 0;
372
+ }
373
+ config.emitDrag({ percentageDragged: 0, originalEvent: detail.originalEvent });
374
+ }
375
+ function onSwipeMove(detail) {
376
+ if (!dragging()) {
377
+ return;
378
+ }
379
+ const event = detail.originalEvent;
380
+ const now = event.timeStamp || performance.now();
381
+ const dx = event.clientX - pointerLastX;
382
+ const dy = event.clientY - pointerLastY;
383
+ const dt = Math.max(1, now - pointerLastTime);
384
+ // Magnitude moved toward the anchored edge (positive = move toward edge).
385
+ const moveTowardEdge = (() => {
386
+ switch (side()) {
387
+ case 'bottom':
388
+ return dy; // pointer moves down → toward bottom edge → positive
389
+ case 'top':
390
+ return -dy;
391
+ case 'right':
392
+ return dx;
393
+ case 'left':
394
+ return -dx;
395
+ }
396
+ })();
397
+ pointerVelocity = moveTowardEdge / dt; // px per ms toward edge
398
+ pointerLastX = event.clientX;
399
+ pointerLastY = event.clientY;
400
+ pointerLastTime = now;
401
+ // Integrate the per-event pointer delta into the cumulative drag offset,
402
+ // clamped to this gesture's bounds. Without snap points the lower bound
403
+ // is 0 (the surface only moves toward the edge to dismiss); with snap
404
+ // points it goes negative so a drag away from the edge grows the surface
405
+ // toward a larger snap. The host's `[style.translate]` binding reflects
406
+ // the offset reactively — no imperative DOM write here.
407
+ const nextOffset = Math.max(dragMinOffset, dragOffset() + moveTowardEdge);
408
+ dragOffset.set(nextOffset);
409
+ const dim = dimensionAtStart || 1;
410
+ // `percentageDragged` tracks progress toward dismiss, so growth (a
411
+ // negative offset) reads as 0 rather than a negative number.
412
+ const percentageDragged = Math.min(1, Math.max(0, nextOffset / dim));
413
+ dragProgress.set(percentageDragged);
414
+ config.emitDrag({ percentageDragged, originalEvent: event });
415
+ }
416
+ function onSwipeRelease(detail) {
417
+ if (!dragging()) {
418
+ return;
419
+ }
420
+ const event = detail.originalEvent;
421
+ const offset = dragOffset();
422
+ const dim = dimensionAtStart || 1;
423
+ const threshold = closeThreshold();
424
+ const points = snapPoints();
425
+ let willClose = false;
426
+ let nextSnap = null;
427
+ if (points && points.length > 0) {
428
+ // `position` is the surface position along the dismissal axis from the
429
+ // anchored edge. With `offset` representing how far the surface has
430
+ // been pulled toward the edge (positive), the effective position from
431
+ // the edge is `currentSnapPosition - offset`.
432
+ //
433
+ // Read snap positions from the pre-validated cache populated by
434
+ // `refreshSnapPositions` at mount and on `onSwipeStart`. The release
435
+ // path is throw-free by construction: any input that would fail
436
+ // monotonicity at the live dimension has already failed before we get
437
+ // here.
438
+ const cached = snapPositionsCache;
439
+ const snapPositions = cached && cached.dimension === dim && cached.snapPoints === points
440
+ ? cached.positions
441
+ : computeSnapPositions(points, dim);
442
+ const position = activeSnapPositionPx(points, snapPositions) - offset;
443
+ const resolved = resolveSnapTarget({
444
+ snapPoints: points,
445
+ snapPositions,
446
+ activeSnapPoint: activeSnapPoint(),
447
+ position,
448
+ velocity: -pointerVelocity, // helper sema: positive = away from edge
449
+ closeThreshold: threshold,
450
+ });
451
+ willClose = resolved.willClose;
452
+ nextSnap = resolved.nextSnapPoint;
453
+ }
454
+ else {
455
+ // No snap points: dismiss when dragged past closeThreshold OR fast
456
+ // flick toward edge.
457
+ willClose = offset >= dim * threshold || pointerVelocity >= 0.4;
458
+ }
459
+ config.emitRelease({ willClose, nextSnapPoint: nextSnap, originalEvent: event });
460
+ // Zero the offset and flip `dragging` off together. Both writes (plus the
461
+ // `activeSnapPoint` change below) flush in one change-detection pass, so
462
+ // the host applies the `data-dragging` removal, the new
463
+ // `data-active-snap-point`, and the `translate` reset to zero in a single
464
+ // style recalc — the consumer's `transition: translate` then animates the
465
+ // drag delta away in lockstep with the snap-position transition, with no
466
+ // intermediate jump to the previous rest position.
467
+ dragOffset.set(0);
468
+ dragProgress.set(0);
469
+ dragging.set(false);
470
+ if (willClose) {
471
+ config.requestClose('swipe');
472
+ return;
473
+ }
474
+ if (nextSnap !== null && nextSnap !== activeSnapPoint()) {
475
+ activeSnapPoint.set(nextSnap);
476
+ }
477
+ }
478
+ function attachSwipe() {
479
+ const el = host.nativeElement;
480
+ return attachSwipeDismiss({
481
+ element: el,
482
+ getDirections: () => dragDirections(side(), !!snapPoints()?.length),
483
+ // Always arm on a tiny gesture and let the move handler decide; we
484
+ // resolve the actual close threshold on release via resolveSnapTarget.
485
+ getThreshold: () => 1,
486
+ onSwipeStart: (detail) => onSwipeStart(detail),
487
+ onSwipeMove: (detail) => onSwipeMove(detail),
488
+ onSwipeEnd: (detail) => onSwipeRelease(detail),
489
+ onSwipeCancel: (detail) => onSwipeRelease(detail),
490
+ });
491
+ }
492
+ // ---- Drag-translate side effect. Publishes the drag delta as the
493
+ // `--for-drawer-translate` custom property (see `dragTranslate` for why a
494
+ // custom property rather than a directly-written `translate`/`transform`).
495
+ // Runs in the same change-detection flush as the host's attribute
496
+ // bindings, so the release path's `data-dragging` removal,
497
+ // `data-active-snap-point` change, and translate reset to "0px 0px" all
498
+ // land in one style recalc and transition together.
499
+ effect(() => {
500
+ host.nativeElement.style.setProperty('--for-drawer-translate', dragTranslate());
501
+ });
502
+ // ---- Mount-time snap validation. Invoked by the directive's pre-shell
503
+ // `afterNextRender` (via the returned `validateOnMount`), after the
504
+ // drawer-stack push and the `closeThreshold` check, so it lands at the same
505
+ // point in the mount sequence it always has — before the shell's
506
+ // dismissable / focus / scroll-lock wiring. A consumer passing a bad
507
+ // snapPoints array gets a clear error before the shell tries to focus an
508
+ // element that may not exist. The mount-time `activeSnapPoint` default is
509
+ // seeded here too. Subsequent runtime `[snapPoints]` rebinds are handled by
510
+ // the effect below, which re-runs the same validation and invalidates the
511
+ // position cache.
512
+ function validateOnMount() {
513
+ const points = snapPoints();
514
+ if (points && points.length > 0 && activeSnapPoint() === null) {
515
+ activeSnapPoint.set(points[0]);
516
+ }
517
+ validateSnapPointConfig(points);
518
+ // Try first measurement. In real browsers `getBoundingClientRect`
519
+ // returns the laid-out dimension here (we're inside `afterNextRender`,
520
+ // post-layout). In jsdom layout doesn't run, so dimension is 0 — defer
521
+ // to `onSwipeStart`'s rect read, which is also pre-gesture.
522
+ if (points && points.length > 0) {
523
+ refreshSnapPositions(points);
524
+ }
525
+ snapConfigMounted = true;
526
+ }
527
+ // ---- Runtime `[snapPoints]` / `[fadeFromIndex]` rebind. `validateOnMount`
528
+ // validates the INITIAL config once; this effect
529
+ // re-validates whenever the consumer swaps the array (or fadeFromIndex)
530
+ // at runtime. Without it a new array that is non-monotonic — or a
531
+ // fadeFromIndex that no longer fits — would slip through, and the
532
+ // position cache (keyed on array identity) would lazily recompute on the
533
+ // next gesture against the new array but never re-run the shape /
534
+ // range checks. Guarded by `snapConfigMounted` so the effect's first
535
+ // run (which fires before `validateOnMount`) doesn't duplicate the
536
+ // mount-time validation or race the layout measurement. Validation +
537
+ // cache invalidation are genuine side effects (route an error / clear a
538
+ // field), not reactive-state propagation, so an `effect` is the right
539
+ // tool here. Validation failures are funnelled through the injected
540
+ // `ErrorHandler` so they reach the consumer's handler exactly like the
541
+ // mount-time `afterNextRender` throw, instead of tearing down the effect.
542
+ effect(() => {
543
+ const points = snapPoints();
544
+ // Track fadeFromIndex too so a runtime range violation is caught.
545
+ fadeFromIndex();
546
+ if (!snapConfigMounted) {
547
+ return;
548
+ }
549
+ // A new array reference invalidates the position cache so the next
550
+ // gesture (and `refreshSnapPositions`) resolves against fresh geometry
551
+ // instead of the previous array's cached positions.
552
+ if (snapPositionsCache && snapPositionsCache.snapPoints !== points) {
553
+ snapPositionsCache = null;
554
+ }
555
+ try {
556
+ validateSnapPointConfig(points);
557
+ if (points && points.length > 0) {
558
+ refreshSnapPositions(points);
559
+ }
560
+ }
561
+ catch (error) {
562
+ errorHandler.handleError(error);
563
+ }
564
+ });
565
+ // ---- Swipe-to-dismiss gate. Arms the pointer listeners only when
566
+ // `swipeToDismiss` is on AND the user hasn't asked for reduced motion
567
+ // (drag animations are vestibular-hostile). Both inputs are read
568
+ // reactively so a runtime flip of either — a `[swipeToDismiss]` rebind or
569
+ // a live `prefers-reduced-motion` change — arms or disarms the gesture,
570
+ // mirroring how `ForDrawerScaleCoordinator` already reacts to the
571
+ // preference. Attaching/detaching listeners is a DOM side effect, so an
572
+ // `effect` is the right tool; `swipeReady` gates the pre-render run so the
573
+ // gesture arms on a host already attached via the shell's portal.
574
+ effect(() => {
575
+ const shouldArm = config.swipeToDismiss() && !prefersReducedMotion();
576
+ if (!swipeReady()) {
577
+ return;
578
+ }
579
+ if (shouldArm && swipeCleanup === null) {
580
+ swipeCleanup = attachSwipe();
581
+ }
582
+ else if (!shouldArm && swipeCleanup !== null) {
583
+ swipeCleanup();
584
+ swipeCleanup = null;
585
+ }
586
+ });
587
+ inject(DestroyRef).onDestroy(() => {
588
+ swipeCleanup?.();
589
+ swipeCleanup = null;
590
+ });
591
+ return {
592
+ dragging: dragging.asReadonly(),
593
+ dragProgress: dragProgress.asReadonly(),
594
+ dragTranslate,
595
+ validateOnMount,
596
+ arm: () => swipeReady.set(true),
597
+ };
598
+ }
599
+
600
+ /**
601
+ * Headless implementation of the [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/),
602
+ * specialised as a side / bottom-sheet drawer with optional swipe-to-dismiss
603
+ * and snap points.
604
+ *
605
+ * Apply `[forDrawer]` on the drawer surface itself — not on a wrapper. The
606
+ * directive moves the host to `document.body` (portal), traps focus, locks
607
+ * body scroll, listens for Escape, and (when `swipeToDismiss` is on) attaches
608
+ * pointer-based swipe handlers that translate the surface and resolve to the
609
+ * nearest snap point or a dismiss on release. `aria-labelledby` and
610
+ * `aria-describedby` wire automatically via `[forDrawerTitle]` /
611
+ * `[forDrawerDescription]`; pass `ariaLabel` instead if you don't render
612
+ * a visible title.
613
+ *
614
+ * Mount/unmount is the consumer's responsibility — the directive does not
615
+ * manage `[hidden]`. Wrap with `@if (open())` and let `animate.enter` /
616
+ * `animate.leave` handle transitions.
617
+ *
618
+ * For programmatic use (open arbitrary components imperatively, manage
619
+ * lifecycle externally), see `ForDrawerManager.open()`.
620
+ */
621
+ class ForDrawer {
622
+ #defaults = inject(FOR_DRAWER_DEFAULTS);
623
+ /**
624
+ * Edge the drawer is anchored to. Default `'bottom'` — the most common
625
+ * mobile-first pattern. The keyboard navigation axis (for swipe direction)
626
+ * derives from this, as does the `data-side` attribute on the host.
627
+ */
628
+ side = input(this.#defaults.side ?? 'bottom', /* @ts-ignore */
629
+ ...(ngDevMode ? [{ debugName: "side" }] : /* istanbul ignore next */ []));
630
+ /**
631
+ * When true (default), Escape, backdrop click, pointer-down outside,
632
+ * focus outside, and a swipe past the close threshold all emit `(dismiss)`.
633
+ * Disable for confirm flows that must be answered explicitly.
634
+ */
635
+ dismissible = input(this.#defaults.dismissible ?? true, { ...(ngDevMode ? { debugName: "dismissible" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
636
+ /**
637
+ * When true (default), sets `aria-modal="true"`, locks body scroll, traps
638
+ * focus, and inerts body siblings. Set to `false` for a non-modal
639
+ * navigation drawer that coexists with the rest of the page.
640
+ */
641
+ modal = input(this.#defaults.modal ?? true, { ...(ngDevMode ? { debugName: "modal" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
642
+ /** When true, role becomes `alertdialog` (interrupts assistive tech). */
643
+ alert = input(false, { ...(ngDevMode ? { debugName: "alert" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
644
+ /** When true (default), focus returns to the previously focused element on close. */
645
+ returnFocus = input(this.#defaults.returnFocus ?? true, { ...(ngDevMode ? { debugName: "returnFocus" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
646
+ /**
647
+ * Where to send focus on mount. `'first'` (default) finds the first
648
+ * focusable descendant; `'container'` focuses the drawer surface itself
649
+ * (useful when there's nothing focusable inside).
650
+ */
651
+ initialFocus = input(this.#defaults.initialFocus ?? 'first', /* @ts-ignore */
652
+ ...(ngDevMode ? [{ debugName: "initialFocus" }] : /* istanbul ignore next */ []));
653
+ /** Manual `aria-label`. Use this when no visible title element exists. */
654
+ ariaLabel = input(null, /* @ts-ignore */
655
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
656
+ /**
657
+ * Portal target for the surface. Defaults to `document.body`. Pass a
658
+ * positioned (`position: relative`) container — paired with
659
+ * `[modal]="false"` — for a drawer scoped to a region instead of the
660
+ * viewport. Read once at mount (the portal moves the host once); changing
661
+ * it after open has no effect. The backdrop portals to the same container.
662
+ */
663
+ container = input(null, /* @ts-ignore */
664
+ ...(ngDevMode ? [{ debugName: "container" }] : /* istanbul ignore next */ []));
665
+ /**
666
+ * Callback invoked just before focus moves into the drawer on mount.
667
+ * `event.preventDefault()` skips the imperative move (Tab cycling and
668
+ * return-focus capture still wire up). See Dialog for the same shape and
669
+ * the rationale for `input<>` rather than `output<>` on free-floating
670
+ * overlays.
671
+ */
672
+ autoFocusOnOpen = input(undefined, /* @ts-ignore */
673
+ ...(ngDevMode ? [{ debugName: "autoFocusOnOpen" }] : /* istanbul ignore next */ []));
674
+ /**
675
+ * Callback invoked just before focus returns to the previously focused
676
+ * element on unmount. `event.preventDefault()` skips the return-focus.
677
+ */
678
+ autoFocusOnClose = input(undefined, /* @ts-ignore */
679
+ ...(ngDevMode ? [{ debugName: "autoFocusOnClose" }] : /* istanbul ignore next */ []));
680
+ /** When true (default), swipe toward the anchored edge dismisses past `closeThreshold`. */
681
+ swipeToDismiss = input(this.#defaults.swipeToDismiss ?? true, { ...(ngDevMode ? { debugName: "swipeToDismiss" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
682
+ /**
683
+ * Fraction past which a release dismisses instead of snapping back.
684
+ * Default `0.25`. Without `snapPoints` it is a fraction of the
685
+ * full drawer dimension along the dismissal axis (dragged > 25% of the
686
+ * surface size toward the edge dismisses); with `snapPoints` it is a
687
+ * fraction of the **lowest snap's** extent, so a small "peek" snap stays
688
+ * dismissable without dragging it entirely off-screen.
689
+ */
690
+ closeThreshold = input(this.#defaults.closeThreshold ?? 0.25, /* @ts-ignore */
691
+ ...(ngDevMode ? [{ debugName: "closeThreshold" }] : /* istanbul ignore next */ []));
692
+ /**
693
+ * When true, the swipe gesture only arms on a pointerdown that started on
694
+ * the registered `[forDrawerHandle]` element. Useful when the surface
695
+ * contains scrollable content that should keep its scroll gesture.
696
+ */
697
+ handleOnly = input(this.#defaults.handleOnly ?? false, { ...(ngDevMode ? { debugName: "handleOnly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
698
+ /**
699
+ * When true, asks the registered `[forDrawerWrapper]` to scale and
700
+ * translate behind this drawer (the "scale background" effect). No
701
+ * effect under `prefers-reduced-motion: reduce`, and a no-op if no
702
+ * wrapper is mounted. Default `false` so existing consumers are
703
+ * untouched.
704
+ */
705
+ scaleBackground = input(this.#defaults.scaleBackground ?? false, { ...(ngDevMode ? { debugName: "scaleBackground" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
706
+ /**
707
+ * When true (default) and `scaleBackground` is active, paints
708
+ * `<body>` with `scaleBackgroundColor` so the gap between the scaled
709
+ * wrapper and the viewport edge does not show through. Only applies
710
+ * when `scaleBackground` is `true`.
711
+ */
712
+ setBackgroundColorOnScale = input(this.#defaults.setBackgroundColorOnScale ?? true, { ...(ngDevMode ? { debugName: "setBackgroundColorOnScale" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
713
+ /**
714
+ * Snap points: each entry is a `number ∈ [0, 1]`,
715
+ * a `'NN%'` string, or a `'NNpx'` string. Strictly increasing
716
+ * (closest-to-edge first). The surface settles at the nearest snap on
717
+ * release; dragging past the lowest snap by `closeThreshold` of that
718
+ * snap's own extent dismisses.
719
+ */
720
+ snapPoints = input(undefined, /* @ts-ignore */
721
+ ...(ngDevMode ? [{ debugName: "snapPoints" }] : /* istanbul ignore next */ []));
722
+ /**
723
+ * Active snap point. Two-way bindable: write via `[(activeSnapPoint)]`
724
+ * to drive the surface programmatically, or just read for analytics.
725
+ * Initialised to `snapPoints?.[0]` on mount when the consumer left it
726
+ * `null`. The implicit `(activeSnapPointChange)` emitter fires on
727
+ * internal transitions (mount-time default and drag release), and
728
+ * stays silent on consumer writes through `[(activeSnapPoint)]`.
729
+ */
730
+ activeSnapPoint = model(null, /* @ts-ignore */
731
+ ...(ngDevMode ? [{ debugName: "activeSnapPoint" }] : /* istanbul ignore next */ []));
732
+ /**
733
+ * First index of `snapPoints` from which the backdrop should reflect
734
+ * `data-fade-from-active`. Consumers tie a CSS opacity transition to
735
+ * that attribute. Out-of-range values throw on mount.
736
+ */
737
+ fadeFromIndex = input(undefined, /* @ts-ignore */
738
+ ...(ngDevMode ? [{ debugName: "fadeFromIndex" }] : /* istanbul ignore next */ []));
739
+ /**
740
+ * Emitted when the drawer wants to close. Consumer wires this to flip
741
+ * the signal that gates the surrounding `@if`.
742
+ */
743
+ dismiss = output();
744
+ /** Vetoable Escape. `preventDefault()` suppresses the auto `(dismiss)`. */
745
+ escapeKeyDown = output();
746
+ /** Vetoable pointer-down outside. `preventDefault()` suppresses the auto `(dismiss)`. */
747
+ pointerDownOutside = output();
748
+ /** Vetoable focus-outside. `preventDefault()` suppresses the auto `(dismiss)`. */
749
+ focusOutside = output();
750
+ /**
751
+ * Composite event: fires alongside `pointerDownOutside` and `focusOutside`
752
+ * and shares their veto state — `preventDefault()` on either suppresses
753
+ * the auto `(dismiss)`.
754
+ */
755
+ interactOutside = output();
756
+ /** Drag stream. `percentageDragged` ∈ `[0, 1]`. */
757
+ dragMove = output();
758
+ /** Release event. The directive has already updated `activeSnapPoint` / requested close. */
759
+ release = output();
760
+ // ---- Internal reactive state ----
761
+ #labelIds = signal([], /* @ts-ignore */
762
+ ...(ngDevMode ? [{ debugName: "#labelIds" }] : /* istanbul ignore next */ []));
763
+ #describedByIds = signal([], /* @ts-ignore */
764
+ ...(ngDevMode ? [{ debugName: "#describedByIds" }] : /* istanbul ignore next */ []));
765
+ #handleEl = signal(null, /* @ts-ignore */
766
+ ...(ngDevMode ? [{ debugName: "#handleEl" }] : /* istanbul ignore next */ []));
767
+ #backdropEl = signal(null, /* @ts-ignore */
768
+ ...(ngDevMode ? [{ debugName: "#backdropEl" }] : /* istanbul ignore next */ []));
769
+ // Captures the `value` argument from the most recent `requestClose(reason, value)`
770
+ // call. Read by `ForDrawerManager` to bridge `[forDrawerClose] [closeWith]`
771
+ // into `ForDrawerRef.close(value)`. Plain in declarative usage (no consumer
772
+ // ever reads it), so the API surface is unchanged.
773
+ #lastCloseValue = signal(undefined, /* @ts-ignore */
774
+ ...(ngDevMode ? [{ debugName: "#lastCloseValue" }] : /* istanbul ignore next */ []));
775
+ labelledBy = computed(() => {
776
+ const ids = this.#labelIds();
777
+ return ids.length === 0 ? null : ids.join(' ');
778
+ }, /* @ts-ignore */
779
+ ...(ngDevMode ? [{ debugName: "labelledBy" }] : /* istanbul ignore next */ []));
780
+ describedBy = computed(() => {
781
+ const ids = this.#describedByIds();
782
+ return ids.length === 0 ? null : ids.join(' ');
783
+ }, /* @ts-ignore */
784
+ ...(ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []));
785
+ activeSnapPointAttr = computed(() => {
786
+ const v = this.activeSnapPoint();
787
+ return v == null ? null : String(v);
788
+ }, /* @ts-ignore */
789
+ ...(ngDevMode ? [{ debugName: "activeSnapPointAttr" }] : /* istanbul ignore next */ []));
790
+ fadeFromActive = computed(() => {
791
+ const idx = this.fadeFromIndex();
792
+ const points = this.snapPoints();
793
+ const active = this.activeSnapPoint();
794
+ if (idx === undefined || !points || active == null) {
795
+ return false;
796
+ }
797
+ const activeIdx = points.indexOf(active);
798
+ return activeIdx >= 0 && activeIdx >= idx;
799
+ }, /* @ts-ignore */
800
+ ...(ngDevMode ? [{ debugName: "fadeFromActive" }] : /* istanbul ignore next */ []));
801
+ #host = inject(ElementRef);
802
+ #scaleCoordinator = inject(ForDrawerScaleCoordinator);
803
+ #drawerStack = inject(ForDrawerStack);
804
+ #parentDrawer = inject(FOR_DRAWER_CONTEXT, { skipSelf: true, optional: true });
805
+ // Pointer / swipe / snap gesture engine. Owns the velocity tracking,
806
+ // per-gesture offset bounds, the dimension-keyed snap-position cache, and
807
+ // the runtime snap re-validation; the directive host-binds the three signals
808
+ // it returns (`dragging`, `dragProgress`, `dragTranslate`) and drives its
809
+ // `validateOnMount()` / `arm()` hooks from its own `afterNextRender`s so the
810
+ // engine's side effects keep `[forDrawer]`'s original ordering relative to
811
+ // the modal shell.
812
+ #drag = injectDrawerDrag({
813
+ side: this.side,
814
+ snapPoints: this.snapPoints,
815
+ closeThreshold: this.closeThreshold,
816
+ handleOnly: this.handleOnly,
817
+ swipeToDismiss: this.swipeToDismiss,
818
+ fadeFromIndex: this.fadeFromIndex,
819
+ activeSnapPoint: this.activeSnapPoint,
820
+ handleEl: this.#handleEl.asReadonly(),
821
+ emitDrag: (event) => this.dragMove.emit(event),
822
+ emitRelease: (event) => this.release.emit(event),
823
+ requestClose: (reason) => this.requestClose(reason),
824
+ });
825
+ /**
826
+ * `true` while a pointer drag gesture is in flight. Mirrors the host's
827
+ * `data-dragging` attribute; surfaced through `ForDrawerContext` so pieces
828
+ * portaled away from the surface (the backdrop) can suppress their own
829
+ * transitions during the gesture.
830
+ */
831
+ dragging = this.#drag.dragging;
832
+ /**
833
+ * Progress of the current drag toward the anchored edge, `∈ [0, 1]`.
834
+ * Surfaced through `ForDrawerContext` so the backdrop can publish it as the
835
+ * `--for-drawer-drag-progress` custom property.
836
+ */
837
+ dragProgress = this.#drag.dragProgress;
838
+ /**
839
+ * Live drag displacement as a CSS `translate` value (`"<x> <y>"`),
840
+ * published on the host as the `--for-drawer-translate` custom property by
841
+ * the gesture engine. The consumer composes it on the surface with
842
+ * `translate: var(--for-drawer-translate, 0px 0px)`.
843
+ *
844
+ * A **custom property** is used (rather than writing `translate` /
845
+ * `transform` directly) because `transform` is reserved for
846
+ * `ForDrawerScaleCoordinator` and a directly-written inline `translate` is
847
+ * dropped by Angular when the consumer also binds a template `[style.*]` on
848
+ * the same host. `"0px 0px"` at rest; the write rides the same change-
849
+ * detection pass as the `data-dragging` / `data-active-snap-point` changes
850
+ * on release so the consumer's `transition: translate` animates back in
851
+ * lockstep with the snap transition rather than jumping to the old rest
852
+ * position first.
853
+ */
854
+ dragTranslate = this.#drag.dragTranslate;
855
+ /** Host element of this drawer surface — exposed via `ForDrawerContext`. */
856
+ get hostElement() {
857
+ return this.#host.nativeElement;
858
+ }
859
+ /** Depth of this drawer in the nesting stack (0 root, 1 first child, …). */
860
+ #depth = signal(0, /* @ts-ignore */
861
+ ...(ngDevMode ? [{ debugName: "#depth" }] : /* istanbul ignore next */ []));
862
+ depthAttr = computed(() => String(this.#depth()), /* @ts-ignore */
863
+ ...(ngDevMode ? [{ debugName: "depthAttr" }] : /* istanbul ignore next */ []));
864
+ /**
865
+ * `true` while at least one descendant `[forDrawer]` is registered with
866
+ * `ForDrawerStack` underneath this one — reflected as
867
+ * `data-state-nested="true"`. The actual nested-visual transform
868
+ * (`scale + translate3d`) on the parent surface is owned by
869
+ * `ForDrawerScaleCoordinator`, which subscribes to the same stack
870
+ * signal and applies the inline `style.transform` once per affected
871
+ * host. The check is one-level (direct child) because the visual
872
+ * contract only cares about whether *some* child is currently covering
873
+ * this drawer; deeper descendants cascade naturally through their own
874
+ * ancestor chain.
875
+ */
876
+ hasChild = computed(() => {
877
+ const stack = this.#drawerStack.stack();
878
+ const me = this.#host.nativeElement;
879
+ return stack.some((n) => n.parent === me);
880
+ }, /* @ts-ignore */
881
+ ...(ngDevMode ? [{ debugName: "hasChild" }] : /* istanbul ignore next */ []));
882
+ /**
883
+ * Reflected as `data-scale-background` on the host: `true` when the
884
+ * directive opted into the effect AND the coordinator is currently
885
+ * applying it (wrapper registered, not under reduced-motion). Lets
886
+ * consumers style the drawer differently in scale mode (e.g. larger
887
+ * corner radii) without polling the coordinator.
888
+ */
889
+ scaleBackgroundActive = computed(() => this.scaleBackground() && this.#scaleCoordinator.active(), /* @ts-ignore */
890
+ ...(ngDevMode ? [{ debugName: "scaleBackgroundActive" }] : /* istanbul ignore next */ []));
891
+ // Cleanup refs for drawer-specific side effects (the modal-shell owns its
892
+ // portal / dismissable / focus-trap / scroll-lock / inert-siblings teardown;
893
+ // the gesture engine owns its own swipe-listener teardown). They are layered
894
+ // around the shell on SETUP — the drawer-stack push runs BEFORE the shell's
895
+ // afterNextRender so descendants see consistent topology, scale runs AFTER so
896
+ // the coordinator composes on a fully-wired surface — and both are torn down
897
+ // from a single `DestroyRef.onDestroy` (see the constructor).
898
+ #scaleCleanup = null;
899
+ #stackCleanup = null;
900
+ constructor() {
901
+ // ---- Pre-shell setup. Runs in its own afterNextRender registered BEFORE
902
+ // injectModalShell so the drawer-stack push, closeThreshold validation, and
903
+ // snap-point validation precede the shell's dismissable / focus /
904
+ // scroll-lock wiring. Descendants observing `hasChild` / depth see
905
+ // consistent topology throughout their mount sequence; consumers passing bad
906
+ // closeThreshold / snapPoints get a clear error before the shell focuses.
907
+ afterNextRender(() => {
908
+ // 1. Drawer-stack push. The `dragging` signal + resolved
909
+ // nested-transform tunables ride along so `ForDrawerScaleCoordinator`
910
+ // owns the `style.transform` write for the parent surface (issue #180)
911
+ // and reads scope-local defaults through this node.
912
+ const handle = this.#drawerStack.push({
913
+ host: this.#host.nativeElement,
914
+ side: this.side(),
915
+ scaleBackground: this.scaleBackground(),
916
+ parent: this.#parentDrawer?.hostElement ?? null,
917
+ dragging: this.dragging,
918
+ nestedScaleAmount: this.#defaults.nestedScaleAmount ?? 0.93,
919
+ nestedTranslateYpx: this.#defaults.nestedTranslateYpx ?? 8,
920
+ });
921
+ this.#depth.set(handle.depth);
922
+ this.#stackCleanup = handle.cleanup;
923
+ // 2. closeThreshold validation. Throws here so consumers get a clear
924
+ // mount-time error instead of a silently-broken dismissal.
925
+ const ct = this.closeThreshold();
926
+ if (!Number.isFinite(ct) || ct < 0 || ct > 1) {
927
+ throw new Error(`[forty-cdk/drawer] closeThreshold must be in [0, 1], got ${ct}.`);
928
+ }
929
+ // 3. Snap-point validation (shape + fadeFromIndex range + first
930
+ // live-dimension measurement) and mount-time `activeSnapPoint`
931
+ // default, owned by the gesture engine. Runs here — after the stack
932
+ // push and closeThreshold check — so the mount sequence matches what
933
+ // it was before the engine was extracted. Runtime `[snapPoints]`
934
+ // rebinds are re-validated by the engine's own effect.
935
+ this.#drag.validateOnMount();
936
+ });
937
+ // ---- The shared modal-shell. Owns: synchronous return-focus capture
938
+ // (WebKit #136), portal, dismissable layer (triple-veto + composite
939
+ // `interactOutside`), modal vs non-modal branching (focus trap + scroll
940
+ // lock + inert siblings), and the `autoFocusOnOpen` / `autoFocusOnClose`
941
+ // veto hooks. Anything drawer-specific (drag gesture, snap points, scale
942
+ // coordinator, drawer-stack registration, backdrop exemption) stays in
943
+ // this directive / the gesture engine.
944
+ injectModalShell({
945
+ modal: this.modal,
946
+ returnFocus: this.returnFocus,
947
+ initialFocus: this.initialFocus,
948
+ container: this.container,
949
+ autoFocusOnOpen: () => this.autoFocusOnOpen(),
950
+ autoFocusOnClose: () => this.autoFocusOnClose(),
951
+ dismiss: {
952
+ dismissible: this.dismissible,
953
+ requestClose: (reason) => this.requestClose(reason),
954
+ emitEscapeKeyDown: (veto) => this.escapeKeyDown.emit(veto),
955
+ emitPointerDownOutside: (veto) => this.pointerDownOutside.emit(veto),
956
+ emitFocusOutside: (veto) => this.focusOutside.emit(veto),
957
+ emitInteractOutside: (veto) => this.interactOutside.emit(veto),
958
+ // The backdrop is portaled to body (sibling of the drawer host), so
959
+ // without exemption a pointer-down on it fires `pointerDownOutside`
960
+ // BEFORE the backdrop's click handler runs — the drawer would close
961
+ // with the wrong reason. Marking it exempt routes the gesture
962
+ // through the backdrop's own click → `requestClose('backdrop')`
963
+ // path instead. The handle is a child of the host so it is already
964
+ // covered by `host.contains(target)`.
965
+ exemptElements: () => {
966
+ const backdrop = this.#backdropEl();
967
+ return backdrop ? [backdrop] : [];
968
+ },
969
+ },
970
+ });
971
+ // ---- Post-shell setup. Runs in its own afterNextRender registered
972
+ // AFTER injectModalShell so swipe-dismiss arms on a host already attached
973
+ // to body via the shell's portal, and the scale coordinator composes on
974
+ // top of a fully-wired surface.
975
+ afterNextRender(() => {
976
+ // Register with the scale coordinator last so the wrapper transition
977
+ // composes after every other side effect has stabilised. The coordinator
978
+ // enforces the reduced-motion + wrapper-presence gates.
979
+ if (this.scaleBackground()) {
980
+ this.#scaleCleanup = this.#scaleCoordinator.registerDrawer({
981
+ setBackgroundColorOnScale: this.setBackgroundColorOnScale(),
982
+ scaleAmount: this.#defaults.scaleAmount ?? 0.95,
983
+ scaleTranslateYpx: this.#defaults.scaleTranslateYpx ?? 14,
984
+ scaleBorderRadiusPx: this.#defaults.scaleBorderRadiusPx ?? 8,
985
+ scaleBackgroundColor: this.#defaults.scaleBackgroundColor ?? 'black',
986
+ });
987
+ }
988
+ // Arm the swipe gate now that the surface is fully wired. The gesture
989
+ // engine's effect owns the actual attach/detach; arming unblocks it.
990
+ this.#drag.arm();
991
+ });
992
+ // ---- Drawer-owned teardown, in one hook. Neither step depends on
993
+ // running before or after the shell's dismissable / focus-trap /
994
+ // scroll-lock teardown: the scale handle is self-contained (pops the scale
995
+ // stack), the swipe listeners are detached by the gesture engine's own
996
+ // destroy hook, and the drawer-stack pop only requires that descendant
997
+ // *drawers* are already popped — which Angular guarantees by destroying
998
+ // child components before their parents. Scale cleanup runs first, then the
999
+ // drawer-stack pop (so any sibling reads of the stack during scale cleanup
1000
+ // still see this node; `ForDrawerStack.cleanup` throws if a descendant is
1001
+ // still registered, surfacing consumer template bugs). The portal's own
1002
+ // destroy hook still removes the host from the DOM after this runs, so
1003
+ // return-focus is unaffected.
1004
+ inject(DestroyRef).onDestroy(() => {
1005
+ this.#scaleCleanup?.();
1006
+ this.#scaleCleanup = null;
1007
+ this.#stackCleanup?.();
1008
+ this.#stackCleanup = null;
1009
+ });
1010
+ }
1011
+ // ---- Context implementation ----
1012
+ registerLabel(id) {
1013
+ this.#labelIds.update((arr) => (arr.includes(id) ? arr : [...arr, id]));
1014
+ }
1015
+ unregisterLabel(id) {
1016
+ this.#labelIds.update((arr) => arr.filter((x) => x !== id));
1017
+ }
1018
+ registerDescription(id) {
1019
+ this.#describedByIds.update((arr) => (arr.includes(id) ? arr : [...arr, id]));
1020
+ }
1021
+ unregisterDescription(id) {
1022
+ this.#describedByIds.update((arr) => arr.filter((x) => x !== id));
1023
+ }
1024
+ registerHandle(el) {
1025
+ const current = this.#handleEl();
1026
+ if (el !== null && current !== null && current !== el) {
1027
+ throw new Error('[forty-cdk/drawer] Multiple [forDrawerHandle] inside the same [forDrawer]; only one is allowed.');
1028
+ }
1029
+ this.#handleEl.set(el);
1030
+ }
1031
+ registerBackdrop(el) {
1032
+ const current = this.#backdropEl();
1033
+ if (el !== null && current !== null && current !== el) {
1034
+ throw new Error('[forty-cdk/drawer] Multiple [forDrawerBackdrop] inside the same [forDrawer]; only one is allowed.');
1035
+ }
1036
+ this.#backdropEl.set(el);
1037
+ }
1038
+ requestClose(reason, value) {
1039
+ if (reason !== 'closeButton' && reason !== 'programmatic' && !this.dismissible()) {
1040
+ return;
1041
+ }
1042
+ this.#lastCloseValue.set(value);
1043
+ this.dismiss.emit(reason);
1044
+ }
1045
+ /**
1046
+ * The `value` argument from the most recent `requestClose(reason, value)`
1047
+ * call. Read by `ForDrawerManager` to bridge `[forDrawerClose] [closeWith]`
1048
+ * into `ForDrawerRef.close(value)`. Declarative consumers never need this —
1049
+ * it is plumbing for the imperative bootstrap path.
1050
+ *
1051
+ * @internal
1052
+ */
1053
+ lastCloseValue = this.#lastCloseValue.asReadonly();
1054
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawer, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1055
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDrawer, isStandalone: true, selector: "[forDrawer]", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null }, dismissible: { classPropertyName: "dismissible", publicName: "dismissible", isSignal: true, isRequired: false, transformFunction: null }, modal: { classPropertyName: "modal", publicName: "modal", isSignal: true, isRequired: false, transformFunction: null }, alert: { classPropertyName: "alert", publicName: "alert", isSignal: true, isRequired: false, transformFunction: null }, returnFocus: { classPropertyName: "returnFocus", publicName: "returnFocus", isSignal: true, isRequired: false, transformFunction: null }, initialFocus: { classPropertyName: "initialFocus", publicName: "initialFocus", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, container: { classPropertyName: "container", publicName: "container", isSignal: true, isRequired: false, transformFunction: null }, autoFocusOnOpen: { classPropertyName: "autoFocusOnOpen", publicName: "autoFocusOnOpen", isSignal: true, isRequired: false, transformFunction: null }, autoFocusOnClose: { classPropertyName: "autoFocusOnClose", publicName: "autoFocusOnClose", isSignal: true, isRequired: false, transformFunction: null }, swipeToDismiss: { classPropertyName: "swipeToDismiss", publicName: "swipeToDismiss", isSignal: true, isRequired: false, transformFunction: null }, closeThreshold: { classPropertyName: "closeThreshold", publicName: "closeThreshold", isSignal: true, isRequired: false, transformFunction: null }, handleOnly: { classPropertyName: "handleOnly", publicName: "handleOnly", isSignal: true, isRequired: false, transformFunction: null }, scaleBackground: { classPropertyName: "scaleBackground", publicName: "scaleBackground", isSignal: true, isRequired: false, transformFunction: null }, setBackgroundColorOnScale: { classPropertyName: "setBackgroundColorOnScale", publicName: "setBackgroundColorOnScale", isSignal: true, isRequired: false, transformFunction: null }, snapPoints: { classPropertyName: "snapPoints", publicName: "snapPoints", isSignal: true, isRequired: false, transformFunction: null }, activeSnapPoint: { classPropertyName: "activeSnapPoint", publicName: "activeSnapPoint", isSignal: true, isRequired: false, transformFunction: null }, fadeFromIndex: { classPropertyName: "fadeFromIndex", publicName: "fadeFromIndex", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { activeSnapPoint: "activeSnapPointChange", dismiss: "dismiss", escapeKeyDown: "escapeKeyDown", pointerDownOutside: "pointerDownOutside", focusOutside: "focusOutside", interactOutside: "interactOutside", dragMove: "dragMove", release: "release" }, host: { attributes: { "data-state": "open", "tabindex": "-1" }, properties: { "attr.role": "alert() ? \"alertdialog\" : \"dialog\"", "attr.aria-modal": "modal() ? \"true\" : null", "attr.aria-label": "ariaLabel() || null", "attr.aria-labelledby": "labelledBy()", "attr.aria-describedby": "describedBy()", "attr.data-side": "side()", "attr.data-active-snap-point": "activeSnapPointAttr()", "attr.data-dragging": "dragging() ? \"\" : null", "attr.data-scale-background": "scaleBackgroundActive() ? \"\" : null", "attr.data-depth": "depthAttr()", "attr.data-state-nested": "hasChild() ? \"true\" : null" } }, providers: [{ provide: FOR_DRAWER_CONTEXT, useExisting: ForDrawer }], exportAs: ["forDrawer"], ngImport: i0 });
1056
+ }
1057
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawer, decorators: [{
1058
+ type: Directive,
1059
+ args: [{
1060
+ selector: '[forDrawer]',
1061
+ exportAs: 'forDrawer',
1062
+ host: {
1063
+ '[attr.role]': 'alert() ? "alertdialog" : "dialog"',
1064
+ '[attr.aria-modal]': 'modal() ? "true" : null',
1065
+ '[attr.aria-label]': 'ariaLabel() || null',
1066
+ '[attr.aria-labelledby]': 'labelledBy()',
1067
+ '[attr.aria-describedby]': 'describedBy()',
1068
+ '[attr.data-side]': 'side()',
1069
+ '[attr.data-active-snap-point]': 'activeSnapPointAttr()',
1070
+ '[attr.data-dragging]': 'dragging() ? "" : null',
1071
+ '[attr.data-scale-background]': 'scaleBackgroundActive() ? "" : null',
1072
+ '[attr.data-depth]': 'depthAttr()',
1073
+ '[attr.data-state-nested]': 'hasChild() ? "true" : null',
1074
+ 'data-state': 'open',
1075
+ tabindex: '-1',
1076
+ },
1077
+ providers: [{ provide: FOR_DRAWER_CONTEXT, useExisting: ForDrawer }],
1078
+ }]
1079
+ }], ctorParameters: () => [], propDecorators: { side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], dismissible: [{ type: i0.Input, args: [{ isSignal: true, alias: "dismissible", required: false }] }], modal: [{ type: i0.Input, args: [{ isSignal: true, alias: "modal", required: false }] }], alert: [{ type: i0.Input, args: [{ isSignal: true, alias: "alert", required: false }] }], returnFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "returnFocus", required: false }] }], initialFocus: [{ type: i0.Input, args: [{ isSignal: true, alias: "initialFocus", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], container: [{ type: i0.Input, args: [{ isSignal: true, alias: "container", required: false }] }], autoFocusOnOpen: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocusOnOpen", required: false }] }], autoFocusOnClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoFocusOnClose", required: false }] }], swipeToDismiss: [{ type: i0.Input, args: [{ isSignal: true, alias: "swipeToDismiss", required: false }] }], closeThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeThreshold", required: false }] }], handleOnly: [{ type: i0.Input, args: [{ isSignal: true, alias: "handleOnly", required: false }] }], scaleBackground: [{ type: i0.Input, args: [{ isSignal: true, alias: "scaleBackground", required: false }] }], setBackgroundColorOnScale: [{ type: i0.Input, args: [{ isSignal: true, alias: "setBackgroundColorOnScale", required: false }] }], snapPoints: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapPoints", required: false }] }], activeSnapPoint: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeSnapPoint", required: false }] }, { type: i0.Output, args: ["activeSnapPointChange"] }], fadeFromIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "fadeFromIndex", required: false }] }], dismiss: [{ type: i0.Output, args: ["dismiss"] }], escapeKeyDown: [{ type: i0.Output, args: ["escapeKeyDown"] }], pointerDownOutside: [{ type: i0.Output, args: ["pointerDownOutside"] }], focusOutside: [{ type: i0.Output, args: ["focusOutside"] }], interactOutside: [{ type: i0.Output, args: ["interactOutside"] }], dragMove: [{ type: i0.Output, args: ["dragMove"] }], release: [{ type: i0.Output, args: ["release"] }] } });
1080
+
1081
+ /**
1082
+ * Button that toggles the drawer when clicked. Apply on a focusable element —
1083
+ * preferably a `<button>` — so keyboard users can reach it.
1084
+ *
1085
+ * Wires `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`, and
1086
+ * `data-state` on the host. Two-way bind `[(open)]` to the same signal that
1087
+ * gates the surrounding `@if` around `[forDrawer]`. Focus return is owned
1088
+ * by `[forDrawer]` (modal mode captures the previously-focused element on
1089
+ * mount and restores it on destroy).
1090
+ *
1091
+ * ```html
1092
+ * <button forDrawerTrigger [(open)]="drawerOpen" controls="my-drawer">Open</button>
1093
+ * @if (drawerOpen()) {
1094
+ * <div forDrawer id="my-drawer" (dismiss)="drawerOpen.set(false)">…</div>
1095
+ * }
1096
+ * ```
1097
+ */
1098
+ class ForDrawerTrigger {
1099
+ /**
1100
+ * Two-way bindable. Bind to the same signal that gates the surrounding
1101
+ * `@if` around `[forDrawer]`. The `model()` change emitter
1102
+ * (`(openChange)`) fires only on internal transitions (trigger click).
1103
+ */
1104
+ open = model(false, /* @ts-ignore */
1105
+ ...(ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []));
1106
+ /**
1107
+ * Id of the controlled drawer surface. Mirrored to `aria-controls` while
1108
+ * the drawer is open. The consumer is responsible for setting the same
1109
+ * `id` on `[forDrawer]`.
1110
+ */
1111
+ controls = input(null, /* @ts-ignore */
1112
+ ...(ngDevMode ? [{ debugName: "controls" }] : /* istanbul ignore next */ []));
1113
+ /**
1114
+ * When true, click is ignored and the host reflects `data-disabled=""`,
1115
+ * `aria-disabled="true"`, and the native `disabled` attribute so the
1116
+ * trigger is announced as disabled by assistive tech and dropped from
1117
+ * the tab order.
1118
+ */
1119
+ disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
1120
+ #host = inject(ElementRef);
1121
+ constructor() {
1122
+ reflectDisabled(this.disabled);
1123
+ }
1124
+ onClick() {
1125
+ if (this.disabled()) {
1126
+ return;
1127
+ }
1128
+ // Same WebKit return-focus quirk as ForDialogTrigger (#136): force focus
1129
+ // back onto the trigger before opening so the drawer captures it as the
1130
+ // return target.
1131
+ this.#host.nativeElement.focus();
1132
+ this.open.update((v) => !v);
1133
+ }
1134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTrigger, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1135
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDrawerTrigger, isStandalone: true, selector: "[forDrawerTrigger]", inputs: { open: { classPropertyName: "open", publicName: "open", isSignal: true, isRequired: false, transformFunction: null }, controls: { classPropertyName: "controls", publicName: "controls", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { open: "openChange" }, host: { attributes: { "type": "button" }, listeners: { "click": "onClick()" }, properties: { "attr.aria-haspopup": "\"dialog\"", "attr.aria-expanded": "open() ? \"true\" : \"false\"", "attr.aria-controls": "open() ? controls() : null", "attr.data-state": "open() ? \"open\" : \"closed\"", "attr.data-disabled": "disabled() ? \"\" : null", "attr.aria-disabled": "disabled() ? \"true\" : null" } }, exportAs: ["forDrawerTrigger"], ngImport: i0 });
1136
+ }
1137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTrigger, decorators: [{
1138
+ type: Directive,
1139
+ args: [{
1140
+ selector: '[forDrawerTrigger]',
1141
+ exportAs: 'forDrawerTrigger',
1142
+ host: {
1143
+ type: 'button',
1144
+ '[attr.aria-haspopup]': '"dialog"',
1145
+ '[attr.aria-expanded]': 'open() ? "true" : "false"',
1146
+ '[attr.aria-controls]': 'open() ? controls() : null',
1147
+ '[attr.data-state]': 'open() ? "open" : "closed"',
1148
+ '[attr.data-disabled]': 'disabled() ? "" : null',
1149
+ '[attr.aria-disabled]': 'disabled() ? "true" : null',
1150
+ '(click)': 'onClick()',
1151
+ },
1152
+ }]
1153
+ }], ctorParameters: () => [], propDecorators: { open: [{ type: i0.Input, args: [{ isSignal: true, alias: "open", required: false }] }, { type: i0.Output, args: ["openChange"] }], controls: [{ type: i0.Input, args: [{ isSignal: true, alias: "controls", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }] } });
1154
+
1155
+ /**
1156
+ * Optional backdrop overlay. Portaled to `document.body` so it sits
1157
+ * underneath the drawer regardless of where it's declared. While mounted
1158
+ * the backdrop is visible; mount/unmount it alongside the drawer with the
1159
+ * same `@if` so `animate.enter` / `animate.leave` works on both.
1160
+ *
1161
+ * Reflects `data-fade-from-active` when `fadeFromIndex` on the drawer root
1162
+ * is set and the active snap point is at or past that index — consumers
1163
+ * tie this to a CSS opacity transition for the "backdrop fades
1164
+ * in once you snap up past N" effect.
1165
+ *
1166
+ * Publishes the live drag progress toward the anchored edge as the
1167
+ * `--for-drawer-drag-progress` custom property (`0` at rest → `1` fully
1168
+ * dragged off-screen) and mirrors the surface's `data-dragging` attribute.
1169
+ * Together these drive the "backdrop fades out as you swipe to
1170
+ * dismiss" effect with pure CSS:
1171
+ *
1172
+ * ```css
1173
+ * [forDrawerBackdrop] {
1174
+ * opacity: calc(1 - var(--for-drawer-drag-progress, 0));
1175
+ * transition: opacity 0.3s ease;
1176
+ * }
1177
+ * [forDrawerBackdrop][data-dragging] {
1178
+ * transition: none; \/* track the pointer 1:1 *\/
1179
+ * }
1180
+ * ```
1181
+ *
1182
+ * The directive applies no visual styles itself.
1183
+ */
1184
+ class ForDrawerBackdrop {
1185
+ ctx = injectDrawerContext('ForDrawerBackdrop');
1186
+ #host = inject(ElementRef);
1187
+ /**
1188
+ * Per-instance drawer id when opened through `ForDrawerManager` (reflected
1189
+ * as `data-for-drawer-id` so the manager can pair this portaled backdrop
1190
+ * with its drawer and drive its exit animation). `null` in the declarative
1191
+ * path, where the host binding emits no attribute.
1192
+ */
1193
+ instanceId = inject(FOR_DRAWER_INSTANCE_ID, { optional: true });
1194
+ constructor() {
1195
+ injectPortal({ target: this.ctx.container });
1196
+ // Register so the drawer's dismissable layer treats pointer-down on the
1197
+ // backdrop as "inside" — see ForDrawerContext#registerBackdrop.
1198
+ this.ctx.registerBackdrop(this.#host.nativeElement);
1199
+ inject(DestroyRef).onDestroy(() => this.ctx.registerBackdrop(null));
1200
+ }
1201
+ onClick(event) {
1202
+ // Only close on direct backdrop click, not events bubbled from a child.
1203
+ if (event.target === event.currentTarget) {
1204
+ this.ctx.requestClose('backdrop');
1205
+ }
1206
+ }
1207
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerBackdrop, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1208
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerBackdrop, isStandalone: true, selector: "[forDrawerBackdrop]", host: { attributes: { "data-for-drawer-backdrop": "", "data-for-modal-peer": "", "data-state": "open" }, listeners: { "click": "onClick($event)" }, properties: { "attr.data-for-drawer-id": "instanceId", "attr.data-fade-from-active": "ctx.fadeFromActive() ? \"\" : null", "attr.data-dragging": "ctx.dragging() ? \"\" : null", "style.--for-drawer-drag-progress": "ctx.dragProgress()" } }, exportAs: ["forDrawerBackdrop"], ngImport: i0 });
1209
+ }
1210
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerBackdrop, decorators: [{
1211
+ type: Directive,
1212
+ args: [{
1213
+ selector: '[forDrawerBackdrop]',
1214
+ exportAs: 'forDrawerBackdrop',
1215
+ host: {
1216
+ 'data-for-drawer-backdrop': '',
1217
+ // Marker the inert-siblings utility looks for so the backdrop, which is
1218
+ // portaled to body alongside the drawer, is not inerted alongside the
1219
+ // rest of the document.
1220
+ 'data-for-modal-peer': '',
1221
+ 'data-state': 'open',
1222
+ '[attr.data-for-drawer-id]': 'instanceId',
1223
+ '[attr.data-fade-from-active]': 'ctx.fadeFromActive() ? "" : null',
1224
+ '[attr.data-dragging]': 'ctx.dragging() ? "" : null',
1225
+ '[style.--for-drawer-drag-progress]': 'ctx.dragProgress()',
1226
+ '(click)': 'onClick($event)',
1227
+ },
1228
+ }]
1229
+ }], ctorParameters: () => [] });
1230
+
1231
+ /**
1232
+ * Visual swipe handle (the "grabber" pill at the top of a bottom sheet).
1233
+ * Registers itself with the drawer so that `[handleOnly]="true"` can scope
1234
+ * the swipe gesture to gestures starting on this element. Reflects
1235
+ * `aria-hidden="true"` because the handle is a decorative styling target —
1236
+ * keyboard users dismiss via Escape / a `[forDrawerClose]` button.
1237
+ *
1238
+ * Apply on the visual handle element. Applies no styles itself; the
1239
+ * consumer is responsible for sizing, positioning, and color.
1240
+ */
1241
+ class ForDrawerHandle {
1242
+ #ctx = injectDrawerContext('ForDrawerHandle');
1243
+ #host = inject(ElementRef);
1244
+ constructor() {
1245
+ this.#ctx.registerHandle(this.#host.nativeElement);
1246
+ inject(DestroyRef).onDestroy(() => this.#ctx.registerHandle(null));
1247
+ }
1248
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerHandle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1249
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerHandle, isStandalone: true, selector: "[forDrawerHandle]", host: { attributes: { "aria-hidden": "true", "data-for-drawer-handle": "" } }, exportAs: ["forDrawerHandle"], ngImport: i0 });
1250
+ }
1251
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerHandle, decorators: [{
1252
+ type: Directive,
1253
+ args: [{
1254
+ selector: '[forDrawerHandle]',
1255
+ exportAs: 'forDrawerHandle',
1256
+ host: {
1257
+ 'aria-hidden': 'true',
1258
+ 'data-for-drawer-handle': '',
1259
+ },
1260
+ }]
1261
+ }], ctorParameters: () => [] });
1262
+
1263
+ /**
1264
+ * Convenience: gives the host element a stable id and registers it as the
1265
+ * drawer's accessible name (`aria-labelledby`). Apply on a heading element
1266
+ * (`<h2>`, `<h3>`...) inside `[forDrawer]`.
1267
+ *
1268
+ * If you don't render visible title text, omit this and pass `ariaLabel`
1269
+ * on the drawer root instead.
1270
+ */
1271
+ class ForDrawerTitle {
1272
+ id = registerA11yName(injectDrawerContext('ForDrawerTitle'), 'for-drawer-title');
1273
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTitle, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1274
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerTitle, isStandalone: true, selector: "[forDrawerTitle]", host: { properties: { "id": "id()" } }, exportAs: ["forDrawerTitle"], ngImport: i0 });
1275
+ }
1276
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerTitle, decorators: [{
1277
+ type: Directive,
1278
+ args: [{
1279
+ selector: '[forDrawerTitle]',
1280
+ exportAs: 'forDrawerTitle',
1281
+ host: {
1282
+ '[id]': 'id()',
1283
+ },
1284
+ }]
1285
+ }] });
1286
+
1287
+ /**
1288
+ * Convenience: gives the host element a stable id and registers it as the
1289
+ * drawer's accessible description (`aria-describedby`). Apply on a `<p>` or
1290
+ * other element inside `[forDrawer]`. Optional — only use when there's
1291
+ * non-title supporting copy.
1292
+ */
1293
+ class ForDrawerDescription {
1294
+ id = registerA11yDescription(injectDrawerContext('ForDrawerDescription'), 'for-drawer-description');
1295
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerDescription, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1296
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerDescription, isStandalone: true, selector: "[forDrawerDescription]", host: { properties: { "id": "id()" } }, exportAs: ["forDrawerDescription"], ngImport: i0 });
1297
+ }
1298
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerDescription, decorators: [{
1299
+ type: Directive,
1300
+ args: [{
1301
+ selector: '[forDrawerDescription]',
1302
+ exportAs: 'forDrawerDescription',
1303
+ host: {
1304
+ '[id]': 'id()',
1305
+ },
1306
+ }]
1307
+ }] });
1308
+
1309
+ /**
1310
+ * Button that closes the surrounding drawer when clicked. Apply on a
1311
+ * `<button>` inside `[forDrawer]`. Pass `[closeWith]` to propagate a return
1312
+ * value to `ForDrawerRef.close()` in programmatic usage; ignored in
1313
+ * declarative usage.
1314
+ */
1315
+ class ForDrawerClose {
1316
+ #ctx = injectDrawerContext('ForDrawerClose');
1317
+ /** Optional value passed to `ForDrawerRef.close()` (programmatic mode). */
1318
+ closeWith = input(undefined, /* @ts-ignore */
1319
+ ...(ngDevMode ? [{ debugName: "closeWith" }] : /* istanbul ignore next */ []));
1320
+ onClick() {
1321
+ this.#ctx.requestClose('closeButton', this.closeWith());
1322
+ }
1323
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerClose, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1324
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: ForDrawerClose, isStandalone: true, selector: "[forDrawerClose]", inputs: { closeWith: { classPropertyName: "closeWith", publicName: "closeWith", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "type": "button", "data-state": "open" }, listeners: { "click": "onClick()" } }, exportAs: ["forDrawerClose"], ngImport: i0 });
1325
+ }
1326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerClose, decorators: [{
1327
+ type: Directive,
1328
+ args: [{
1329
+ selector: '[forDrawerClose]',
1330
+ exportAs: 'forDrawerClose',
1331
+ host: {
1332
+ type: 'button',
1333
+ 'data-state': 'open',
1334
+ '(click)': 'onClick()',
1335
+ },
1336
+ }]
1337
+ }], propDecorators: { closeWith: [{ type: i0.Input, args: [{ isSignal: true, alias: "closeWith", required: false }] }] } });
1338
+
1339
+ /**
1340
+ * Marks the consumer's app-shell wrapper so {@link ForDrawer}'s
1341
+ * `scaleBackground` effect can transform the rest of the page when a
1342
+ * drawer opens — applied through Angular DI rather than a global selector.
1343
+ *
1344
+ * Apply on the element that contains everything the drawer should appear
1345
+ * to recede behind (typically the root shell). Reflects
1346
+ * `data-state="scaled"` while the effect is active and `"idle"` at rest
1347
+ * — useful for layering CSS styling without re-querying the coordinator.
1348
+ *
1349
+ * Only one wrapper may be registered at a time; mounting a second
1350
+ * `[forDrawerWrapper]` while another is alive throws. Drawers that opt
1351
+ * into `scaleBackground` without a registered wrapper are a no-op (no
1352
+ * effect applied) so test composition and partial consumer adoption
1353
+ * remain safe.
1354
+ */
1355
+ class ForDrawerWrapper {
1356
+ #coordinator = inject(ForDrawerScaleCoordinator);
1357
+ #host = inject(ElementRef);
1358
+ /** `'scaled'` while the coordinator is applying the effect, otherwise `'idle'`. */
1359
+ state = computed(() => this.#coordinator.active() ? 'scaled' : 'idle', /* @ts-ignore */
1360
+ ...(ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []));
1361
+ constructor() {
1362
+ const cleanup = this.#coordinator.registerWrapper(this.#host.nativeElement);
1363
+ inject(DestroyRef).onDestroy(cleanup);
1364
+ }
1365
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerWrapper, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1366
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerWrapper, isStandalone: true, selector: "[forDrawerWrapper]", host: { properties: { "attr.data-state": "state()" } }, exportAs: ["forDrawerWrapper"], ngImport: i0 });
1367
+ }
1368
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerWrapper, decorators: [{
1369
+ type: Directive,
1370
+ args: [{
1371
+ selector: '[forDrawerWrapper]',
1372
+ exportAs: 'forDrawerWrapper',
1373
+ host: {
1374
+ '[attr.data-state]': 'state()',
1375
+ },
1376
+ }]
1377
+ }], ctorParameters: () => [] });
1378
+
1379
+ /**
1380
+ * Handle returned by `ForDrawerManager.open()`. Inject inside the opened
1381
+ * component to drive close imperatively. Both reactive (`isClosed`, `result`)
1382
+ * and awaitable (`closed`) APIs are exposed — pick what fits the call site.
1383
+ *
1384
+ * `R` is the close-result type. Default `unknown`; specify it on
1385
+ * `manager.open<C, R>(...)` to get type safety.
1386
+ */
1387
+ class ForDrawerRef extends OverlayRef {
1388
+ }
1389
+
1390
+ /**
1391
+ * @internal Exposes the element injector at a child of the row's `[forDrawer]`
1392
+ * element so the user component rendered via `NgComponentOutlet` resolves
1393
+ * `FOR_DRAWER_CONTEXT` from the enclosing `[forDrawer]` host. The shared body
1394
+ * lives in `OverlayContextInjector`; this only carries the drawer selector.
1395
+ */
1396
+ class ForDrawerContextInjector extends OverlayContextInjector {
1397
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerContextInjector, deps: null, target: i0.ɵɵFactoryTarget.Directive });
1398
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.2", type: ForDrawerContextInjector, isStandalone: true, selector: "[forDrawerContextInjector]", exportAs: ["forDrawerContextInjector"], usesInheritance: true, ngImport: i0 });
1399
+ }
1400
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerContextInjector, decorators: [{
1401
+ type: Directive,
1402
+ args: [{
1403
+ selector: '[forDrawerContextInjector]',
1404
+ exportAs: 'forDrawerContextInjector',
1405
+ }]
1406
+ }] });
1407
+ /**
1408
+ * @internal Outlet component created once by `ForDrawerManager` on the first
1409
+ * `open()` call. Renders every live entry from the manager's entries signal
1410
+ * with `@for`, so Angular's control-flow unmount triggers `animate.leave` on
1411
+ * close — identical to the declarative `@if (open()) { <div forDrawer …> }` path.
1412
+ *
1413
+ * Constructed exclusively by `ForDrawerManager`. Not exported from
1414
+ * `public-api.ts`.
1415
+ */
1416
+ class ForDrawerOutlet {
1417
+ #destroyRef = inject(DestroyRef);
1418
+ #host = null;
1419
+ /**
1420
+ * @internal Called once by `ForDrawerManager` right after creating this
1421
+ * outlet to wire the reactive entries accessor and the destroy hook.
1422
+ */
1423
+ init(host) {
1424
+ this.#host = host;
1425
+ this.#destroyRef.onDestroy(() => host.closeAllForDestroy());
1426
+ }
1427
+ entries() {
1428
+ return this.#host?.entries() ?? [];
1429
+ }
1430
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerOutlet, deps: [], target: i0.ɵɵFactoryTarget.Component });
1431
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: ForDrawerOutlet, isStandalone: true, selector: "for-drawer-outlet", ngImport: i0, template: `
1432
+ @for (entry of entries(); track entry.id) {
1433
+ <div
1434
+ forDrawer
1435
+ [attr.data-for-drawer-id]="entry.id"
1436
+ [animate.enter]="entry.animateEnter ?? ''"
1437
+ [class]="entry.hostClass"
1438
+ [side]="entry.side ?? 'bottom'"
1439
+ [dismissible]="entry.dismissible ?? true"
1440
+ [modal]="entry.modal ?? true"
1441
+ [alert]="entry.alert ?? false"
1442
+ [returnFocus]="entry.returnFocus ?? true"
1443
+ [initialFocus]="entry.initialFocus ?? 'first'"
1444
+ [ariaLabel]="entry.ariaLabel ?? null"
1445
+ [container]="entry.container ?? null"
1446
+ [autoFocusOnOpen]="entry.autoFocusOnOpen"
1447
+ [autoFocusOnClose]="entry.autoFocusOnClose"
1448
+ [swipeToDismiss]="entry.swipeToDismiss ?? true"
1449
+ [closeThreshold]="entry.closeThreshold ?? 0.25"
1450
+ [handleOnly]="entry.handleOnly ?? false"
1451
+ [scaleBackground]="entry.scaleBackground ?? false"
1452
+ [setBackgroundColorOnScale]="entry.setBackgroundColorOnScale ?? true"
1453
+ [snapPoints]="entry.snapPoints"
1454
+ [activeSnapPoint]="entry.activeSnapPoint ?? null"
1455
+ [fadeFromIndex]="entry.fadeFromIndex"
1456
+ (dismiss)="entry.handleClose(fd.lastCloseValue())"
1457
+ (escapeKeyDown)="entry.escapeKeyDown?.($event)"
1458
+ (pointerDownOutside)="entry.pointerDownOutside?.($event)"
1459
+ (focusOutside)="entry.focusOutside?.($event)"
1460
+ (interactOutside)="entry.interactOutside?.($event)"
1461
+ (dragMove)="entry.onDrag?.($event)"
1462
+ (release)="entry.onRelease?.($event)"
1463
+ (activeSnapPointChange)="entry.onActiveSnapPointChange?.($event)"
1464
+ #fd="forDrawer"
1465
+ >
1466
+ <ng-container forDrawerContextInjector #ctx="forDrawerContextInjector">
1467
+ <ng-container
1468
+ [ngComponentOutlet]="entry.component"
1469
+ [ngComponentOutletInjector]="entry.injectorFor(ctx.injector)"
1470
+ />
1471
+ </ng-container>
1472
+ </div>
1473
+ }
1474
+ `, isInline: true, dependencies: [{ kind: "directive", type: ForDrawer, selector: "[forDrawer]", inputs: ["side", "dismissible", "modal", "alert", "returnFocus", "initialFocus", "ariaLabel", "container", "autoFocusOnOpen", "autoFocusOnClose", "swipeToDismiss", "closeThreshold", "handleOnly", "scaleBackground", "setBackgroundColorOnScale", "snapPoints", "activeSnapPoint", "fadeFromIndex"], outputs: ["activeSnapPointChange", "dismiss", "escapeKeyDown", "pointerDownOutside", "focusOutside", "interactOutside", "dragMove", "release"], exportAs: ["forDrawer"] }, { kind: "directive", type: NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletEnvironmentInjector", "ngComponentOutletContent", "ngComponentOutletNgModule"], exportAs: ["ngComponentOutlet"] }, { kind: "directive", type: ForDrawerContextInjector, selector: "[forDrawerContextInjector]", exportAs: ["forDrawerContextInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1475
+ }
1476
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerOutlet, decorators: [{
1477
+ type: Component,
1478
+ args: [{
1479
+ selector: 'for-drawer-outlet',
1480
+ changeDetection: ChangeDetectionStrategy.OnPush,
1481
+ imports: [ForDrawer, NgComponentOutlet, ForDrawerContextInjector],
1482
+ template: `
1483
+ @for (entry of entries(); track entry.id) {
1484
+ <div
1485
+ forDrawer
1486
+ [attr.data-for-drawer-id]="entry.id"
1487
+ [animate.enter]="entry.animateEnter ?? ''"
1488
+ [class]="entry.hostClass"
1489
+ [side]="entry.side ?? 'bottom'"
1490
+ [dismissible]="entry.dismissible ?? true"
1491
+ [modal]="entry.modal ?? true"
1492
+ [alert]="entry.alert ?? false"
1493
+ [returnFocus]="entry.returnFocus ?? true"
1494
+ [initialFocus]="entry.initialFocus ?? 'first'"
1495
+ [ariaLabel]="entry.ariaLabel ?? null"
1496
+ [container]="entry.container ?? null"
1497
+ [autoFocusOnOpen]="entry.autoFocusOnOpen"
1498
+ [autoFocusOnClose]="entry.autoFocusOnClose"
1499
+ [swipeToDismiss]="entry.swipeToDismiss ?? true"
1500
+ [closeThreshold]="entry.closeThreshold ?? 0.25"
1501
+ [handleOnly]="entry.handleOnly ?? false"
1502
+ [scaleBackground]="entry.scaleBackground ?? false"
1503
+ [setBackgroundColorOnScale]="entry.setBackgroundColorOnScale ?? true"
1504
+ [snapPoints]="entry.snapPoints"
1505
+ [activeSnapPoint]="entry.activeSnapPoint ?? null"
1506
+ [fadeFromIndex]="entry.fadeFromIndex"
1507
+ (dismiss)="entry.handleClose(fd.lastCloseValue())"
1508
+ (escapeKeyDown)="entry.escapeKeyDown?.($event)"
1509
+ (pointerDownOutside)="entry.pointerDownOutside?.($event)"
1510
+ (focusOutside)="entry.focusOutside?.($event)"
1511
+ (interactOutside)="entry.interactOutside?.($event)"
1512
+ (dragMove)="entry.onDrag?.($event)"
1513
+ (release)="entry.onRelease?.($event)"
1514
+ (activeSnapPointChange)="entry.onActiveSnapPointChange?.($event)"
1515
+ #fd="forDrawer"
1516
+ >
1517
+ <ng-container forDrawerContextInjector #ctx="forDrawerContextInjector">
1518
+ <ng-container
1519
+ [ngComponentOutlet]="entry.component"
1520
+ [ngComponentOutletInjector]="entry.injectorFor(ctx.injector)"
1521
+ />
1522
+ </ng-container>
1523
+ </div>
1524
+ }
1525
+ `,
1526
+ }]
1527
+ }] });
1528
+
1529
+ /**
1530
+ * Injection token for the `data` payload passed to `ForDrawerManager.open(component, { data })`.
1531
+ * Inject inside the opened component:
1532
+ *
1533
+ * ```ts
1534
+ * readonly data = inject(FOR_DRAWER_DATA) as MyShape;
1535
+ * ```
1536
+ *
1537
+ * Prefer `injectDrawerData<T>()` for typed access without manual casts.
1538
+ */
1539
+ const FOR_DRAWER_DATA = new InjectionToken('FOR_DRAWER_DATA');
1540
+ /** Typed accessor for the `data` payload. Equivalent to `inject(FOR_DRAWER_DATA) as T`. */
1541
+ function injectDrawerData() {
1542
+ return inject(FOR_DRAWER_DATA);
1543
+ }
1544
+ /**
1545
+ * Programmatic drawer opener — symmetric with `ForDialogManager`. Inject
1546
+ * anywhere, call `open(MyComponent, { data, side, ... })` and get a
1547
+ * `ForDrawerRef<R>` back. Internally the manager renders the user component
1548
+ * inside a manager-owned `@for` outlet, so Angular's control-flow unmount
1549
+ * fires `animate.leave` before the node leaves the DOM — identical to the
1550
+ * declarative `@if (open()) { <div forDrawer animate.leave="…"> }` path.
1551
+ *
1552
+ * Inside the opened component the usual drawer pieces work without any extra
1553
+ * wiring: `[forDrawerTitle]`, `[forDrawerDescription]`, `[forDrawerBackdrop]`,
1554
+ * `[forDrawerHandle]`, `[forDrawerClose]`. `[forDrawerClose] [closeWith]`
1555
+ * propagates through to `ForDrawerRef.close(value)`.
1556
+ *
1557
+ * Inject `ForDrawerRef` to drive close imperatively and `FOR_DRAWER_DATA` (or
1558
+ * `injectDrawerData<T>()`) for the payload.
1559
+ */
1560
+ class ForDrawerManager extends OverlayManagerCore {
1561
+ #defaults = inject(FOR_DRAWER_DEFAULTS);
1562
+ constructor() {
1563
+ super({
1564
+ idPrefix: 'for-drawer-instance',
1565
+ idAttribute: 'data-for-drawer-id',
1566
+ backdropAttribute: 'data-for-drawer-backdrop',
1567
+ createOutlet: (environmentInjector) => createComponent(ForDrawerOutlet, { environmentInjector }),
1568
+ });
1569
+ }
1570
+ open(component, config = {}) {
1571
+ if (config.closeThreshold !== undefined) {
1572
+ const ct = config.closeThreshold;
1573
+ if (!Number.isFinite(ct) || ct < 0 || ct > 1) {
1574
+ throw new Error(`[forty-cdk/drawer] closeThreshold must be in [0, 1], got ${ct}.`);
1575
+ }
1576
+ }
1577
+ const { id, remove } = this.nextId();
1578
+ const animateEnter = config.animateEnter ?? this.#defaults.animateEnter;
1579
+ const animateLeave = config.animateLeave ?? this.#defaults.animateLeave;
1580
+ const backdropAnimateLeave = config.backdropAnimateLeave ?? this.#defaults.backdropAnimateLeave;
1581
+ const ref = new ForDrawerRef(() => this.beginLeave(id, animateLeave, backdropAnimateLeave, remove));
1582
+ const hostClass = resolveConfigClass(config) ?? '';
1583
+ const data = config.data ?? null;
1584
+ const entry = {
1585
+ id,
1586
+ component: component,
1587
+ hostClass,
1588
+ side: config.side ?? this.#defaults.side,
1589
+ dismissible: config.dismissible ?? this.#defaults.dismissible,
1590
+ modal: config.modal ?? this.#defaults.modal,
1591
+ alert: config.alert,
1592
+ returnFocus: config.returnFocus ?? this.#defaults.returnFocus,
1593
+ initialFocus: config.initialFocus ?? this.#defaults.initialFocus,
1594
+ ariaLabel: config.ariaLabel,
1595
+ container: config.container,
1596
+ animateEnter,
1597
+ autoFocusOnOpen: config.autoFocusOnOpen,
1598
+ autoFocusOnClose: config.autoFocusOnClose,
1599
+ swipeToDismiss: config.swipeToDismiss ?? this.#defaults.swipeToDismiss,
1600
+ closeThreshold: config.closeThreshold ?? this.#defaults.closeThreshold,
1601
+ handleOnly: config.handleOnly ?? this.#defaults.handleOnly,
1602
+ scaleBackground: config.scaleBackground ?? this.#defaults.scaleBackground,
1603
+ setBackgroundColorOnScale: config.setBackgroundColorOnScale ?? this.#defaults.setBackgroundColorOnScale,
1604
+ snapPoints: config.snapPoints,
1605
+ activeSnapPoint: config.defaultSnapPoint,
1606
+ fadeFromIndex: config.fadeFromIndex,
1607
+ escapeKeyDown: config.escapeKeyDown,
1608
+ pointerDownOutside: config.pointerDownOutside,
1609
+ focusOutside: config.focusOutside,
1610
+ interactOutside: config.interactOutside,
1611
+ onDrag: config.onDrag,
1612
+ onRelease: config.onRelease,
1613
+ onActiveSnapPointChange: config.onActiveSnapPointChange,
1614
+ ref: ref,
1615
+ handleClose(value) {
1616
+ ref.close(value);
1617
+ },
1618
+ injectorFor: this.createInjectorFactory([
1619
+ { provide: FOR_DRAWER_DATA, useValue: data },
1620
+ { provide: FOR_DRAWER_INSTANCE_ID, useValue: id },
1621
+ { provide: ForDrawerRef, useValue: ref },
1622
+ ...(config.providers ?? []),
1623
+ ]),
1624
+ };
1625
+ this.register(entry);
1626
+ return ref;
1627
+ }
1628
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerManager, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
1629
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerManager, providedIn: 'root' });
1630
+ }
1631
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: ForDrawerManager, decorators: [{
1632
+ type: Injectable,
1633
+ args: [{ providedIn: 'root' }]
1634
+ }], ctorParameters: () => [] });
1635
+
1636
+ /**
1637
+ * Generated bundle index. Do not edit.
1638
+ */
1639
+
1640
+ export { FOR_DRAWER_CONTEXT, FOR_DRAWER_DATA, FOR_DRAWER_DEFAULTS, ForDrawer, ForDrawerBackdrop, ForDrawerClose, ForDrawerDescription, ForDrawerHandle, ForDrawerManager, ForDrawerRef, ForDrawerTitle, ForDrawerTrigger, ForDrawerWrapper, injectDrawerData, provideForDrawerDefaults };
1641
+ //# sourceMappingURL=forty-cdk-drawer.mjs.map